15 Commits
4 changed files with 350 additions and 27 deletions

No files matched your search

+199 -12
View File
@@ -24,10 +24,13 @@ 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 FireMode Mode = FireMode.Auto;
internal static bool FastReload;
internal static KeyCode FlashlightKey = KeyCode.F;
internal static bool Verbose; internal static bool Verbose;
private static string _configPath; private static string _configPath;
@@ -42,14 +45,21 @@ 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}. Flashlight key: {FlashlightKey}. Guns only. Middle mouse cycles modes. Console: fullauto");
} }
private void OnUpdate() private void OnUpdate()
{ {
CheckFireInputPatch.UpdateTrigger(); CheckFireInputPatch.UpdateTrigger();
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,7 +71,7 @@ public class FullAuto : SonsMod
_ => FireMode.Auto _ => FireMode.Auto
}; };
CheckFireInputPatch.CancelBurst(); CheckFireInputPatch.CancelBurst();
SonsTools.ShowMessage(ModeName(), 2f); ShowMode();
RLog.Msg($"FullAuto mode: {ModeName()}"); RLog.Msg($"FullAuto mode: {ModeName()}");
} }
} }
@@ -85,14 +95,25 @@ 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 "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 +134,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";
SonsTools.ShowMessage(text, 5f); SonsTools.ShowMessage(text, 5f);
RLog.Msg(text); RLog.Msg(text);
} }
@@ -128,9 +149,15 @@ public class FullAuto : SonsMod
}; };
} }
internal static void ShowMode()
{
SonsTools.ShowMessage(ModeName(), 2f);
}
internal static void Announce() internal static void Announce()
{ {
var text = Enabled ? $"{ModeName()} ({Rpm} RPM)" : "FullAuto OFF"; var reload = FastReload ? ", fast reload on" : "";
var text = Enabled ? $"{ModeName()} ({Rpm} RPM{reload})" : $"FullAuto OFF{reload}";
SonsTools.ShowMessage(text); SonsTools.ShowMessage(text);
RLog.Msg(text); RLog.Msg(text);
} }
@@ -153,6 +180,10 @@ 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 == "flashlightkey" && Enum.TryParse<KeyCode>(val, true, out var flashKey))
FlashlightKey = flashKey;
} }
} }
catch (Exception e) catch (Exception e)
@@ -165,7 +196,7 @@ public class FullAuto : SonsMod
{ {
try try
{ {
File.WriteAllText(_configPath, $"rpm={Rpm}\n"); File.WriteAllText(_configPath, $"rpm={Rpm}\nfastreload={FastReload.ToString().ToLowerInvariant()}\nflashlightkey={FlashlightKey}\n");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -195,20 +226,156 @@ internal static class CheckFireInputPatch
private static float _nextShot; private static float _nextShot;
private static int _burstLeft; private static int _burstLeft;
private static float _lastGunTime = -1f; private static float _lastGunTime = -1f;
private static IntPtr _lastGunPtr = IntPtr.Zero;
private static bool _triggerDown; private static bool _triggerDown;
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 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;
}
}
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;
@@ -291,7 +458,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;
} }
@@ -330,6 +497,25 @@ internal static class CheckFireInputPatch
return true; return true;
var allowed = IsAllowed(__instance); 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 && FullAuto.Enabled && (!HoldingGun || ptr != _lastGunPtr))
FullAuto.ShowMode();
if (allowed)
_lastGunPtr = ptr;
_lastGunTime = allowed ? Time.time : -1f; _lastGunTime = allowed ? Time.time : -1f;
if (!FullAuto.Enabled || !allowed) if (!FullAuto.Enabled || !allowed)
@@ -341,7 +527,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;
@@ -449,4 +635,5 @@ internal static class CheckFireInputPatch
RLog.Msg($"FullAuto {name}: {(allowed ? "full-auto" : "ignored")}"); RLog.Msg($"FullAuto {name}: {(allowed ? "full-auto" : "ignored")}");
return allowed; return allowed;
} }
} }
+134
View File
@@ -0,0 +1,134 @@
[CmdletBinding()]
param(
[string]$GameDir
)
$ErrorActionPreference = 'Stop'
function Write-Step {
param([string]$Text)
Write-Host ''
Write-Host $Text -ForegroundColor Cyan
}
function Find-GameDir {
$steam = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath
if (-not $steam) { return $null }
$libs = @($steam)
$vdf = Join-Path $steam 'steamapps\libraryfolders.vdf'
if (Test-Path $vdf) {
Select-String -Path $vdf -Pattern '"path"\s+"(.+?)"' -AllMatches |
ForEach-Object { $_.Matches } |
ForEach-Object { $libs += $_.Groups[1].Value.Replace('\\','\') }
}
foreach ($lib in $libs) {
$candidate = Join-Path $lib 'steamapps\common\Sons Of The Forest'
if (Test-Path (Join-Path $candidate 'SonsOfTheForest.exe')) { return $candidate }
}
return $null
}
function Get-ReleaseZip {
param([string]$Repo, [string]$Pattern, [string]$OutFile)
$release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ 'User-Agent' = 'ps' }
$asset = $release.assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1
if (-not $asset) { throw "Could not find a download matching $Pattern in the latest $Repo release" }
Write-Host " version $($release.tag_name)"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $OutFile -UseBasicParsing
return $asset.name
}
try {
Write-Host ''
Write-Host '====================================' -ForegroundColor Green
Write-Host ' FullAuto installer' -ForegroundColor Green
Write-Host '====================================' -ForegroundColor Green
if (Get-Process -Name 'SonsOfTheForest' -ErrorAction SilentlyContinue) {
throw 'Sons of the Forest is running. Close the game and run the installer again.'
}
Write-Step 'Looking for Sons of the Forest'
if (-not $GameDir) { $GameDir = Find-GameDir }
if (-not $GameDir -or -not (Test-Path (Join-Path $GameDir 'SonsOfTheForest.exe'))) {
Write-Host ' Could not find the game automatically.' -ForegroundColor Yellow
Write-Host ' In Steam, right-click Sons Of The Forest, then Manage, then Browse local files.'
Write-Host ' Copy the folder path from the address bar and paste it below.'
$GameDir = (Read-Host ' Game folder').Trim('"')
if (-not (Test-Path (Join-Path $GameDir 'SonsOfTheForest.exe'))) {
throw "SonsOfTheForest.exe was not found in $GameDir"
}
}
Write-Host " found: $GameDir" -ForegroundColor Green
$needsFirstLaunch = $false
Write-Step 'Checking for RedLoader'
if (Test-Path (Join-Path $GameDir '_RedLoader\net6\SonsSdk.dll')) {
Write-Host ' already installed' -ForegroundColor Green
}
else {
Write-Host ' not installed, downloading it now'
$rlZip = Join-Path $env:TEMP 'RedLoader.zip'
Get-ReleaseZip -Repo 'ToniMacaroni/RedLoader' -Pattern '^RedLoader.*\.zip$' -OutFile $rlZip | Out-Null
$rlStage = Join-Path $env:TEMP 'redloader-stage'
if (Test-Path $rlStage) { Remove-Item $rlStage -Recurse -Force }
Expand-Archive -Path $rlZip -DestinationPath $rlStage -Force
Copy-Item -Path (Join-Path $rlStage '*') -Destination $GameDir -Recurse -Force
Remove-Item $rlZip -Force -ErrorAction SilentlyContinue
Remove-Item $rlStage -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ' installed' -ForegroundColor Green
$needsFirstLaunch = $true
}
Write-Step 'Downloading FullAuto'
$modZip = Join-Path $env:TEMP 'FullAuto.zip'
Get-ReleaseZip -Repo 'JustinOros/sotf-fullauto' -Pattern '^FullAuto\.zip$' -OutFile $modZip | Out-Null
Write-Step 'Installing FullAuto'
$modsDir = Join-Path $GameDir 'Mods'
New-Item -ItemType Directory -Force -Path $modsDir | Out-Null
Remove-Item (Join-Path $modsDir 'FullAuto.dll') -Force -ErrorAction SilentlyContinue
Remove-Item (Join-Path $modsDir 'FullAuto') -Recurse -Force -ErrorAction SilentlyContinue
Expand-Archive -Path $modZip -DestinationPath $modsDir -Force
Remove-Item $modZip -Force -ErrorAction SilentlyContinue
if (-not (Test-Path (Join-Path $modsDir 'FullAuto.dll'))) {
throw 'Something went wrong, FullAuto.dll is not in the Mods folder'
}
if (-not (Test-Path (Join-Path $modsDir 'FullAuto\manifest.json'))) {
throw 'Something went wrong, manifest.json is not in the Mods\FullAuto folder'
}
Write-Host ' installed' -ForegroundColor Green
Write-Host ''
Write-Host '====================================' -ForegroundColor Green
Write-Host ' Done' -ForegroundColor Green
Write-Host '====================================' -ForegroundColor Green
Write-Host ''
if ($needsFirstLaunch) {
Write-Host 'RedLoader was installed for the first time, so the next time you start'
Write-Host 'the game it will take a few extra minutes to get ready. That is normal.'
Write-Host ''
}
Write-Host 'Start Sons of the Forest, then:'
Write-Host ' 1. Check that MODS appears on the main menu and lists FullAuto'
Write-Host ' 2. Load into a game and equip a pistol, revolver, shotgun or rifle'
Write-Host ' 3. Hold the fire button to shoot full-auto'
Write-Host ' 4. Click the middle mouse button to switch between'
Write-Host ' Full-automatic, Semi-automatic and 3-Round Burst'
Write-Host ''
}
catch {
Write-Host ''
Write-Host "Install failed: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Message -match 'denied') {
Write-Host 'Try again from PowerShell opened with Run as administrator.' -ForegroundColor Yellow
}
Write-Host ''
}
Read-Host 'Press Enter to close' | Out-Null
+15 -13
View File
@@ -12,7 +12,20 @@ players do not need the mod.
## Installation ## Installation
### Step 1: Install RedLoader ### Easy install
Close the game, open PowerShell and paste:
```powershell
irm https://raw.githubusercontent.com/JustinOros/sotf-fullauto/main/Install.ps1 | iex
```
It finds your game, installs RedLoader if needed, and installs the latest
FullAuto. Run it again any time to update.
### Manual install
#### Step 1: Install RedLoader
[RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest) is the mod [RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest) is the mod
loader for Sons of the Forest. The game cannot load any mod without it, so loader for Sons of the Forest. The game cannot load any mod without it, so
@@ -27,7 +40,7 @@ install it first. You only have to do this once.
takes a few minutes while RedLoader processes the game files takes a few minutes while RedLoader processes the game files
4. Check that `MODS` appears on the main menu, then quit 4. Check that `MODS` appears on the main menu, then quit
### Step 2: Install FullAuto #### Step 2: Install FullAuto
1. Download `FullAuto.zip` from the 1. Download `FullAuto.zip` from the
[latest release](https://github.com/JustinOros/sotf-fullauto/releases/latest) [latest release](https://github.com/JustinOros/sotf-fullauto/releases/latest)
@@ -76,16 +89,5 @@ behavior.
## Building from source ## Building from source
Requires the .NET 8 SDK and RedLoader installed with its game assemblies
generated.
```powershell
.\build.ps1 -Install
```
Use `-Package` to build `FullAuto.zip` for a release. Pass `-GameDir "path"` if
the game is not found automatically.
## Troubleshooting
Check `_RedLoader\Latest.log` in your game folder. FullAuto logs a line when it
+2 -2
View File
@@ -3,8 +3,8 @@
"id": "FullAuto", "id": "FullAuto",
"name": "FullAuto", "name": "FullAuto",
"author": "JustinOros", "author": "JustinOros",
"version": "1.0.0", "version": "1.4.0",
"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"