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 ScrapBank v0.0.2
BepInEx/plugins/ScrapBank/ScrapBank.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; 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("ScrapBank")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.0.2.0")] [assembly: AssemblyInformationalVersion("0.0.2")] [assembly: AssemblyProduct("ScrapBank")] [assembly: AssemblyTitle("ScrapBank")] [assembly: AssemblyVersion("0.0.2.0")] 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; } } } namespace GJ.ScrapBank { internal static class BankState { internal static long Balance { get; private set; } internal static long SavedBalance { get; private set; } internal static bool Loaded { get; private set; } internal static bool SaveHealthy { get; private set; } internal static bool RecoveryRequired { get; set; } internal static string LastSaveError { get; private set; } = string.Empty; internal static bool Dirty { get { if (Loaded) { return Balance != SavedBalance; } return false; } } internal static void SetLoaded(long value) { value = ((value < 0) ? 0 : value); Balance = value; SavedBalance = value; Loaded = true; SaveHealthy = true; LastSaveError = string.Empty; } internal static void SetBalanceLocal(long value) { Balance = ((value < 0) ? 0 : value); Loaded = true; } internal static void MarkSaveFailure(string error) { SaveHealthy = false; LastSaveError = error ?? "Unknown save error"; } internal static void ResetLocal() { Balance = 0L; SavedBalance = 0L; Loaded = false; SaveHealthy = false; RecoveryRequired = false; LastSaveError = string.Empty; } } internal static class NetworkBridge { private const string MsgSync = "GJ.ScrapBank.Sync.v4"; private const string MsgWorld = "GJ.ScrapBank.World.v4"; private const string MsgReqSync = "GJ.ScrapBank.ReqSync.v4"; private const string MsgReqStore = "GJ.ScrapBank.ReqStore.v4"; private const string MsgReqSell = "GJ.ScrapBank.ReqSell.v4"; private const string MsgResult = "GJ.ScrapBank.Result.v4"; private static NetworkManager? _registeredOn; internal static void EnsureRegistered() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown NetworkManager singleton = NetworkManager.Singleton; if (!((Object)(object)singleton == (Object)null) && singleton.CustomMessagingManager != null && _registeredOn != singleton) { _registeredOn = singleton; singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.Sync.v4", new HandleNamedMessageDelegate(OnSync)); singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.World.v4", new HandleNamedMessageDelegate(OnWorld)); singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.Result.v4", new HandleNamedMessageDelegate(OnResult)); if (singleton.IsServer) { singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.ReqSync.v4", new HandleNamedMessageDelegate(OnReqSync)); singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.ReqStore.v4", new HandleNamedMessageDelegate(OnReqStore)); singleton.CustomMessagingManager.RegisterNamedMessageHandler("GJ.ScrapBank.ReqSell.v4", new HandleNamedMessageDelegate(OnReqSell)); } else if (singleton.IsClient) { RequestSync(); } } } internal static void Reset() { _registeredOn = null; BankState.ResetLocal(); } internal static void BroadcastBalance(long balance) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer || singleton.CustomMessagingManager == null) { return; } foreach (ulong connectedClientsId in singleton.ConnectedClientsIds) { SendBalance(connectedClientsId, balance); } } internal unsafe static void BroadcastWorldState(long balance, int credits, int quota, int profit) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer || singleton.CustomMessagingManager == null) { return; } FastBufferWriter val = default(FastBufferWriter); foreach (ulong connectedClientsId in singleton.ConnectedClientsIds) { if (connectedClientsId != singleton.LocalClientId) { ((FastBufferWriter)(ref val))..ctor(20, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref balance, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref credits, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref quota, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref profit, default(ForPrimitives)); singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.World.v4", connectedClientsId, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } } } internal static void RequestStoreAll() { RequestStore(0, 0L, 0uL); } internal static void RequestStoreAmount(long amount) { RequestStore(1, amount, 0uL); } internal static void RequestStoreItem(ulong networkObjectId) { RequestStore(2, 0L, networkObjectId); } internal static void RequestStoreItemGroup(ulong representativeNetworkObjectId) { RequestStore(3, 0L, representativeNetworkObjectId); } private unsafe static void RequestStore(int mode, long amount, ulong networkObjectId) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0060: 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) EnsureRegistered(); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsClient || singleton.IsServer) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(20, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref mode, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref amount, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref networkObjectId, default(ForPrimitives)); singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.ReqStore.v4", 0uL, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } internal unsafe static void RequestSell(int mode, long rawAmount) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) EnsureRegistered(); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsClient || singleton.IsServer) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(12, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref mode, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref rawAmount, default(ForPrimitives)); singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.ReqSell.v4", 0uL, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } internal unsafe static void RequestSync() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsClient || singleton.IsServer || singleton.CustomMessagingManager == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(1, (Allocator)2, -1); try { singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.ReqSync.v4", 0uL, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void OnReqSync(ulong senderId, FastBufferReader reader) { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer) { if (!BankState.Loaded) { SaveStore.TryLoadBalance(out long _, out string _); } SendBalance(senderId, BankState.Balance); } } private static void OnReqStore(ulong senderId, FastBufferReader reader) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer) { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); long requestedRaw = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref requestedRaw, default(ForPrimitives)); ulong num2 = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref num2, default(ForPrimitives)); Action<ScrapBankManager.StoreResult> completed = delegate(ScrapBankManager.StoreResult result) { SendStoreResult(senderId, result); }; if (num switch { 1 => ScrapBankManager.BeginStoreAmount(requestedRaw, completed, out string immediateMessage) ? 1 : 0, 2 => ScrapBankManager.BeginStoreItem(num2, completed, out immediateMessage) ? 1 : 0, 3 => ScrapBankManager.BeginStoreItemGroup(num2, completed, out immediateMessage) ? 1 : 0, _ => ScrapBankManager.BeginStoreAll(completed, out immediateMessage) ? 1 : 0, } == 0) { SendStoreResult(senderId, new ScrapBankManager.StoreResult(success: false, 0, 0L, BankState.Balance, immediateMessage)); } } } private static void OnReqSell(ulong senderId, FastBufferReader reader) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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) //IL_0038: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer) { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); long requestedRaw = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref requestedRaw, default(ForPrimitives)); SendSellResult(senderId, num switch { 1 => ScrapBankManager.SellAll(), 2 => ScrapBankManager.SellQuota(), _ => ScrapBankManager.SellRaw(requestedRaw), }); } } private unsafe static void SendBalance(ulong clientId, long balance) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(8, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref balance, default(ForPrimitives)); singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.Sync.v4", clientId, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void SendStoreResult(ulong clientId, ScrapBankManager.StoreResult r) { SendResult(clientId, r.Success ? 1 : (-1), r.ItemCount, r.RawValue, 0, r.NewBalance, 0); } private static void SendSellResult(ulong clientId, ScrapBankManager.SellResult r) { SendResult(clientId, r.Success ? 2 : (-2), 0, r.RawUsed, r.CreditsReceived, r.NewBalance, r.RatePercent); } private unsafe static void SendResult(ulong clientId, int status, int itemCount, long raw, int credits, long balance, int ratePct) { //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) //IL_003b: 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) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: 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: 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_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(32, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref status, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref itemCount, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref raw, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref credits, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref balance, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref ratePct, default(ForPrimitives)); singleton.CustomMessagingManager.SendNamedMessage("GJ.ScrapBank.Result.v4", clientId, val, (NetworkDelivery)2); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void OnSync(ulong senderId, 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) long balanceLocal = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref balanceLocal, default(ForPrimitives)); BankState.SetBalanceLocal(balanceLocal); } private static void OnWorld(ulong senderId, 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) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) long balance = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref balance, default(ForPrimitives)); int credits = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref credits, default(ForPrimitives)); int quota = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref quota, default(ForPrimitives)); int profit = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref profit, default(ForPrimitives)); ScrapBankManager.ApplyClientWorldState(balance, credits, quota, profit); } private static void OnResult(ulong senderId, 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) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); int num2 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives)); long num3 = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num3, default(ForPrimitives)); int num4 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num4, default(ForPrimitives)); long num5 = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num5, default(ForPrimitives)); int num6 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num6, default(ForPrimitives)); BankState.SetBalanceLocal(num5); string text = num switch { 1 => $"Stored {num2} scrap item(s), ${num3:N0}. Scrap Money: ${num5:N0}", -1 => $"STORE failed or recovery is required. Scrap Money: ${num5:N0}", 2 => $"Used ${num3:N0} at {num6}% rate; received ${num4:N0}. Scrap Money: ${num5:N0}", -2 => $"SELL failed. Scrap Money: ${num5:N0}", _ => $"Scrap Money: ${num5:N0}", }; if (num == 1 || num == -1) { TerminalPatch.OnNetworkStoreResult(num == 1, num2, num3, num5, text); } try { if ((Object)(object)HUDManager.Instance != (Object)null) { HUDManager.Instance.DisplayTip("ScrapBank", text, num < 0, false, "GJ_ScrapBank_Result"); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Could not display result: " + ex.Message)); } } } [HarmonyPatch] internal static class Patches { [HarmonyPostfix] [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] private static void PlayerConnected() { NetworkBridge.EnsureRegistered(); if ((Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsServer && BankState.Loaded) { NetworkBridge.BroadcastBalance(BankState.Balance); } } [HarmonyPrefix] [HarmonyPatch(typeof(StartOfRound), "LoadShipGrabbableItems")] private static void BeforeShipItemsLoaded() { ScrapBankManager.RestorePendingRollbackBeforeVanillaLoad(); } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "LoadShipGrabbableItems")] private static void ShipItemsLoaded() { NetworkBridge.EnsureRegistered(); ScrapBankManager.LoadHostBalanceAndRecover(); } [HarmonyPrefix] [HarmonyPatch(typeof(StartOfRound), "SetTimeAndPlanetToSavedSettings")] private static void BeforeSavedSettingsLoaded() { ScrapBankManager.RestorePendingRollbackBeforeVanillaLoad(); } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "SetTimeAndPlanetToSavedSettings")] private static void SavedSettingsLoaded() { NetworkBridge.EnsureRegistered(); if ((Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsServer && !BankState.Loaded) { SaveStore.TryLoadBalance(out long _, out string _); } } [HarmonyPrefix] [HarmonyPatch(typeof(GameNetworkManager), "SaveItemsInShip")] private static bool DisconnectShipSaveGate() { return ScrapBankManager.AllowDisconnectTimeVanillaSave("SaveItemsInShip"); } [HarmonyPrefix] [HarmonyPatch(typeof(GameNetworkManager), "SaveGameValues")] private static bool DisconnectGameSaveGate() { return ScrapBankManager.AllowDisconnectTimeVanillaSave("SaveGameValues"); } [HarmonyPostfix] [HarmonyPatch(typeof(GameNetworkManager), "SaveGame")] private static void VanillaSaveGameCompleted() { ScrapBankManager.OnVanillaSaveGameCompleted(); } [HarmonyPostfix] [HarmonyPatch(typeof(GameNetworkManager), "ResetSavedGameValues")] private static void ResetSavedGameValues() { if ((Object)(object)GameNetworkManager.Instance != (Object)null && GameNetworkManager.Instance.isHostingGame) { SaveStore.ResetSaveData(); } } [HarmonyPrefix] [HarmonyPatch(typeof(StartOfRound), "StartGame")] private static void StartGame() { ScrapBankManager.OnShipLeverStarted(); } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "ShipHasLeft")] private static void ShipHasLeft() { ScrapBankManager.OnShipHasLeft(); } [HarmonyPrefix] [HarmonyPatch(typeof(GameNetworkManager), "StartDisconnect")] private static void DisconnectPrefix() { ScrapBankManager.OnDisconnectStarted(); } [HarmonyPostfix] [HarmonyPatch(typeof(GameNetworkManager), "StartDisconnect")] private static void DisconnectPostfix() { ScrapBankManager.OnDisconnectFinished(); NetworkBridge.Reset(); } } [BepInPlugin("GJ.ScrapBank", "ScrapBank", "0.0.2")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "GJ.ScrapBank"; public const string PluginName = "ScrapBank"; public const string PluginVersion = "0.0.2"; internal static ManualLogSource Log; internal static Plugin Instance; private Harmony? _harmony; private void Awake() { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ScrapBankConfig.Bind(((BaseUnityPlugin)this).Config); _harmony = new Harmony("GJ.ScrapBank"); _harmony.PatchAll(); Log.LogInfo((object)"ScrapBank 0.0.2 loaded."); } } internal static class SaveStore { internal const string BalanceKey = "GJ.ScrapBank.Balance.v2"; private const string LegacyBalanceKey = "GJ.ScrapBank.Balance.v1"; private const string StoreActiveKey = "GJ.ScrapBank.Store.Active.v2"; private const string StoreTxKey = "GJ.ScrapBank.Store.Tx.v2"; private const string StoreOldBalanceKey = "GJ.ScrapBank.Store.OldBalance.v2"; private const string StoreRecordsKey = "GJ.ScrapBank.Store.Records.v2"; private const string StoreCommitKey = "GJ.ScrapBank.Store.Commit.v2"; private const string SaleActiveKey = "GJ.ScrapBank.Sale.Active.v2"; private const string SalePayloadKey = "GJ.ScrapBank.Sale.Payload.v2"; private const string SaleCommitKey = "GJ.ScrapBank.Sale.Commit.v2"; private const string CheckpointActiveKey = "GJ.ScrapBank.Checkpoint.Active.v1"; private const string CheckpointBalanceKey = "GJ.ScrapBank.Checkpoint.Balance.v1"; private const string RollbackSuffix = ".scrapbank.rollback"; private const string CommitSuffix = ".scrapbank.commit"; private const string CommitActiveKey = "Active"; private const string CommitBalanceKey = "Balance"; private static string FileName { get { if ((Object)(object)GameNetworkManager.Instance == (Object)null || string.IsNullOrWhiteSpace(GameNetworkManager.Instance.currentSaveFileName)) { throw new InvalidOperationException("Current Lethal Company save file is not available."); } return GameNetworkManager.Instance.currentSaveFileName; } } private static string RollbackFileName => FileName + ".scrapbank.rollback"; private static string CommitFileName => FileName + ".scrapbank.commit"; internal static bool HasVanillaRollbackSnapshot() { try { if ((Object)(object)GameNetworkManager.Instance == (Object)null || string.IsNullOrWhiteSpace(GameNetworkManager.Instance.currentSaveFileName)) { return false; } return ES3.FileExists(RollbackFileName); } catch { return false; } } internal static bool TryCreateVanillaRollbackSnapshot(out string error) { error = string.Empty; try { string fileName = FileName; string rollbackFileName = RollbackFileName; string commitFileName = CommitFileName; if (ES3.FileExists(rollbackFileName)) { return true; } if (!ES3.FileExists(fileName)) { throw new InvalidOperationException("The current vanilla save file does not exist yet."); } if (ES3.FileExists(commitFileName)) { ES3.DeleteFile(commitFileName); } ES3.CopyFile(fileName, rollbackFileName); if (!ES3.FileExists(rollbackFileName)) { throw new InvalidOperationException("Rollback snapshot copy was not created."); } Plugin.Log.LogInfo((object)("[Save] Captured last vanilla save snapshot before ScrapBank transaction: " + fileName)); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; Plugin.Log.LogError((object)$"Could not create vanilla rollback snapshot: {ex}"); return false; } } internal static bool TryCommitAtVanillaSave(long balance, out string error) { error = string.Empty; balance = Math.Max(0L, balance); try { string fileName = FileName; string rollbackFileName = RollbackFileName; string commitFileName = CommitFileName; if (ES3.FileExists(commitFileName)) { ES3.DeleteFile(commitFileName); } ES3.Save<long>("Balance", balance, commitFileName); ES3.Save<bool>("Active", true, commitFileName); ES3.Save<long>("GJ.ScrapBank.Balance.v2", balance, fileName); long num = ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, long.MinValue); if (num != balance) { throw new InvalidOperationException($"Balance verification failed. wrote={balance}, read={num}"); } if (ES3.FileExists(rollbackFileName)) { ES3.DeleteFile(rollbackFileName); } if (ES3.FileExists(commitFileName)) { ES3.DeleteFile(commitFileName); } BankState.SetLoaded(balance); Plugin.Log.LogInfo((object)$"[Save] ScrapBank committed with vanilla SaveGame: ${balance:N0}."); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Failed committing ScrapBank with vanilla SaveGame: {ex}"); return false; } } internal static bool TryRestoreVanillaRollbackSnapshot(out string error) { error = string.Empty; try { string fileName = FileName; string rollbackFileName = RollbackFileName; string commitFileName = CommitFileName; if (!ES3.FileExists(rollbackFileName)) { return true; } if (ES3.FileExists(commitFileName) && ES3.Load<bool>("Active", commitFileName, false)) { long num = Math.Max(0L, ES3.Load<long>("Balance", commitFileName, 0L)); ES3.Save<long>("GJ.ScrapBank.Balance.v2", num, fileName); if (ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, long.MinValue) != num) { throw new InvalidOperationException("Could not finish interrupted vanilla-save bank commit."); } ES3.DeleteFile(rollbackFileName); ES3.DeleteFile(commitFileName); Plugin.Log.LogWarning((object)$"[Save] Finished interrupted ScrapBank vanilla-save commit: ${num:N0}."); return true; } if (ES3.FileExists(fileName)) { ES3.DeleteFile(fileName); } ES3.CopyFile(rollbackFileName, fileName); if (!ES3.FileExists(fileName)) { throw new InvalidOperationException("Rollback snapshot could not be restored."); } ES3.DeleteFile(rollbackFileName); if (ES3.FileExists(commitFileName)) { ES3.DeleteFile(commitFileName); } Plugin.Log.LogWarning((object)"[Save] Restored last vanilla save because ScrapBank changes were never autosaved."); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; Plugin.Log.LogError((object)$"Could not restore vanilla rollback snapshot: {ex}"); return false; } } internal static void ClearVanillaRollbackFiles() { try { string rollbackFileName = RollbackFileName; string commitFileName = CommitFileName; if (ES3.FileExists(rollbackFileName)) { ES3.DeleteFile(rollbackFileName); } if (ES3.FileExists(commitFileName)) { ES3.DeleteFile(commitFileName); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not clear ScrapBank rollback files: " + ex.Message)); } } internal static bool TryLoadBalance(out long balance, out string error) { balance = 0L; error = string.Empty; try { string fileName = FileName; if (!TryRestoreVanillaRollbackSnapshot(out string error2)) { throw new InvalidOperationException("Rollback recovery failed: " + error2); } if (ES3.Load<bool>("GJ.ScrapBank.Checkpoint.Active.v1", fileName, false)) { long num = Math.Max(0L, ES3.Load<long>("GJ.ScrapBank.Checkpoint.Balance.v1", fileName, 0L)); ES3.Save<long>("GJ.ScrapBank.Balance.v2", num, fileName); long num2 = ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, long.MinValue); if (num2 != num) { throw new InvalidOperationException($"Checkpoint recovery verification failed. wrote={num}, read={num2}"); } DeleteKeys("GJ.ScrapBank.Checkpoint.Active.v1", "GJ.ScrapBank.Checkpoint.Balance.v1"); Plugin.Log.LogWarning((object)$"Recovered ScrapBank checkpoint balance: ${num:N0}."); } if (ES3.KeyExists("GJ.ScrapBank.Balance.v2", fileName)) { balance = Math.Max(0L, ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, 0L)); } else if (ES3.KeyExists("GJ.ScrapBank.Balance.v1", fileName)) { balance = Math.Max(0L, ES3.Load<long>("GJ.ScrapBank.Balance.v1", fileName, 0L)); ES3.Save<long>("GJ.ScrapBank.Balance.v2", balance, fileName); } BankState.SetLoaded(balance); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Failed loading ScrapBank balance: {ex}"); return false; } } internal static bool TrySaveBalance(long balance, out string error) { error = string.Empty; balance = Math.Max(0L, balance); try { string fileName = FileName; ES3.Save<long>("GJ.ScrapBank.Balance.v2", balance, fileName); long num = ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, long.MinValue); if (num != balance) { throw new InvalidOperationException($"Balance verification failed. wrote={balance}, read={num}"); } BankState.SetLoaded(balance); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Failed saving ScrapBank balance: {ex}"); return false; } } internal static bool TryCommitVanillaCheckpoint(long balance, out string error) { error = string.Empty; balance = Math.Max(0L, balance); try { string fileName = FileName; ES3.Save<long>("GJ.ScrapBank.Checkpoint.Balance.v1", balance, fileName); ES3.Save<bool>("GJ.ScrapBank.Checkpoint.Active.v1", true, fileName); if (!ES3.Load<bool>("GJ.ScrapBank.Checkpoint.Active.v1", fileName, false) || ES3.Load<long>("GJ.ScrapBank.Checkpoint.Balance.v1", fileName, long.MinValue) != balance) { throw new InvalidOperationException("Checkpoint journal verification failed."); } ES3.Save<long>("GJ.ScrapBank.Balance.v2", balance, fileName); long num = ES3.Load<long>("GJ.ScrapBank.Balance.v2", fileName, long.MinValue); if (num != balance) { throw new InvalidOperationException($"Checkpoint balance verification failed. wrote={balance}, read={num}"); } DeleteKeys("GJ.ScrapBank.Checkpoint.Active.v1", "GJ.ScrapBank.Checkpoint.Balance.v1"); BankState.SetLoaded(balance); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Failed committing ScrapBank at vanilla checkpoint: {ex}"); return false; } } internal static void BackupIfEnabled() { if (!ScrapBankConfig.BackupBeforeStore.Value) { return; } try { string fileName = FileName; if (ES3.FileExists(fileName)) { ES3.CreateBackup(fileName); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not create save backup: " + ex.Message)); } } internal static bool TryBeginStore(long oldBalance, string[] records, out string txId, out string error) { txId = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); error = string.Empty; try { string fileName = FileName; ES3.Save<string>("GJ.ScrapBank.Store.Tx.v2", txId, fileName); ES3.Save<long>("GJ.ScrapBank.Store.OldBalance.v2", oldBalance, fileName); ES3.Save<string[]>("GJ.ScrapBank.Store.Records.v2", records ?? Array.Empty<string>(), fileName); ES3.Save<bool>("GJ.ScrapBank.Store.Active.v2", true, fileName); bool num = ES3.Load<bool>("GJ.ScrapBank.Store.Active.v2", fileName, false); string text = ES3.Load<string>("GJ.ScrapBank.Store.Tx.v2", fileName, string.Empty); string[] array = ES3.Load<string[]>("GJ.ScrapBank.Store.Records.v2", fileName, Array.Empty<string>()); if (!num || text != txId || array.Length != ((records != null) ? records.Length : 0)) { throw new InvalidOperationException("STORE journal verification failed."); } return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Could not create STORE journal: {ex}"); return false; } } internal static bool TryReplaceStoreRecords(string txId, string[] records, out string error) { error = string.Empty; try { string fileName = FileName; if (!ES3.Load<bool>("GJ.ScrapBank.Store.Active.v2", fileName, false) || ES3.Load<string>("GJ.ScrapBank.Store.Tx.v2", fileName, string.Empty) != txId) { throw new InvalidOperationException("STORE journal changed while transaction was running."); } ES3.Save<string[]>("GJ.ScrapBank.Store.Records.v2", records ?? Array.Empty<string>(), fileName); if (!ES3.Load<string[]>("GJ.ScrapBank.Store.Records.v2", fileName, Array.Empty<string>()).SequenceEqual(records ?? Array.Empty<string>())) { throw new InvalidOperationException("STORE removed-item journal verification failed."); } return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Could not update STORE journal: {ex}"); return false; } } internal static bool TryCommitStore(string txId, long newBalance, out string error) { error = string.Empty; try { string fileName = FileName; string text = MakeCommit(txId, newBalance); ES3.Save<string>("GJ.ScrapBank.Store.Commit.v2", text, fileName); if (ES3.Load<string>("GJ.ScrapBank.Store.Commit.v2", fileName, string.Empty) != text) { throw new InvalidOperationException("STORE commit-marker verification failed."); } if (!TrySaveBalance(newBalance, out error)) { return false; } return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Could not commit STORE: {ex}"); return false; } } internal static bool TryGetStorePending(out StorePending pending) { pending = default(StorePending); try { string fileName = FileName; if (!ES3.Load<bool>("GJ.ScrapBank.Store.Active.v2", fileName, false)) { return false; } string text = ES3.Load<string>("GJ.ScrapBank.Store.Tx.v2", fileName, string.Empty); if (string.IsNullOrWhiteSpace(text)) { return false; } long oldBalance = ES3.Load<long>("GJ.ScrapBank.Store.OldBalance.v2", fileName, 0L); string[] records = ES3.Load<string[]>("GJ.ScrapBank.Store.Records.v2", fileName, Array.Empty<string>()); string commit = ES3.Load<string>("GJ.ScrapBank.Store.Commit.v2", fileName, string.Empty); pending = new StorePending(text, oldBalance, records, commit); return true; } catch (Exception ex) { Plugin.Log.LogError((object)$"Could not read STORE journal: {ex}"); BankState.MarkSaveFailure(ex.GetBaseException().Message); return false; } } internal static void ClearStorePending() { DeleteKeys("GJ.ScrapBank.Store.Active.v2", "GJ.ScrapBank.Store.Tx.v2", "GJ.ScrapBank.Store.OldBalance.v2", "GJ.ScrapBank.Store.Records.v2", "GJ.ScrapBank.Store.Commit.v2"); } internal static bool TryBeginSale(SalePending sale, out string error) { error = string.Empty; try { string fileName = FileName; string text = sale.Serialize(); ES3.Save<string>("GJ.ScrapBank.Sale.Payload.v2", text, fileName); ES3.Save<bool>("GJ.ScrapBank.Sale.Active.v2", true, fileName); if (!ES3.Load<bool>("GJ.ScrapBank.Sale.Active.v2", fileName, false) || ES3.Load<string>("GJ.ScrapBank.Sale.Payload.v2", fileName, string.Empty) != text) { throw new InvalidOperationException("SALE journal verification failed."); } return true; } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Could not create SALE journal: {ex}"); return false; } } internal static bool TryCommitSaleBank(SalePending sale, out string error) { error = string.Empty; try { string fileName = FileName; string text = MakeCommit(sale.TxId, sale.NewBalance); ES3.Save<string>("GJ.ScrapBank.Sale.Commit.v2", text, fileName); if (ES3.Load<string>("GJ.ScrapBank.Sale.Commit.v2", fileName, string.Empty) != text) { throw new InvalidOperationException("SALE commit-marker verification failed."); } return TrySaveBalance(sale.NewBalance, out error); } catch (Exception ex) { error = ex.GetBaseException().Message; BankState.MarkSaveFailure(error); Plugin.Log.LogError((object)$"Could not commit SALE bank debit: {ex}"); return false; } } internal static bool TryGetSalePending(out SalePending sale, out bool committed) { sale = default(SalePending); committed = false; try { string fileName = FileName; if (!ES3.Load<bool>("GJ.ScrapBank.Sale.Active.v2", fileName, false)) { return false; } if (!SalePending.TryParse(ES3.Load<string>("GJ.ScrapBank.Sale.Payload.v2", fileName, string.Empty), out sale)) { throw new InvalidOperationException("SALE journal payload is invalid."); } committed = ES3.Load<string>("GJ.ScrapBank.Sale.Commit.v2", fileName, string.Empty) == MakeCommit(sale.TxId, sale.NewBalance); return true; } catch (Exception ex) { Plugin.Log.LogError((object)$"Could not read SALE journal: {ex}"); BankState.MarkSaveFailure(ex.GetBaseException().Message); return false; } } internal static void ClearSalePending() { DeleteKeys("GJ.ScrapBank.Sale.Active.v2", "GJ.ScrapBank.Sale.Payload.v2", "GJ.ScrapBank.Sale.Commit.v2"); } internal static void ResetSaveData() { DeleteKeys("GJ.ScrapBank.Balance.v2", "GJ.ScrapBank.Balance.v1", "GJ.ScrapBank.Store.Active.v2", "GJ.ScrapBank.Store.Tx.v2", "GJ.ScrapBank.Store.OldBalance.v2", "GJ.ScrapBank.Store.Records.v2", "GJ.ScrapBank.Store.Commit.v2", "GJ.ScrapBank.Sale.Active.v2", "GJ.ScrapBank.Sale.Payload.v2", "GJ.ScrapBank.Sale.Commit.v2", "GJ.ScrapBank.Checkpoint.Active.v1", "GJ.ScrapBank.Checkpoint.Balance.v1"); ClearVanillaRollbackFiles(); BankState.ResetLocal(); } internal static string MakeItemRecord(string name, int value) { string text = Convert.ToBase64String(Encoding.UTF8.GetBytes(name ?? "Unknown")); return Math.Max(0, value).ToString(CultureInfo.InvariantCulture) + ":" + text; } internal static bool TryParseItemRecord(string record, out string name, out int value) { name = "Unknown"; value = 0; if (string.IsNullOrWhiteSpace(record)) { return false; } int num = record.IndexOf(':'); if (num <= 0 || !int.TryParse(record.Substring(0, num), NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) { return false; } try { name = Encoding.UTF8.GetString(Convert.FromBase64String(record.Substring(num + 1))); value = Math.Max(0, value); return true; } catch { return false; } } internal static bool TryReadCommit(string commit, string txId, out long balance) { balance = 0L; if (string.IsNullOrWhiteSpace(commit)) { return false; } int num = commit.IndexOf('|'); if (num <= 0 || commit.Substring(0, num) != txId) { return false; } return long.TryParse(commit.Substring(num + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out balance); } private static string MakeCommit(string txId, long balance) { return txId + "|" + balance.ToString(CultureInfo.InvariantCulture); } private static void DeleteKeys(params string[] keys) { try { string fileName = FileName; foreach (string text in keys) { if (ES3.KeyExists(text, fileName)) { ES3.DeleteKey(text, fileName); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not clear ScrapBank save keys: " + ex.Message)); } } } internal readonly struct StorePending { internal readonly string TxId; internal readonly long OldBalance; internal readonly string[] Records; internal readonly string Commit; internal StorePending(string txId, long oldBalance, string[] records, string commit) { TxId = txId; OldBalance = oldBalance; Records = records ?? Array.Empty<string>(); Commit = commit ?? string.Empty; } } internal readonly struct SalePending { internal readonly string TxId; internal readonly long OldBalance; internal readonly long NewBalance; internal readonly int OldCredits; internal readonly int NewCredits; internal readonly int OldQuota; internal readonly int NewQuota; internal readonly int Profit; internal SalePending(string txId, long oldBalance, long newBalance, int oldCredits, int newCredits, int oldQuota, int newQuota, int profit) { TxId = txId; OldBalance = oldBalance; NewBalance = newBalance; OldCredits = oldCredits; NewCredits = newCredits; OldQuota = oldQuota; NewQuota = newQuota; Profit = profit; } internal string Serialize() { return string.Join("|", TxId, OldBalance, NewBalance, OldCredits, NewCredits, OldQuota, NewQuota, Profit); } internal static bool TryParse(string payload, out SalePending sale) { sale = default(SalePending); string[] array = (payload ?? string.Empty).Split('|'); if (array.Length != 8 || string.IsNullOrWhiteSpace(array[0])) { return false; } if (!long.TryParse(array[1], out var result) || !long.TryParse(array[2], out var result2) || !int.TryParse(array[3], out var result3) || !int.TryParse(array[4], out var result4) || !int.TryParse(array[5], out var result5) || !int.TryParse(array[6], out var result6) || !int.TryParse(array[7], out var result7)) { return false; } sale = new SalePending(array[0], result, result2, result3, result4, result5, result6, result7); return true; } } internal static class ScrapBankConfig { internal static ConfigEntry<bool> BackupBeforeStore; internal static ConfigEntry<bool> IncludeZeroValueScrap; internal static ConfigEntry<bool> AllowHeldScrap; internal static ConfigEntry<int> DespawnBatchSize; internal static ConfigEntry<string> ExcludedItemNames; internal static ConfigEntry<string> ScrapAllBlacklist; internal static ConfigEntry<bool> EnableSellAlias; internal static void Bind(ConfigFile config) { BackupBeforeStore = config.Bind<bool>("Safety", "BackupBeforeStore", true, "Create an ES3 backup of the current Lethal Company save before SCRAPSTORE."); IncludeZeroValueScrap = config.Bind<bool>("Storage", "IncludeZeroValueScrap", false, "If true, zero-value scrap is removed too. It adds no Scrap Money."); AllowHeldScrap = config.Bind<bool>("Storage", "AllowHeldScrap", false, "Unsafe. If false, scrap held or pocketed by a player is never stored."); DespawnBatchSize = config.Bind<int>("Storage", "DespawnBatchSize", 40, "How many scrap NetworkObjects the host despawns per frame. Lower values reduce one-frame spikes for very large piles."); ExcludedItemNames = config.Bind<string>("Storage", "ExcludedItemNames", string.Empty, "Comma-separated item names to never store by any ScrapBank command. Matches Item.itemName or prefab object name, case-insensitive."); ScrapAllBlacklist = config.Bind<string>("Storage", "ScrapAllBlacklist", "Key, Gift, Kitchen knife, Shotgun, Whoopie cushion", "Comma-separated item names skipped only by SCRAP ALL. Blacklisted items stay in the ship, remain visible in SCRAP LIST, and can still be stored manually. Host value is authoritative in multiplayer. Matches Item.itemName or prefab object name, case-insensitive."); string text = (ScrapAllBlacklist.Value ?? string.Empty).Trim(); if (text.Length == 0 || string.Equals(text, "Apparatus, Bee hive, Fancy lamp", StringComparison.OrdinalIgnoreCase)) { ScrapAllBlacklist.Value = "Key, Gift, Kitchen knife, Shotgun, Whoopie cushion"; config.Save(); } EnableSellAlias = config.Bind<bool>("Terminal", "EnableSellAlias", false, "If true, SELL 2000 / SELL QUOTA / SELL ALL are handled by ScrapBank. Leave false if another sell-command mod is installed."); } } internal static class ScrapBankManager { internal readonly struct StoreResult { internal readonly bool Success; internal readonly int ItemCount; internal readonly long RawValue; internal readonly long NewBalance; internal readonly string Message; internal StoreResult(bool success, int count, long value, long balance, string message) { Success = success; ItemCount = count; RawValue = value; NewBalance = balance; Message = message; } } internal readonly struct SellResult { internal readonly bool Success; internal readonly long RawUsed; internal readonly int CreditsReceived; internal readonly long NewBalance; internal readonly int RatePercent; internal readonly string Message; internal SellResult(bool success, long raw, int credits, long balance, int rate, string message) { Success = success; RawUsed = raw; CreditsReceived = credits; NewBalance = balance; RatePercent = rate; Message = message; } } internal readonly struct ScrapItemChoice { internal readonly ulong NetworkObjectId; internal readonly int Value; internal ScrapItemChoice(ulong networkObjectId, int value) { NetworkObjectId = networkObjectId; Value = value; } } private sealed class ScrapDisplayGroup { internal readonly string Name; internal readonly List<GrabbableObject> Items; internal readonly long TotalValue; internal readonly int MinValue; internal readonly int MaxValue; internal ScrapDisplayGroup(string name, List<GrabbableObject> items) { Name = name; Items = items; TotalValue = ((IEnumerable<GrabbableObject>)items).Sum((Func<GrabbableObject, long>)((GrabbableObject g) => Math.Max(0, g.scrapValue))); MinValue = ((items.Count != 0) ? items.Min((GrabbableObject g) => Math.Max(0, g.scrapValue)) : 0); MaxValue = ((items.Count != 0) ? items.Max((GrabbableObject g) => Math.Max(0, g.scrapValue)) : 0); } } private static bool _companySaleWindow; private static bool _storeRunning; private static bool _sellRunning; private static bool _pendingTransactionSession; private static bool _disconnecting; internal const int ShipListPageSize = 10; internal static bool IsServer { get { if ((Object)(object)NetworkManager.Singleton != (Object)null) { return NetworkManager.Singleton.IsServer; } return false; } } internal static bool IsBusy { get { if (!_storeRunning) { return _sellRunning; } return true; } } private static bool TryStartRoutine(IEnumerator routine, string purpose) { if (routine == null) { Plugin.Log.LogError((object)("Could not start ScrapBank coroutine (" + purpose + "): routine was null.")); return false; } MonoBehaviour[] array = (MonoBehaviour[])(object)new MonoBehaviour[3] { (MonoBehaviour)GameNetworkManager.Instance, (MonoBehaviour)StartOfRound.Instance, (MonoBehaviour)Plugin.Instance }; foreach (MonoBehaviour val in array) { if ((Object)(object)val == (Object)null) { continue; } try { if (!((Behaviour)val).isActiveAndEnabled || !((Component)val).gameObject.activeInHierarchy) { continue; } val.StartCoroutine(routine); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Coroutine host " + ((object)val).GetType().Name + " failed for " + purpose + ": " + ex.GetBaseException().Message)); } } Plugin.Log.LogError((object)("Could not start ScrapBank coroutine (" + purpose + "): no active coroutine host was available.")); return false; } internal static bool BeginStoreAll(Action<StoreResult> completed, out string immediateMessage) { List<GrabbableObject> displayScrapSnapshot = GetDisplayScrapSnapshot(); List<GrabbableObject> list = displayScrapSnapshot.Where((GrabbableObject g) => !IsScrapAllBlacklisted(g)).ToList(); int num = displayScrapSnapshot.Count - list.Count; if (num > 0) { string arg = string.Join(", ", displayScrapSnapshot.Where(IsScrapAllBlacklisted).Select(GetStableName).Distinct<string>(StringComparer.OrdinalIgnoreCase)); Plugin.Log.LogInfo((object)$"[SCRAP ALL] Blacklist skipped {num} item(s): {arg}"); } if (list.Count == 0 && num > 0) { immediateMessage = $"SCRAP ALL found no items to convert.\nBlacklisted items left in ship: {num}"; return false; } return BeginStoreTargets(list, completed, out immediateMessage, "ALL"); } internal static bool BeginStoreAmount(long requestedRaw, Action<StoreResult> completed, out string immediateMessage) { immediateMessage = string.Empty; if (requestedRaw <= 0) { immediateMessage = "Store amount must be greater than 0."; return false; } List<GrabbableObject> displayScrapSnapshot = GetDisplayScrapSnapshot(); if (displayScrapSnapshot.Count == 0) { immediateMessage = "No eligible scrap found inside the ship."; return false; } long selectedRaw; List<GrabbableObject> list = SelectTargetsForAmount(displayScrapSnapshot, requestedRaw, out selectedRaw); if (list.Count == 0 || selectedRaw <= 0) { immediateMessage = $"No scrap combination at or below ${requestedRaw:N0}.\nUse: SHIP SCRAP LIST"; return false; } return BeginStoreTargets(list, completed, out immediateMessage, $"${selectedRaw:N0}"); } internal static bool BeginStoreItem(ulong networkObjectId, Action<StoreResult> completed, out string immediateMessage) { immediateMessage = string.Empty; GrabbableObject val = ((IEnumerable<GrabbableObject>)GetDisplayScrapSnapshot()).FirstOrDefault((Func<GrabbableObject, bool>)((GrabbableObject g) => (Object)(object)((NetworkBehaviour)g).NetworkObject != (Object)null && ((NetworkBehaviour)g).NetworkObject.NetworkObjectId == networkObjectId)); if ((Object)(object)val == (Object)null) { immediateMessage = "That scrap item is no longer available.\nUse: SHIP SCRAP LIST"; return false; } return BeginStoreTargets(new List<GrabbableObject> { val }, completed, out immediateMessage, GetStableName(val)); } internal static bool BeginStoreItemGroup(ulong representativeNetworkObjectId, Action<StoreResult> completed, out string immediateMessage) { immediateMessage = string.Empty; List<GrabbableObject> displayScrapSnapshot = GetDisplayScrapSnapshot(); GrabbableObject val = ((IEnumerable<GrabbableObject>)displayScrapSnapshot).FirstOrDefault((Func<GrabbableObject, bool>)((GrabbableObject g) => (Object)(object)((NetworkBehaviour)g).NetworkObject != (Object)null && ((NetworkBehaviour)g).NetworkObject.NetworkObjectId == representativeNetworkObjectId)); if ((Object)(object)val == (Object)null) { immediateMessage = "That scrap group is no longer available.\nUse: SCRAP LIST"; return false; } string groupName = GetStableName(val); List<GrabbableObject> list = displayScrapSnapshot.Where((GrabbableObject g) => string.Equals(GetStableName(g), groupName, StringComparison.OrdinalIgnoreCase)).ToList(); if (list.Count == 0) { immediateMessage = "That scrap group is no longer available.\nUse: SCRAP LIST"; return false; } return BeginStoreTargets(list, completed, out immediateMessage, $"{groupName} x{list.Count}"); } private static bool BeginStoreTargets(List<GrabbableObject> targets, Action<StoreResult> completed, out string immediateMessage, string label) { immediateMessage = string.Empty; if (!IsServer) { immediateMessage = "Only the host/server can store scrap."; return false; } if (_storeRunning || _sellRunning) { immediateMessage = "ScrapBank is already processing another transaction."; return false; } if (BankState.RecoveryRequired) { immediateMessage = "ScrapBank recovery is required. Rejoin/restart this save before making another transaction."; return false; } if (!EnsureSaveReady(out string error)) { immediateMessage = "ScrapBank save data is unavailable: " + error; return false; } targets = targets.Where((GrabbableObject g) => (Object)(object)g != (Object)null && IsEligibleShipScrap(g)).Distinct().ToList(); if (targets.Count == 0) { immediateMessage = "No eligible scrap found inside the ship."; return false; } bool openedPendingSession = !_pendingTransactionSession; if (!EnsureRollbackBaseline(out string error2)) { immediateMessage = "Could not prepare a rollback checkpoint: " + error2; return false; } long num = ((IEnumerable<GrabbableObject>)targets).Sum((Func<GrabbableObject, long>)((GrabbableObject g) => Math.Max(0, g.scrapValue))); _storeRunning = true; if (!TryStartRoutine(StoreRoutine(targets, completed, openedPendingSession), "store transaction")) { _storeRunning = false; immediateMessage = "Could not start the ScrapBank store routine."; return false; } immediateMessage = $"STORE STARTED\nItems: {targets.Count}\nValue: ${num:N0}\nPlease wait..."; return true; } internal static string BuildShipScrapList(int page) { List<ScrapDisplayGroup> displayScrapGroups = GetDisplayScrapGroups(); if (displayScrapGroups.Count == 0) { return "=== SHIP SCRAP ===\nNo eligible scrap in ship."; } int num = displayScrapGroups.Sum((ScrapDisplayGroup g) => g.Items.Count); int num2 = 10; int num3 = Math.Max(1, (displayScrapGroups.Count + num2 - 1) / num2); page = Mathf.Clamp(page, 1, num3); int num4 = (page - 1) * num2; int num5 = Math.Min(num4 + num2, displayScrapGroups.Count); long num6 = displayScrapGroups.Sum((ScrapDisplayGroup g) => g.TotalValue); List<string> list = new List<string> { $"=== SHIP SCRAP {page}/{num3} ===", $"Items: {num} Types: {displayScrapGroups.Count} Value: ${num6:N0}", string.Empty }; for (int num7 = num4; num7 < num5; num7++) { int num8 = num7 - num4 + 1; ScrapDisplayGroup scrapDisplayGroup = displayScrapGroups[num7]; string text = ((scrapDisplayGroup.Items.Count > 1) ? $" x{scrapDisplayGroup.Items.Count}" : string.Empty); string arg = ShortName(scrapDisplayGroup.Name + text, 22); string arg2 = $"${scrapDisplayGroup.TotalValue:N0}"; list.Add($"{num8,2}. {arg,-22} {arg2}"); } list.Add(string.Empty); list.Add("Type item number + ENTER"); list.Add("번호 입력 후 ENTER"); if (num3 > 1) { list.Add("Page: SCRAP LIST <page>"); } return string.Join("\n", list); } internal static bool TryGetDisplayedGroupItems(ulong representativeNetworkObjectId, out string groupName, out List<ScrapItemChoice> choices) { groupName = string.Empty; choices = new List<ScrapItemChoice>(); List<GrabbableObject> displayScrapSnapshot = GetDisplayScrapSnapshot(); GrabbableObject val = ((IEnumerable<GrabbableObject>)displayScrapSnapshot).FirstOrDefault((Func<GrabbableObject, bool>)((GrabbableObject g) => (Object)(object)((NetworkBehaviour)g).NetworkObject != (Object)null && ((NetworkBehaviour)g).NetworkObject.IsSpawned && ((NetworkBehaviour)g).NetworkObject.NetworkObjectId == representativeNetworkObjectId)); if ((Object)(object)val == (Object)null) { return false; } groupName = GetStableName(val); string stableGroupName = groupName; foreach (GrabbableObject item in from g in displayScrapSnapshot where string.Equals(GetStableName(g), stableGroupName, StringComparison.OrdinalIgnoreCase) orderby Math.Max(0, g.scrapValue), (!((Object)(object)((NetworkBehaviour)g).NetworkObject != (Object)null)) ? ulong.MaxValue : ((NetworkBehaviour)g).NetworkObject.NetworkObjectId select g) { NetworkObject networkObject = ((NetworkBehaviour)item).NetworkObject; if (!((Object)(object)networkObject == (Object)null) && networkObject.IsSpawned) { choices.Add(new ScrapItemChoice(networkObject.NetworkObjectId, Math.Max(0, item.scrapValue))); } } return choices.Count > 0; } internal static int ClampShipScrapListPage(int page) { int count = GetDisplayScrapGroups().Count; int num = Math.Max(1, (count + 10 - 1) / 10); return Mathf.Clamp(page, 1, num); } internal static bool TryGetDisplayedGroup(int oneBasedIndex, out ulong representativeNetworkObjectId, out string groupName, out int itemCount, out long totalValue, out string valueSummary) { representativeNetworkObjectId = 0uL; groupName = string.Empty; itemCount = 0; totalValue = 0L; valueSummary = string.Empty; List<ScrapDisplayGroup> displayScrapGroups = GetDisplayScrapGroups(); int num = oneBasedIndex - 1; if (num < 0 || num >= displayScrapGroups.Count) { return false; } ScrapDisplayGroup scrapDisplayGroup = displayScrapGroups[num]; GrabbableObject val = scrapDisplayGroup.Items[0]; if ((Object)(object)((NetworkBehaviour)val).NetworkObject == (Object)null || !((NetworkBehaviour)val).NetworkObject.IsSpawned) { return false; } representativeNetworkObjectId = ((NetworkBehaviour)val).NetworkObject.NetworkObjectId; groupName = scrapDisplayGroup.Name; itemCount = scrapDisplayGroup.Items.Count; totalValue = scrapDisplayGroup.TotalValue; if (scrapDisplayGroup.Items.Count == 1) { valueSummary = $"${Math.Max(0, val.scrapValue):N0}"; } else if (scrapDisplayGroup.MinValue == scrapDisplayGroup.MaxValue) { valueSummary = $"${scrapDisplayGroup.MinValue:N0} each / ${scrapDisplayGroup.TotalValue:N0} total"; } else if (scrapDisplayGroup.Items.Count <= 6) { valueSummary = string.Join(" + ", scrapDisplayGroup.Items.Select((GrabbableObject g) => $"${Math.Max(0, g.scrapValue):N0}")) + $" = ${scrapDisplayGroup.TotalValue:N0}"; } else { valueSummary = $"${scrapDisplayGroup.MinValue:N0}~${scrapDisplayGroup.MaxValue:N0} each / ${scrapDisplayGroup.TotalValue:N0} total"; } return true; } private static List<ScrapDisplayGroup> GetDisplayScrapGroups() { return (from @group in GetDisplayScrapSnapshot().GroupBy<GrabbableObject, string>((GrabbableObject g) => GetStableName(g), StringComparer.OrdinalIgnoreCase) select new ScrapDisplayGroup(@group.Key, @group.ToList()) into @group orderby @group.TotalValue, @group.MinValue select @group).ThenBy<ScrapDisplayGroup, string>((ScrapDisplayGroup group) => group.Name, StringComparer.OrdinalIgnoreCase).ToList(); } private static List<GrabbableObject> GetDisplayScrapSnapshot() { return Object.FindObjectsOfType<GrabbableObject>().Where(IsEligibleShipScrap).OrderBy<GrabbableObject, string>((GrabbableObject g) => GetStableName(g), StringComparer.OrdinalIgnoreCase) .ThenBy((GrabbableObject g) => Math.Max(0, g.scrapValue)) .ThenBy((GrabbableObject g) => (!((Object)(object)((NetworkBehaviour)g).NetworkObject != (Object)null)) ? ulong.MaxValue : ((NetworkBehaviour)g).NetworkObject.NetworkObjectId) .ToList(); } private static List<GrabbableObject> SelectTargetsForAmount(List<GrabbableObject> available, long requestedRaw, out long selectedRaw) { selectedRaw = 0L; List<GrabbableObject> list = available.Where((GrabbableObject g) => (Object)(object)g != (Object)null && g.scrapValue > 0).ToList(); long num = ((IEnumerable<GrabbableObject>)list).Sum((Func<GrabbableObject, long>)((GrabbableObject g) => g.scrapValue)); if (requestedRaw >= num) { selectedRaw = num; return list; } if (requestedRaw <= 50000 && list.Count <= 2500) { int num2 = (int)requestedRaw; bool[] array = new bool[num2 + 1]; int[] array2 = Enumerable.Repeat(-1, num2 + 1).ToArray(); int[] array3 = Enumerable.Repeat(-1, num2 + 1).ToArray(); array[0] = true; for (int num3 = 0; num3 < list.Count; num3++) { int scrapValue = list[num3].scrapValue; if (scrapValue <= 0 || scrapValue > num2) { continue; } for (int num4 = num2; num4 >= scrapValue; num4--) { if (!array[num4] && array[num4 - scrapValue]) { array[num4] = true; array2[num4] = num4 - scrapValue; array3[num4] = num3; } } if (array[num2]) { break; } } int num5 = num2; while (num5 > 0 && !array[num5]) { num5--; } if (num5 > 0) { List<GrabbableObject> list2 = new List<GrabbableObject>(); int num6 = num5; while (num6 > 0 && array3[num6] >= 0) { int index = array3[num6]; list2.Add(list[index]); num6 = array2[num6]; } selectedRaw = num5; return list2; } } long num7 = requestedRaw; List<GrabbableObject> list3 = new List<GrabbableObject>(); foreach (GrabbableObject item in list.OrderByDescending((GrabbableObject g) => g.scrapValue)) { int scrapValue2 = item.scrapValue; if (scrapValue2 > 0 && scrapValue2 <= num7) { list3.Add(item); selectedRaw += scrapValue2; num7 -= scrapValue2; if (num7 == 0L) { break; } } } return list3; } private static string ShortName(string value, int max) { if (string.IsNullOrEmpty(value)) { return "Unknown"; } if (value.Length <= max) { return value; } return value.Substring(0, Math.Max(1, max - 2)) + ".."; } private static IEnumerator StoreRoutine(List<GrabbableObject> snapshot, Action<StoreResult> completed, bool openedPendingSession) { long oldBalance = BankState.Balance; SaveStore.BackupIfEnabled(); int removed = 0; long removedValue = 0L; int batch = Mathf.Clamp(ScrapBankConfig.DespawnBatchSize.Value, 1, 200); int processedThisFrame = 0; foreach (GrabbableObject item in snapshot) { if ((Object)(object)item == (Object)null || !IsEligibleShipScrap(item)) { continue; } int num = Math.Max(0, item.scrapValue); try { NetworkObject networkObject = ((NetworkBehaviour)item).NetworkObject; if ((Object)(object)networkObject == (Object)null || !networkObject.IsSpawned) { Plugin.Log.LogWarning((object)("Skipped non-spawned scrap during STORE: " + GetStableName(item))); continue; } networkObject.Despawn(true); removed++; removedValue = checked(removedValue + num); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not despawn scrap '" + GetStableName(item) + "': " + ex.Message)); } processedThisFrame++; if (processedThisFrame >= batch) { processedThisFrame = 0; yield return null; } } if (removed == 0) { _storeRunning = false; if (openedPendingSession && !BankState.Dirty) { _pendingTransactionSession = false; SaveStore.ClearVanillaRollbackFiles(); } completed(new StoreResult(success: false, 0, 0L, oldBalance, "STORE removed no items. Scrap Money was not changed.")); yield break; } long num2; try { num2 = checked(oldBalance + removedValue); } catch (OverflowException) { _storeRunning = false; completed(new StoreResult(success: false, removed, removedValue, oldBalance, "STORE value overflow. Force-close/rejoin before the next vanilla save to roll this session back.")); BankState.RecoveryRequired = true; Plugin.Log.LogError((object)"STORE overflow after despawning scrap. Force-close/rejoin to roll back the pending session before continuing."); yield break; } BankState.SetBalanceLocal(num2); NetworkBridge.BroadcastBalance(num2); _storeRunning = false; completed(new StoreResult(success: true, removed, removedValue, num2, $"STORE COMPLETE\nItems: {removed}\nAdded: ${removedValue:N0}\nBalance: ${num2:N0}\nPending vanilla save.")); } internal static SellResult SellRaw(long requestedRaw) { if (!IsServer) { return FailSell("Only the host/server can sell Scrap Money."); } if (_storeRunning || _sellRunning) { return FailSell("ScrapBank is already processing another transaction."); } if (BankState.RecoveryRequired) { return FailSell("ScrapBank recovery is required. Rejoin/restart this save first."); } if (!EnsureSaveReady(out string error)) { return FailSell("ScrapBank save data is unavailable: " + error); } if (!IsAtCompany()) { return FailSell("SELL is only available at The Company.\n회사에서만 판매할 수 있습니다."); } if (BankState.Balance <= 0) { return FailSell("Scrap Money is $0."); } if (requestedRaw <= 0) { return FailSell("Sell amount must be greater than 0."); } Terminal val = Object.FindObjectOfType<Terminal>(); if ((Object)(object)val == (Object)null || (Object)(object)TimeOfDay.Instance == (Object)null || (Object)(object)StartOfRound.Instance == (Object)null) { return FailSell("Game sale state is not ready."); } long num = Math.Min(requestedRaw, BankState.Balance); float num2 = Mathf.Max(0f, StartOfRound.Instance.companyBuyingRate); long num3 = (long)((float)num * num2); if (num3 <= 0) { return FailSell($"Current Company rate ({Mathf.RoundToInt(num2 * 100f)}%) would pay $0."); } if (num3 > int.MaxValue) { return FailSell("Sale is too large for the game's credit counter."); } int num4 = (int)num3; long num5 = BankState.Balance - num; int groupCredits = val.groupCredits; int quotaFulfilled = TimeOfDay.Instance.quotaFulfilled; if ((long)groupCredits + (long)num4 > int.MaxValue || (long)quotaFulfilled + (long)num4 > int.MaxValue) { return FailSell("Sale would overflow the game's credit/quota counters."); } int num6 = groupCredits + num4; int quota = quotaFulfilled + num4; if (!EnsureRollbackBaseline(out string error2)) { return FailSell("Could not prepare a rollback checkpoint: " + error2); } _sellRunning = true; BankState.SetBalanceLocal(num5); ApplyWorldSaleState(num6, quota); ShowVanillaSaleReward(num4, num6); NetworkBridge.BroadcastWorldState(num5, num6, quota, num4); _sellRunning = false; int num7 = Mathf.RoundToInt(num2 * 100f); return new SellResult(success: true, num, num4, num5, num7, $"SALE COMPLETE\nBank used: ${num:N0}\nRate: {num7}%\nReceived: ${num4:N0}\nBalance: ${num5:N0}\nPending vanilla save."); } internal static SellResult SellAll() { if (!EnsureSaveReady(out string error)) { return FailSell("ScrapBank save data is unavailable: " + error); } return SellRaw(BankState.Balance); } internal static SellResult SellQuota() { if (!IsAtCompany()) { return FailSell("SELL is only available at The Company.\n회사에서만 판매할 수 있습니다."); } if (!EnsureSaveReady(out string error)) { return FailSell("ScrapBank save data is unavailable: " + error); } if ((Object)(object)TimeOfDay.Instance == (Object)null || (Object)(object)StartOfRound.Instance == (Object)null) { return FailSell("Game quota state is not ready."); } int num = Math.Max(0, TimeOfDay.Instance.profitQuota - TimeOfDay.Instance.quotaFulfilled); if (num <= 0) { return FailSell("Quota is already fulfilled."); } float num2 = Mathf.Max(0f, StartOfRound.Instance.companyBuyingRate); if (num2 <= 0f) { return FailSell("Company buying rate is 0%."); } return SellRaw(Math.Min((long)Math.Ceiling((double)num / (double)num2), BankState.Balance)); } internal static void LoadHostBalanceAndRecover() { if (IsServer && !((Object)(object)GameNetworkManager.Instance == (Object)null)) { ResetCheckpointTracking(); if (!SaveStore.TryLoadBalance(out long _, out string error)) { Plugin.Log.LogError((object)("ScrapBank persistence disabled for this session: " + error)); } else { TryStartRoutine(RecoverAfterLoad(), "load recovery"); } } } private static IEnumerator RecoverAfterLoad() { yield return null; yield return null; RecoverStorePending(); RecoverSalePending(); NetworkBridge.BroadcastWorldState(BankState.Balance, Object.FindObjectOfType<Terminal>()?.groupCredits ?? 0, TimeOfDay.Instance?.quotaFulfilled ?? 0, 0); } private static void RecoverStorePending() { if (!SaveStore.TryGetStorePending(out var pending)) { return; } Plugin.Log.LogWarning((object)("Recovering STORE transaction " + pending.TxId + ".")); Dictionary<string, int> dictionary = BuildCounts(pending.Records); if (SaveStore.TryReadCommit(pending.Commit, pending.TxId, out var balance)) { if (BankState.Balance != balance && !SaveStore.TrySaveBalance(balance, out string error)) { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)("Could not restore committed STORE balance: " + error)); return; } int num = RemoveMatchingPhysicalItems(dictionary); if (ForceSaveMethod("SaveItemsInShip", out string error2)) { SaveStore.ClearStorePending(); } else { Plugin.Log.LogWarning((object)("Committed STORE recovery could not force ship save: " + error2)); } Plugin.Log.LogWarning((object)$"Committed STORE recovery removed {num} stale physical duplicate(s)."); return; } Dictionary<string, int> dictionary2 = BuildCurrentCounts(); int num2 = 0; long num3 = 0L; foreach (KeyValuePair<string, int> item in dictionary) { int value; int num4 = (dictionary2.TryGetValue(item.Key, out value) ? value : 0); int num5 = Math.Max(0, item.Value - num4); if (num5 > 0 && TrySplitCountKey(item.Key, out string _, out int value2)) { num2 += num5; num3 += (long)num5 * (long)value2; } } if (num2 == 0) { SaveStore.ClearStorePending(); Plugin.Log.LogWarning((object)"Uncommitted STORE journal found, but no persisted removals were detected; transaction was cancelled."); return; } long num6 = checked(pending.OldBalance + num3); string error4; if (!SaveStore.TryReplaceStoreRecords(pending.TxId, RecordsForMissing(dictionary, dictionary2), out string error3)) { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)("STORE automatic recovery could not update the journal: " + error3)); } else if (!SaveStore.TryCommitStore(pending.TxId, num6, out error4)) { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)("STORE automatic recovery could not commit the recovered balance: " + error4)); } else { SaveStore.ClearStorePending(); NetworkBridge.BroadcastBalance(num6); Plugin.Log.LogWarning((object)$"Recovered {num2} removed scrap item(s), ${num3:N0}; Scrap Money=${num6:N0}."); } } private static void RecoverSalePending() { if (!SaveStore.TryGetSalePending(out var sale, out var committed)) { return; } Plugin.Log.LogWarning((object)$"Recovering SALE transaction {sale.TxId}; committed={committed}."); if (!committed) { SaveStore.ClearSalePending(); return; } if (BankState.Balance != sale.NewBalance && !SaveStore.TrySaveBalance(sale.NewBalance, out string error)) { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)("Could not restore committed SALE balance: " + error)); return; } Terminal terminal = Object.FindObjectOfType<Terminal>(); if ((Object)(object)terminal == (Object)null || (Object)(object)TimeOfDay.Instance == (Object)null) { BankState.RecoveryRequired = true; return; } bool num = ReconcileIntState("credits", terminal.groupCredits, sale.OldCredits, sale.NewCredits, delegate(int v) { terminal.groupCredits = v; }); bool flag = ReconcileIntState("quota", TimeOfDay.Instance.quotaFulfilled, sale.OldQuota, sale.NewQuota, delegate(int v) { TimeOfDay.Instance.quotaFulfilled = v; }); if (!num || !flag) { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)"SALE recovery found ambiguous vanilla state. Automatic transactions are locked to avoid duplication/loss."); return; } TimeOfDay.Instance.UpdateProfitQuotaCurrentTime(); NetworkBridge.BroadcastWorldState(sale.NewBalance, terminal.groupCredits, TimeOfDay.Instance.quotaFulfilled, 0); if (ForceSaveMethod("SaveGameValues", out string error2)) { SaveStore.ClearSalePending(); } else { Plugin.Log.LogWarning((object)("SALE recovery could not force save: " + error2)); } } private static bool ReconcileIntState(string label, int current, int oldValue, int newValue, Action<int> set) { if (current == newValue) { return true; } if (current == oldValue) { set(newValue); return true; } Plugin.Log.LogError((object)$"Ambiguous {label} during recovery: current={current}, expected old={oldValue} or new={newValue}."); return false; } internal static void ApplyClientWorldState(long balance, int credits, int quota, int profit) { BankState.SetBalanceLocal(balance); Terminal val = Object.FindObjectOfType<Terminal>(); if ((Object)(object)val != (Object)null) { val.groupCredits = credits; } if ((Object)(object)TimeOfDay.Instance != (Object)null) { TimeOfDay.Instance.quotaFulfilled = quota; TimeOfDay.Instance.UpdateProfitQuotaCurrentTime(); } if (profit > 0) { ShowVanillaSaleReward(profit, credits); } } private static void ShowVanillaSaleReward(int profit, int newCredits) { if (profit <= 0) { return; } try { HUDManager instance = HUDManager.Instance; if (!((Object)(object)instance == (Object)null)) { MethodInfo methodInfo = AccessTools.Method(typeof(HUDManager), "DisplayCreditsEarning", new Type[3] { typeof(int), typeof(GrabbableObject[]), typeof(int) }, (Type[])null); if (methodInfo != null) { methodInfo.Invoke(instance, new object[3] { profit, Array.Empty<GrabbableObject>(), newCredits }); } else { instance.DisplayTip("ITEMS SOLD", $"Received ${profit:N0}", false, false, "GJ_ScrapBank_SaleFallback"); } PlaySaleSuccessSound(instance); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not show vanilla sale reward popup: " + ex.GetBaseException().Message)); } } private static void PlaySaleSuccessSound(HUDManager hud) { try { TryStartRoutine(PlaySaleSuccessSoundRoutine(hud), "sale success sound"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not start vanilla sale sound routine: " + ex.GetBaseException().Message)); } } private static IEnumerator PlaySaleSuccessSoundRoutine(HUDManager hud) { AudioClip addClip = hud.addToScrapTotalSFX; AudioClip finishClip = hud.finishAddingToTotalSFX; if ((Object)(object)finishClip == (Object)null) { finishClip = hud.reachedQuotaSFX; } if ((Object)(object)finishClip == (Object)null) { finishClip = hud.globalNotificationSFX; } if ((Object)(object)addClip == (Object)null) { addClip = finishClip; } if ((Object)(object)addClip == (Object)null && (Object)(object)finishClip == (Object)null) { Plugin.Log.LogWarning((object)"Sale popup shown, but no vanilla sale/reward AudioClip was available."); yield break; } AudioClip[] array = (AudioClip[])(object)new AudioClip[2] { addClip, finishClip }; foreach (AudioClip val in array) { if ((Object)(object)val == (Object)null) { continue; } try { if ((int)val.loadState == 0) { val.LoadAudioData(); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("AudioClip load request failed: " + ex.Message)); } } float wait = 0f; while (wait < 1f) { bool num = (Object)(object)addClip == (Object)null || (int)addClip.loadState == 2; bool flag = (Object)(object)finishClip == (Object)null || (int)finishClip.loadState == 2; if (num && flag) { break; } wait += 0.05f; yield return (object)new WaitForSecondsRealtime(0.05f); } AudioSource uiSource = hud.UIAudio; GameObject fallbackObject = null; bool usingFallback = false; if ((Object)(object)uiSource == (Object)null) { try { fallbackObject = new GameObject("ScrapBank_SaleSuccessSFX"); uiSource = fallbackObject.AddComponent<AudioSource>(); uiSource.playOnAwake = false; uiSource.loop = false; uiSource.spatialBlend = 0f; uiSource.volume = 1f; uiSource.ignoreListenerPause = true; uiSource.priority = 0; uiSource.bypassEffects = true; uiSource.bypassListenerEffects = true; uiSource.bypassReverbZones = true; usingFallback = true; } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Could not create sale AudioSource: " + ex2.GetBaseException().Message)); if ((Object)(object)fallbackObject != (Object)null) { Object.Destroy((Object)(object)fallbackObject); } yield break; } } try { uiSource.ignoreListenerPause = true; } catch { } if ((Object)(object)addClip != (Object)null) { try { uiSource.PlayOneShot(addClip, 1f); } catch (Exception ex3) { Plugin.Log.LogWarning((object)("Could not play CounterAdd SFX: " + ex3.GetBaseException().Message)); } } yield return (object)new WaitForSecondsRealtime(0.12f); if ((Object)(object)finishClip != (Object)null) { try { uiSource.PlayOneShot(finishClip, 1f); } catch (Exception ex4) { Plugin.Log.LogWarning((object)("Could not play CounterAddFinish SFX: " + ex4.GetBaseException().Message)); } } Plugin.Log.LogInfo((object)string.Format("[Sale] Played vanilla counter SFX sequence: {0} -> {1}; source={2}; listenerVolume={3:0.00}", ((Object)(object)addClip != (Object)null) ? ((Object)addClip).name : "none", ((Object)(object)finishClip != (Object)null) ? ((Object)finishClip).name : "none", usingFallback ? "direct-2D" : "HUD.UIAudio", AudioListener.volume)); AudioClip obj2 = addClip; float val2 = ((obj2 != null) ? obj2.length : 0f); AudioClip obj3 = finishClip; float num2 = Math.Max(val2, (obj3 != null) ? obj3.length : 0f); if (num2 > 0f) { yield return (object)new WaitForSecondsRealtime(Mathf.Min(3f, num2 + 0.1f)); } if ((Object)(object)fallbackObject != (Object)null) { Object.Destroy((Object)(object)fallbackObject); } } private static void ApplyWorldSaleState(int credits, int quota) { Terminal val = Object.FindObjectOfType<Terminal>(); if ((Object)(object)val != (Object)null) { val.groupCredits = credits; } if ((Object)(object)TimeOfDay.Instance != (Object)null) { TimeOfDay.Instance.quotaFulfilled = quota; TimeOfDay.Instance.UpdateProfitQuotaCurrentTime(); } } private static bool EnsureSaveReady(out string error) { error = string.Empty; if (BankState.Loaded && BankState.SaveHealthy) { return true; } if (SaveStore.TryLoadBalance(out var _, out error)) { return true; } return false; } private static bool IsEligibleShipScrap(GrabbableObject g) { if ((Object)(object)g == (Object)null || (Object)(object)g.itemProperties == (Object)null || !g.itemProperties.isScrap || !g.isInShipRoom) { return false; } if ((Object)(object)((NetworkBehaviour)g).NetworkObject == (Object)null || !((NetworkBehaviour)g).NetworkObject.IsSpawned) { return false; } if (!ScrapBankConfig.AllowHeldScrap.Value && (g.isHeld || g.isPocketed || (Object)(object)g.playerHeldBy != (Object)null)) { return false; } if (!ScrapBankConfig.IncludeZeroValueScrap.Value && g.scrapValue <= 0) { return false; } if (IsExcluded(g)) { return false; } return true; } private static bool IsExcluded(GrabbableObject g) { string text = ScrapBankConfig.ExcludedItemNames.Value ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) { return false; } string stableName = GetStableName(g); string b = ((Object)g).name.Replace("(Clone)", string.Empty).Trim(); string[] array = text.Split(','); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0 && (string.Equals(text2, stableName, StringComparison.OrdinalIgnoreCase) || string.Equals(text2, b, StringComparison.OrdinalIgnoreCase))) { return true; } } return false; } private static bool IsScrapAllBlacklisted(GrabbableObject g) { string text = ScrapBankConfig.ScrapAllBlacklist.Value ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) { return false; } string stableName = GetStableName(g); string value = ((Object)g).name.Replace("(Clone)", string.Empty).Trim(); string text2 = NormalizeItemToken(stableName); string text3 = NormalizeItemToken(value); string[] array = text.Split(','); for (int i = 0; i < array.Length; i++) { string text4 = array[i].Trim(); if (text4.Length != 0) { string text5 = NormalizeItemToken(text4); if (text5 == text2 || text5 == text3) { return true; } if (text5 == "gift" && text3 == "giftbox") { return true; } } } return false; } private static string NormalizeItemToken(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(value.Length); foreach (char c in value) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private static string GetStableName(GrabbableObject g) { if ((Object)(object)g?.itemProperties != (Object)null && !string.IsNullOrWhiteSpace(g.itemProperties.itemName)) { return g.itemProperties.itemName; } if (!((Object)(object)g != (Object)null)) { return "Unknown"; } return ((Object)g).name.Replace("(Clone)", string.Empty).Trim(); } private static string CountKey(string name, int value) { return value + "\u001f" + name; } private static bool TrySplitCountKey(string key, out string name, out int value) { name = "Unknown"; value = 0; int num = key.IndexOf('\u001f'); if (num <= 0 || !int.TryParse(key.Substring(0, num), out value)) { return false; } name = key.Substring(num + 1); return true; } private static Dictionary<string, int> BuildCounts(IEnumerable<string> records) { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); foreach (string item in records ?? Array.Empty<string>()) { if (SaveStore.TryParseItemRecord(item, out string name, out int value)) { string key = CountKey(name, value); dictionary[key] = ((!dictionary.TryGetValue(key, out var value2)) ? 1 : (value2 + 1)); } } return dictionary; } private static Dictionary<string, int> BuildCurrentCounts() { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); foreach (GrabbableObject val in array) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val.itemProperties == (Object)null) && val.itemProperties.isScrap && val.isInShipRoom) { string key = CountKey(GetStableName(val), Math.Max(0, val.scrapValue)); dictionary[key] = ((!dictionary.TryGetValue(key, out var value)) ? 1 : (value + 1)); } } return dictionary; } private static string[] RecordsForMissing(Dictionary<string, int> expected, Dictionary<string, int> present) { List<string> list = new List<string>(); foreach (KeyValuePair<string, int> item in expected) { int value; int num = (present.TryGetValue(item.Key, out value) ? value : 0); int num2 = Math.Max(0, item.Value - num); if (TrySplitCountKey(item.Key, out string name, out int value2)) { for (int i = 0; i < num2; i++) { list.Add(SaveStore.MakeItemRecord(name, value2)); } } } return list.ToArray(); } private static int RemoveMatchingPhysicalItems(Dictionary<string, int> toRemove) { int num = 0; Dictionary<string, int> dictionary = new Dictionary<string, int>(toRemove, StringComparer.OrdinalIgnoreCase); GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); foreach (GrabbableObject val in array) { if ((Object)(object)val == (Object)null || (Object)(object)val.itemProperties == (Object)null || !val.itemProperties.isScrap || !val.isInShipRoom) { continue; } string text = CountKey(GetStableName(val), Math.Max(0, val.scrapValue)); if (!dictionary.TryGetValue(text, out var value) || value <= 0) { continue; } try { NetworkObject networkObject = ((NetworkBehaviour)val).NetworkObject; if ((Object)(object)networkObject != (Object)null && networkObject.IsSpawned) { networkObject.Despawn(true); dictionary[text] = value - 1; num++; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Duplicate cleanup failed for " + text + ": " + ex.Message)); } } return num; } private static bool EnsureRollbackBaseline(out string error) { error = string.Empty; if (_pendingTransactionSession) { return true; } if (!IsServer) { error = "Only the host/server can create the rollback snapshot."; return false; } if (!SaveStore.TryCreateVanillaRollbackSnapshot(out error)) { return false; } _pendingTransactionSession = true; Plugin.Log.LogInfo((object)$"[Save] ScrapBank transaction is now pending vanilla SaveGame. Current runtime balance=${BankState.Balance:N0}; last saved balance=${BankState.SavedBalance:N0}."); return true; } internal static void OnVanillaSaveGameCompleted() { if (!_disconnecting && _pendingTransactionSession && IsServer && BankState.Loaded) { string error; if (IsBusy) { Plugin.Log.LogWarning((object)"[Save] Vanilla SaveGame finished while ScrapBank was still processing; leaving the transaction pending for the next vanilla save."); } else if (SaveStore.TryCommitAtVanillaSave(BankState.Balance, out error)) { _pendingTransactionSession = false; NetworkBridge.BroadcastBalance(BankState.Balance); Plugin.Log.LogInfo((object)"[Save] ScrapBank committed at the same checkpoint as vanilla SaveGame."); } else { BankState.RecoveryRequired = true; Plugin.Log.LogError((object)("ScrapBank could not commit at vanilla SaveGame: " + error)); } } } internal static void OnShipLeverStarted() { MarkCompanyTripStarted(); } internal static void OnShipHasLeft() { ResetCompanySaleWindow(); } internal static void OnDisconnectStarted() { _disconnecting = true; if (_pendingTransactionSession) { Plugin.Log.LogInfo((object)"[Save] Disconnect started with unsaved ScrapBank changes. Disconnect-time vanilla saves will be blocked and the rollback snapshot will be restored before the next load."); } } internal static bool AllowDisconnectTimeVanillaSave(string methodName) { if (_disconnecting && _pendingTransactionSession) { Plugin.Log.LogInfo((object)("[Save] Blocked disconnect-time " + methodName + "; unsaved ScrapBank changes must not overwrite the last vanilla checkpoint.")); return false; } return true; } internal static void OnDisconnectFinished() { ResetCompanySaleWindow(); if (_pendingTransactionSession) { Plugin.Log.LogInfo((object)"[Save] Disconnect finished with rollback snapshot still armed for the next load."); TryStartRoutine(ClearDisconnectGateAfterDelay(), "disconnect save gate delay"); } else { _disconnecting = false; } } private static IEnumerator ClearDisconnectGateAfterDelay() { yield return (object)new WaitForSecondsRealtime(2f); _disconnecting = false; _pendingTransactionSession = false; } internal static void RestorePendingRollbackBeforeVanillaLoad() { string error; if (!SaveStore.HasVanillaRollbackSnapshot()) { if (_disconnecting) { _disconnecting = false; _pendingTransactionSession = false; } } else if (SaveStore.TryRestoreVanillaRollbackSnapshot(out error)) { BankState.ResetLocal(); _pendingTransactionSession = false; _disconnecting = false; Plugin.Log.LogWarning((object)"[Save] Restored pending ScrapBank rollback BEFORE vanilla loaded save data."); } else { Plugin.Log.LogError((object)("[Save] Could not restore pending ScrapBank rollback before vanilla load: " + error)); } } internal static void ResetCheckpointTracking() { _pendingTransactionSession = false; _disconnecting = false; } private static bool ForceSaveMethod(string methodName, out string error) { error = string.Empty; try { if ((Object)(object)GameNetworkManager.Instance == (Object)null) { error = "GameNetworkManager is null."; return false; } MethodInfo methodInfo = AccessTools.Method(typeof(GameNetworkManager), methodName, (Type[])null, (Type[])null); if (methodInfo == null) { error = methodName + " method not found."; return false; } methodInfo.Invoke(GameNetworkManager.Instance, null); return true; } catch (Exception ex) { error = ex.GetBaseException().Message; Plugin.Log.LogError((object)$"{methodName} failed: {ex}"); return false; } } internal static void MarkCompanyTripStarted() { StartOfRound instance = StartOfRound.Instance; _companySaleWindow = (Object)(object)instance != (Object)null && IsCompanyLevel(instance); if (_companySaleWindow) { Plugin.Log.LogInfo((object)"[Company] Sale window opened as soon as the ship lever started the Company trip."); } } internal static void ResetCompanySaleWindow() { _companySaleWindow = false; } internal static bool IsAtCompany() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { _companySaleWindow = false; return false; } if (!IsCompanyLevel(instance)) { _companySaleWindow = false; return false; } bool flag = false; try { FieldInfo fieldInfo = AccessTools.Field(typeof(StartOfRound), "travellingToNewLevel"); if (fieldInfo != null && fieldInfo.GetValue(instance) is bool flag2) { flag = flag2; } } catch { } if (flag || !instance.inShipPhase) { _companySaleWindow = true; } return _companySaleWindow; } private static bool IsCompanyLevel(StartOfRound round) { if ((Object)(object)round.currentLevel == (Object)null) { return false; } string text = round.currentLevel.PlanetName ?? string.Empty; if (text.IndexOf("Gordion", StringComparison.OrdinalIgnoreCase) < 0) { return text.IndexOf("Company", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static SellResult FailSell(string msg) { return new SellResult(success: false, 0L, 0, BankState.Balance, 0, msg); } } [HarmonyPatch(typeof(Terminal), "LoadNewNode", new Type[] { typeof(TerminalNode) })] internal static class TerminalHelpPatch { [HarmonyPrefix] [HarmonyPriority(0)] private static void Prefix(TerminalNode node) { try { if ((Object)(object)node == (Object)null || string.IsNullOrEmpty(node.displayText)) { return; } string displayText = node.displayText; if (displayText.IndexOf(">BANK", StringComparison.OrdinalIgnoreCase) < 0 && displayText.IndexOf(">MOONS", StringComparison.OrdinalIgnoreCase) >= 0 && displayText.IndexOf(">STORE", StringComparison.OrdinalIgnoreCase) >= 0 && displayText.IndexOf(">BESTIARY", StringComparison.OrdinalIgnoreCase) >= 0 && displayText.IndexOf(">OTHER", StringComparison.OrdinalIgnoreCase) >= 0) { int sectionStart = displayText.IndexOf(">OTHER", StringComparison.OrdinalIgnoreCase); int num = FindSectionEnd(displayText, sectionStart); if (num >= 0) { node.displayText = displayText.Insert(num, ">BANK\n스크랩 은행을 열고 저장/판매 명령어를 봅니다.\n\n"); } else { node.displayText = displayText + (displayText.EndsWith("\n") ? "" : "\n") + ">BANK\n스크랩 은행을 열고 저장/판매 명령어를 봅니다.\n\n"; } } } catch (Exception ex) { try { Plugin.Log.LogDebug((object)("Could not add BANK to terminal root menu: " + ex.Message)); } catch { } } } private static int FindSectionEnd(string text, int sectionStart) { if (sectionStart < 0 || sectionStart >= text.Length) { return -1; } int num = text.IndexOf("\n\n", sectionStart, StringComparison.Ordinal); if (num >= 0) { return num + 2; } int num2 = text.IndexOf("\r\n\r\n", sectionStart, StringComparison.Ordinal); if (num2 >= 0) { return num2 + 4; } int num3 = text.IndexOf('\n', sectionStart); if (num3 < 0) { return text.Length; } int num4 = text.IndexOf('\n', num3 + 1); if (num4 < 0) { return text.Length; } return num4 + 1; } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class TerminalPatch { private static bool _listSelectionMode; private static int _activeListPage = 1; private static bool _pendingItemConfirm; private static ulong _pendingItemNetworkId; private static string _pendingItemDescription = string.Empty; private static int _pendingItemDisplayNumber; private static int _pendingGroupCount; private static long _pendingGroupTotalValue; private static string _pendingGroupValueSummary = string.Empty; private static bool _singleItemSelectionMode; private static List<ScrapBankManager.ScrapItemChoice> _singleItemChoices = new List<ScrapBankManager.ScrapItemChoice>(); private static bool _returnToListAfterStore; [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(Terminal __instance, ref TerminalNode __result) { if ((Object)(object)__instance.screenText == (Object)null) { return true; } foreach (string inputCandidate in GetInputCandidates(__instance)) { if (TryHandle(inputCandidate, out string output)) { try { Plugin.Log.LogInfo((object)("[Terminal] Intercepted: " + inputCandidate)); } catch { } TerminalNode val = ScriptableObject.CreateInstance<TerminalNode>(); val.clearPreviousText = true; val.displayText = output + "\n\n"; val.maxCharactersToType = Math.Max(64, val.displayText.Length + 8); ResetTerminalInput(__instance); __result = val; return false; } } try { string value = NormalizeInput(GetLastNonEmptyLine(__instance.screenText.text ?? string.Empty)); if (LooksLikeOurCommand(value)) { Plugin.Log.LogWarning((object)$"[Terminal] ScrapBank-like command was not intercepted. textAdded={__instance.textAdded}, tail='{EscapeForLog(value)}'"); } } catch { } return true; } private static IEnumerable