using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SharedLGUCurrency")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("SharedLGUCurrency")] [assembly: AssemblyTitle("SharedLGUCurrency")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SharedLGUCurrency { [HarmonyPatch] internal static class AddCurrencyAmountPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(LguReflection.CurrencyManagerType, "AddCurrencyAmount", new Type[2] { typeof(int), typeof(bool) }, (Type[])null); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPrefix] private static bool Prefix(int amount, bool trackSpent) { if (MilkMolarsPatches.InMolarActivation) { return false; } if (!NetworkSyncManager.IsReady) { return true; } NetworkSyncManager.RequestDelta(amount, trackSpent, DetectSource()); return false; } private static string DetectSource() { try { StackTrace stackTrace = new StackTrace(2, fNeedFileInfo: false); for (int i = 0; i < Math.Min(stackTrace.FrameCount, 8); i++) { Type type = stackTrace.GetFrame(i)?.GetMethod()?.DeclaringType; if (type != null && type.FullName != null && !type.FullName.StartsWith("MoreShipUpgrades.Managers.CurrencyManager", StringComparison.Ordinal)) { return type.FullName; } } } catch { } return "LGU AddCurrencyAmount"; } } [HarmonyPatch] internal static class RemoveCurrencyAmountPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(LguReflection.CurrencyManagerType, "RemoveCurrencyAmount", new Type[2] { typeof(int), typeof(bool) }, (Type[])null); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPrefix] private static bool Prefix(int amount, bool trackSpent) { if (!NetworkSyncManager.IsReady) { return true; } NetworkSyncManager.RequestDelta(-amount, trackSpent, "LGU RemoveCurrencyAmount"); return false; } } [HarmonyPatch] internal static class AtomicAlternateCurrencyPurchasePatch { private static MethodBase? TargetMethod() { Type type = AccessTools.TypeByName("MoreShipUpgrades.UI.Application.UpgradeStoreApplication"); if (type == null) { return null; } MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == "PurchaseUpgradeAlternateCurrency" && methodInfo.GetParameters().Length == 3) { return methodInfo; } } return null; } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPrefix] private static bool Prefix(object __0, int __1, Action __2) { string nodeName = LguReflection.GetNodeName(__0); if (string.IsNullOrEmpty(nodeName)) { LguReflection.ShowResult("Shared purchase rejected", "Could not identify the LGU upgrade."); return false; } NetworkSyncManager.RequestPurchase(nodeName, __1); try { __2?.Invoke(); } catch (Exception ex) { Plugin.Verbose("Could not return from LGU purchase screen: " + ex.Message); } return false; } } [HarmonyPatch] internal static class QuotaCurrencyPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(LguReflection.CurrencyManagerType, "AddCurrencyAmountFromQuotaClientRpc", new Type[1] { typeof(int) }, (Type[])null); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPrefix] private static bool Prefix(object __instance, int quotaFullfilled) { if (Plugin.Settings.DisableQuotaCurrency.Value) { Plugin.Verbose("Suppressed LGU quota currency payout."); return false; } if (NetworkSyncManager.IsServer && __instance.GetType().GetMethod("GetCurrencyAmountFromQuota", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(__instance, new object[1] { quotaFullfilled }) is int delta) { SharedCurrencyManager.Instance.HostApplyDelta(delta, trackSpent: false, NetworkManager.Singleton.LocalClientId, "LGU quota", out string _); } return false; } } [HarmonyPatch] internal static class CurrencyResetPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(LguReflection.CurrencyManagerType, "ResetAllValues", (Type[])null, (Type[])null); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPostfix] private static void Postfix() { if (Plugin.Settings.ResetSharedCurrencyOnLGUReset.Value && NetworkSyncManager.IsServer) { SharedCurrencyManager.Instance.HostReset("LGU reset"); } } } [HarmonyPatch] internal static class LguSaveLoadPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(LguReflection.LguStoreType, "UpdateUpgradeBus", new Type[2] { typeof(bool), typeof(bool) }, (Type[])null); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPostfix] private static void Postfix() { if (NetworkSyncManager.IsReady) { if (NetworkSyncManager.IsServer) { SharedCurrencyManager.Instance.HostImportFromLgu("save/load"); } else { NetworkSyncManager.RequestSync(); } } } } [HarmonyPatch] internal static class UnsafePlayerCreditOperationPatch { [HarmonyTargetMethods] private static IEnumerable TargetMethods() { (string, string)[] array = new(string, string)[3] { ("MoreShipUpgrades.UI.Application.TradePlayerCreditsApplication", "ExecuteTrade"), ("MoreShipUpgrades.UI.Application.ConvertPlayerCreditApplication", "TryConvertPCsToCredits"), ("MoreShipUpgrades.UI.Application.ConvertPlayerCreditApplication", "TryConvertCreditsToPCs") }; (string TypeName, string MethodName)[] array2 = array; for (int i = 0; i < array2.Length; i++) { (string, string) tuple = array2[i]; string item = tuple.Item1; string methodName = tuple.Item2; Type type = AccessTools.TypeByName(item); if (type == null) { continue; } MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == methodName) { yield return methodInfo; } } } } [HarmonyPrefix] private static bool Prefix(object[] __args) { if (!Plugin.Settings.DisableUnsafePlayerCreditOperations.Value) { return true; } LguReflection.ShowResult("SharedLGUCurrency", "This per-player Player Credit operation is disabled for the shared crew wallet."); foreach (object obj in __args) { if (obj is Action action) { try { action(); } catch { } break; } } return false; } } [HarmonyPatch] internal static class UnsafePlayerCreditRefundPatch { private static MethodBase? TargetMethod() { return AccessTools.TypeByName("MoreShipUpgrades.UI.Application.UpgradeStoreApplication")?.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo method) => method.Name == "RefundUpgrade" && method.GetParameters().Length == 3); } private static bool Prepare() { return TargetMethod() != null; } [HarmonyPrefix] private static bool Prefix(object __1, Action __2) { if (!Plugin.Settings.DisableUnsafePlayerCreditOperations.Value || !string.Equals(__1.ToString(), "AlternateCurrency", StringComparison.Ordinal)) { return true; } LguReflection.ShowResult("SharedLGUCurrency", "Player Credit refunds are disabled because LGU 3.14.1 does not host-authorize them."); try { __2?.Invoke(); } catch { } return false; } } internal static class LguReflection { internal const string CurrencyManagerTypeName = "MoreShipUpgrades.Managers.CurrencyManager"; internal const string LguStoreTypeName = "MoreShipUpgrades.Managers.LguStore"; internal const string UpgradeBusTypeName = "MoreShipUpgrades.Managers.UpgradeBus"; internal const string UpgradeStoreApplicationTypeName = "MoreShipUpgrades.UI.Application.UpgradeStoreApplication"; internal static Type? CurrencyManagerType => AccessTools.TypeByName("MoreShipUpgrades.Managers.CurrencyManager"); internal static Type? LguStoreType => AccessTools.TypeByName("MoreShipUpgrades.Managers.LguStore"); internal static Type? UpgradeBusType => AccessTools.TypeByName("MoreShipUpgrades.Managers.UpgradeBus"); internal static object? CurrencyInstance => GetStaticMember(CurrencyManagerType, "Instance"); internal static object? LguStoreInstance => GetStaticMember(LguStoreType, "Instance"); internal static object? UpgradeBusInstance => GetStaticMember(UpgradeBusType, "Instance"); internal static int ReadBalance() { return GetInt(CurrencyInstance, "CurrencyAmount"); } internal static int ReadSpent() { return GetInt(CurrencyInstance, "SpentCurrencyAmount"); } internal static void MirrorBalance(int balance, int spent) { object currencyInstance = CurrencyInstance; if (currencyInstance != null) { SetMember(currencyInstance, "CurrencyAmount", balance); SetMember(currencyInstance, "SpentCurrencyAmount", spent); } } internal static int GetMaximumBalance() { Type currencyManagerType = CurrencyManagerType; if (currencyManagerType == null) { return int.MaxValue; } int num = ((GetStaticMember(currencyManagerType, "MaximumAmountPerPlayer") is int num2) ? num2 : (-1)); if (num <= 0) { return int.MaxValue; } return num; } internal static string GetNodeName(object node) { return (GetMember(node, "OriginalName") as string) ?? string.Empty; } internal static string GetDisplayName(object node) { return (GetMember(node, "Name") as string) ?? GetNodeName(node); } internal static bool TryResolvePurchase(string upgradeName, int expectedCurrencyPrice, out object? node, out int actualCurrencyPrice, out bool increment, out string rejection) { node = null; actualCurrencyPrice = 0; increment = false; rejection = string.Empty; object upgradeBusInstance = UpgradeBusInstance; object currencyInstance = CurrencyInstance; if (upgradeBusInstance == null || currencyInstance == null) { rejection = "LGU is not ready yet."; return false; } node = upgradeBusInstance.GetType().GetMethod("GetUpgradeNode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(upgradeBusInstance, new object[1] { upgradeName }); if (node == null) { rejection = "Unknown LGU upgrade."; return false; } if (!GetBool(node, "SharedUpgrade")) { rejection = "LGU did not mark this upgrade as shared. Enable 'Convert all upgrades to be shared.'."; return false; } bool flag = GetBool(node, "Unlocked"); int num = GetInt(node, "CurrentUpgrade"); int num2 = GetInt(node, "MaxUpgrade"); if (flag && num >= num2) { rejection = "Upgrade is already at its maximum level."; return false; } if (!(node.GetType().GetMethod("GetCurrentPrice", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(node, null) is int num3) || num3 < 0 || num3 == int.MaxValue) { rejection = "LGU returned an invalid upgrade price."; return false; } if (!(currencyInstance.GetType().GetMethod("GetCurrencyAmountFromCredits", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(currencyInstance, new object[1] { num3 }) is int num4) || num4 < 0) { rejection = "LGU could not convert the upgrade price to Player Credits."; return false; } actualCurrencyPrice = num4; if (expectedCurrencyPrice != actualCurrencyPrice) { rejection = "The displayed price is stale; reopen the upgrade screen and try again."; return false; } increment = flag; return true; } internal static void ApplySharedPurchase(object node, bool increment) { object obj = LguStoreInstance ?? throw new InvalidOperationException("LGU store is not available."); MethodInfo methodInfo = obj.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo method) => method.Name == "HandleUpgrade" && method.GetParameters().Length == 3); if (methodInfo == null) { throw new MissingMethodException(obj.GetType().FullName, "HandleUpgrade(CustomTerminalNode, bool, bool)"); } methodInfo.Invoke(obj, new object[3] { node, increment, false }); } internal static void ShowResult(string title, string message) { try { Type type = AccessTools.TypeByName("HUDManager"); object staticMember = GetStaticMember(type, "Instance"); if (staticMember == null) { return; } MethodInfo methodInfo = (from method in staticMember.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where method.Name == "DisplayTip" orderby method.GetParameters().Length select method).FirstOrDefault((MethodInfo method) => method.GetParameters().Length >= 2); if (!(methodInfo == null)) { ParameterInfo[] parameters = methodInfo.GetParameters(); object[] array = new object[parameters.Length]; array[0] = title; array[1] = message; for (int num = 2; num < array.Length; num++) { array[num] = (parameters[num].HasDefaultValue ? parameters[num].DefaultValue : GetDefault(parameters[num].ParameterType)); } methodInfo.Invoke(staticMember, array); } } catch (Exception ex) { Plugin.Verbose("Could not show HUD result: " + ex.Message); } } internal static object? GetMember(object? instance, string name) { if (instance == null) { return null; } Type type = instance.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(instance, null); } return type.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance); } internal static void SetMember(object instance, string name, object value) { Type type = instance.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)property != null && property.CanWrite) { property.SetValue(instance, value, null); } else { type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(instance, value); } } internal static object? GetStaticMember(Type? type, string name) { if (type == null) { return null; } PropertyInfo property = type.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(null, null); } return type.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } internal static int GetInt(object? instance, string name) { object member = GetMember(instance, name); if (member is int) { return (int)member; } return 0; } internal static bool GetBool(object? instance, string name) { object member = GetMember(instance, name); bool flag = default(bool); int num; if (member is bool) { flag = (bool)member; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } private static object? GetDefault(Type type) { if (!type.IsValueType) { return null; } return Activator.CreateInstance(type); } } [HarmonyPatch] internal static class MilkMolarsPatches { private static readonly HashSet CreditedMolarIds = new HashSet(); private static bool _forcedConfigLogged; [ThreadStatic] private static int _activationDepth; internal static bool InMolarActivation => _activationDepth > 0; [HarmonyTargetMethods] private static IEnumerable TargetMethods() { string[] array = new string[2] { "MilkMolars.MilkMolarBehavior", "MilkMolars.MegaMilkMolarBehavior" }; foreach (string text in array) { Type type = AccessTools.TypeByName(text); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "ActivateMolarRpc", new Type[1] { typeof(int) }, (Type[])null)); if (methodInfo != null) { yield return methodInfo; } } } [HarmonyPrefix] private static void Prefix(object __instance, int __0) { _activationDepth++; if (!NetworkSyncManager.IsServer) { return; } NetworkBehaviour val = (NetworkBehaviour)((__instance is NetworkBehaviour) ? __instance : null); if (val == null) { return; } ulong networkObjectId = val.NetworkObjectId; if (CreditedMolarIds.Contains(networkObjectId)) { Plugin.Verbose($"Ignored duplicate molar activation for network object {networkObjectId}."); return; } if (__0 < 0) { Plugin.Verbose($"Ignored molar {networkObjectId}: no activating player was supplied."); return; } if (!ValidateReturnToShip(__instance, (ulong)__0)) { Plugin.Log.LogWarning((object)$"Rejected early ReturnToShip activation for molar {networkObjectId} from player {__0}."); return; } int num = ReadNetworkVariableInt(LguReflection.GetMember(__instance, "ContributeAmount")); if (num > 0) { CreditedMolarIds.Add(networkObjectId); string molarName = (__instance.GetType().Name.StartsWith("Mega", StringComparison.Ordinal) ? "Mega Milk Molar" : "Milk Molar"); SharedCurrencyManager.Instance.HostCreditMolar(num, (ulong)__0, molarName); } } [HarmonyFinalizer] private static Exception? Finalizer(Exception? __exception) { if (_activationDepth > 0) { _activationDepth--; } return __exception; } internal static void ResetSession() { CreditedMolarIds.Clear(); } internal static void ApplyForcedConfig() { if (Plugin.Settings.ForceSharedContributeAmountToZero.Value) { string[] array = new string[2] { "MilkMolars.MilkMolarBehavior", "MilkMolars.MegaMilkMolarBehavior" }; foreach (string text in array) { AccessTools.TypeByName(text)?.GetField("cfgSharedContributeAmount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(null, 0); } if (!_forcedConfigLogged) { _forcedConfigLogged = true; Plugin.Log.LogInfo((object)"Forced MilkMolars Shared Contribute Amount to 0."); } } } private static bool ValidateReturnToShip(object instance, ulong playerId) { //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) object member = LguReflection.GetMember(instance, "ActivateMethod"); if (!string.Equals(LguReflection.GetMember(member, "Value")?.ToString(), "ReturnToShip", StringComparison.OrdinalIgnoreCase)) { return true; } try { Type type = AccessTools.TypeByName("StartOfRound"); object staticMember = LguReflection.GetStaticMember(type, "Instance"); if (staticMember == null) { return false; } object member2 = LguReflection.GetMember(staticMember, "allPlayerScripts"); if (member2 is Array array) { foreach (object item in array) { if (item != null && LguReflection.GetMember(item, "actualClientId") is ulong num && num == playerId && LguReflection.GetBool(item, "isInHangarShipRoom")) { return true; } } } object member3 = LguReflection.GetMember(staticMember, "shipInnerRoomBounds"); Collider val = (Collider)((member3 is Collider) ? member3 : null); if (val != null) { Component val2 = (Component)((instance is Component) ? instance : null); if (val2 != null) { Bounds bounds = val.bounds; ((Bounds)(ref bounds)).Expand(1f); return ((Bounds)(ref bounds)).Contains(val2.transform.position); } } } catch (Exception ex) { Plugin.Verbose("ReturnToShip validation failed: " + ex.Message); } return false; } private static int ReadNetworkVariableInt(object? networkVariable) { object member = LguReflection.GetMember(networkVariable, "Value"); if (member is int) { return (int)member; } return 0; } } [HarmonyPatch] internal static class MilkMolarStartConfigPatch { [HarmonyTargetMethods] private static IEnumerable TargetMethods() { string[] array = new string[2] { "MilkMolars.MilkMolarBehavior", "MilkMolars.MegaMilkMolarBehavior" }; foreach (string text in array) { Type type = AccessTools.TypeByName(text); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "Start", (Type[])null, (Type[])null)); if (methodInfo != null) { yield return methodInfo; } } } [HarmonyPrefix] private static void Prefix() { MilkMolarsPatches.ApplyForcedConfig(); } } internal sealed class ModConfig { internal ConfigEntry Enabled { get; } internal ConfigEntry ForceSharedContributeAmountToZero { get; } internal ConfigEntry DisableQuotaCurrency { get; } internal ConfigEntry ResetSharedCurrencyOnLGUReset { get; } internal ConfigEntry DisableUnsafePlayerCreditOperations { get; } internal ConfigEntry VerboseLogging { get; } internal ConfigEntry MaximumExternalDeltaPerRequest { get; } internal ModConfig(ConfigFile config) { Enabled = config.Bind("General", "Enabled", true, "Enable the shared, host-authoritative LGU currency wallet."); ForceSharedContributeAmountToZero = config.Bind("MilkMolars", "ForceSharedContributeAmountToZero", true, "Force MilkMolars' Shared Contribute Amount to zero so one molar can never reward every player."); DisableQuotaCurrency = config.Bind("Currency", "DisableQuotaCurrency", true, "Suppress LGU's automatic quota-to-player-credit payout."); ResetSharedCurrencyOnLGUReset = config.Bind("Currency", "ResetSharedCurrencyOnLGUReset", true, "Reset the crew wallet whenever LGU resets its CurrencyManager."); DisableUnsafePlayerCreditOperations = config.Bind("Currency", "DisableUnsafePlayerCreditOperations", true, "Disable LGU's per-player trade/conversion and Player Credit refund UIs; LGU 3.14.1 performs their side effects before host confirmation."); VerboseLogging = config.Bind("Networking", "VerboseLogging", false, "Log network requests, mirrors and synchronization details."); MaximumExternalDeltaPerRequest = config.Bind("Networking", "MaximumExternalDeltaPerRequest", 1000, "Reject non-purchase currency API requests whose absolute delta exceeds this value. Set to 0 to disable the limit."); } } internal sealed class NetworkSyncManager : MonoBehaviour { [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__HandleBalance; public static HandleNamedMessageDelegate <1>__HandlePurchaseResult; public static HandleNamedMessageDelegate <2>__HandleDeltaRequest; public static HandleNamedMessageDelegate <3>__HandlePurchaseRequest; public static HandleNamedMessageDelegate <4>__HandleSyncRequest; public static Action <5>__HandleClientConnected; } private const string Protocol = "timo.sharedlgucurrency/v1/"; private const string DeltaRequestMessage = "timo.sharedlgucurrency/v1/delta"; private const string PurchaseRequestMessage = "timo.sharedlgucurrency/v1/purchase"; private const string SyncRequestMessage = "timo.sharedlgucurrency/v1/sync-request"; private const string BalanceMessage = "timo.sharedlgucurrency/v1/balance"; private const string PurchaseResultMessage = "timo.sharedlgucurrency/v1/purchase-result"; private static NetworkSyncManager? _instance; private static NetworkManager? _registeredNetwork; private static readonly Dictionary> SeenNoncesByClient = new Dictionary>(); private static ulong _nextNonce; internal static bool IsReady { get { if ((Object)(object)_registeredNetwork != (Object)null) { return _registeredNetwork.IsListening; } return false; } } internal static bool IsServer { get { if (IsReady) { return _registeredNetwork.IsServer; } return false; } } private void Awake() { if ((Object)(object)_instance != (Object)null && (Object)(object)_instance != (Object)(object)this) { Object.Destroy((Object)(object)((Component)this).gameObject); } else { _instance = this; } } private void Update() { NetworkManager singleton = NetworkManager.Singleton; if (!((Object)(object)singleton == (Object)(object)_registeredNetwork) || !((Object)(object)singleton != (Object)null) || !singleton.IsListening) { Unregister(); if (!((Object)(object)singleton == (Object)null) && singleton.IsListening) { Register(singleton); } } } private void OnDestroy() { Unregister(); } private static void Register(NetworkManager network) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Expected O, but got Unknown //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Expected O, but got Unknown _registeredNetwork = network; SeenNoncesByClient.Clear(); _nextNonce = 0uL; CustomMessagingManager customMessagingManager = network.CustomMessagingManager; object obj = <>O.<0>__HandleBalance; if (obj == null) { HandleNamedMessageDelegate val = HandleBalance; <>O.<0>__HandleBalance = val; obj = (object)val; } customMessagingManager.RegisterNamedMessageHandler("timo.sharedlgucurrency/v1/balance", (HandleNamedMessageDelegate)obj); object obj2 = <>O.<1>__HandlePurchaseResult; if (obj2 == null) { HandleNamedMessageDelegate val2 = HandlePurchaseResult; <>O.<1>__HandlePurchaseResult = val2; obj2 = (object)val2; } customMessagingManager.RegisterNamedMessageHandler("timo.sharedlgucurrency/v1/purchase-result", (HandleNamedMessageDelegate)obj2); if (network.IsServer) { object obj3 = <>O.<2>__HandleDeltaRequest; if (obj3 == null) { HandleNamedMessageDelegate val3 = HandleDeltaRequest; <>O.<2>__HandleDeltaRequest = val3; obj3 = (object)val3; } customMessagingManager.RegisterNamedMessageHandler("timo.sharedlgucurrency/v1/delta", (HandleNamedMessageDelegate)obj3); object obj4 = <>O.<3>__HandlePurchaseRequest; if (obj4 == null) { HandleNamedMessageDelegate val4 = HandlePurchaseRequest; <>O.<3>__HandlePurchaseRequest = val4; obj4 = (object)val4; } customMessagingManager.RegisterNamedMessageHandler("timo.sharedlgucurrency/v1/purchase", (HandleNamedMessageDelegate)obj4); object obj5 = <>O.<4>__HandleSyncRequest; if (obj5 == null) { HandleNamedMessageDelegate val5 = HandleSyncRequest; <>O.<4>__HandleSyncRequest = val5; obj5 = (object)val5; } customMessagingManager.RegisterNamedMessageHandler("timo.sharedlgucurrency/v1/sync-request", (HandleNamedMessageDelegate)obj5); } network.OnClientConnectedCallback += HandleClientConnected; SharedCurrencyManager.Instance.BeginNetworkSession(network.IsServer); if (network.IsServer) { SharedCurrencyManager.Instance.HostInitializeIfNeeded(); } else { RequestSync(); } Plugin.Verbose("Network protocol registered as " + (network.IsServer ? "server" : "client") + "."); } private static void Unregister() { if (!((Object)(object)_registeredNetwork == (Object)null)) { try { CustomMessagingManager customMessagingManager = _registeredNetwork.CustomMessagingManager; customMessagingManager.UnregisterNamedMessageHandler("timo.sharedlgucurrency/v1/balance"); customMessagingManager.UnregisterNamedMessageHandler("timo.sharedlgucurrency/v1/purchase-result"); customMessagingManager.UnregisterNamedMessageHandler("timo.sharedlgucurrency/v1/delta"); customMessagingManager.UnregisterNamedMessageHandler("timo.sharedlgucurrency/v1/purchase"); customMessagingManager.UnregisterNamedMessageHandler("timo.sharedlgucurrency/v1/sync-request"); _registeredNetwork.OnClientConnectedCallback -= HandleClientConnected; } catch (Exception ex) { Plugin.Verbose("Network cleanup ignored: " + ex.Message); } _registeredNetwork = null; SeenNoncesByClient.Clear(); } } internal unsafe static void RequestDelta(int delta, bool trackSpent, string source) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) if (!IsReady) { return; } if (IsServer) { SharedCurrencyManager.Instance.HostApplyDelta(delta, trackSpent, _registeredNetwork.LocalClientId, source, out string _); return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(512, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(ref delta, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(ref trackSpent, default(ForPrimitives)); ulong num = ++_nextNonce; ((FastBufferWriter)(ref val)).WriteValueSafe(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(source, false); _registeredNetwork.CustomMessagingManager.SendNamedMessage("timo.sharedlgucurrency/v1/delta", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } internal unsafe static void RequestPurchase(string upgradeName, int expectedCurrencyPrice) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) if (!IsReady) { LguReflection.ShowResult("SharedLGUCurrency", "Network is not ready; purchase was not sent."); return; } if (IsServer) { SharedCurrencyManager.Instance.HostHandlePurchase(_registeredNetwork.LocalClientId, upgradeName, expectedCurrencyPrice); return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(512, (Allocator)2, -1); try { ulong num = ++_nextNonce; ((FastBufferWriter)(ref val)).WriteValueSafe(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(upgradeName, false); ((FastBufferWriter)(ref val)).WriteValueSafe(ref expectedCurrencyPrice, default(ForPrimitives)); _registeredNetwork.CustomMessagingManager.SendNamedMessage("timo.sharedlgucurrency/v1/purchase", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } internal unsafe static void RequestSync() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) if (!IsReady || IsServer) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(1, (Allocator)2, -1); try { _registeredNetwork.CustomMessagingManager.SendNamedMessage("timo.sharedlgucurrency/v1/sync-request", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } internal static void BroadcastBalance(int balance, int spent) { if (!IsServer) { return; } foreach (ulong connectedClientsId in _registeredNetwork.ConnectedClientsIds) { SendBalance(connectedClientsId, balance, spent); } } internal unsafe static void SendPurchaseResult(ulong clientId, bool success, string message) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) if (!IsServer) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(768, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(ref success, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(message, false); _registeredNetwork.CustomMessagingManager.SendNamedMessage("timo.sharedlgucurrency/v1/purchase-result", clientId, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void HandleDeltaRequest(ulong senderClientId, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) int delta = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref delta, default(ForPrimitives)); bool trackSpent = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref trackSpent, default(ForPrimitives)); ulong nonce = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref nonce, default(ForPrimitives)); string source = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref source, false); if (AcceptNonce(senderClientId, nonce) && !SharedCurrencyManager.Instance.HostApplyDelta(delta, trackSpent, senderClientId, source, out string rejection)) { SendPurchaseResult(senderClientId, success: false, "Currency request rejected: " + rejection); } } private static void HandlePurchaseRequest(ulong senderClientId, FastBufferReader reader) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) ulong nonce = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref nonce, default(ForPrimitives)); string upgradeName = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref upgradeName, false); int expectedPrice = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref expectedPrice, default(ForPrimitives)); if (AcceptNonce(senderClientId, nonce)) { SharedCurrencyManager.Instance.HostHandlePurchase(senderClientId, upgradeName, expectedPrice); } } private static void HandleSyncRequest(ulong senderClientId, FastBufferReader reader) { SharedCurrencyManager.Instance.HostInitializeIfNeeded(); SendBalance(senderClientId, SharedCurrencyManager.Instance.Balance, SharedCurrencyManager.Instance.Spent); Plugin.Log.LogInfo((object)$"Late joiner synchronized: {SharedCurrencyManager.Instance.Balance} PC (client {senderClientId})"); } private static void HandleBalance(ulong senderClientId, FastBufferReader reader) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_registeredNetwork == (Object)null) && senderClientId == 0L) { int balance = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref balance, default(ForPrimitives)); int spent = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref spent, default(ForPrimitives)); SharedCurrencyManager.Instance.ApplyRemoteMirror(balance, spent); } } private static void HandlePurchaseResult(ulong senderClientId, FastBufferReader reader) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_registeredNetwork == (Object)null) && senderClientId == 0L) { bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag, default(ForPrimitives)); string message = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref message, false); LguReflection.ShowResult(flag ? "Shared purchase approved" : "Shared purchase rejected", message); } } private static void HandleClientConnected(ulong clientId) { if (IsServer) { SendBalance(clientId, SharedCurrencyManager.Instance.Balance, SharedCurrencyManager.Instance.Spent); } } private unsafe static void SendBalance(ulong clientId, int balance, int spent) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) if (!IsServer) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(16, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(ref balance, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(ref spent, default(ForPrimitives)); _registeredNetwork.CustomMessagingManager.SendNamedMessage("timo.sharedlgucurrency/v1/balance", clientId, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static bool AcceptNonce(ulong clientId, ulong nonce) { if (nonce == 0L) { return false; } if (!SeenNoncesByClient.TryGetValue(clientId, out HashSet value)) { value = new HashSet(); SeenNoncesByClient[clientId] = value; } if (!value.Add(nonce)) { Plugin.Log.LogWarning((object)$"Rejected replayed request {nonce} from client {clientId}."); return false; } if (value.Count > 2048) { value.Clear(); value.Add(nonce); } return true; } } [BepInPlugin("timo.sharedlgucurrency", "SharedLGUCurrency", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "timo.sharedlgucurrency"; public const string PluginName = "SharedLGUCurrency"; public const string PluginVersion = "1.0.0"; public const string LguGuid = "com.malco.lethalcompany.moreshipupgrades"; public const string MilkMolarsGuid = "Snowlance.MilkMolars"; private Harmony? _harmony; internal static ManualLogSource Log { get; private set; } internal static ModConfig Settings { get; private set; } private void Awake() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Settings = new ModConfig(((BaseUnityPlugin)this).Config); if (!Settings.Enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"SharedLGUCurrency is disabled by config."); return; } SharedCurrencyManager.Initialize(); MilkMolarsPatches.ApplyForcedConfig(); _harmony = new Harmony("timo.sharedlgucurrency"); _harmony.PatchAll(typeof(Plugin).Assembly); GameObject val = new GameObject("SharedLGUCurrency.NetworkBridge"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; val.AddComponent(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"SharedLGUCurrency 1.0.0 loaded for LGU 3.14.1 / MilkMolars 1.0.0."); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static void Verbose(string message) { if (Settings.VerboseLogging.Value) { Log.LogInfo((object)message); } } } internal sealed class SharedCurrencyManager { private readonly object _gate = new object(); private readonly Dictionary _purchaseLocks = new Dictionary(StringComparer.OrdinalIgnoreCase); private int _balance; private int _spent; private bool _hostInitialized; internal static SharedCurrencyManager Instance { get; private set; } internal int Balance { get { lock (_gate) { return _balance; } } } internal int Spent { get { lock (_gate) { return _spent; } } } internal static void Initialize() { Instance = new SharedCurrencyManager(); } internal void BeginNetworkSession(bool isServer) { lock (_gate) { _balance = 0; _spent = 0; _hostInitialized = false; _purchaseLocks.Clear(); } LguReflection.MirrorBalance(0, 0); MilkMolarsPatches.ResetSession(); if (isServer) { Plugin.Log.LogInfo((object)"Crew wallet initialized: 0 PC"); } } internal void HostImportFromLgu(string reason) { if (NetworkSyncManager.IsServer) { int num = Math.Max(0, LguReflection.ReadBalance()); int spent = LguReflection.ReadSpent(); lock (_gate) { _balance = num; _spent = spent; _hostInitialized = true; } MirrorAndBroadcast(); Plugin.Log.LogInfo((object)$"Crew wallet loaded from LGU ({reason}): {num} PC"); } } internal void HostInitializeIfNeeded() { if (!NetworkSyncManager.IsServer) { return; } lock (_gate) { if (_hostInitialized) { return; } _balance = Math.Max(0, LguReflection.ReadBalance()); _spent = LguReflection.ReadSpent(); _hostInitialized = true; } MirrorAndBroadcast(); } internal bool HostApplyDelta(int delta, bool trackSpent, ulong senderClientId, string source, out string rejection) { rejection = string.Empty; if (!NetworkSyncManager.IsServer) { rejection = "Only the host may mutate the crew wallet."; return false; } int value = Plugin.Settings.MaximumExternalDeltaPerRequest.Value; if (value > 0 && Math.Abs((long)delta) > value) { rejection = $"Currency delta {delta} exceeds the configured request limit ({value})."; Plugin.Log.LogWarning((object)$"Rejected currency request from client {senderClientId}: {rejection}"); return false; } lock (_gate) { if (delta < 0 && _balance < -delta) { rejection = "Insufficient shared currency."; return false; } long val = (long)_balance + (long)delta; int maximumBalance = LguReflection.GetMaximumBalance(); _balance = (int)Math.Max(0L, Math.Min(val, maximumBalance)); if (trackSpent) { _spent = SafeInt((long)_spent - (long)delta); } _hostInitialized = true; } MirrorAndBroadcast(); Plugin.Verbose($"Accepted wallet delta {delta:+#;-#;0} PC from client {senderClientId} ({source}). Shared balance: {Balance} PC"); return true; } internal void HostCreditMolar(int amount, ulong playerId, string molarName) { if (amount > 0) { if (HostApplyDelta(amount, trackSpent: false, playerId, molarName, out string rejection)) { Plugin.Log.LogInfo((object)$"Player {playerId} returned {molarName}: +{amount} PC"); Plugin.Log.LogInfo((object)$"Shared balance: {Balance} PC"); } else { Plugin.Log.LogWarning((object)$"Rejected {molarName} reward for player {playerId}: {rejection}"); } } } internal void HostReset(string reason) { if (!NetworkSyncManager.IsServer) { return; } lock (_gate) { if (_balance == 0 && _spent == 0 && _hostInitialized) { return; } _balance = 0; _spent = 0; _hostInitialized = true; _purchaseLocks.Clear(); } MirrorAndBroadcast(); Plugin.Log.LogInfo((object)("Crew wallet reset (" + reason + "): 0 PC")); } internal void ApplyRemoteMirror(int balance, int spent) { lock (_gate) { _balance = Math.Max(0, balance); _spent = spent; } LguReflection.MirrorBalance(Balance, Spent); Plugin.Verbose($"Mirrored shared balance: {Balance} PC"); } internal void HostHandlePurchase(ulong buyerClientId, string upgradeName, int expectedPrice) { if (!NetworkSyncManager.IsServer) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; lock (_gate) { if (_purchaseLocks.TryGetValue(upgradeName, out var value) && value > realtimeSinceStartup) { NetworkSyncManager.SendPurchaseResult(buyerClientId, success: false, "Another purchase for this upgrade is still being committed."); return; } _purchaseLocks[upgradeName] = realtimeSinceStartup + 0.25f; } if (!LguReflection.TryResolvePurchase(upgradeName, expectedPrice, out object node, out int actualCurrencyPrice, out bool increment, out string rejection)) { RejectPurchase(buyerClientId, rejection); return; } int balance; int spent; lock (_gate) { if (_balance < actualCurrencyPrice) { RejectPurchase(buyerClientId, "Insufficient shared currency."); return; } balance = _balance; spent = _spent; _balance -= actualCurrencyPrice; _spent = SafeInt((long)_spent + (long)actualCurrencyPrice); } MirrorAndBroadcast(); try { LguReflection.ApplySharedPurchase(node, increment); string displayName = LguReflection.GetDisplayName(node); Plugin.Log.LogInfo((object)$"Player {buyerClientId} purchased {displayName} for {actualCurrencyPrice} PC"); Plugin.Log.LogInfo((object)$"Shared balance: {Balance} PC"); NetworkSyncManager.SendPurchaseResult(buyerClientId, success: true, $"Purchased {displayName} for {actualCurrencyPrice} PC. Crew balance: {Balance} PC."); } catch (Exception arg) { lock (_gate) { _balance = balance; _spent = spent; } MirrorAndBroadcast(); Plugin.Log.LogError((object)$"LGU purchase failed and was rolled back: {arg}"); RejectPurchase(buyerClientId, "LGU failed to apply the upgrade; the shared currency deduction was rolled back."); } } private void RejectPurchase(ulong buyerClientId, string reason) { Plugin.Log.LogWarning((object)$"Purchase rejected for client {buyerClientId}: {reason}"); NetworkSyncManager.SendPurchaseResult(buyerClientId, success: false, reason); } private void MirrorAndBroadcast() { int balance = Balance; int spent = Spent; LguReflection.MirrorBalance(balance, spent); NetworkSyncManager.BroadcastBalance(balance, spent); } private static int SafeInt(long value) { return (int)Math.Max(-2147483648L, Math.Min(2147483647L, value)); } } }