using System; using System.Collections.Generic; using System.IO; using HarmonyLib; using RedLoader; using RedLoader.Utils; using Sons.Weapon; using SonsSdk; using SonsSdk.Attributes; using TheForest.Utils; using UnityEngine; using SonsInput = Sons.Input.InputSystem; namespace FullAuto; public enum FireMode { Auto, Burst, Semi } public class FullAuto : SonsMod { internal const float MinRpm = 60f; internal const float MaxRpm = 3000f; internal const float ReloadMultiplier = 2f; internal static bool Enabled = true; internal static float Rpm = 900f; internal static readonly Dictionary Modes = new(); internal static string CurrentGun = ""; internal static FireMode Mode { get => Modes.TryGetValue(CurrentGun, out var mode) ? mode : FireMode.Auto; set => Modes[CurrentGun] = value; } internal static bool FastReload; internal static bool AutoReload; internal static KeyCode FlashlightKey = KeyCode.F; internal static bool Verbose; private static string _configPath; public FullAuto() { HarmonyPatchAll = true; OnUpdateCallback = OnUpdate; } protected override void OnSdkInitialized() { _configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "FullAuto.txt"); Load(); Save(); RLog.Msg($"FullAuto loaded. RPM: {Rpm}. Fast reload: {FastReload}. Auto reload: {AutoReload}. Flashlight key: {FlashlightKey}. Guns only. Middle mouse cycles modes. Console: fullauto"); } private void OnUpdate() { CheckFireInputPatch.UpdateTrigger(); if (!CheckFireInputPatch.HoldingGun) CheckFireInputPatch.RestoreReloadSpeed(); if (Input.GetKeyDown(FlashlightKey) && CheckFireInputPatch.InGame && CheckFireInputPatch.HoldingRanged) CheckFireInputPatch.ToggleFlashlight(); if (!Enabled || !CheckFireInputPatch.InGame || !CheckFireInputPatch.HoldingGun) return; if (Input.GetMouseButtonDown(2)) { Mode = Mode switch { FireMode.Auto => FireMode.Semi, FireMode.Semi => FireMode.Burst, _ => FireMode.Auto }; CheckFireInputPatch.CancelBurst(); Save(); ShowMode(); RLog.Msg($"FullAuto {CurrentGun} mode: {ModeName()}"); } } [DebugCommand("fullauto")] private static void FullAutoCommand(string args) { var parts = (args ?? string.Empty).Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) { Announce(); return; } switch (parts[0].ToLowerInvariant()) { case "on": Enabled = true; break; case "off": Enabled = false; break; case "debug": Verbose = !Verbose; SonsTools.ShowMessage($"FullAuto debug: {Verbose}"); RLog.Msg($"FullAuto debug: {Verbose}"); return; case "fastreload": if (parts.Length > 1 && (parts[1].ToLowerInvariant() == "on" || parts[1].ToLowerInvariant() == "off")) { FastReload = parts[1].ToLowerInvariant() == "on"; if (!FastReload) CheckFireInputPatch.RestoreReloadSpeed(); Save(); var text = $"Fast reload {(FastReload ? "ON" : "OFF")}"; SonsTools.ShowMessage(text); RLog.Msg(text); return; } Usage(); return; case "autoreload": if (parts.Length > 1 && (parts[1].ToLowerInvariant() == "on" || parts[1].ToLowerInvariant() == "off")) { AutoReload = parts[1].ToLowerInvariant() == "on"; Save(); var autoText = $"Auto reload {(AutoReload ? "ON" : "OFF")}"; SonsTools.ShowMessage(autoText); RLog.Msg(autoText); return; } Usage(); return; case "rpm": if (parts.Length > 1 && float.TryParse(parts[1], out var value)) { Rpm = Mathf.Clamp(value, MinRpm, MaxRpm); Save(); break; } Usage(); return; default: Usage(); return; } CheckFireInputPatch.CancelBurst(); Announce(); } private static void Usage() { var text = $"Usage: fullauto [on|off|debug], fullauto rpm <{MinRpm}-{MaxRpm}>, fullauto fastreload on|off, fullauto autoreload on|off"; SonsTools.ShowMessage(text, 5f); RLog.Msg(text); } private static string ModeName() { return Mode switch { FireMode.Auto => "Full-automatic", FireMode.Burst => "3-Round Burst", _ => "Semi-automatic" }; } internal static void ShowMode() { SonsTools.ShowMessage(ModeName(), 2f); } internal static void Announce() { var reload = (FastReload ? ", fast reload on" : "") + (AutoReload ? ", auto reload on" : ""); var text = Enabled ? $"{ModeName()} ({Rpm} RPM{reload})" : $"FullAuto OFF{reload}"; SonsTools.ShowMessage(text); RLog.Msg(text); } private static void Load() { try { if (!File.Exists(_configPath)) return; foreach (var line in File.ReadAllLines(_configPath)) { var kv = line.Split('=', 2); if (kv.Length != 2) continue; var key = kv[0].Trim().ToLowerInvariant(); var val = kv[1].Trim(); if (key == "rpm" && float.TryParse(val, out var rpm)) Rpm = Mathf.Clamp(rpm, MinRpm, MaxRpm); else if (key == "fastreload" && bool.TryParse(val, out var fast)) FastReload = fast; else if (key == "autoreload" && bool.TryParse(val, out var auto)) AutoReload = auto; else if (key == "flashlightkey" && Enum.TryParse(val, true, out var flashKey)) FlashlightKey = flashKey; else if (key.StartsWith("mode.") && Enum.TryParse(val, true, out var gunMode)) Modes[key.Substring(5)] = gunMode; } } catch (Exception e) { RLog.Error($"FullAuto could not read config: {e.Message}"); } } private static void Save() { try { var sb = new System.Text.StringBuilder(); sb.Append($"rpm={Rpm}\n"); sb.Append($"fastreload={FastReload.ToString().ToLowerInvariant()}\n"); sb.Append($"autoreload={AutoReload.ToString().ToLowerInvariant()}\n"); sb.Append($"flashlightkey={FlashlightKey}\n"); foreach (var gun in CheckFireInputPatch.GunKeys) { var mode = Modes.TryGetValue(gun, out var m) ? m : FireMode.Auto; sb.Append($"mode.{gun}={mode.ToString().ToLowerInvariant()}\n"); } File.WriteAllText(_configPath, sb.ToString()); } catch (Exception e) { RLog.Error($"FullAuto could not write config: {e.Message}"); } } } [HarmonyPatch(typeof(RangedWeaponController), nameof(RangedWeaponController.CheckFireInput))] internal static class CheckFireInputPatch { private const int MaxShotsPerFrame = 4; private const int BurstSize = 3; private const float ReleaseTime = 0.15f; private const float AutoReloadDelay = 0.2f; private const float AutoReloadNextDelay = 0.05f; private const float AutoReloadRoundTime = 0.9f; private static readonly Dictionary Guns = new() { { "CompactPistolWeaponController", "pistol" }, { "RevolverWeaponController", "revolver" }, { "ShotgunWeaponController", "shotgun" }, { "RifleAnimatorController", "rifle" } }; internal static IEnumerable GunKeys => Guns.Values; private static readonly Dictionary GunNames = new(); private static readonly Dictionary Allowed = new(); private static readonly Dictionary FireDelays = new(); private static readonly HashSet Reported = new(); private static float _nextShot; private static int _burstLeft; private static float _lastGunTime = -1f; private static IntPtr _lastGunPtr = IntPtr.Zero; private static bool _triggerDown; private static float _releasedFor; private static bool _actionFailed; private static bool _holding; private static RangedWeaponController _speedGun; private static Animator _speedPlayer; private static Animator _speedWeapon; private static float _playerSpeed = 1f; private static float _weaponSpeed = 1f; private static RangedWeaponController _currentRanged; private static float _rangedTime = -1f; private static bool _flashlightOff; private static IntPtr _lightsOwner = IntPtr.Zero; private static readonly List Lights = new(); private static float _emptySince = -1f; private static float _nextReloadTry; private static bool _autoReloading; private static IntPtr _autoReloadGun = IntPtr.Zero; private static float _lastReloadCall = -10f; private static bool _sawReloading; private static bool _loggedHook; internal static bool HoldingGun => _lastGunTime >= 0f && Time.time - _lastGunTime < 0.25f; internal static bool InGame => GameState.IsPlayerControllable && !Cursor.visible && !LocalPlayer.IsInInventory && Time.timeScale > 0f; internal static bool HoldingRanged => _currentRanged != null && _rangedTime >= 0f && Time.time - _rangedTime < 0.25f; private static void FindLights(RangedWeaponController controller) { Lights.Clear(); _lightsOwner = controller.Pointer; try { foreach (var light in controller.GetComponentsInChildren(true)) { if (light && light.type == LightType.Spot) Lights.Add(light); } if (FullAuto.Verbose) { foreach (var light in Lights) RLog.Msg($"FullAuto flashlight found: {light.gameObject.name}"); if (Lights.Count == 0) RLog.Msg($"FullAuto no flashlight found on {controller.GetIl2CppType().Name}"); } } catch (Exception e) { RLog.Warning($"FullAuto could not find flashlight: {e.Message}"); } } internal static void ToggleFlashlight() { var controller = _currentRanged; if (!controller) return; FindLights(controller); if (Lights.Count == 0) return; _flashlightOff = !_flashlightOff; ApplyFlashlight(); if (FullAuto.Verbose) RLog.Msg($"FullAuto flashlight {(_flashlightOff ? "off" : "on")}"); } private static void ApplyFlashlight() { foreach (var light in Lights) { if (light && light.enabled == _flashlightOff) light.enabled = !_flashlightOff; } } private static void TryAutoReload(RangedWeaponController controller) { if (!FullAuto.AutoReload || !InGame) { _emptySince = -1f; _autoReloading = false; return; } if (controller._isReloading || controller.IsReloading() || controller.IsReloadQueued()) { _sawReloading = true; _emptySince = -1f; return; } var weapon = controller.GetRangedWeapon(); if (!weapon) return; var ammo = weapon.GetAmmo(); if (ammo == null) return; if (_autoReloading && (_autoReloadGun != controller.Pointer || ammo.IsFull())) _autoReloading = false; if (FirePressed()) { _autoReloading = false; _emptySince = -1f; return; } if (!ammo.IsEmpty() && !_autoReloading) { _emptySince = -1f; return; } var now = Time.time; if (_emptySince < 0f) { _emptySince = now; return; } var wait = _autoReloading ? AutoReloadNextDelay : AutoReloadDelay; if (now - _emptySince < wait || now < _nextReloadTry) return; var roundTime = FullAuto.FastReload ? AutoReloadRoundTime / FullAuto.ReloadMultiplier : AutoReloadRoundTime; if (_autoReloading && !_sawReloading && now - _lastReloadCall < roundTime) return; _nextReloadTry = now + 0.3f; if (!controller.CanReload()) { _autoReloading = false; return; } controller.Reload(); _autoReloading = true; _autoReloadGun = controller.Pointer; _lastReloadCall = now; _sawReloading = false; if (FullAuto.Verbose) RLog.Msg($"FullAuto auto reload {controller.GetIl2CppType().Name}, ammo {ammo.GetRemainingAmmo()}/{ammo.GetCapacity()}"); } internal static void CancelBurst() { _burstLeft = 0; _holding = false; } private static void UpdateReloadSpeed(RangedWeaponController controller, bool allowed) { var want = allowed && FullAuto.FastReload && (controller._isReloading || controller.IsReloading()); if (!want) { RestoreReloadSpeed(); return; } if (_speedGun != null && _speedGun.Pointer == controller.Pointer) return; RestoreReloadSpeed(); try { _speedGun = controller; _speedPlayer = controller._playerAnimator; _speedWeapon = controller._weaponAnimator; if (_speedPlayer) { _playerSpeed = _speedPlayer.speed; _speedPlayer.speed = _playerSpeed * FullAuto.ReloadMultiplier; } if (_speedWeapon) { _weaponSpeed = _speedWeapon.speed; _speedWeapon.speed = _weaponSpeed * FullAuto.ReloadMultiplier; } if (FullAuto.Verbose) RLog.Msg($"FullAuto fast reload start, player {_playerSpeed} weapon {_weaponSpeed}"); } catch (Exception e) { RLog.Warning($"FullAuto could not speed up reload: {e.Message}"); } } internal static void RestoreReloadSpeed() { if (_speedGun == null) return; try { if (_speedPlayer) _speedPlayer.speed = _playerSpeed; if (_speedWeapon) _speedWeapon.speed = _weaponSpeed; } catch (Exception e) { RLog.Warning($"FullAuto could not restore reload speed: {e.Message}"); } _speedGun = null; _speedPlayer = null; _speedWeapon = null; if (FullAuto.Verbose) RLog.Msg("FullAuto fast reload end"); } private static void HoldAfterBurst(RangedWeaponController controller) { var first = !_holding; _holding = true; try { controller.ClearTriggeredAttack(); if (first) controller.AttackEndCallback(); controller._attackState = RangedWeaponController.AttackState.Idle; } catch (Exception e) { if (first) RLog.Warning($"FullAuto could not end attack: {e.Message}"); } if (first && FullAuto.Verbose) { RLog.Msg("FullAuto burst done, holding"); DumpAnimator("player", controller._playerAnimator); DumpAnimator("weapon", controller._weaponAnimator); } } private static void DumpAnimator(string label, Animator animator) { if (!animator) { RLog.Msg($"FullAuto {label} animator: none"); return; } try { var active = new List(); foreach (var p in animator.parameters) { if (p.type == AnimatorControllerParameterType.Bool || p.type == AnimatorControllerParameterType.Trigger) { if (animator.GetBool(p.name)) active.Add($"{p.name}({p.type})"); } } RLog.Msg($"FullAuto {label} animator active: {string.Join(", ", active)}"); } catch (Exception e) { RLog.Warning($"FullAuto could not read {label} animator: {e.Message}"); } } private static bool FirePressed() { if (Input.GetMouseButton(0)) return true; if (_actionFailed) return false; try { return SonsInput.InputMapping.@default.PrimaryAction.IsPressed(); } catch (Exception e) { _actionFailed = true; RLog.Warning($"FullAuto could not read PrimaryAction: {e.Message}"); return false; } } internal static void UpdateTrigger() { if (FirePressed()) { _releasedFor = 0f; if (_triggerDown) return; _triggerDown = true; if (FullAuto.Enabled && FullAuto.Mode == FireMode.Burst && HoldingGun && InGame) StartBurst(); return; } _releasedFor += Time.unscaledDeltaTime; if (_triggerDown && _releasedFor >= ReleaseTime) { _triggerDown = false; _holding = false; if (FullAuto.Verbose) RLog.Msg("FullAuto trigger released"); } } private static void StartBurst() { if (_burstLeft > 0) return; _burstLeft = BurstSize; _nextShot = 0f; if (FullAuto.Verbose) RLog.Msg("FullAuto burst start"); } private static bool Prefix(RangedWeaponController __instance) { if (!_loggedHook) { _loggedHook = true; RLog.Msg("FullAuto: CheckFireInput hook is firing"); } if (!__instance.IsLocalPlayer()) return true; var allowed = IsAllowed(__instance); var ptr = __instance.Pointer; _currentRanged = __instance; _rangedTime = Time.time; if (_flashlightOff) { if (_lightsOwner != ptr) FindLights(__instance); ApplyFlashlight(); } UpdateReloadSpeed(__instance, allowed); if (allowed) TryAutoReload(__instance); if (allowed && GunNames.TryGetValue(ptr, out var gunName)) { var switched = !HoldingGun || ptr != _lastGunPtr; FullAuto.CurrentGun = gunName; if (switched) { CancelBurst(); if (FullAuto.Enabled) FullAuto.ShowMode(); } } if (allowed) _lastGunPtr = ptr; _lastGunTime = allowed ? Time.time : -1f; if (!FullAuto.Enabled || !allowed) return true; if (FullAuto.Mode == FireMode.Semi) return true; var pressed = FirePressed(); var held = _triggerDown || pressed; if (!InGame) { _burstLeft = 0; return !held; } if (__instance._isReloading || __instance.IsReloading()) return Stop(); var weapon = __instance.GetRangedWeapon(); if (!weapon) return Stop(); var ammo = weapon.GetAmmo(); if (ammo == null || ammo.GetRemainingAmmo() <= 0) return Stop(); if (FullAuto.Mode == FireMode.Burst && _burstLeft <= 0) { if (!held) return true; HoldAfterBurst(__instance); return false; } if (FullAuto.Mode == FireMode.Auto && !pressed) return true; if (__instance._mustAimToFire && !__instance.IsAiming) return Stop(); var now = Time.time; var interval = 60f / FullAuto.Rpm; if (_nextShot < now - interval) _nextShot = now; var shots = 0; while (_nextShot <= now && shots < MaxShotsPerFrame && ammo.GetRemainingAmmo() > 0) { if (FullAuto.Mode == FireMode.Burst) { if (_burstLeft <= 0) break; _burstLeft--; } Fire(__instance, ammo); _nextShot += interval; shots++; } if (FireDelays.TryGetValue(__instance.Pointer, out var delay)) __instance._fireDelay = delay; if (FullAuto.Mode == FireMode.Burst && _burstLeft <= 0 && held) HoldAfterBurst(__instance); return false; } private static bool Stop() { CancelBurst(); return true; } private static void Fire(RangedWeaponController controller, RangedWeapon.Ammo ammo) { var before = ammo.GetRemainingAmmo(); controller._fireDelay = 0f; controller._nextFireDelay = 0f; controller.FireWeapon(); if (controller._playFireAudio == RangedWeaponController.FireAudio.OnAnimShootCallback) { controller.TriggerShotFiredAudio(); if (controller.ShouldShowMuzzleFlash()) controller.OnMuzzleFlash(); } controller._attackState = RangedWeaponController.AttackState.Idle; var after = ammo.GetRemainingAmmo(); var name = controller.GetIl2CppType().Name; if (Reported.Add(name)) RLog.Msg($"FullAuto first shot with {name}: ammo {before} -> {after}, audio {controller._playFireAudio}"); if (FullAuto.Verbose) RLog.Msg($"FullAuto shot {name} ({FullAuto.Mode}): ammo {before} -> {after}"); } private static bool IsAllowed(RangedWeaponController controller) { var ptr = controller.Pointer; if (Allowed.TryGetValue(ptr, out var allowed)) return allowed; var name = controller.GetIl2CppType().Name; allowed = Guns.TryGetValue(name, out var gunKey); Allowed[ptr] = allowed; if (allowed) { FireDelays[ptr] = controller._fireDelay; GunNames[ptr] = gunKey; } RLog.Msg($"FullAuto {name}: {(allowed ? "full-auto" : "ignored")}"); return allowed; } }