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 LocalDeposit v1.1.7
LocalDeposit.dll
Decompiled 8 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; 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: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LocalDeposit { internal sealed class LedgerEntry { public int Id; public int ItemIndex; public string Name; public int Value; public Vector3 Position; public bool IsScrap; public bool SavesVariable; public int SaveData; } internal interface IShipWorld { object Respawn(LedgerEntry entry); bool IsAlive(object handle); void Despawn(object handle); } internal sealed class GuardOutcome { public int Restored; public int RestoreFailed; public int Redeposited; public int Stranded; public int Vanished; public bool Skipped; public readonly List<string> Warnings = new List<string>(); } internal sealed class AutosaveGuard { private sealed class Temp { public LedgerEntry Entry; public object Handle; } private readonly IList<LedgerEntry> entries; private readonly IShipWorld world; private readonly List<Temp> temps = new List<Temp>(); public bool Active { get; private set; } public AutosaveGuard(IList<LedgerEntry> entries, IShipWorld world) { this.entries = entries; this.world = world; } public GuardOutcome Before() { GuardOutcome guardOutcome = new GuardOutcome(); if (Active) { guardOutcome.Skipped = true; return guardOutcome; } Active = true; foreach (LedgerEntry item in new List<LedgerEntry>(entries)) { try { object handle = world.Respawn(item); temps.Add(new Temp { Entry = item, Handle = handle }); guardOutcome.Restored++; } catch (Exception ex) { guardOutcome.RestoreFailed++; guardOutcome.Warnings.Add("could not restore " + item.Name + " for the save (" + ex.Message + "); it is NOT in this save"); } } return guardOutcome; } public GuardOutcome After() { GuardOutcome guardOutcome = new GuardOutcome(); if (!Active) { guardOutcome.Skipped = true; return guardOutcome; } foreach (Temp temp in temps) { if (!world.IsAlive(temp.Handle)) { guardOutcome.Vanished++; continue; } try { world.Despawn(temp.Handle); guardOutcome.Redeposited++; } catch (Exception ex) { if (world.IsAlive(temp.Handle)) { entries.Remove(temp.Entry); guardOutcome.Stranded++; guardOutcome.Warnings.Add(temp.Entry.Name + " ($" + temp.Entry.Value + ") could not be re-deposited (" + ex.Message + ") and was left on the ship"); } else { guardOutcome.Vanished++; } } } temps.Clear(); Active = false; return guardOutcome; } } internal static class Commands { private const string Usage = "USAGE\n /deposit deposit eligible ship scrap (blacklist applied by default; Gift Boxes never touched)\n /deposit -b same, but skip every item on the configured blacklist\n /deposit --blacklist alias of -b\n /deposit --list show what is currently deposited\n /undeposit restore everything you deposited\n"; public static bool IsOurs(string lowerInput) { if (!MatchesWord(lowerInput, "/deposit")) { return MatchesWord(lowerInput, "/undeposit"); } return true; } private static bool MatchesWord(string s, string word) { if (!s.StartsWith(word, StringComparison.Ordinal)) { return false; } if (s.Length != word.Length) { return char.IsWhiteSpace(s[word.Length]); } return true; } public static string Run(string lowerInput) { try { if (HqolCheck.IsLoaded) { return "LocalDeposit is disabled because HQoL is installed.\nHQoL is more important and takes precedence, so /deposit and /undeposit do nothing while it is active.\nUse HQoL's own deposit command, or remove HQoL if you want to use LocalDeposit.\n"; } string[] array = lowerInput.Split(new char[4] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { return "USAGE\n /deposit deposit eligible ship scrap (blacklist applied by default; Gift Boxes never touched)\n /deposit -b same, but skip every item on the configured blacklist\n /deposit --blacklist alias of -b\n /deposit --list show what is currently deposited\n /undeposit restore everything you deposited\n"; } if (!HostCheck.IsHost) { return "ERROR: Only the lobby host can deposit scrap.\nClients cannot remove items from the shared ship, so this command is disabled for you.\n"; } if (Ledger.Busy) { return "ERROR: A deposit/restore is already in progress. Try again in a moment.\n"; } if (array[0] == "/undeposit") { if (array.Length > 1) { return "ERROR: /undeposit takes no options.\n\nUSAGE\n /deposit deposit eligible ship scrap (blacklist applied by default; Gift Boxes never touched)\n /deposit -b same, but skip every item on the configured blacklist\n /deposit --blacklist alias of -b\n /deposit --list show what is currently deposited\n /undeposit restore everything you deposited\n"; } return Undeposit(); } bool blacklist = Plugin.Cfg.PlainDepositUsesBlacklist.Value; bool flag = false; for (int i = 1; i < array.Length; i++) { switch (array[i]) { case "-b": case "--blacklist": blacklist = true; break; case "-l": case "--list": flag = true; break; case "-h": case "--help": case "help": return "USAGE\n /deposit deposit eligible ship scrap (blacklist applied by default; Gift Boxes never touched)\n /deposit -b same, but skip every item on the configured blacklist\n /deposit --blacklist alias of -b\n /deposit --list show what is currently deposited\n /undeposit restore everything you deposited\n"; default: return "ERROR: Unknown option '" + array[i] + "'.\n\nUSAGE\n /deposit deposit eligible ship scrap (blacklist applied by default; Gift Boxes never touched)\n /deposit -b same, but skip every item on the configured blacklist\n /deposit --blacklist alias of -b\n /deposit --list show what is currently deposited\n /undeposit restore everything you deposited\n"; } } if (flag) { return List(); } return Deposit(blacklist); } catch (Exception ex) { Plugin.Log.LogError((object)("Command failed: " + ex)); return "ERROR: Something went wrong; nothing was changed unexpectedly. See BepInEx log.\n"; } } private static string Deposit(bool blacklist) { DepositReport depositReport = Ledger.Deposit(blacklist); if (depositReport.Refused) { return "ERROR: Cannot deposit: " + depositReport.RefusedReason + ".\n"; } StringBuilder stringBuilder = new StringBuilder(); if (depositReport.Count == 0) { stringBuilder.Append("Nothing to deposit: no eligible scrap found on the ship.\n"); } else { stringBuilder.Append("DEPOSIT COMPLETE\n"); stringBuilder.Append("Deposited ").Append(depositReport.Count).Append((depositReport.Count == 1) ? " item" : " items") .Append(" worth $") .Append(depositReport.Value) .Append(blacklist ? " (blacklist applied)" : "") .Append(".\n"); } if (depositReport.Skipped.Count > 0) { stringBuilder.Append("\nSkipped:\n"); foreach (KeyValuePair<string, int> item in depositReport.Skipped) { stringBuilder.Append(" ").Append(item.Value).Append("x ") .Append(item.Key) .Append('\n'); } } if (depositReport.IgnoredOutsideShip > 0) { stringBuilder.Append("\n").Append(depositReport.IgnoredOutsideShip).Append(" scrap item(s) outside the ship were ignored.\n"); } if (depositReport.Failed > 0) { stringBuilder.Append("\nWARNING: ").Append(depositReport.Failed).Append(" item(s) could not be removed and were left on the ship.\n"); } stringBuilder.Append("\nTotal deposited: ").Append(Ledger.Count).Append(" items, $") .Append(Ledger.TotalValue) .Append(".\n"); if (Ledger.Count > 0) { stringBuilder.Append("Deposited scrap cannot be sold until you /undeposit.\n"); } return stringBuilder.ToString(); } private static string Undeposit() { if (Ledger.Count == 0) { return "Nothing is deposited.\n"; } RestoreReport restoreReport = Ledger.RestoreAll("/undeposit"); StringBuilder stringBuilder = new StringBuilder(); if (restoreReport.Problem != null && restoreReport.Restored == 0 && restoreReport.Failed == 0) { return "ERROR: Could not restore: " + restoreReport.Problem + ".\n"; } stringBuilder.Append("UNDEPOSIT COMPLETE\n"); stringBuilder.Append("Restored ").Append(restoreReport.Restored).Append((restoreReport.Restored == 1) ? " item" : " items") .Append(" worth $") .Append(restoreReport.RestoredValue) .Append(".\n"); if (restoreReport.Failed > 0) { stringBuilder.Append("\nWARNING: ").Append(restoreReport.Failed).Append(" item(s) worth $") .Append(restoreReport.FailedValue) .Append(" could NOT be restored and are still stored. Run /undeposit again.\n"); } stringBuilder.Append("\nStill deposited: ").Append(Ledger.Count).Append(" items, $") .Append(Ledger.TotalValue) .Append(".\n"); return stringBuilder.ToString(); } private static string List() { if (Ledger.Count == 0) { return "Nothing is deposited.\n"; } SortedDictionary<string, int> sortedDictionary = new SortedDictionary<string, int>(); Dictionary<string, int> dictionary = new Dictionary<string, int>(); foreach (LedgerEntry entry in Ledger.Entries) { sortedDictionary.TryGetValue(entry.Name, out var value); sortedDictionary[entry.Name] = value + 1; dictionary.TryGetValue(entry.Name, out var value2); dictionary[entry.Name] = value2 + entry.Value; } StringBuilder stringBuilder = new StringBuilder("DEPOSITED SCRAP\n"); foreach (KeyValuePair<string, int> item in sortedDictionary) { stringBuilder.Append(" ").Append(item.Value).Append("x ") .Append(item.Key) .Append(" $") .Append(dictionary[item.Key]) .Append('\n'); } stringBuilder.Append("\nTotal: ").Append(Ledger.Count).Append(" items, $") .Append(Ledger.TotalValue) .Append(".\n"); return stringBuilder.ToString(); } } internal static class HostCheck { public static bool IsHost { get { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null) { return singleton.IsHost; } return false; } } } internal static class HqolCheck { public const string Message = "LocalDeposit is disabled because HQoL is installed.\nHQoL is more important and takes precedence, so /deposit and /undeposit do nothing while it is active.\nUse HQoL's own deposit command, or remove HQoL if you want to use LocalDeposit.\n"; public static bool IsLoaded { get { try { foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { string obj = pluginInfo.Key ?? ""; string text = ((pluginInfo.Value != null && pluginInfo.Value.Metadata != null) ? (pluginInfo.Value.Metadata.Name ?? "") : ""); if (obj.IndexOf("hqol", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("hqol", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } catch { } return false; } } } internal sealed class DepositReport { public int Count; public int Value; public int Failed; public int IgnoredOutsideShip; public readonly SortedDictionary<string, int> Skipped = new SortedDictionary<string, int>(); public bool Refused; public string RefusedReason; } internal sealed class RestoreReport { public int Restored; public int RestoredValue; public int Failed; public int FailedValue; public string Problem; } internal static class Ledger { private enum Verdict { Eligible, SilentSkip, ReportedSkip, OutsideShip } private sealed class UnityShipWorld : IShipWorld { public object Respawn(LedgerEntry entry) { StartOfRound instance = StartOfRound.Instance; NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)instance == (Object)null || (Object)(object)instance.allItemsList == (Object)null || (Object)(object)instance.elevatorTransform == (Object)null || (Object)(object)singleton == (Object)null || !singleton.IsServer) { throw new InvalidOperationException("ship/network not available"); } return SpawnEntry(entry, instance); } public bool IsAlive(object handle) { GrabbableObject val = (GrabbableObject)((handle is GrabbableObject) ? handle : null); if ((Object)(object)val != (Object)null && (Object)(object)((NetworkBehaviour)val).NetworkObject != (Object)null) { return ((NetworkBehaviour)val).NetworkObject.IsSpawned; } return false; } public void Despawn(object handle) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown RemoveFromWorld((GrabbableObject)handle); } } private static readonly List<LedgerEntry> entries = new List<LedgerEntry>(); private static int nextId = 1; private static int totalValue; private static AutosaveGuard guard; public static bool Busy { get; private set; } public static int UnsavedCount { get; set; } public static int Count => entries.Count; public static int TotalValue => totalValue; public static IList<LedgerEntry> Entries => entries; private static AutosaveGuard Guard { get { if (guard == null) { guard = new AutosaveGuard(entries, new UnityShipWorld()); } return guard; } } private static void Recount() { int num = 0; for (int i = 0; i < entries.Count; i++) { num += entries[i].Value; } totalValue = num; } private static Verdict Classify(GrabbableObject g, bool useBlacklist, StartOfRound sor, out string reason) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) reason = null; if ((Object)(object)g == (Object)null || (Object)(object)g.itemProperties == (Object)null) { return Verdict.SilentSkip; } Item itemProperties = g.itemProperties; if (!itemProperties.isScrap) { return Verdict.SilentSkip; } NetworkObject networkObject = ((NetworkBehaviour)g).NetworkObject; if ((Object)(object)networkObject == (Object)null || !networkObject.IsSpawned) { return Verdict.SilentSkip; } if (g.isHeld || g.isPocketed || g.isHeldByEnemy || g.heldByPlayerOnServer) { reason = "being held"; return Verdict.ReportedSkip; } if (g.deactivated || g.itemUsedUp) { return Verdict.SilentSkip; } if (g.scrapValue <= 0) { reason = "no scrap value"; return Verdict.ReportedSkip; } if (!((Object)(object)sor.shipInnerRoomBounds == (Object)null)) { Bounds bounds = sor.shipInnerRoomBounds.bounds; if (((Bounds)(ref bounds)).Contains(((Component)g).transform.position)) { if (PluginConfig.Norm(((Object)itemProperties).name).Contains("giftbox")) { reason = "Gift Box (always kept)"; return Verdict.ReportedSkip; } Transform parent = ((Component)g).transform.parent; if (Plugin.Cfg.SkipItemsOnShipFurniture.Value && (Object)(object)parent != (Object)null && (Object)(object)((Component)parent).gameObject.GetComponent<AutoParentToShip>() != (Object)null) { reason = "attached to ship furniture"; return Verdict.ReportedSkip; } if ((Object)(object)sor.allItemsList == (Object)null || sor.allItemsList.itemsList.IndexOf(itemProperties) < 0 || (Object)(object)itemProperties.spawnPrefab == (Object)null) { reason = "cannot be respawned (not in game item list / no prefab)"; return Verdict.ReportedSkip; } if (useBlacklist) { string text = null; ScanNodeProperties componentInChildren = ((Component)g).GetComponentInChildren<ScanNodeProperties>(); if ((Object)(object)componentInChildren != (Object)null) { text = componentInChildren.headerText; } if (Plugin.Cfg.IsBlacklisted(((Object)itemProperties).name, itemProperties.itemName, text)) { reason = "blacklisted"; return Verdict.ReportedSkip; } } return Verdict.Eligible; } } return Verdict.OutsideShip; } public static DepositReport Deposit(bool useBlacklist) { //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) DepositReport depositReport = new DepositReport(); if (HqolCheck.IsLoaded) { depositReport.Refused = true; depositReport.RefusedReason = "HQoL is installed and takes precedence"; return depositReport; } if (!HostCheck.IsHost) { depositReport.Refused = true; depositReport.RefusedReason = "only the lobby host can deposit"; return depositReport; } if (Busy) { depositReport.Refused = true; depositReport.RefusedReason = "another deposit/restore is still running"; return depositReport; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.allItemsList == (Object)null) { depositReport.Refused = true; depositReport.RefusedReason = "the ship is not loaded"; return depositReport; } Busy = true; try { GrabbableObject[] array = Object.FindObjectsByType<GrabbableObject>((FindObjectsSortMode)0); foreach (GrabbableObject val in array) { Verdict verdict; string reason; try { verdict = Classify(val, useBlacklist, instance, out reason); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Classify failed: " + ex.Message)); continue; } switch (verdict) { case Verdict.OutsideShip: depositReport.IgnoredOutsideShip++; continue; case Verdict.ReportedSkip: { string key = reason + ": " + DisplayName(val); depositReport.Skipped.TryGetValue(key, out var value); depositReport.Skipped[key] = value + 1; continue; } case Verdict.SilentSkip: continue; } LedgerEntry ledgerEntry; try { ledgerEntry = new LedgerEntry(); ledgerEntry.Id = nextId++; ledgerEntry.Name = DisplayName(val); ledgerEntry.ItemIndex = instance.allItemsList.itemsList.IndexOf(val.itemProperties); ledgerEntry.Value = val.scrapValue; ledgerEntry.Position = ((Component)val).transform.position; ledgerEntry.IsScrap = val.itemProperties.isScrap; ledgerEntry.SavesVariable = val.itemProperties.saveItemVariable; if (ledgerEntry.SavesVariable) { ledgerEntry.SaveData = val.GetItemDataToSave(); } } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Could not capture " + DisplayName(val) + ": " + ex2.Message)); string key2 = "state could not be read: " + DisplayName(val); depositReport.Skipped.TryGetValue(key2, out var value2); depositReport.Skipped[key2] = value2 + 1; continue; } entries.Add(ledgerEntry); try { RemoveFromWorld(val); depositReport.Count++; depositReport.Value += ledgerEntry.Value; } catch (Exception ex3) { entries.Remove(ledgerEntry); depositReport.Failed++; Plugin.Log.LogError((object)("Despawn failed for " + ledgerEntry.Name + ", entry rolled back: " + ex3)); } } } finally { Recount(); Busy = false; } Plugin.Log.LogInfo((object)("Deposit: " + depositReport.Count + " items, $" + depositReport.Value + ". Ledger now " + Count + " items, $" + TotalValue + ".")); return depositReport; } public static RestoreReport RestoreAll(string reason) { //IL_0149: Unknown result type (might be due to invalid IL or missing references) RestoreReport restoreReport = new RestoreReport(); if (entries.Count == 0) { return restoreReport; } if (!HostCheck.IsHost) { restoreReport.Problem = "not the host"; return restoreReport; } if (Busy) { restoreReport.Problem = "busy"; return restoreReport; } StartOfRound instance = StartOfRound.Instance; NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)instance == (Object)null || (Object)(object)instance.allItemsList == (Object)null || (Object)(object)instance.elevatorTransform == (Object)null || (Object)(object)singleton == (Object)null || !singleton.IsServer) { restoreReport.Problem = "ship/network not available - " + entries.Count + " items stay in the ledger"; Plugin.Log.LogWarning((object)("RestoreAll(" + reason + ") skipped: " + restoreReport.Problem)); return restoreReport; } Busy = true; List<NetworkObjectReference> list = new List<NetworkObjectReference>(); List<int> list2 = new List<int>(); try { foreach (LedgerEntry item in new List<LedgerEntry>(entries)) { try { GrabbableObject val = SpawnEntry(item, instance); Item itemProperties = val.itemProperties; entries.Remove(item); restoreReport.Restored++; restoreReport.RestoredValue += item.Value; if (itemProperties.isScrap) { list.Add(NetworkObjectReference.op_Implicit(((NetworkBehaviour)val).NetworkObject)); list2.Add(item.Value); } } catch (Exception ex) { restoreReport.Failed++; restoreReport.FailedValue += item.Value; Plugin.Log.LogError((object)("Could not restore " + item.Name + " ($" + item.Value + "): " + ex.Message + " - entry kept.")); } } } finally { Recount(); Busy = false; } if (list.Count > 0 && (Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(SyncValuesToClients(list, list2)); } Plugin.Log.LogInfo((object)("RestoreAll(" + reason + "): restored " + restoreReport.Restored + ", failed " + restoreReport.Failed + ".")); return restoreReport; } private static IEnumerator SyncValuesToClients(List<NetworkObjectReference> refs, List<int> values) { yield return null; RoundManager rm = RoundManager.Instance; if ((Object)(object)rm == (Object)null) { yield break; } for (int i = 0; i < refs.Count; i += 100) { int count = Math.Min(100, refs.Count - i); try { rm.SyncScrapValuesClientRpc(refs.GetRange(i, count).ToArray(), values.GetRange(i, count).ToArray()); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Value sync failed: " + ex.Message)); break; } yield return null; } } internal static GrabbableObject SpawnEntry(LedgerEntry e, StartOfRound sor) { //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) if (e.ItemIndex < 0 || e.ItemIndex >= sor.allItemsList.itemsList.Count) { throw new InvalidOperationException("item index " + e.ItemIndex + " no longer valid"); } Item val = sor.allItemsList.itemsList[e.ItemIndex]; if ((Object)(object)val == (Object)null || (Object)(object)val.spawnPrefab == (Object)null) { throw new InvalidOperationException("no spawn prefab"); } GameObject val2 = Object.Instantiate<GameObject>(val.spawnPrefab, e.Position, Quaternion.identity, sor.elevatorTransform); try { GrabbableObject component = val2.GetComponent<GrabbableObject>(); if ((Object)(object)component == (Object)null) { throw new InvalidOperationException("prefab has no GrabbableObject"); } component.fallTime = 0f; component.scrapPersistedThroughRounds = true; if ((Object)(object)component.radarIcon != (Object)null) { Object.Destroy((Object)(object)((Component)component.radarIcon).gameObject); } component.isInElevator = true; component.isInShipRoom = true; if (val.isScrap) { component.SetScrapValue(e.Value); } if (e.SavesVariable && val.saveItemVariable) { component.LoadItemSaveData(e.SaveData); } ((NetworkBehaviour)component).NetworkObject.Spawn(false); return component; } catch { try { NetworkObject component2 = val2.GetComponent<NetworkObject>(); if ((Object)(object)component2 != (Object)null && component2.IsSpawned) { component2.Despawn(true); } else { Object.Destroy((Object)(object)val2); } } catch { } throw; } } internal static void RemoveFromWorld(GrabbableObject g) { ((NetworkBehaviour)g).NetworkObject.Despawn(true); try { ((Component)g).gameObject.SetActive(false); } catch { } } public static void BeforeSave() { if (entries.Count != 0 && !Busy && HostCheck.IsHost) { Busy = true; Stopwatch stopwatch = Stopwatch.StartNew(); GuardOutcome guardOutcome = Guard.Before(); if (guardOutcome.Skipped) { Busy = false; return; } UnsavedCount = guardOutcome.RestoreFailed; Plugin.Log.LogInfo((object)("Autosave: restored " + guardOutcome.Restored + " deposited items for the save (" + stopwatch.ElapsedMilliseconds + " ms), " + guardOutcome.RestoreFailed + " failed.")); Report(guardOutcome); } } public static void AfterSave() { if (Guard.Active) { Stopwatch stopwatch = Stopwatch.StartNew(); GuardOutcome guardOutcome = Guard.After(); Recount(); Busy = false; Plugin.Log.LogInfo((object)("Autosave: re-deposited " + guardOutcome.Redeposited + " items (" + stopwatch.ElapsedMilliseconds + " ms). Ledger " + Count + " items, $" + TotalValue + ".")); Report(guardOutcome); } } internal static void Warn(string text) { Plugin.Log.LogWarning((object)text); try { if ((Object)(object)HUDManager.Instance != (Object)null) { HUDManager.Instance.DisplayTip("LocalDeposit warning", text, true, false, "LC_LocalDepositWarn"); } } catch { } } private static void Report(GuardOutcome o) { if (o.Warnings.Count == 0) { return; } foreach (string warning in o.Warnings) { Plugin.Log.LogWarning((object)("Autosave: " + warning)); } try { if ((Object)(object)HUDManager.Instance != (Object)null) { HUDManager.Instance.DisplayTip("LocalDeposit warning", o.Warnings.Count + " deposited item problem(s) during autosave. Check the terminal / log; use /undeposit if unsure.", true, false, "LC_LocalDepositWarn"); } } catch { } } public static void Clear(string reason) { if (entries.Count > 0) { Plugin.Log.LogWarning((object)("Ledger cleared (" + reason + "): dropping " + entries.Count + " items worth $" + totalValue + ".")); } entries.Clear(); totalValue = 0; Busy = false; } private static string DisplayName(GrabbableObject g) { string text = (((Object)(object)g.itemProperties != (Object)null) ? g.itemProperties.itemName : null); if (string.IsNullOrEmpty(text) && (Object)(object)g.itemProperties != (Object)null) { text = ((Object)g.itemProperties).name; } if (!string.IsNullOrEmpty(text)) { return text; } return "Unknown item"; } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class TerminalParsePatch { [HarmonyPrefix] private static bool Prefix(Terminal __instance, ref TerminalNode __result) { try { string text = __instance.screenText.text; int textAdded = __instance.textAdded; if (string.IsNullOrEmpty(text) || textAdded <= 0 || textAdded > text.Length) { return true; } string lowerInput = text.Substring(text.Length - textAdded).Trim().ToLowerInvariant(); if (!Commands.IsOurs(lowerInput)) { return true; } string text2 = Commands.Run(lowerInput); TerminalNode val = ScriptableObject.CreateInstance<TerminalNode>(); val.displayText = text2 + "\n"; val.clearPreviousText = true; __result = val; return false; } catch (Exception ex) { Plugin.Log.LogError((object)("Terminal patch error (falling back to vanilla): " + ex)); return true; } } } [HarmonyPatch(typeof(Terminal), "Update")] internal static class TerminalDisplayPatch { [HarmonyPostfix] private static void Postfix(Terminal __instance) { if (__instance.terminalInUse && Ledger.TotalValue > 0 && !((Object)(object)__instance.topRightText == (Object)null)) { ((TMP_Text)__instance.topRightText).text = ((TMP_Text)__instance.topRightText).text + "\n<color=#09DBB5>$" + Ledger.TotalValue; } } } [HarmonyPatch(typeof(GameNetworkManager), "SaveGame")] internal static class SaveGamePatch { [HarmonyPrefix] private static bool Prefix() { try { Ledger.UnsavedCount = 0; if (Ledger.Count == 0) { return true; } if (Plugin.Cfg.AutosaveBehaviour.Value == AutosaveMode.RestoreOnly) { RestoreReport restoreReport = Ledger.RestoreAll("before save"); Plugin.Log.LogWarning((object)("Save: " + restoreReport.Restored + " deposited items restored and left on the ship; " + restoreReport.Failed + " failed.")); Ledger.UnsavedCount = Ledger.Count; } else { Ledger.BeforeSave(); } if (Ledger.UnsavedCount > 0 && Plugin.Cfg.SkipSaveIfItemsCannotBeRestored.Value) { Ledger.Warn(Ledger.UnsavedCount + " deposited items could not be restored, so this save was SKIPPED to protect your previous save file."); return false; } return true; } catch (Exception ex) { Plugin.Log.LogError((object)("Save guard failed: " + ex)); return Ledger.Count <= 0 || !Plugin.Cfg.SkipSaveIfItemsCannotBeRestored.Value; } } [HarmonyFinalizer] private static void Finalizer() { try { Ledger.AfterSave(); } catch (Exception ex) { Plugin.Log.LogError((object)("AfterSave failed: " + ex)); } } } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] internal static class DisconnectPatch { [HarmonyPrefix] private static void Prefix(GameNetworkManager __instance) { if (!__instance.isDisconnecting) { Ledger.RestoreAll("leaving game"); } } } [HarmonyPatch(typeof(StartOfRound), "ResetShip")] internal static class ResetShipPatch { [HarmonyPrefix] private static void Prefix() { Ledger.Clear("ship reset (fired / new run)"); } } [HarmonyPatch(typeof(StartOfRound), "Awake")] internal static class SessionStartPatch { [HarmonyPostfix] private static void Postfix() { Ledger.Clear("new session"); if (Plugin.Cfg.LogItemIdentifiers.Value) { ItemIdLogger.Log(); } } } [HarmonyPatch(typeof(StartOfRound), "OnDestroy")] internal static class SessionEndPatch { [HarmonyPostfix] private static void Postfix() { Ledger.Clear("session ended"); } } internal static class ItemIdLogger { public static void Log() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.allItemsList == (Object)null) { return; } Plugin.Log.LogInfo((object)"---- LocalDeposit item identifiers (asset name | display name | isScrap) ----"); foreach (Item items in instance.allItemsList.itemsList) { if ((Object)(object)items != (Object)null) { Plugin.Log.LogInfo((object)(((Object)items).name + " | " + items.itemName + " | " + items.isScrap)); } } } } [BepInPlugin("local.deposit.lethalcompany", "LocalDeposit", "1.1.7")] public class Plugin : BaseUnityPlugin { public const string Guid = "local.deposit.lethalcompany"; public const string Name = "LocalDeposit"; public const string Version = "1.1.7"; internal static ManualLogSource Log; internal static Plugin Instance; internal static PluginConfig Cfg; private void Start() { if (HqolCheck.IsLoaded) { Log.LogWarning((object)"HQoL detected: LocalDeposit commands are disabled. HQoL is more important and takes precedence."); } } private void Awake() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; Cfg = new PluginConfig(((BaseUnityPlugin)this).Config); new Harmony("local.deposit.lethalcompany").PatchAll(typeof(Plugin).Assembly); Log.LogInfo((object)"LocalDeposit 1.1.7 loaded. Host-side only; clients need nothing installed."); } } internal enum AutosaveMode { RestoreSaveRedeposit, RestoreOnly } internal sealed class PluginConfig { public readonly ConfigEntry<AutosaveMode> AutosaveBehaviour; public readonly ConfigEntry<bool> SkipSaveIfItemsCannotBeRestored; private readonly ConfigEntry<string> blacklist; public readonly ConfigEntry<bool> PlainDepositUsesBlacklist; public readonly ConfigEntry<bool> BlacklistPartialMatch; public readonly ConfigEntry<bool> SkipItemsOnShipFurniture; public readonly ConfigEntry<bool> LogItemIdentifiers; private HashSet<string> blacklistSet = new HashSet<string>(); public PluginConfig(ConfigFile cfg) { blacklist = cfg.Bind<string>("Blacklist", "Items", "ShotgunItem, KnifeItem, Shotgun, Knife, Kitchen knife, GiftBox, Gift Box", "Comma-separated items that '/deposit -b' (or '/deposit --blacklist') will NOT deposit. Each entry is matched, ignoring case, spaces, '_' and '-', against THREE identifiers of an item: (1) its internal asset name (Item.name, e.g. GiftBox), (2) its display name (Item.itemName), (3) its scan-node header text. Unknown/modded names are simply ignored. To find valid identifiers set [Debug] LogItemIdentifiers = true and read BepInEx/LogOutput.log after loading into a lobby as host: every item the game knows is printed there."); PlainDepositUsesBlacklist = cfg.Bind<bool>("Behaviour", "PlainDepositUsesBlacklist", true, "If true, plain '/deposit' also applies the blacklist (so it behaves like '/deposit -b'). Default true. If false, plain '/deposit' deposits every eligible scrap item except Gift Boxes, which are ALWAYS skipped because deleting one destroys its hidden contents."); BlacklistPartialMatch = cfg.Bind<bool>("Blacklist", "PartialMatch", true, "true: an entry also matches any item whose name CONTAINS it (so 'knife' matches 'Kitchen knife' and 'KnifeItem', 'shotgun' matches 'Shotgun'). Entries shorter than 3 letters are always exact. false: whole-name match only."); SkipItemsOnShipFurniture = cfg.Bind<bool>("Behaviour", "SkipItemsOnShipFurniture", true, "true: scrap sitting on/inside ship furniture (the cabinet, shelves, other placeable objects) is never deposited, matching HQoL. false: such scrap is deposited like any other. This is separate from the blacklist."); AutosaveBehaviour = cfg.Bind<AutosaveMode>("Behaviour", "AutosaveMode", AutosaveMode.RestoreOnly, "What happens to deposited items when the game saves (end of round, ship ready to land, leaving). RestoreSaveRedeposit: items are respawned for the save and removed again right after it, so every save contains them (this briefly spawns and despawns every deposited item; with thousands of items expect a short hitch). RestoreOnly: items are restored before the save and STAY on the ship; run /deposit again yourself. Either way the mod never writes anything to the save file itself."); SkipSaveIfItemsCannotBeRestored = cfg.Bind<bool>("Behaviour", "SkipSaveIfItemsCannotBeRestored", true, "If deposited items could not be put back on the ship when the game is about to save (typically during Alt+F4 / application quit, when the network is already shutting down), skip that save entirely so your previous save file stays intact. Otherwise the game would overwrite it with a ship that is missing the deposited items."); LogItemIdentifiers = cfg.Bind<bool>("Debug", "LogItemIdentifiers", false, "Print every item's asset name / display name when a lobby loads (host only). Use this to fill the blacklist."); blacklist.SettingChanged += delegate { Rebuild(); }; Rebuild(); } public static string Norm(string s) { if (string.IsNullOrEmpty(s)) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (!char.IsWhiteSpace(c) && c != '_' && c != '-') { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private void Rebuild() { HashSet<string> hashSet = new HashSet<string>(); string[] array = (blacklist.Value ?? "").Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = Norm(array[i]); if (text.Length > 0) { hashSet.Add(text); } } blacklistSet = hashSet; } public bool IsBlacklisted(params string[] identifiers) { bool value = BlacklistPartialMatch.Value; for (int i = 0; i < identifiers.Length; i++) { string text = Norm(identifiers[i]); if (text.Length == 0) { continue; } if (blacklistSet.Contains(text)) { return true; } if (!value) { continue; } foreach (string item in blacklistSet) { if (item.Length >= 3 && text.Contains(item)) { return true; } } } return false; } } }