22 Commits
Author SHA1 Message Date
Justin Oros 840171451c Add downloads and latest version badges 2026-09-28 12:42:27 -07:00
Justin Oros 6def40ffeb Auto reload no longer waits for trigger release 2026-09-20 23:35:53 -07:00
Justin Oros 6defa601d5 Auto reload presses the reload key so the game plays its reload animation 2026-09-20 23:24:34 -07:00
Justin Oros 9f67baf307 Auto reload uses the game reload queue instead of calling Reload directly 2026-09-20 23:11:33 -07:00
Justin Oros f6c51a51be Auto reload uses the game reload queue instead of calling Reload directly 2026-09-20 23:11:04 -07:00
Justin Oros 35fc5dc2a4 Auto reload waits for trigger release and paces shell reloads 2026-09-20 22:52:53 -07:00
Justin Oros 2f41fa01c8 Auto reload keeps loading until the gun is full 2026-09-20 22:41:36 -07:00
Justin Oros 4eea13b0a8 Add autoreload console option, bump to 1.5.0 2026-09-20 22:35:07 -07:00
Justin Oros a3d922fb5a Add autoreload console option 2026-09-20 22:33:40 -07:00
Justin Oros 6ce2c6bdf6 Remember fire mode per gun across restarts 2026-09-17 09:05:38 -07:00
Justin Oros 13bebcea8d Remember fire mode per gun, bump to 1.4.0 2026-09-16 22:26:14 -07:00
Justin Oros 1457284cd5 Remember fire mode per gun across restarts 2026-09-16 22:25:51 -07:00
Justin Oros 3cf60a89b5 Remember fire mode per gun across restarts 2026-09-16 22:25:24 -07:00
Justin Oros bca9b113c9 Update manifest description 2026-09-16 18:21:46 -07:00
Justin Oros 0d333b2367 Bump version to 1.3.0 2026-09-16 18:19:41 -07:00
Justin Oros 3ab0c52260 Update documentation 2026-09-16 18:19:11 -07:00
Justin Oros 2a495fbf29 Toggle gun flashlight silently with F 2026-09-16 18:18:04 -07:00
Justin Oros b9aa1d92e9 Toggle gun flashlight silently with F 2026-09-16 18:11:03 -07:00
Justin Oros 0bdd0e65e8 Bump version to 1.2.0 2026-09-16 16:21:03 -07:00
Justin Oros 10d6f82d0d Do not fire while menus or inventory are open 2026-09-16 16:16:43 -07:00
Justin Oros c42b6ac94d Update README.md 2026-09-16 16:06:35 -07:00
Justin Oros 3381c89f12 Add fastreload console option 2026-09-16 16:05:28 -07:00
3 changed files with 384 additions and 26 deletions
+368 -20
View File
@@ -24,10 +24,22 @@ public class FullAuto : SonsMod
{ {
internal const float MinRpm = 60f; internal const float MinRpm = 60f;
internal const float MaxRpm = 3000f; internal const float MaxRpm = 3000f;
internal const float ReloadMultiplier = 2f;
internal static bool Enabled = true; internal static bool Enabled = true;
internal static float Rpm = 900f; internal static float Rpm = 900f;
internal static FireMode Mode = FireMode.Auto; internal static readonly Dictionary<string, FireMode> 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 string ReloadKey = "r";
internal static bool Verbose; internal static bool Verbose;
private static string _configPath; private static string _configPath;
@@ -42,14 +54,22 @@ public class FullAuto : SonsMod
{ {
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "FullAuto.txt"); _configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "FullAuto.txt");
Load(); Load();
RLog.Msg($"FullAuto loaded. RPM: {Rpm}. Guns only. Middle mouse cycles modes. Console: fullauto"); 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() private void OnUpdate()
{ {
CheckFireInputPatch.UpdateTrigger(); CheckFireInputPatch.UpdateTrigger();
CheckFireInputPatch.UpdateReloadKey();
if (!Enabled || !GameState.IsPlayerControllable || !CheckFireInputPatch.HoldingGun) if (!CheckFireInputPatch.HoldingGun)
CheckFireInputPatch.RestoreReloadSpeed();
if (Input.GetKeyDown(FlashlightKey) && CheckFireInputPatch.InGame && CheckFireInputPatch.HoldingRanged)
CheckFireInputPatch.ToggleFlashlight();
if (!Enabled || !CheckFireInputPatch.InGame || !CheckFireInputPatch.HoldingGun)
return; return;
if (Input.GetMouseButtonDown(2)) if (Input.GetMouseButtonDown(2))
@@ -61,8 +81,9 @@ public class FullAuto : SonsMod
_ => FireMode.Auto _ => FireMode.Auto
}; };
CheckFireInputPatch.CancelBurst(); CheckFireInputPatch.CancelBurst();
Save();
ShowMode(); ShowMode();
RLog.Msg($"FullAuto mode: {ModeName()}"); RLog.Msg($"FullAuto {CurrentGun} mode: {ModeName()}");
} }
} }
@@ -85,14 +106,37 @@ public class FullAuto : SonsMod
case "off": case "off":
Enabled = false; Enabled = false;
break; break;
case "toggle":
Enabled = !Enabled;
break;
case "debug": case "debug":
Verbose = !Verbose; Verbose = !Verbose;
SonsTools.ShowMessage($"FullAuto debug: {Verbose}"); SonsTools.ShowMessage($"FullAuto debug: {Verbose}");
RLog.Msg($"FullAuto debug: {Verbose}"); RLog.Msg($"FullAuto debug: {Verbose}");
return; 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": case "rpm":
if (parts.Length > 1 && float.TryParse(parts[1], out var value)) if (parts.Length > 1 && float.TryParse(parts[1], out var value))
{ {
@@ -113,7 +157,7 @@ public class FullAuto : SonsMod
private static void Usage() private static void Usage()
{ {
var text = $"Usage: fullauto [on|off|toggle|debug] or fullauto rpm <{MinRpm}-{MaxRpm}>"; var text = $"Usage: fullauto [on|off|debug], fullauto rpm <{MinRpm}-{MaxRpm}>, fullauto fastreload on|off, fullauto autoreload on|off";
SonsTools.ShowMessage(text, 5f); SonsTools.ShowMessage(text, 5f);
RLog.Msg(text); RLog.Msg(text);
} }
@@ -135,7 +179,8 @@ public class FullAuto : SonsMod
internal static void Announce() internal static void Announce()
{ {
var text = Enabled ? $"{ModeName()} ({Rpm} RPM)" : "FullAuto OFF"; var reload = (FastReload ? ", fast reload on" : "") + (AutoReload ? ", auto reload on" : "");
var text = Enabled ? $"{ModeName()} ({Rpm} RPM{reload})" : $"FullAuto OFF{reload}";
SonsTools.ShowMessage(text); SonsTools.ShowMessage(text);
RLog.Msg(text); RLog.Msg(text);
} }
@@ -158,6 +203,16 @@ public class FullAuto : SonsMod
if (key == "rpm" && float.TryParse(val, out var rpm)) if (key == "rpm" && float.TryParse(val, out var rpm))
Rpm = Mathf.Clamp(rpm, MinRpm, MaxRpm); 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 == "reloadkey" && val.Length > 0)
ReloadKey = val.ToLowerInvariant();
else if (key == "flashlightkey" && Enum.TryParse<KeyCode>(val, true, out var flashKey))
FlashlightKey = flashKey;
else if (key.StartsWith("mode.") && Enum.TryParse<FireMode>(val, true, out var gunMode))
Modes[key.Substring(5)] = gunMode;
} }
} }
catch (Exception e) catch (Exception e)
@@ -170,7 +225,18 @@ public class FullAuto : SonsMod
{ {
try try
{ {
File.WriteAllText(_configPath, $"rpm={Rpm}\n"); 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");
sb.Append($"reloadkey={ReloadKey}\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) catch (Exception e)
{ {
@@ -185,15 +251,20 @@ internal static class CheckFireInputPatch
private const int MaxShotsPerFrame = 4; private const int MaxShotsPerFrame = 4;
private const int BurstSize = 3; private const int BurstSize = 3;
private const float ReleaseTime = 0.15f; private const float ReleaseTime = 0.15f;
private const float AutoReloadDelay = 0.2f;
private static readonly HashSet<string> Guns = new() private static readonly Dictionary<string, string> Guns = new()
{ {
"CompactPistolWeaponController", { "CompactPistolWeaponController", "pistol" },
"RevolverWeaponController", { "RevolverWeaponController", "revolver" },
"ShotgunWeaponController", { "ShotgunWeaponController", "shotgun" },
"RifleAnimatorController" { "RifleAnimatorController", "rifle" }
}; };
internal static IEnumerable<string> GunKeys => Guns.Values;
private static readonly Dictionary<IntPtr, string> GunNames = new();
private static readonly Dictionary<IntPtr, bool> Allowed = new(); private static readonly Dictionary<IntPtr, bool> Allowed = new();
private static readonly Dictionary<IntPtr, float> FireDelays = new(); private static readonly Dictionary<IntPtr, float> FireDelays = new();
private static readonly HashSet<string> Reported = new(); private static readonly HashSet<string> Reported = new();
@@ -205,16 +276,266 @@ internal static class CheckFireInputPatch
private static float _releasedFor; private static float _releasedFor;
private static bool _actionFailed; private static bool _actionFailed;
private static bool _holding; 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<Light> Lights = new();
private static float _emptySince = -1f;
private static float _nextReloadTry;
private static UnityEngine.InputSystem.Controls.KeyControl _reloadControl;
private static string _reloadControlKey;
private static UnityEngine.InputSystem.Controls.KeyControl _pressedKey;
private static int _releaseFrame = -1;
private static bool _keySimFailed;
private static bool _loggedHook; private static bool _loggedHook;
internal static bool HoldingGun => _lastGunTime >= 0f && Time.time - _lastGunTime < 0.25f; 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<Light>(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;
return;
}
if (controller._isReloading || controller.IsReloading() || controller.IsReloadQueued())
{
_emptySince = -1f;
return;
}
var weapon = controller.GetRangedWeapon();
if (!weapon)
return;
var ammo = weapon.GetAmmo();
if (ammo == null || !ammo.IsEmpty())
{
_emptySince = -1f;
return;
}
var now = Time.time;
if (_emptySince < 0f)
{
_emptySince = now;
return;
}
if (now - _emptySince < AutoReloadDelay || now < _nextReloadTry)
return;
_nextReloadTry = now + 0.5f;
if (!controller.CanReload())
{
if (FullAuto.Verbose)
RLog.Msg($"FullAuto auto reload waiting, game says cannot reload (aiming: {controller.IsAiming})");
return;
}
if (PressReloadKey())
{
if (FullAuto.Verbose)
RLog.Msg($"FullAuto auto reload pressed {FullAuto.ReloadKey} on {controller.GetIl2CppType().Name}");
return;
}
controller._reloadQueued = true;
if (FullAuto.Verbose)
RLog.Msg($"FullAuto auto reload queued on {controller.GetIl2CppType().Name}");
}
private static bool PressReloadKey()
{
if (_keySimFailed || _pressedKey != null)
return false;
try
{
if (_reloadControl == null || _reloadControlKey != FullAuto.ReloadKey)
{
_reloadControlKey = FullAuto.ReloadKey;
var control = UnityEngine.InputSystem.InputSystem.FindControl($"<Keyboard>/{FullAuto.ReloadKey}");
_reloadControl = control?.TryCast<UnityEngine.InputSystem.Controls.KeyControl>();
}
if (_reloadControl == null)
{
_keySimFailed = true;
RLog.Warning($"FullAuto could not find reload key {FullAuto.ReloadKey}");
return false;
}
UnityEngine.InputSystem.InputControlExtensions.QueueValueChange<float>(_reloadControl, 1f, -1d);
_pressedKey = _reloadControl;
_releaseFrame = Time.frameCount + 3;
return true;
}
catch (Exception e)
{
_keySimFailed = true;
RLog.Warning($"FullAuto could not press reload key: {e.Message}");
return false;
}
}
internal static void UpdateReloadKey()
{
if (_pressedKey == null || Time.frameCount < _releaseFrame)
return;
try
{
UnityEngine.InputSystem.InputControlExtensions.QueueValueChange<float>(_pressedKey, 0f, -1d);
}
catch (Exception e)
{
RLog.Warning($"FullAuto could not release reload key: {e.Message}");
}
_pressedKey = null;
}
internal static void CancelBurst() internal static void CancelBurst()
{ {
_burstLeft = 0; _burstLeft = 0;
_holding = false; _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) private static void HoldAfterBurst(RangedWeaponController controller)
{ {
var first = !_holding; var first = !_holding;
@@ -297,7 +618,7 @@ internal static class CheckFireInputPatch
return; return;
_triggerDown = true; _triggerDown = true;
if (FullAuto.Enabled && FullAuto.Mode == FireMode.Burst && HoldingGun && GameState.IsPlayerControllable) if (FullAuto.Enabled && FullAuto.Mode == FireMode.Burst && HoldingGun && InGame)
StartBurst(); StartBurst();
return; return;
} }
@@ -338,8 +659,32 @@ internal static class CheckFireInputPatch
var allowed = IsAllowed(__instance); var allowed = IsAllowed(__instance);
var ptr = __instance.Pointer; var ptr = __instance.Pointer;
if (allowed && FullAuto.Enabled && (!HoldingGun || ptr != _lastGunPtr)) _currentRanged = __instance;
FullAuto.ShowMode(); _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) if (allowed)
_lastGunPtr = ptr; _lastGunPtr = ptr;
@@ -354,7 +699,7 @@ internal static class CheckFireInputPatch
var pressed = FirePressed(); var pressed = FirePressed();
var held = _triggerDown || pressed; var held = _triggerDown || pressed;
if (!GameState.IsPlayerControllable) if (!InGame)
{ {
_burstLeft = 0; _burstLeft = 0;
return !held; return !held;
@@ -455,10 +800,13 @@ internal static class CheckFireInputPatch
return allowed; return allowed;
var name = controller.GetIl2CppType().Name; var name = controller.GetIl2CppType().Name;
allowed = Guns.Contains(name); allowed = Guns.TryGetValue(name, out var gunKey);
Allowed[ptr] = allowed; Allowed[ptr] = allowed;
if (allowed) if (allowed)
{
FireDelays[ptr] = controller._fireDelay; FireDelays[ptr] = controller._fireDelay;
GunNames[ptr] = gunKey;
}
RLog.Msg($"FullAuto {name}: {(allowed ? "full-auto" : "ignored")}"); RLog.Msg($"FullAuto {name}: {(allowed ? "full-auto" : "ignored")}");
return allowed; return allowed;
} }
+14 -4
View File
@@ -1,5 +1,7 @@
# FullAuto # FullAuto
[![Downloads](https://img.shields.io/github/downloads/JustinOros/sotf-fullauto/total?label=downloads)](https://github.com/JustinOros/sotf-fullauto/releases) [![Latest](https://img.shields.io/github/v/release/JustinOros/sotf-fullauto?label=latest)](https://github.com/JustinOros/sotf-fullauto/releases/latest)
Hold the fire button to shoot guns full-auto in Sons of the Forest. Hold the fire button to shoot guns full-auto in Sons of the Forest.
Works with the pistol, revolver, shotgun and rifle. The fire rate is Works with the pistol, revolver, shotgun and rifle. The fire rate is
@@ -64,7 +66,11 @@ With a gun equipped, click the middle mouse button to cycle fire modes:
| Semi-automatic | Normal game behavior, one shot per click | | Semi-automatic | Normal game behavior, one shot per click |
| 3-Round Burst | Fires 3 shots, then release and press again for the next 3 | | 3-Round Burst | Fires 3 shots, then release and press again for the next 3 |
The mod starts in full-automatic every time the game loads. Each gun remembers its own fire mode, even after you restart the game. Guns
start in full-automatic until you change them.
Press F to turn a flashlight attached to your gun or crossbow on or off.
The key can be changed with the `flashlightkey=` line in `UserData\FullAuto.txt`.
Press F1 to open the console, then use these commands: Press F1 to open the console, then use these commands:
@@ -73,13 +79,16 @@ Press F1 to open the console, then use these commands:
| `fullauto` | Show current settings | | `fullauto` | Show current settings |
| `fullauto on` | Enable | | `fullauto on` | Enable |
| `fullauto off` | Disable until the game restarts | | `fullauto off` | Disable until the game restarts |
| `fullauto toggle` | Switch between on and off |
| `fullauto rpm 1200` | Set the fire rate, 60 to 3000 rounds per minute | | `fullauto rpm 1200` | Set the fire rate, 60 to 3000 rounds per minute |
| `fullauto fastreload on` | Reload guns twice as fast, `off` to turn it back off |
| `fullauto autoreload on` | Reload automatically when the gun is empty, `off` to turn it back off. It presses your reload key for you, so if you changed it from R in the game settings, set `reloadkey=` in `FullAuto.txt` to match |
| `fullauto debug` | Log every shot to `_RedLoader\Latest.log` | | `fullauto debug` | Log every shot to `_RedLoader\Latest.log` |
The default is 900 rpm. The rpm also sets how fast burst shots fire. The default is 900 rpm. The rpm also sets how fast burst shots fire.
The rpm is saved to `UserData\FullAuto.txt` in your game folder. The rpm, fast reload, auto reload, flashlight key, reload key and each gun's fire
mode are saved to
`UserData\FullAuto.txt` in your game folder.
## Supported weapons ## Supported weapons
@@ -101,4 +110,5 @@ the game is not found automatically.
## Troubleshooting ## Troubleshooting
Check `_RedLoader\Latest.log` in your game folder. FullAuto logs a line when it Check `_RedLoader\Latest.log` in your game folder. FullAuto logs a line when it loads and a line for each weapon showing whether it is full-auto or
ignored.
+2 -2
View File
@@ -3,8 +3,8 @@
"id": "FullAuto", "id": "FullAuto",
"name": "FullAuto", "name": "FullAuto",
"author": "JustinOros", "author": "JustinOros",
"version": "1.1.0", "version": "1.5.5",
"description": "Hold fire to shoot any gun full-auto at a configurable rate", "description": "Full-auto, semi-auto and 3-round burst for guns, with fast reload and a flashlight toggle",
"gameVersion": "1.0.0", "gameVersion": "1.0.0",
"platform": "Client", "platform": "Client",
"type": "Mod" "type": "Mod"