using System; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("TerminalRelativeScroll")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("TerminalRelativeScroll")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("19e0a1ba-5c2c-4d94-99f0-91c651f9621e")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] namespace TerminalRelativeScroll; [BepInPlugin("brabru.TerminalScrollPatch", "Terminal Scroll Patch", "1.0.0")] [BepInProcess("Lethal Company.exe")] public class Plugin : BaseUnityPlugin { internal static Plugin Instance; internal static PluginSettings Settings; internal static ManualLogSource StaticLogger; private void Awake() { Instance = this; StaticLogger = ((BaseUnityPlugin)this).Logger; Settings = new PluginSettings(((BaseUnityPlugin)this).Config); StaticLogger.LogInfo((object)$"Config: CustomScroll={Settings.RelativeScrollEnabled.Value}, LinesToScroll={Settings.LinesToScroll.Value}"); Harmony.CreateAndPatchAll(typeof(TerminalScrollPatch), (string)null); ((BaseUnityPlugin)this).Logger.LogInfo((object)"TerminalScrollPatch mod loaded (v81)"); } } public class PluginSettings { public ConfigEntry RelativeScrollEnabled { get; } public ConfigEntry LinesToScroll { get; } public PluginSettings(ConfigFile config) { RelativeScrollEnabled = config.Bind("General", "CustomScrollEnabled", true, "If true, terminal scrolling is relative to line count. If false, vanilla behavior."); LinesToScroll = config.Bind("General", "LinesToScroll", 8, "Number of lines to scroll when CustomScrollEnabled is true."); } } [HarmonyPatch] internal class TerminalScrollPatch { private static string cachedText; private static float scrollAmount; private static float originalScrollbarValue; private static float currentScrollDelta; private static bool shouldOverride; private static FieldInfo terminalField; private static bool terminalFieldFromStartOfRound; static TerminalScrollPatch() { cachedText = ""; scrollAmount = 1f / 3f; terminalField = AccessTools.GetDeclaredFields(typeof(PlayerControllerB)).FirstOrDefault((FieldInfo f) => f.FieldType == typeof(Terminal)); if (terminalField == null) { terminalField = AccessTools.GetDeclaredFields(typeof(StartOfRound)).FirstOrDefault((FieldInfo f) => f.FieldType == typeof(Terminal)); terminalFieldFromStartOfRound = true; } else { terminalFieldFromStartOfRound = false; } if (terminalField != null) { Plugin.StaticLogger.LogInfo((object)("TerminalScrollPatch: found Terminal field '" + terminalField.Name + "' in " + terminalField.DeclaringType.Name)); } else { Plugin.StaticLogger.LogWarning((object)"TerminalScrollPatch: no Terminal field found! Will fallback to FindObjectOfType."); } } [HarmonyTargetMethod] public static MethodBase TargetMethod() { string[] obj = new string[3] { "ScrollMouse_performed", "UpdateTerminalMouseScroll", "TerminalScroll_performed" }; Type typeFromHandle = typeof(PlayerControllerB); string[] array = obj; foreach (string text in array) { MethodInfo methodInfo = AccessTools.Method(typeFromHandle, text, new Type[1] { typeof(CallbackContext) }, (Type[])null); if (methodInfo != null) { Plugin.StaticLogger.LogInfo((object)("TerminalScrollPatch: method found -> " + methodInfo.DeclaringType?.Name + "." + methodInfo.Name)); return methodInfo; } } Plugin.StaticLogger.LogError((object)"TerminalScrollPatch: no terminal scroll method found! Mod will not work."); return null; } [HarmonyPrefix] private static void Prefix(PlayerControllerB __instance, CallbackContext context) { Plugin.StaticLogger.LogDebug((object)"Prefix: Entered"); currentScrollDelta = ((CallbackContext)(ref context)).ReadValue(); Plugin.StaticLogger.LogDebug((object)$"Prefix: scrollDelta = {currentScrollDelta}"); if (Mathf.Approximately(currentScrollDelta, 0f)) { shouldOverride = false; Plugin.StaticLogger.LogDebug((object)"Prefix: scrollDelta zero, setting shouldOverride=false"); return; } if ((Object)(object)__instance.terminalScrollVertical != (Object)null) { originalScrollbarValue = __instance.terminalScrollVertical.value; Plugin.StaticLogger.LogDebug((object)$"Prefix: originalScrollbarValue = {originalScrollbarValue}"); } else { Plugin.StaticLogger.LogWarning((object)"Prefix: terminalScrollVertical is null!"); } Terminal terminal = GetTerminal(__instance); bool inTerminalMenu = __instance.inTerminalMenu; bool flag = Plugin.Settings != null; bool flag2 = flag && Plugin.Settings.RelativeScrollEnabled.Value; bool flag3 = (Object)(object)terminal != (Object)null; Plugin.StaticLogger.LogDebug((object)$"Prefix: inTerminalMenu={inTerminalMenu}, settingsExist={flag}, relativeEnabled={flag2}, terminalExists={flag3}"); shouldOverride = inTerminalMenu && flag && flag2 && flag3; Plugin.StaticLogger.LogDebug((object)$"Prefix: shouldOverride = {shouldOverride}"); } [HarmonyPostfix] private static void Postfix(PlayerControllerB __instance) { Plugin.StaticLogger.LogDebug((object)"Postfix: Entered"); if (!shouldOverride) { Plugin.StaticLogger.LogDebug((object)"Postfix: shouldOverride false, exiting"); return; } Scrollbar terminalScrollVertical = __instance.terminalScrollVertical; if ((Object)(object)terminalScrollVertical == (Object)null) { Plugin.StaticLogger.LogWarning((object)"Postfix: scrollbar is null"); return; } Terminal terminal = GetTerminal(__instance); if ((Object)(object)terminal == (Object)null) { Plugin.StaticLogger.LogWarning((object)"Postfix: terminal is null"); return; } float num = Mathf.Sign(currentScrollDelta); Plugin.StaticLogger.LogDebug((object)$"Postfix: direction = {num}"); string currentText = terminal.currentText; if (string.CompareOrdinal(currentText, cachedText) != 0) { cachedText = currentText; int num2 = cachedText.Count((char c) => c == '\n') + 1; scrollAmount = (float)Plugin.Settings.LinesToScroll.Value / (float)num2; Plugin.StaticLogger.LogInfo((object)$"Recalculated scroll: lines={num2}, scrollAmount={scrollAmount:F4}"); } float num3 = originalScrollbarValue + num * scrollAmount; num3 = Mathf.Clamp01(num3); Plugin.StaticLogger.LogInfo((object)$"Postfix: original={originalScrollbarValue}, desired={num3}, delta={num * scrollAmount}"); terminalScrollVertical.value = num3; } private static Terminal GetTerminal(PlayerControllerB player) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown if (terminalField != null) { if (!terminalFieldFromStartOfRound) { return (Terminal)terminalField.GetValue(player); } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null) { return (Terminal)terminalField.GetValue(instance); } } return Object.FindObjectOfType(); } }