RifleSuppressor v1.0.0 split from RifleScope
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
*.zip
|
||||
+130
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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="path", 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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user