Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SharedLGUCurrency v1.0.0
SharedLGUCurrency.dll
Decompiled 11 hours agousing 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<MethodBase> 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<ulong> CreditedMolarIds = new HashSet<ulong>(); private static bool _forcedConfigLogged; [ThreadStatic] private static int _activationDepth; internal static bool InMolarActivation => _activationDepth > 0; [HarmonyTargetMethods] private static IEnumerable<MethodBase> 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<MethodBase> 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<bool> Enabled { get; } internal ConfigEntry<bool> ForceSharedContributeAmountToZero { get; } internal ConfigEntry<bool> DisableQuotaCurrency { get; } internal ConfigEntry<bool> ResetSharedCurrencyOnLGUReset { get; } internal ConfigEntry<bool> DisableUnsafePlayerCreditOperations { get; } internal ConfigEntry<bool> VerboseLogging { get; } internal ConfigEntry<int> MaximumExternalDeltaPerRequest { get; } internal ModConfig(ConfigFile config) { Enabled = config.Bind<bool>("General", "Enabled", true, "Enable the shared, host-authoritative LGU currency wallet."); ForceSharedContributeAmountToZero = config.Bind<bool>("MilkMolars", "ForceSharedContributeAmountToZero", true, "Force MilkMolars' Shared Contribute Amount to zero so one molar can never reward every player."); DisableQuotaCurrency = config.Bind<bool>("Currency", "DisableQuotaCurrency", true, "Suppress LGU's automatic quota-to-player-credit payout."); ResetSharedCurrencyOnLGUReset = config.Bind<bool>("Currency", "ResetSharedCurrencyOnLGUReset", true, "Reset the crew wallet whenever LGU resets its CurrencyManager."); DisableUnsafePlayerCreditOperations = config.Bind<bool>("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<bool>("Networking", "VerboseLogging", false, "Log network requests, mirrors and synchronization details."); MaximumExternalDeltaPerRequest = config.Bind<int>("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<ulong> <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<ulong, HashSet<ulong>> SeenNoncesByClient = new Dictionary<ulong, HashSet<ulong>>(); 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<int>(ref delta, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref trackSpent, default(ForPrimitives)); ulong num = ++_nextNonce; ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(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<ulong>(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(upgradeName, false); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(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<bool>(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<int>(ref delta, default(ForPrimitives)); bool trackSpent = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref trackSpent, default(ForPrimitives)); ulong nonce = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(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<ulong>(ref nonce, default(ForPrimitives)); string upgradeName = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref upgradeName, false); int expectedPrice = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(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<int>(ref balance, default(ForPrimitives)); int spent = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(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<bool>(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<int>(ref balance, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(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<ulong> value)) { value = new HashSet<ulong>(); 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<NetworkSyncManager>(); ((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<string, float> _purchaseLocks = new Dictionary<string, float>(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)); } } }