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 PersistentTerminal v1.3.5
BepInEx/plugins/PersistentTerminal.dll
Decompiled 2 months 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.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.UI; [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("PersistentTerminal")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Terminal remembers your last screen")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0")] [assembly: AssemblyProduct("PersistentTerminal")] [assembly: AssemblyTitle("PersistentTerminal")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.3.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 PersistentTerminal { [BepInPlugin("mrbub.persistentterminal", "PersistentTerminal", "1.3.0")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Logger; public static ConfigEntry<string> TerminalTheme; public static ConfigEntry<string> CustomTextColor; public static ConfigEntry<string> CustomBackgroundColor; public static ConfigEntry<string> CustomScreenLightColor; public static ConfigEntry<bool> AlwaysOnScreen; public static ConfigEntry<bool> AlwaysOnLight; private void Awake() { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) Logger = ((BaseUnityPlugin)this).Logger; TerminalTheme = ((BaseUnityPlugin)this).Config.Bind<string>("Theme", "Preset", "Default", new ConfigDescription("Terminal color theme preset.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[7] { "Default", "Green", "Amber", "Blue", "Red", "Purple", "Cyan" }), Array.Empty<object>())); CustomTextColor = ((BaseUnityPlugin)this).Config.Bind<string>("Custom Colors", "TextColor", "", "Custom text color in RGB format (0-255). Overrides the theme preset if set.\nExample: 0,255,0 for green\nLeave empty to use theme preset."); CustomBackgroundColor = ((BaseUnityPlugin)this).Config.Bind<string>("Custom Colors", "BackgroundColor", "", "Custom background color in RGB format (0-255). Overrides the theme preset if set.\nExample: 0,20,0 for dark green\nLeave empty to use theme preset."); CustomScreenLightColor = ((BaseUnityPlugin)this).Config.Bind<string>("Custom Colors", "ScreenLightColor", "", "Custom screen light color in RGB format (0-255). Overrides the theme preset if set.\nExample: 0,255,0 for green\nLeave empty to use theme preset."); AlwaysOnScreen = ((BaseUnityPlugin)this).Config.Bind<bool>("Behavior", "AlwaysOnScreen", false, "Keep the terminal screen always on, even when not in use."); AlwaysOnLight = ((BaseUnityPlugin)this).Config.Bind<bool>("Behavior", "AlwaysOnLight", false, "Keep the terminal light always on."); TerminalPresets.InitializeConfig(((BaseUnityPlugin)this).Config); new Harmony("mrbub.persistentterminal").PatchAll(); Logger.LogInfo((object)"PersistentTerminal v1.3.0 loaded!"); } } public static class TerminalPresets { public struct PresetItem { public string ItemName; public int Quantity; public int ItemIndex; public int UnitCost; public PresetItem(string name, int qty) { ItemName = name; Quantity = qty; ItemIndex = -1; UnitCost = 0; } } public static Dictionary<string, List<PresetItem>> Presets = new Dictionary<string, List<PresetItem>>(StringComparer.OrdinalIgnoreCase); private static string pendingPresetName = null; private static int pendingTotalCost = 0; private static List<PresetItem> pendingItems = null; public static ConfigEntry<string> Preset1; public static ConfigEntry<string> Preset2; public static ConfigEntry<string> Preset3; public static ConfigEntry<string> Preset4; public static ConfigEntry<string> Preset5; public static void InitializeConfig(ConfigFile config) { Preset1 = config.Bind<string>("Presets", "Preset1", "ESSENTIALS=flashlight:4,shovel:2,walkie-talkie:4", "Preset format: NAME=item1:qty,item2:qty,item3:qty\nItem names must match store names (case-insensitive).\nLeave empty to disable."); Preset2 = config.Bind<string>("Presets", "Preset2", "LIGHTS=flashlight:4,pro-flashlight:2", "Preset format: NAME=item1:qty,item2:qty,item3:qty"); Preset3 = config.Bind<string>("Presets", "Preset3", "", "Preset format: NAME=item1:qty,item2:qty,item3:qty"); Preset4 = config.Bind<string>("Presets", "Preset4", "", "Preset format: NAME=item1:qty,item2:qty,item3:qty"); Preset5 = config.Bind<string>("Presets", "Preset5", "", "Preset format: NAME=item1:qty,item2:qty,item3:qty"); ParsePresets(); } public static void ParsePresets() { Presets.Clear(); ParsePresetString(Preset1.Value); ParsePresetString(Preset2.Value); ParsePresetString(Preset3.Value); ParsePresetString(Preset4.Value); ParsePresetString(Preset5.Value); Plugin.Logger.LogInfo((object)$"[PRESETS] Loaded {Presets.Count} presets"); } private static void ParsePresetString(string presetString) { if (string.IsNullOrWhiteSpace(presetString)) { return; } try { int num = presetString.IndexOf('='); if (num <= 0) { Plugin.Logger.LogWarning((object)("[PRESETS] Invalid preset format (no '='): " + presetString)); return; } string text = presetString.Substring(0, num).Trim().ToUpper(); string text2 = presetString.Substring(num + 1); List<PresetItem> list = new List<PresetItem>(); string[] array = text2.Split(','); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); if (string.IsNullOrEmpty(text3)) { continue; } int num2 = text3.LastIndexOf(':'); if (num2 <= 0) { Plugin.Logger.LogWarning((object)("[PRESETS] Invalid item format (no ':'): " + text3)); continue; } string text4 = text3.Substring(0, num2).Trim(); if (int.TryParse(text3.Substring(num2 + 1).Trim(), out var result) && result > 0) { list.Add(new PresetItem(text4, result)); Plugin.Logger.LogInfo((object)$"[PRESETS] Parsed item: {text4} x{result}"); } } if (list.Count > 0) { Presets[text] = list; Plugin.Logger.LogInfo((object)$"[PRESETS] Added preset '{text}' with {list.Count} items"); } } catch (Exception ex) { Plugin.Logger.LogError((object)("[PRESETS] Error parsing preset '" + presetString + "': " + ex.Message)); } } public static bool TryHandlePresetCommand(Terminal terminal, string input, out string response) { response = null; string text = input.Trim(); input = text.ToUpper(); if (string.IsNullOrEmpty(input)) { return false; } if (pendingPresetName != null) { if ("CONFIRM".StartsWith(input) || "YES".StartsWith(input) || input == "Y") { response = ExecutePendingPreset(terminal, text); return true; } if ("DENY".StartsWith(input) || "NO".StartsWith(input) || "CANCEL".StartsWith(input) || input == "N") { _ = pendingPresetName; pendingPresetName = null; pendingItems = null; pendingTotalCost = 0; response = ">" + text.ToUpper() + "\n\nCancelled order.\n\n"; return true; } } if (input.Length >= 2 && ("PRESETS".StartsWith(input) || "PRESET".StartsWith(input))) { pendingPresetName = null; pendingItems = null; pendingTotalCost = 0; response = GeneratePresetsListText(terminal, text); return true; } if (input.Length >= 2) { foreach (KeyValuePair<string, List<PresetItem>> preset in Presets) { if (preset.Key.StartsWith(input)) { response = GeneratePresetConfirmation(terminal, preset.Key, preset.Value, text); return true; } } } if (input.StartsWith("BUY ") && input.Length > 5) { string value = input.Substring(4).Trim(); foreach (KeyValuePair<string, List<PresetItem>> preset2 in Presets) { if (preset2.Key.StartsWith(value)) { response = GeneratePresetConfirmation(terminal, preset2.Key, preset2.Value, text); return true; } } } return false; } private static string GeneratePresetsListText(Terminal terminal, string originalInput) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(">" + originalInput.ToUpper() + "\n\n"); stringBuilder.Append("Welcome to Terminal Presets.\n"); stringBuilder.Append("Type a preset name to order in bulk.\n"); stringBuilder.Append("Use CONFIRM and DENY to finalize.\n"); stringBuilder.Append("________________________________________\n\n"); if (Presets.Count == 0) { stringBuilder.Append("No presets configured.\nConfigure presets in the mod config file.\n\n"); return stringBuilder.ToString(); } foreach (KeyValuePair<string, List<PresetItem>> preset in Presets) { int num = 0; int num2 = 0; foreach (PresetItem item in preset.Value) { int num3 = FindItemIndex(terminal, item.ItemName); if (num3 >= 0) { int itemCost = GetItemCost(terminal, num3); num += itemCost * item.Quantity; num2 += item.Quantity; } } stringBuilder.Append($"* {preset.Key} // ${num} ({num2} items)\n"); } stringBuilder.Append("\n"); return stringBuilder.ToString(); } private static string GeneratePresetConfirmation(Terminal terminal, string presetName, List<PresetItem> items, string originalInput) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(">" + originalInput.ToUpper() + "\n\n"); int num = 0; int num2 = 0; List<PresetItem> list = new List<PresetItem>(); bool flag = false; stringBuilder.Append("You have requested to order " + presetName + ":\n\n"); foreach (PresetItem item2 in items) { int num3 = FindItemIndex(terminal, item2.ItemName); if (num3 >= 0) { int itemCost = GetItemCost(terminal, num3); int num4 = itemCost * item2.Quantity; num += num4; num2 += item2.Quantity; PresetItem item = new PresetItem(item2.ItemName, item2.Quantity); item.ItemIndex = num3; item.UnitCost = itemCost; list.Add(item); stringBuilder.Append($"* {item2.ItemName} x{item2.Quantity} // ${num4}\n"); } else { stringBuilder.Append($"* {item2.ItemName} x{item2.Quantity} // NOT IN STORE\n"); flag = true; } } stringBuilder.Append($"\nTotal: ${num}\n"); stringBuilder.Append($"Your balance is ${terminal.groupCredits}.\n"); if (flag) { stringBuilder.Append("\nWARNING: Some items not found in store!\n"); } if (num > terminal.groupCredits) { stringBuilder.Append("\nYou could not afford these items!\n\n"); return stringBuilder.ToString(); } if (num2 + terminal.numberOfItemsInDropship > 12) { stringBuilder.Append("\nThe dropship cannot hold this many items!\n\n"); return stringBuilder.ToString(); } pendingPresetName = presetName; pendingTotalCost = num; pendingItems = list; stringBuilder.Append("\nPlease CONFIRM or DENY.\n\n"); return stringBuilder.ToString(); } private static string ExecutePendingPreset(Terminal terminal, string originalInput) { if (pendingItems == null || pendingPresetName == null) { return ">" + originalInput.ToUpper() + "\n\nNo pending order.\n\n"; } if (pendingTotalCost > terminal.groupCredits) { ClearPending(); return ">" + originalInput.ToUpper() + "\n\nYou could not afford these items!\n\n"; } int num = 0; foreach (PresetItem pendingItem in pendingItems) { num += pendingItem.Quantity; } if (num + terminal.numberOfItemsInDropship > 12) { ClearPending(); return ">" + originalInput.ToUpper() + "\n\nThe dropship cannot hold this many items!\n\n"; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(">" + originalInput.ToUpper() + "\n\n"); stringBuilder.Append("Ordered " + pendingPresetName + ":\n\n"); foreach (PresetItem pendingItem2 in pendingItems) { if (pendingItem2.ItemIndex >= 0) { for (int i = 0; i < pendingItem2.Quantity; i++) { terminal.orderedItemsFromTerminal.Add(pendingItem2.ItemIndex); terminal.numberOfItemsInDropship++; } stringBuilder.Append($"* {pendingItem2.ItemName} x{pendingItem2.Quantity}\n"); } } terminal.groupCredits -= pendingTotalCost; terminal.PlayTerminalAudioServerRpc(0); if (!((NetworkBehaviour)terminal).IsServer) { terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship); } else { typeof(Terminal).GetMethod("SyncGroupCreditsClientRpc", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(terminal, new object[2] { terminal.groupCredits, terminal.numberOfItemsInDropship }); } stringBuilder.Append($"\nYour new balance is ${terminal.groupCredits}.\n\n"); stringBuilder.Append("Our contractors enjoy fast, free shipping while on the job! Any purchased items will arrive within approximately one hour.\n\n"); ClearPending(); return stringBuilder.ToString(); } private static void ClearPending() { pendingPresetName = null; pendingTotalCost = 0; pendingItems = null; } private static int FindItemIndex(Terminal terminal, string itemName) { if (terminal.buyableItemsList == null) { return -1; } itemName = itemName.ToLower().Replace("-", " ").Replace("_", " "); for (int i = 0; i < terminal.buyableItemsList.Length; i++) { string text = terminal.buyableItemsList[i].itemName.ToLower(); if (text == itemName || text.Replace("-", " ") == itemName) { return i; } } for (int j = 0; j < terminal.buyableItemsList.Length; j++) { string text2 = terminal.buyableItemsList[j].itemName.ToLower(); if (text2.Contains(itemName) || itemName.Contains(text2)) { return j; } } return -1; } private static int GetItemCost(Terminal terminal, int itemIndex) { if (itemIndex < 0 || itemIndex >= terminal.buyableItemsList.Length) { return 0; } int creditsWorth = terminal.buyableItemsList[itemIndex].creditsWorth; int num = terminal.itemSalesPercentages[itemIndex]; return (int)((float)creditsWorth * ((float)num / 100f)); } } public static class PluginInfo { public const string PLUGIN_GUID = "mrbub.persistentterminal"; public const string PLUGIN_NAME = "PersistentTerminal"; public const string PLUGIN_VERSION = "1.3.0"; } } namespace PersistentTerminal.Patches { [HarmonyPatch(typeof(Terminal))] internal class TerminalPatches { [CompilerGenerated] private sealed class <ForceScrollbarUp>d__27 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public Terminal terminal; private int <i>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ForceScrollbarUp>d__27(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <i>5__2 = 0; break; case 1: <>1__state = -1; terminal.scrollBarVertical.value = 1f; <i>5__2++; break; } if (<i>5__2 < 5) { <>2__current = null; <>1__state = 1; return true; } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static TerminalNode lastNode = null; private static string lastScreenText = ""; private static int lastTextPosition = 0; private static int lastSaveFileNum = -1; private static bool isRestoring = false; private static bool hasRestoredThisSession = false; private static bool skipSoundPlayback = false; private static bool isShowingPresetPage = false; private static string lastPresetPageText = ""; private static List<string> commandHistory = new List<string>(); private static int historyIndex = -1; private static string currentInput = ""; private static string savedInput = ""; private static bool themeApplied = false; internal static Light terminalLight = null; private static Image terminalBackground = null; internal static bool isPlayerInShip = true; internal static Terminal cachedTerminal = null; [HarmonyPatch("BeginUsingTerminal")] [HarmonyPrefix] private static void BeginUsingTerminalPrefix(Terminal __instance) { if ((Object)(object)cachedTerminal == (Object)null) { cachedTerminal = __instance; } if (!((Object)(object)__instance == (Object)null) && !((Object)(object)GameNetworkManager.Instance == (Object)null) && !hasRestoredThisSession) { int saveFileNum = GameNetworkManager.Instance.saveFileNum; if (saveFileNum != lastSaveFileNum) { ClearSavedState(); lastSaveFileNum = saveFileNum; } else if ((Object)(object)lastNode != (Object)null && !string.IsNullOrEmpty(lastScreenText)) { isRestoring = true; } else if (isShowingPresetPage && !string.IsNullOrEmpty(lastPresetPageText)) { isRestoring = true; } } } [HarmonyPatch("BeginUsingTerminal")] [HarmonyPostfix] private static void BeginUsingTerminalPostfix(Terminal __instance) { if (!themeApplied) { ApplyTheme(__instance); themeApplied = true; } if (!isRestoring) { if (!string.IsNullOrEmpty(savedInput) && (Object)(object)__instance.screenText != (Object)null) { Plugin.Logger.LogInfo((object)("[INPUT] Restoring saved input: '" + savedInput + "'")); TMP_InputField screenText = __instance.screenText; screenText.text += savedInput; __instance.textAdded = savedInput.Length; __instance.currentText = __instance.screenText.text; Plugin.Logger.LogInfo((object)$"[INPUT] Input restored, textAdded: {__instance.textAdded}"); savedInput = ""; } return; } try { if (isShowingPresetPage && !string.IsNullOrEmpty(lastPresetPageText)) { Plugin.Logger.LogInfo((object)"[RESTORE] Restoring preset page"); typeof(Terminal).GetField("modifyingText", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(__instance, true); __instance.screenText.text = lastPresetPageText; __instance.currentText = lastPresetPageText; __instance.textAdded = 0; __instance.screenText.ActivateInputField(); ((Selectable)__instance.screenText).Select(); Plugin.Logger.LogInfo((object)"[RESTORE] Successfully restored preset page"); hasRestoredThisSession = true; isRestoring = false; } else if (!((Object)(object)lastNode == (Object)null)) { Plugin.Logger.LogInfo((object)("[RESTORE] Starting restoration of node: " + ((Object)lastNode).name)); skipSoundPlayback = true; __instance.LoadNewNode(lastNode); skipSoundPlayback = false; if (lastTextPosition > 0 && (Object)(object)__instance.screenText != (Object)null) { __instance.textAdded = lastTextPosition; Plugin.Logger.LogInfo((object)$"[RESTORE] Restored scroll position: {lastTextPosition}"); } if (!string.IsNullOrEmpty(savedInput) && (Object)(object)__instance.screenText != (Object)null) { Plugin.Logger.LogInfo((object)("[INPUT] Restoring saved input after node restore: '" + savedInput + "'")); TMP_InputField screenText2 = __instance.screenText; screenText2.text += savedInput; __instance.textAdded = savedInput.Length; __instance.currentText = __instance.screenText.text; Plugin.Logger.LogInfo((object)$"[INPUT] Input restored, textAdded: {__instance.textAdded}"); savedInput = ""; } Plugin.Logger.LogInfo((object)("[RESTORE] Successfully restored to: " + ((Object)lastNode).name)); hasRestoredThisSession = true; } } catch (Exception ex) { Plugin.Logger.LogError((object)("Error restoring terminal state: " + ex.Message)); skipSoundPlayback = false; } finally { isRestoring = false; } } [HarmonyPatch("LoadNewNode")] [HarmonyPrefix] private static bool LoadNewNodePrefix(Terminal __instance, TerminalNode node) { if (isRestoring && (Object)(object)node != (Object)null && ((Object)node).name == "Start") { return false; } return true; } [HarmonyPatch("LoadNewNode")] [HarmonyPostfix] private static void LoadNewNodePostfix(Terminal __instance, TerminalNode node) { if (isRestoring || skipSoundPlayback || !__instance.terminalInUse) { return; } if ((Object)(object)node != (Object)null) { isShowingPresetPage = false; lastPresetPageText = ""; } if ((Object)(object)node != (Object)null && (Object)(object)__instance.currentNode == (Object)(object)node) { lastNode = node; if ((Object)(object)__instance.screenText != (Object)null) { lastScreenText = __instance.screenText.text; lastTextPosition = __instance.textAdded; } } } [HarmonyPatch("PlayTerminalAudioServerRpc")] [HarmonyPrefix] private static bool PlayTerminalAudioServerRpcPrefix(int clipIndex) { if (skipSoundPlayback) { return false; } return true; } [HarmonyPatch("Update")] [HarmonyPostfix] private static void UpdatePostfix(Terminal __instance) { if (!__instance.terminalInUse || (Object)(object)__instance.screenText == (Object)null) { return; } Keyboard current = Keyboard.current; if (current == null) { return; } if (((ButtonControl)current.upArrowKey).wasPressedThisFrame && commandHistory.Count > 0) { if (historyIndex == -1) { currentInput = GetCurrentInput(__instance); Plugin.Logger.LogInfo((object)("[HISTORY] Saved current input: '" + currentInput + "'")); } if (historyIndex < commandHistory.Count - 1) { historyIndex++; string text = commandHistory[commandHistory.Count - 1 - historyIndex]; Plugin.Logger.LogInfo((object)$"[HISTORY] Up arrow - showing command {historyIndex + 1}/{commandHistory.Count}: '{text}'"); SetCurrentInput(__instance, text); } else { Plugin.Logger.LogInfo((object)"[HISTORY] Already at oldest command"); } } else if (((ButtonControl)current.downArrowKey).wasPressedThisFrame && commandHistory.Count > 0) { if (historyIndex > 0) { historyIndex--; string text2 = commandHistory[commandHistory.Count - 1 - historyIndex]; Plugin.Logger.LogInfo((object)$"[HISTORY] Down arrow - showing command {historyIndex + 1}/{commandHistory.Count}: '{text2}'"); SetCurrentInput(__instance, text2); } else if (historyIndex == 0) { Plugin.Logger.LogInfo((object)("[HISTORY] Down arrow - restoring current input: '" + currentInput + "'")); historyIndex = -1; SetCurrentInput(__instance, currentInput); } else { Plugin.Logger.LogInfo((object)"[HISTORY] Already at newest input"); } } } [HarmonyPatch("OnSubmit")] [HarmonyPrefix] private static bool OnSubmitPrefix(Terminal __instance) { if ((Object)(object)__instance.screenText == (Object)null || __instance.textAdded <= 0) { return true; } string text = GetCurrentInput(__instance); if (!string.IsNullOrWhiteSpace(text)) { if (TerminalPresets.TryHandlePresetCommand(__instance, text, out var response)) { Plugin.Logger.LogInfo((object)("[PRESETS] Handled preset command: '" + text + "'")); if (commandHistory.Count == 0 || commandHistory[commandHistory.Count - 1] != text) { commandHistory.Add(text); } historyIndex = -1; currentInput = ""; typeof(Terminal).GetField("modifyingText", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(__instance, true); string text2 = "\n\n\n" + response; __instance.screenText.text = text2; __instance.currentText = text2; __instance.textAdded = 0; isShowingPresetPage = true; lastPresetPageText = text2; lastNode = null; Plugin.Logger.LogInfo((object)$"[PRESETS] Set screen text, length: {__instance.screenText.text.Length}"); __instance.screenText.ActivateInputField(); ((Selectable)__instance.screenText).Select(); FieldInfo field = typeof(Terminal).GetField("forceScrollbarCoroutine", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { object? value = field.GetValue(__instance); Coroutine val = (Coroutine)((value is Coroutine) ? value : null); if (val != null) { ((MonoBehaviour)__instance).StopCoroutine(val); } Coroutine value2 = ((MonoBehaviour)__instance).StartCoroutine(ForceScrollbarUp(__instance)); field.SetValue(__instance, value2); } return false; } if (commandHistory.Count == 0 || commandHistory[commandHistory.Count - 1] != text) { commandHistory.Add(text); Plugin.Logger.LogInfo((object)$"[HISTORY] Added command to history: '{text}' (total: {commandHistory.Count})"); if (commandHistory.Count > 50) { string text3 = commandHistory[0]; commandHistory.RemoveAt(0); Plugin.Logger.LogInfo((object)("[HISTORY] Removed oldest command: '" + text3 + "'")); } } else { Plugin.Logger.LogInfo((object)("[HISTORY] Skipped duplicate command: '" + text + "'")); } } historyIndex = -1; currentInput = ""; return true; } private static string GetCurrentInput(Terminal terminal) { if ((Object)(object)terminal.screenText == (Object)null || terminal.textAdded <= 0) { return ""; } int num = terminal.screenText.text.Length - terminal.textAdded; if (num < 0 || num >= terminal.screenText.text.Length) { return ""; } return terminal.screenText.text.Substring(num); } private static void SetCurrentInput(Terminal terminal, string input) { if (!((Object)(object)terminal.screenText == (Object)null)) { if (terminal.textAdded > 0) { terminal.screenText.text = terminal.screenText.text.Substring(0, terminal.screenText.text.Length - terminal.textAdded); } TMP_InputField screenText = terminal.screenText; screenText.text += input; terminal.textAdded = input.Length; terminal.currentText = terminal.screenText.text; } } [IteratorStateMachine(typeof(<ForceScrollbarUp>d__27))] private static IEnumerator ForceScrollbarUp(Terminal terminal) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ForceScrollbarUp>d__27(0) { terminal = terminal }; } [HarmonyPatch("QuitTerminal")] [HarmonyPrefix] private static void QuitTerminalPrefix(Terminal __instance) { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.currentNode == (Object)null) { return; } try { lastNode = __instance.currentNode; if ((Object)(object)__instance.screenText != (Object)null) { lastScreenText = __instance.screenText.text; lastTextPosition = __instance.textAdded; savedInput = GetCurrentInput(__instance); if (!string.IsNullOrEmpty(savedInput)) { Plugin.Logger.LogInfo((object)("[INPUT] Saved player input: '" + savedInput + "'")); } } if ((Object)(object)GameNetworkManager.Instance != (Object)null) { lastSaveFileNum = GameNetworkManager.Instance.saveFileNum; } Plugin.Logger.LogInfo((object)$"[HISTORY] Command history size: {commandHistory.Count}"); hasRestoredThisSession = false; } catch (Exception ex) { Plugin.Logger.LogError((object)("Error saving terminal state: " + ex.Message)); } } [HarmonyPatch(typeof(StartOfRound), "ResetShip")] [HarmonyPostfix] private static void ResetShipPostfix() { ClearSavedState(); } [HarmonyPatch(typeof(StartOfRound), "EndOfGame")] [HarmonyPostfix] private static void EndOfGamePostfix() { ClearSavedState(); } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] [HarmonyPostfix] private static void DisconnectPostfix() { Plugin.Logger.LogInfo((object)"[STATE] Disconnected - clearing terminal state"); ClearSavedState(); } private static void ClearSavedState() { Plugin.Logger.LogInfo((object)"[STATE] Clearing all saved state"); lastNode = null; lastScreenText = ""; lastTextPosition = 0; isRestoring = false; hasRestoredThisSession = false; skipSoundPlayback = false; isShowingPresetPage = false; lastPresetPageText = ""; commandHistory.Clear(); historyIndex = -1; currentInput = ""; savedInput = ""; themeApplied = false; Plugin.Logger.LogInfo((object)"[STATE] State cleared"); } [HarmonyPatch("Start")] [HarmonyPostfix] private static void StartPostfix(Terminal __instance) { //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_0234: Unknown result type (might be due to invalid IL or missing references) //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_02f7: Unknown result type (might be due to invalid IL or missing references) //IL_030b: Unknown result type (might be due to invalid IL or missing references) //IL_031f: Unknown result type (might be due to invalid IL or missing references) //IL_03e7: Unknown result type (might be due to invalid IL or missing references) //IL_03f6: Unknown result type (might be due to invalid IL or missing references) //IL_040a: Unknown result type (might be due to invalid IL or missing references) //IL_041e: Unknown result type (might be due to invalid IL or missing references) Plugin.Logger.LogInfo((object)"[INIT] ========== Terminal.Start - Full Object Discovery =========="); Plugin.Logger.LogInfo((object)("[INIT] Terminal GameObject: " + ((Object)((Component)__instance).gameObject).name)); Plugin.Logger.LogInfo((object)("[INIT] Terminal transform: " + ((Object)((Component)__instance).transform).name)); if ((Object)(object)__instance.terminalLight != (Object)null) { terminalLight = __instance.terminalLight; Plugin.Logger.LogInfo((object)$"[INIT] terminalLight: {((Object)((Component)__instance.terminalLight).gameObject).name} (enabled: {((Behaviour)__instance.terminalLight).enabled})"); Plugin.Logger.LogInfo((object)$"[INIT] terminalLight color: R:{__instance.terminalLight.color.r} G:{__instance.terminalLight.color.g} B:{__instance.terminalLight.color.b}"); if (Plugin.AlwaysOnLight.Value) { ((Behaviour)terminalLight).enabled = true; Plugin.Logger.LogInfo((object)"[ALWAYS-ON] Terminal light enabled"); } } else { Plugin.Logger.LogWarning((object)"[INIT] terminalLight is null"); } if ((Object)(object)__instance.terminalUIScreen != (Object)null) { Plugin.Logger.LogInfo((object)("[INIT] terminalUIScreen: " + ((Object)__instance.terminalUIScreen).name)); LogGameObjectHierarchy(((Component)__instance.terminalUIScreen).gameObject, 0, 3); } else { Plugin.Logger.LogWarning((object)"[INIT] terminalUIScreen is null"); } if ((Object)(object)__instance.screenText != (Object)null) { Plugin.Logger.LogInfo((object)("[INIT] screenText GameObject: " + ((Object)((Component)__instance.screenText).gameObject).name)); if ((Object)(object)__instance.screenText.textComponent != (Object)null) { Plugin.Logger.LogInfo((object)("[INIT] screenText.textComponent: " + ((object)__instance.screenText.textComponent).GetType().Name)); Plugin.Logger.LogInfo((object)$"[INIT] screenText color: R:{((Graphic)__instance.screenText.textComponent).color.r} G:{((Graphic)__instance.screenText.textComponent).color.g} B:{((Graphic)__instance.screenText.textComponent).color.b}"); } } else { Plugin.Logger.LogWarning((object)"[INIT] screenText is null"); } Plugin.Logger.LogInfo((object)"[INIT] === Terminal GameObject Children ==="); LogGameObjectHierarchy(((Component)__instance).gameObject, 0, 3); Plugin.Logger.LogInfo((object)"[INIT] === Searching for UI Image components ==="); Image[] componentsInChildren = ((Component)__instance.terminalUIScreen).GetComponentsInChildren<Image>(true); Plugin.Logger.LogInfo((object)$"[INIT] Found {componentsInChildren.Length} Image components in Canvas"); Image[] array = componentsInChildren; foreach (Image val in array) { Plugin.Logger.LogInfo((object)$"[INIT] Image: {((Object)((Component)val).gameObject).name} - Color: R:{((Graphic)val).color.r:F2} G:{((Graphic)val).color.g:F2} B:{((Graphic)val).color.b:F2} A:{((Graphic)val).color.a:F2}"); if (((Object)((Component)val).gameObject).name == "MainContainer" && (Object)(object)terminalBackground == (Object)null) { terminalBackground = val; Plugin.Logger.LogInfo((object)"[INIT] *** Cached MainContainer as terminal background ***"); } } Plugin.Logger.LogInfo((object)"[INIT] === Searching for Light components ==="); Light[] componentsInChildren2 = ((Component)__instance).GetComponentsInChildren<Light>(true); Plugin.Logger.LogInfo((object)$"[INIT] Found {componentsInChildren2.Length} Light components in Terminal"); Light[] array2 = componentsInChildren2; foreach (Light val2 in array2) { Plugin.Logger.LogInfo((object)$"[INIT] Light: {((Object)((Component)val2).gameObject).name} - Type: {val2.type} - Color: R:{val2.color.r:F2} G:{val2.color.g:F2} B:{val2.color.b:F2} - Enabled: {((Behaviour)val2).enabled}"); } Plugin.Logger.LogInfo((object)"[INIT] ========== End Terminal Object Discovery =========="); if (Plugin.AlwaysOnScreen.Value) { Plugin.Logger.LogInfo((object)"[INIT] Always-on enabled, applying theme immediately"); ApplyTheme(__instance); themeApplied = true; } if (Plugin.AlwaysOnScreen.Value && (Object)(object)__instance.terminalNodes != (Object)null && __instance.terminalNodes.specialNodes != null && __instance.terminalNodes.specialNodes.Count > 1) { Plugin.Logger.LogInfo((object)"[ALWAYS-ON] Loading store node for always-on screen"); __instance.LoadNewNode(__instance.terminalNodes.specialNodes[1]); } } private static void LogGameObjectHierarchy(GameObject obj, int depth, int maxDepth) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)obj == (Object)null || depth > maxDepth) { return; } string text = new string(' ', depth * 2); Plugin.Logger.LogInfo((object)("[HIERARCHY] " + text + "└─ " + ((Object)obj).name)); Component[] components = obj.GetComponents<Component>(); foreach (Component val in components) { if ((Object)(object)val != (Object)null && !(val is Transform)) { Plugin.Logger.LogInfo((object)("[HIERARCHY] " + text + " ├─ Component: " + ((object)val).GetType().Name)); } } foreach (Transform item in obj.transform) { LogGameObjectHierarchy(((Component)item).gameObject, depth + 1, maxDepth); } } [HarmonyPatch("waitUntilFrameEndToSetActive")] [HarmonyPrefix] private static void WaitUntilFrameEndToSetActivePrefix(Terminal __instance, ref bool active) { if (!isPlayerInShip) { active = false; } else if (Plugin.AlwaysOnScreen.Value) { Plugin.Logger.LogInfo((object)$"[ALWAYS-ON] Forcing terminal canvas to stay active (was: {active})"); active = true; } } [HarmonyPatch("SetTerminalInUseClientRpc")] [HarmonyPostfix] private static void SetTerminalInUseClientRpcPostfix(Terminal __instance, bool inUse) { if (!isPlayerInShip) { if ((Object)(object)terminalLight != (Object)null) { ((Behaviour)terminalLight).enabled = false; } } else if (Plugin.AlwaysOnLight.Value && (Object)(object)terminalLight != (Object)null) { ((Behaviour)terminalLight).enabled = true; Plugin.Logger.LogInfo((object)$"[ALWAYS-ON] Terminal light kept enabled (inUse: {inUse})"); } } private static void ApplyTheme(Terminal terminal) { //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_03c0: Unknown result type (might be due to invalid IL or missing references) //IL_03d0: Unknown result type (might be due to invalid IL or missing references) //IL_03db: Unknown result type (might be due to invalid IL or missing references) //IL_03e6: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_0406: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_0414: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_04d1: Unknown result type (might be due to invalid IL or missing references) //IL_04e1: Unknown result type (might be due to invalid IL or missing references) //IL_04ec: Unknown result type (might be due to invalid IL or missing references) //IL_04f7: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_052f: Unknown result type (might be due to invalid IL or missing references) //IL_053f: Unknown result type (might be due to invalid IL or missing references) //IL_054a: Unknown result type (might be due to invalid IL or missing references) //IL_0555: Unknown result type (might be due to invalid IL or missing references) //IL_048b: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)terminal.screenText == (Object)null || (Object)(object)terminal.screenText.textComponent == (Object)null) { Plugin.Logger.LogWarning((object)"[THEME] Cannot apply theme - screenText or textComponent is null"); return; } bool flag = !string.IsNullOrWhiteSpace(Plugin.CustomTextColor.Value); bool flag2 = !string.IsNullOrWhiteSpace(Plugin.CustomScreenLightColor.Value); bool flag3 = !string.IsNullOrWhiteSpace(Plugin.CustomBackgroundColor.Value); Color val = default(Color); Color val2 = default(Color); Color val3 = default(Color); if (flag || flag2 || flag3) { Plugin.Logger.LogInfo((object)"[THEME] Using custom colors (overriding theme)"); val = (Color)(flag ? ParseCustomColor(Plugin.CustomTextColor.Value) : new Color(0f, 1f, 0f, 1f)); val2 = (Color)(flag2 ? ParseCustomColor(Plugin.CustomScreenLightColor.Value) : new Color(0f, 1f, 0f, 1f)); val3 = (Color)(flag3 ? ParseCustomColor(Plugin.CustomBackgroundColor.Value) : new Color(0f, 0.08f, 0f, 1f)); } else { string text = Plugin.TerminalTheme.Value.ToLower(); switch (text) { case "green": ((Color)(ref val))..ctor(0f, 1f, 0f, 1f); ((Color)(ref val2))..ctor(0f, 1f, 0f, 1f); ((Color)(ref val3))..ctor(0f, 0.08f, 0f, 1f); break; case "amber": ((Color)(ref val))..ctor(1f, 0.75f, 0f, 1f); ((Color)(ref val2))..ctor(1f, 0.75f, 0f, 1f); ((Color)(ref val3))..ctor(0.08f, 0.06f, 0f, 1f); break; case "blue": ((Color)(ref val))..ctor(0.3f, 0.7f, 1f, 1f); ((Color)(ref val2))..ctor(0.3f, 0.7f, 1f, 1f); ((Color)(ref val3))..ctor(0f, 0.03f, 0.08f, 1f); break; case "red": ((Color)(ref val))..ctor(1f, 0.2f, 0.2f, 1f); ((Color)(ref val2))..ctor(1f, 0.2f, 0.2f, 1f); ((Color)(ref val3))..ctor(0.08f, 0f, 0f, 1f); break; case "purple": ((Color)(ref val))..ctor(0.8f, 0.4f, 1f, 1f); ((Color)(ref val2))..ctor(0.8f, 0.4f, 1f, 1f); ((Color)(ref val3))..ctor(0.06f, 0f, 0.08f, 1f); break; case "cyan": ((Color)(ref val))..ctor(0f, 1f, 1f, 1f); ((Color)(ref val2))..ctor(0f, 1f, 1f, 1f); ((Color)(ref val3))..ctor(0f, 0.08f, 0.08f, 1f); break; default: Plugin.Logger.LogInfo((object)"[THEME] Using default terminal colors"); return; } Plugin.Logger.LogInfo((object)("[THEME] Using theme preset: " + text)); } ((Graphic)terminal.screenText.textComponent).color = val; Plugin.Logger.LogInfo((object)$"[THEME] Applied text color: R:{val.r:F2} G:{val.g:F2} B:{val.b:F2}"); if (terminal.screenText.caretColor != val) { terminal.screenText.caretColor = val; Plugin.Logger.LogInfo((object)"[THEME] Applied caret color"); } if ((Object)(object)terminal.terminalUIScreen != (Object)null) { TextMeshProUGUI[] componentsInChildren = ((Component)terminal.terminalUIScreen).GetComponentsInChildren<TextMeshProUGUI>(true); Plugin.Logger.LogInfo((object)$"[THEME] Found {componentsInChildren.Length} TextMeshProUGUI components"); TextMeshProUGUI[] array = componentsInChildren; foreach (TextMeshProUGUI val4 in array) { if (!((Object)(object)val4 == (Object)(object)terminal.screenText.textComponent)) { ((Graphic)val4).color = val; Plugin.Logger.LogInfo((object)("[THEME] Colored text component: " + ((Object)((Component)val4).gameObject).name)); } } } if ((Object)(object)terminalLight != (Object)null) { terminalLight.color = val2; Plugin.Logger.LogInfo((object)$"[THEME] Applied light color: R:{val2.r:F2} G:{val2.g:F2} B:{val2.b:F2}"); } else { Plugin.Logger.LogInfo((object)"[THEME] No terminal light found to color"); } if ((Object)(object)terminalBackground != (Object)null) { ((Graphic)terminalBackground).color = val3; Plugin.Logger.LogInfo((object)$"[THEME] Applied background color: R:{val3.r:F2} G:{val3.g:F2} B:{val3.b:F2}"); } else { Plugin.Logger.LogInfo((object)"[THEME] No terminal background found to color"); } } private static Color ParseCustomColor(string colorString) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: 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) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) try { string[] array = colorString.Split(','); if (array.Length == 3) { float num = float.Parse(array[0].Trim()) / 255f; float num2 = float.Parse(array[1].Trim()) / 255f; float num3 = float.Parse(array[2].Trim()) / 255f; return new Color(num, num2, num3, 1f); } } catch (Exception ex) { Plugin.Logger.LogError((object)("[THEME] Failed to parse custom color '" + colorString + "': " + ex.Message)); } Plugin.Logger.LogWarning((object)"[THEME] Using default green color due to parse error"); return new Color(0f, 1f, 0f, 1f); } } [HarmonyPatch(typeof(PlayerControllerB))] internal class PlayerControllerPatches { private static float updateTimer; private const float UPDATE_INTERVAL = 0.5f; [HarmonyPatch("Update")] [HarmonyPostfix] private static void UpdatePostfix(PlayerControllerB __instance) { if (((NetworkBehaviour)__instance).IsOwner && __instance.isPlayerControlled) { updateTimer += Time.deltaTime; if (!(updateTimer < 0.5f)) { updateTimer = 0f; UpdateTerminalPerformanceState(__instance); } } } private static void UpdateTerminalPerformanceState(PlayerControllerB player) { if ((Object)(object)TerminalPatches.cachedTerminal == (Object)null) { Terminal val = Object.FindObjectOfType<Terminal>(); if (!((Object)(object)val != (Object)null)) { return; } TerminalPatches.cachedTerminal = val; } bool isInHangarShipRoom = player.isInHangarShipRoom; if (isInHangarShipRoom == TerminalPatches.isPlayerInShip) { return; } TerminalPatches.isPlayerInShip = isInHangarShipRoom; if (isInHangarShipRoom) { if (Plugin.AlwaysOnScreen.Value && (Object)(object)TerminalPatches.cachedTerminal.terminalUIScreen != (Object)null) { GameObject gameObject = ((Component)TerminalPatches.cachedTerminal.terminalUIScreen).gameObject; if ((Object)(object)gameObject != (Object)null) { gameObject.SetActive(true); Plugin.Logger.LogInfo((object)"[PERFORMANCE] Player entered ship - terminal screen enabled"); } } if (Plugin.AlwaysOnLight.Value && (Object)(object)TerminalPatches.terminalLight != (Object)null) { ((Behaviour)TerminalPatches.terminalLight).enabled = true; Plugin.Logger.LogInfo((object)"[PERFORMANCE] Player entered ship - terminal light enabled"); } return; } if ((Object)(object)TerminalPatches.cachedTerminal.terminalUIScreen != (Object)null) { GameObject gameObject2 = ((Component)TerminalPatches.cachedTerminal.terminalUIScreen).gameObject; if ((Object)(object)gameObject2 != (Object)null) { gameObject2.SetActive(false); Plugin.Logger.LogInfo((object)"[PERFORMANCE] Player exited ship - terminal screen disabled"); } } if ((Object)(object)TerminalPatches.terminalLight != (Object)null) { ((Behaviour)TerminalPatches.terminalLight).enabled = false; Plugin.Logger.LogInfo((object)"[PERFORMANCE] Player exited ship - terminal light disabled"); } } } }