RifleSuppressor v1.0.0 split from RifleScope

This commit is contained in:
Justin Oros
2026-09-28 11:56:41 -07:00
commit 60861ff90c
7 changed files with 866 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
bin/
obj/
dist/
*.zip
+130
View File
@@ -0,0 +1,130 @@
[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 ' Rifle Suppressor 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 RifleSuppressor'
$modZip = Join-Path $env:TEMP 'RifleSuppressor.zip'
Get-ReleaseZip -Repo 'JustinOros/sotf-riflesuppressor' -Pattern '^RifleSuppressor\.zip$' -OutFile $modZip | Out-Null
Write-Step 'Installing RifleSuppressor'
$modsDir = Join-Path $GameDir 'Mods'
New-Item -ItemType Directory -Force -Path $modsDir | Out-Null
Remove-Item (Join-Path $modsDir 'RifleSuppressor.dll') -Force -ErrorAction SilentlyContinue
Remove-Item (Join-Path $modsDir 'RifleSuppressor') -Recurse -Force -ErrorAction SilentlyContinue
Expand-Archive -Path $modZip -DestinationPath $modsDir -Force
Remove-Item $modZip -Force -ErrorAction SilentlyContinue
if (-not (Test-Path (Join-Path $modsDir 'RifleSuppressor.dll'))) {
throw 'Something went wrong, RifleSuppressor.dll is not in the Mods folder'
}
if (-not (Test-Path (Join-Path $modsDir 'RifleSuppressor\manifest.json'))) {
throw 'Something went wrong, manifest.json is not in the Mods\RifleSuppressor 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 RifleSuppressor'
Write-Host ' 2. Load into a game, equip the rifle and fire'
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 ''
}
+78
View File
@@ -0,0 +1,78 @@
# Rifle Suppressor
Puts the pistol suppressor on the rifle in Sons of the Forest. Rifle shots use
the suppressed pistol shot sound. You do not need a suppressor in your inventory.
This used to be part of [RifleScope](https://github.com/JustinOros/sotf-riflescope)
and is now its own mod, so you can use either one or both.
## Multiplayer
Client side only. Nothing gets installed on a dedicated server, and other players
do not need the mod.
## Installation
### Easiest
Use the [Sons of the Forest mod loader](https://justinoros.github.io/sotf-modloader/)
in Chrome or Edge.
### Easy install
Close the game, open PowerShell and paste:
```powershell
irm https://raw.githubusercontent.com/JustinOros/sotf-riflesuppressor/main/Install.ps1 | iex
```
It finds your game, installs RedLoader if needed, and installs the latest
RifleSuppressor. Run it again any time to update.
### Manual install
1. Install [RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest)
into your Sons of the Forest folder and launch the game once
2. Download `RifleSuppressor.zip` from the
[latest release](https://github.com/JustinOros/sotf-riflesuppressor/releases/latest)
3. Extract it into the `Mods` folder inside your game folder
4. You should end up with:
```
Mods\RifleSuppressor.dll
Mods\RifleSuppressor\manifest.json
```
## Usage
The suppressor is on as soon as the mod is installed. Equip the rifle and fire.
Press F1 to open the console, then use these commands:
| Command | Action |
| --- | --- |
| `suppressor` | Show whether the rifle suppressor is on or off |
| `suppressor on` | Put the suppressor on the rifle |
| `suppressor off` | Take the suppressor off the rifle |
The setting is saved to `UserData\RifleSuppressor.txt` in your game folder.
If you used the suppressor from RifleScope 1.1.1 or older, update RifleScope to
1.2.0 or newer so the two mods do not both register the `suppressor` command.
## Building from source
Requires the .NET 8 SDK and RedLoader installed with its game assemblies
generated.
```powershell
.\build.ps1 -Install
```
Use `-Package` to build `RifleSuppressor.zip` for a release. Pass `-GameDir "path"`
if the game is not found automatically.
## Troubleshooting
Check `_RedLoader\Latest.log` in your game folder. RifleSuppressor logs a line
when it loads.
+447
View File
@@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using RedLoader;
using RedLoader.Utils;
using Sons.FieldOfView;
using Sons.Items.Core;
using Sons.Weapon;
using SonsSdk;
using SonsSdk.Attributes;
using TheForest.Utils;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace RifleSuppressor;
public class RifleSuppressor : SonsMod
{
private const string SuppressorModName = "CompactPistolSuppressorMod";
private const string SuppressedFireEvent = "event:/SotF Events/player sounds/Weapons/PistolTactical/PistolTacticalFire";
private const string MuzzleName = "RifleSuppressorMuzzle";
private const string RifleFireEvent = "event:/SotF Events/player sounds/Weapons/Rifle/rifle_fire";
private const string UpgradeParameter = "upgrade";
private const float SuppressedUpgradeValue = 0.2f;
private static bool _enabled = true;
private static string _configPath;
private static string _originalFireEvent;
private static HarmonyLib.Harmony _audioHarmony;
private static bool _loggedParamError;
private static bool _loggedPistolParams;
private static bool _reissuing;
private static RifleAnimatorController _rifle;
private static RifleAnimatorController _pendingRifle;
private static ScriptableObject _suppressorMod;
public RifleSuppressor()
{
OnUpdateCallback = OnUpdate;
HarmonyPatchAll = true;
}
protected override void OnSdkInitialized()
{
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "RifleSuppressor.txt");
Load();
RLog.Msg($"RifleSuppressor loaded. Suppressor is {(_enabled ? "on" : "off")}. Change it with: suppressor on or suppressor off");
}
private void OnUpdate()
{
try
{
if (_rifle && _rifle.gameObject.activeInHierarchy)
return;
_rifle = null;
var pending = _pendingRifle;
_pendingRifle = null;
if (!pending || !pending.gameObject.activeInHierarchy)
return;
AdoptRifle(pending);
}
catch (Exception e)
{
RLog.Error($"RifleSuppressor error: {e.Message}");
_rifle = null;
}
}
internal static void OnWeaponTick(RangedWeaponController controller)
{
if (!controller || (_rifle && _rifle.Pointer == controller.Pointer))
return;
var rifle = controller.TryCast<RifleAnimatorController>();
if (rifle && rifle.IsLocalPlayer())
_pendingRifle = rifle;
}
private static void AdoptRifle(RifleAnimatorController rifle)
{
_rifle = rifle;
_originalFireEvent ??= rifle._gunShotAudioEvent;
if (_enabled)
SetSuppressor(rifle, true);
}
private static WeaponMod FindSuppressorMod()
{
if (_suppressorMod)
return _suppressorMod.TryCast<WeaponMod>();
foreach (var mod in Resources.FindObjectsOfTypeAll<WeaponMod>())
{
if (mod && mod.name == SuppressorModName)
{
_suppressorMod = mod;
return mod;
}
}
return null;
}
private static Transform FindChild(Transform root, string name)
{
foreach (var t in root.GetComponentsInChildren<Transform>(true))
{
if (t.name == name)
return t;
}
return null;
}
private static bool EnsureMuzzleSlot(RangedWeaponController rifle, WeaponMods mods)
{
var links = mods._modLocations;
for (var i = 0; i < links.Count; i++)
{
if (links[i].Slot == WeaponMod.Slot.Muzzle)
return true;
}
var anchor = FindChild(rifle.transform, "ProjectileVisualTransform");
if (!anchor)
return false;
var muzzle = FindChild(anchor.parent, MuzzleName);
if (!muzzle)
{
var go = new GameObject(MuzzleName);
go.layer = anchor.gameObject.layer;
muzzle = go.transform;
muzzle.SetParent(anchor.parent, false);
muzzle.localPosition = anchor.localPosition;
muzzle.localRotation = Quaternion.identity;
muzzle.localScale = Vector3.one;
}
var link = new WeaponMods.ModLink();
link.Slot = WeaponMod.Slot.Muzzle;
link.Location = muzzle;
links.Add(link);
return true;
}
private static Type FindGameType(string name)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var asm in assemblies)
{
try
{
var t = asm.GetType(name, false) ?? asm.GetType("Il2Cpp." + name, false);
if (t != null)
return t;
}
catch
{
}
}
foreach (var asm in assemblies)
{
var asmName = asm.GetName().Name ?? string.Empty;
if (asm.IsDynamic || asmName.StartsWith("System", StringComparison.OrdinalIgnoreCase) ||
asmName.StartsWith("Unity", StringComparison.OrdinalIgnoreCase) ||
asmName.StartsWith("Il2Cppmscorlib", StringComparison.OrdinalIgnoreCase) ||
asmName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) ||
asmName.StartsWith("mscorlib", StringComparison.OrdinalIgnoreCase))
continue;
Type[] types;
try
{
types = asm.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
types = e.Types.Where(t => t != null).ToArray();
}
catch
{
continue;
}
foreach (var t in types)
{
if (t != null && t.Name == name)
return t;
}
}
return null;
}
private static void InstallAudioPatches()
{
if (_audioHarmony != null)
return;
_audioHarmony = new HarmonyLib.Harmony("RifleSuppressor.Audio");
var handler = FindGameType("FMOD_AnimationEventHandler");
var animPrefix = new HarmonyMethod(typeof(RifleSuppressor).GetMethod(nameof(AnimEventPrefix), BindingFlags.NonPublic | BindingFlags.Static));
var count = 0;
if (handler != null)
{
foreach (var m in handler.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
var ps = m.GetParameters();
if (m.Name != "playFMODEvent" || ps.Length == 0 || ps[0].ParameterType != typeof(string))
continue;
_audioHarmony.Patch(m, prefix: animPrefix);
count++;
}
}
var common = FindGameType("FMODCommon");
var oneshotPrefix = new HarmonyMethod(typeof(RifleSuppressor).GetMethod(nameof(OneshotPrefix), BindingFlags.NonPublic | BindingFlags.Static));
if (common != null)
{
foreach (var m in common.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly))
{
var ps = m.GetParameters();
if (m.Name != "PlayOneshotInternal" || ps.Length < 4 || ps[0].ParameterType != typeof(string))
continue;
_audioHarmony.Patch(m, prefix: oneshotPrefix);
count++;
}
}
RLog.Msg($"Suppressor audio: patched {count} methods (handler={(handler != null)}, common={(common != null)})");
}
private static bool RifleSuppressedNow() =>
_enabled && _rifle && _rifle.gameObject.activeInHierarchy;
private static bool AnimEventPrefix(Component __instance, object[] __args)
{
try
{
if (!RifleSuppressedNow() || __args == null || __args.Length == 0 || __args[0] as string != RifleFireEvent)
return true;
if (!__instance || __instance.transform.root != _rifle.transform.root)
return true;
return false;
}
catch
{
return true;
}
}
private static bool OneshotPrefix(MethodBase __originalMethod, object[] __args)
{
if (_reissuing)
return true;
try
{
if (__args == null || __args.Length < 4 || __args[0] as string != SuppressedFireEvent)
return true;
var existing = __args[3] as Il2CppReferenceArray<Il2CppSystem.Object>;
if (!RifleSuppressedNow())
{
if (!_loggedPistolParams && existing != null && existing.Length > 0)
{
_loggedPistolParams = true;
RLog.Msg($"Suppressor audio: pistol params [{string.Join(", ", existing.Select(Describe))}]");
}
return true;
}
var oldCount = existing?.Length ?? 0;
for (var i = 0; i < oldCount; i++)
{
if (existing[i] != null && existing[i].ToString() == UpgradeParameter)
return true;
}
var array = new Il2CppReferenceArray<Il2CppSystem.Object>(oldCount + 2);
for (var i = 0; i < oldCount; i++)
array[i] = existing[i];
array[oldCount] = new Il2CppSystem.Object(IL2CPP.ManagedStringToIl2Cpp(UpgradeParameter));
array[oldCount + 1] = BoxFloat(SuppressedUpgradeValue);
var args = (object[])__args.Clone();
args[3] = array;
_reissuing = true;
try
{
__originalMethod.Invoke(null, args);
}
finally
{
_reissuing = false;
}
return false;
}
catch (Exception e)
{
if (!_loggedParamError)
{
_loggedParamError = true;
RLog.Error($"Suppressor audio: could not play suppressed shot: {(e.InnerException ?? e).Message}");
}
return true;
}
}
private static Il2CppSystem.Object BoxFloat(float value)
{
object single = new Il2CppSystem.Single();
typeof(Il2CppSystem.Single).GetField("m_value", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.SetValue(single, value);
return ((Il2CppSystem.Single)single).BoxIl2CppObject();
}
private static string Describe(Il2CppSystem.Object o)
{
if (o == null)
return "null";
try
{
return $"{o.GetIl2CppType().Name}:{o.ToString()}";
}
catch
{
return "?";
}
}
private static void SetSuppressor(RangedWeaponController rifle, bool on)
{
var weapon = rifle._rangedWeapon;
var mods = weapon ? weapon._weaponMods : null;
if (!mods)
{
Say("Suppressor: rifle has no WeaponMods");
return;
}
var mod = FindSuppressorMod();
if (!mod)
{
Say("Suppressor: pistol suppressor mod is not loaded");
return;
}
if (on)
{
InstallAudioPatches();
if (!EnsureMuzzleSlot(rifle, mods))
{
Say("Suppressor: could not find the rifle muzzle");
return;
}
var applied = mods.HasMod(mod.ModItemId) || mods.ApplyMod(mod);
rifle._gunShotAudioEvent = SuppressedFireEvent;
RLog.Msg($"Suppressor on: applied={applied} slot={mod.AttachesToSlot} required={mod.RequiredSlot} item={mod.ModItemId}");
return;
}
if (mods.HasMod(mod.ModItemId))
mods.RemoveMod(mod);
if (_originalFireEvent != null)
rifle._gunShotAudioEvent = _originalFireEvent;
}
[DebugCommand("suppressor")]
private static void SuppressorCommand(string args)
{
args = (args ?? string.Empty).Trim().ToLowerInvariant();
if (args != "on" && args != "off")
{
Say($"Rifle suppressor is {(_enabled ? "on" : "off")}. Usage: suppressor on or suppressor off");
return;
}
_enabled = args == "on";
Save();
if (_rifle)
SetSuppressor(_rifle, _enabled);
Say($"Rifle suppressor {(_enabled ? "on" : "off")}");
}
private static void Say(string text)
{
RLog.Msg(text);
try
{
SonsTools.ShowMessage(text, 5f);
}
catch
{
}
}
private static void Load()
{
try
{
if (!File.Exists(_configPath))
return;
_enabled = File.ReadAllText(_configPath).Trim() != "0";
}
catch (Exception e)
{
RLog.Error($"RifleSuppressor could not read config: {e.Message}");
}
}
private static void Save()
{
try
{
File.WriteAllText(_configPath, _enabled ? "1" : "0");
}
catch (Exception e)
{
RLog.Error($"RifleSuppressor could not write config: {e.Message}");
}
}
}
[HarmonyPatch(typeof(RangedWeaponController), nameof(RangedWeaponController.CheckFireInput))]
internal static class CheckFireInputPatch
{
private static void Postfix(RangedWeaponController __instance)
{
try
{
global::RifleSuppressor.RifleSuppressor.OnWeaponTick(__instance);
}
catch
{
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AssemblyName>RifleSuppressor</AssemblyName>
<RootNamespace>RifleSuppressor</RootNamespace>
<Version>1.0.0</Version>
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(GameDir)' == ''">
<GameDir>$(SOTF_PATH)</GameDir>
</PropertyGroup>
<Target Name="CheckGameDir" BeforeTargets="BeforeBuild">
<Error Condition="'$(GameDir)' == ''"
Text="GameDir is not set. Run build.ps1, pass -p:GameDir=&quot;path&quot;, or set SOTF_PATH." />
<Error Condition="!Exists('$(GameDir)\_RedLoader\Game\Sons.dll')"
Text="No RedLoader interop assemblies at $(GameDir)\_RedLoader\Game. Install RedLoader and launch the game once." />
</Target>
<ItemGroup>
<Reference Include="$(GameDir)\_RedLoader\Game\*.dll" Private="false" />
<Reference Include="$(GameDir)\_RedLoader\net6\RedLoader.dll" Private="false" />
<Reference Include="$(GameDir)\_RedLoader\net6\SonsSdk.dll" Private="false" />
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Runtime.dll" Private="false" />
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Common.dll" Private="false" />
<Reference Include="$(GameDir)\_RedLoader\net6\0Harmony.dll" Private="false" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="manifest.json" />
</ItemGroup>
</Project>
+159
View File
@@ -0,0 +1,159 @@
[CmdletBinding()]
param(
[string]$GameDir,
[switch]$Install,
[switch]$Package
)
$ErrorActionPreference = 'Stop'
function Test-Dotnet {
$cmd = Get-Command dotnet -ErrorAction SilentlyContinue
if (-not $cmd) { return $false }
$sdks = & dotnet --list-sdks 2>$null
return ($LASTEXITCODE -eq 0 -and $sdks)
}
function Update-Path {
$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('Path','User')
$dotnetRoot = Join-Path $env:ProgramFiles 'dotnet'
if ((Test-Path $dotnetRoot) -and ($env:Path -notlike "*$dotnetRoot*")) {
$env:Path += ";$dotnetRoot"
}
}
function Install-Dotnet {
if (Get-Command winget -ErrorAction SilentlyContinue) {
Write-Host 'Installing the .NET 8 SDK with winget' -ForegroundColor Cyan
winget install --id Microsoft.DotNet.SDK.8 --accept-source-agreements --accept-package-agreements --silent
Update-Path
if (Test-Dotnet) { return }
Write-Host ' winget install did not make dotnet available, falling back' -ForegroundColor Yellow
}
Write-Host 'Downloading the official dotnet-install script' -ForegroundColor Cyan
$script = Join-Path $env:TEMP 'dotnet-install.ps1'
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $script -UseBasicParsing
Write-Host 'Installing the .NET 8 SDK to your user profile' -ForegroundColor Cyan
& $script -Channel 8.0 -InstallDir "$env:LOCALAPPDATA\Microsoft\dotnet"
$userDotnet = "$env:LOCALAPPDATA\Microsoft\dotnet"
$env:Path = "$userDotnet;$env:Path"
[Environment]::SetEnvironmentVariable(
'Path',
"$userDotnet;" + [Environment]::GetEnvironmentVariable('Path','User'),
'User')
Remove-Item $script -Force -ErrorAction SilentlyContinue
}
function Install-RedLoader {
param([string]$Target)
Write-Host 'Looking up the latest RedLoader release' -ForegroundColor Cyan
$release = Invoke-RestMethod 'https://api.github.com/repos/ToniMacaroni/RedLoader/releases/latest' -Headers @{ 'User-Agent' = 'ps' }
$asset = $release.assets | Where-Object { $_.name -match '^RedLoader.*\.zip$' } | Select-Object -First 1
if (-not $asset) { throw 'Could not find RedLoader.zip in the latest release' }
Write-Host " $($release.tag_name) / $($asset.name)"
$zip = Join-Path $env:TEMP $asset.name
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -UseBasicParsing
$stage = Join-Path $env:TEMP 'redloader-extract'
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
Expand-Archive -Path $zip -DestinationPath $stage -Force
Copy-Item -Path (Join-Path $stage '*') -Destination $Target -Recurse -Force
Remove-Item $zip -Force -ErrorAction SilentlyContinue
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ' RedLoader installed' -ForegroundColor Green
}
function Find-GameDir {
if ($env:SOTF_PATH) { return $env:SOTF_PATH }
$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
}
if (-not (Test-Dotnet)) {
Write-Host ''
Write-Host 'The .NET 8 SDK is required to build this mod and was not found.' -ForegroundColor Yellow
$answer = Read-Host 'Download and install it now? (y/N)'
if ($answer -ne 'y') {
throw 'Cannot build without the .NET SDK. Get it at https://dotnet.microsoft.com/download'
}
Install-Dotnet
if (-not (Test-Dotnet)) {
throw 'The SDK was installed but dotnet is still not on PATH. Reopen PowerShell and rerun this script.'
}
}
Write-Host ("dotnet SDK: " + (dotnet --version)) -ForegroundColor Green
if (-not $GameDir) { $GameDir = Find-GameDir }
if (-not $GameDir) {
throw 'Could not find Sons Of The Forest. Pass -GameDir "path" or set SOTF_PATH.'
}
Write-Host "Game directory: $GameDir" -ForegroundColor Cyan
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\net6\SonsSdk.dll'))) {
Write-Host ''
Write-Host 'RedLoader is not installed in your game folder.' -ForegroundColor Yellow
$answer = Read-Host 'Download and install the latest RedLoader release now? (y/N)'
if ($answer -ne 'y') {
throw 'RedLoader is required. Get it at https://github.com/ToniMacaroni/RedLoader/releases'
}
Install-RedLoader -Target $GameDir
}
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\Game\Sons.dll'))) {
Write-Host ''
Write-Host 'RedLoader is installed but has not generated its game assemblies yet.' -ForegroundColor Yellow
Write-Host 'Launch Sons of the Forest once and wait for the main menu. The first'
Write-Host 'launch takes a few minutes while it processes the game files.'
Write-Host ''
Write-Host 'Then quit the game and run this script again.'
return
}
Write-Host 'RedLoader ready' -ForegroundColor Green
dotnet build -c Release --nologo -p:GameDir="$GameDir"
if ($LASTEXITCODE -ne 0) { throw 'Build failed' }
$dll = Join-Path $PSScriptRoot 'bin\Release\net6\RifleSuppressor.dll'
if ($Install) {
$modsDir = Join-Path $GameDir 'Mods'
$modDataDir = Join-Path $modsDir 'RifleSuppressor'
New-Item -ItemType Directory -Force -Path $modDataDir | Out-Null
Copy-Item $dll -Destination $modsDir -Force
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination $modDataDir -Force
Write-Host "Installed to $modsDir" -ForegroundColor Green
}
if ($Package) {
$stage = Join-Path $PSScriptRoot 'dist'
$zip = Join-Path $PSScriptRoot 'RifleSuppressor.zip'
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'RifleSuppressor') | Out-Null
Copy-Item $dll -Destination $stage -Force
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination (Join-Path $stage 'RifleSuppressor') -Force
if (Test-Path $zip) { Remove-Item $zip -Force }
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zip
Remove-Item $stage -Recurse -Force
Write-Host "Packaged $zip" -ForegroundColor Green
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://raw.githubusercontent.com/ToniMacaroni/RedLoader/main/MetadataSchema.json",
"id": "RifleSuppressor",
"name": "RifleSuppressor",
"author": "JustinOros",
"version": "1.0.0",
"description": "Puts the pistol suppressor on the rifle for quiet, suppressed shots",
"gameVersion": "1.0.0",
"platform": "Client",
"type": "Mod"
}