v1.1.0 rifle suppressor with suppressed shot sound

This commit is contained in:
Justin Oros
2026-09-27 17:19:49 -07:00
parent 7ac7c41645
commit b3aecc2bd5
4 changed files with 353 additions and 148 deletions
+43 -135
View File
@@ -1,206 +1,114 @@
\# Rifle Scope
# Rifle Scope
A stronger rifle scope for Sons of the Forest. Aiming down the rifle scope zooms
5x further than the vanilla scope and shows a full screen scope view with a
crosshair reticle, mil dots and a rangefinder instead of the red dot. You can
also put the pistol suppressor on the rifle for quiet, suppressed shots.
crosshair reticle, mil dots and a rangefinder instead of the red dot.
\## Multiplayer
## Multiplayer
Client side only. Nothing gets installed on a dedicated server, and other players
do not need the mod.
## Installation
\## Installation
\### Easy install
### Easy install
Close the game, open PowerShell and paste:
```powershell
irm https://raw.githubusercontent.com/JustinOros/sotf-riflescope/main/Install.ps1 | iex
```
It finds your game, installs RedLoader if needed, and installs the latest
RifleScope. Run it again any time to update.
### Manual install
#### Step 1: Install RedLoader
\### 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
install it first. You only have to do this once.
1. Download `RedLoader.zip` from the
[latest RedLoader release](https://github.com/ToniMacaroni/RedLoader/releases/latest)
2. Extract it into your Sons of the Forest folder, the one containing
`SonsOfTheForest.exe`, usually
`C:\Program Files (x86)\Steam\steamapps\common\Sons Of The Forest`
3. Launch the game once and wait until you reach the main menu. The first launch
takes a few minutes while RedLoader processes the game files
4. Check that `MODS` appears on the main menu, then quit
#### Step 2: Install RifleScope
1\. Download `RedLoader.zip` from the
  \[latest RedLoader release](https://github.com/ToniMacaroni/RedLoader/releases/latest)
2\. Extract it into your Sons of the Forest folder, the one containing
  `SonsOfTheForest.exe`, usually
  `C:\\Program Files (x86)\\Steam\\steamapps\\common\\Sons Of The Forest`
3\. Launch the game once and wait until you reach the main menu. The first launch
  takes a few minutes while RedLoader processes the game files
4\. Check that `MODS` appears on the main menu, then quit
\#### Step 2: Install RifleScope
1\. Download `RifleScope.zip` from the
  \[latest release](https://github.com/JustinOros/sotf-riflescope/releases/latest)
2\. Extract it into the `Mods` folder inside your game folder
3\. You should end up with:
1. Download `RifleScope.zip` from the
[latest release](https://github.com/JustinOros/sotf-riflescope/releases/latest)
2. Extract it into the `Mods` folder inside your game folder
3. You should end up with:
```
Mods\\RifleScope.dll
Mods\\RifleScope\\manifest.json
Mods\RifleScope.dll
Mods\RifleScope\manifest.json
```
\## Usage
## Usage
Equip the rifle and aim. Once zoomed in, the rifle model is hidden and the scope
view is shown. Your shot goes where the crosshair is.
The red number at the bottom left of the scope is the range in meters to whatever
is under the crosshair.
The reticle has 10 mil dots in each direction. At 5x each dot is 2 mils, which
covers 20 cm at 100 m. At 10x and higher each dot is 1 mil. Use them to hold over
or to the side at long range.
Press F1 to open the console, then use these commands:
| Command | Action |
| --- | --- |
| `scope` | Show whether the mod is on or off |
| `scope off` | Turn the mod off and go back to the vanilla scope |
| `scope on` | Turn the mod back on |
| `scope` | Show whether the scope is on or off |
| `scope off` | Go back to the vanilla scope. The suppressor is not affected |
| `scope on` | Turn the scope back on |
| `scopezoom` | Show the current zoom |
| `scopezoom 5` | Set the zoom as a multiple of the vanilla scope, 1 to 50. The default is 5 |
| `scopetrim` | Show the current vertical trim |
| `scopetrim 50` | Set the vertical trim, -200 to 200. Positive moves hits down, negative moves hits up. The default is 50 |
| `scopetrim 60` | Set the vertical trim, -200 to 200. Positive moves hits down, negative moves hits up. The default is 60 |
| `scopewind` | Show the current windage |
| `scopewind 20` | Set the windage, -200 to 200. Positive moves hits right, negative moves hits left. The default is 0 |
| `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 |
| `scopedebug` | Toggle a debug readout above the range |
### Suppressor
Run `suppressor on` to put the pistol suppressor on the end of the rifle barrel.
Rifle shots then use the suppressed pistol shot sound. It stays on until you run
`suppressor off`, even after restarting the game. You do not need a suppressor in
your inventory.
Trim and windage are angle adjustments, so a setting that is right at one range
stays right at every range.
The on or off setting, zoom, trim and windage are saved to `UserData\\RifleScope.txt` in your game
The on or off setting, suppressor, zoom, trim and windage are saved to `UserData\RifleScope.txt` in your game
folder. Setting the zoom to 1 keeps vanilla zoom and the normal scope view,
without the red dot.
\## Building from source
## Building from source
Requires the .NET 8 SDK and RedLoader installed with its game assemblies
generated.
```powershell
.\\build.ps1 -Install
.\build.ps1 -Install
```
Use `-Package` to build `RifleScope.zip` for a release. Pass `-GameDir "path"` if
the game is not found automatically.
## Troubleshooting
\## Troubleshooting
Check `\_RedLoader\\Latest.log` in your game folder. RifleScope logs a line when it
loads.
Check `_RedLoader\Latest.log` in your game folder. RifleScope logs a line when it
loads.
+306 -9
View File
@@ -2,9 +2,15 @@ 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;
@@ -20,10 +26,16 @@ public class RifleScope : SonsMod
private const float DefaultZoom = 5f;
private const float MinZoom = 1f;
private const float MaxZoom = 50f;
private const float DefaultTrimCm = 50f;
private const float DefaultTrimCm = 60f;
private const float MaxTrimCm = 200f;
private const float MaxWindCm = 200f;
private const float TrimReferenceRange = 50f;
private const string SuppressorModName = "CompactPistolSuppressorMod";
private const string SuppressedFireEvent = "event:/SotF Events/player sounds/Weapons/PistolTactical/PistolTacticalFire";
private const string MuzzleName = "RifleScopeMuzzle";
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 const int MilDotCount = 10;
private const float MilDotSize = 0.4f;
private const float MinDotPx = 7f;
@@ -44,6 +56,12 @@ public class RifleScope : SonsMod
private static float _windCm;
private static bool _debug;
private static bool _enabled = true;
private static bool _suppressor;
private static string _originalFireEvent;
private static HarmonyLib.Harmony _audioHarmony;
private static bool _loggedParamError;
private static bool _loggedPistolParams;
private static bool _reissuing;
private static readonly List<(Renderer Renderer, int Slot, Material Original)> RedDotSlots = new();
private static string _configPath;
@@ -101,9 +119,6 @@ public class RifleScope : SonsMod
private static void Tick()
{
if (!_enabled)
return;
if (!_rifle || !_rifle.gameObject.activeInHierarchy)
{
_rifle = null;
@@ -115,6 +130,9 @@ public class RifleScope : SonsMod
return;
}
if (!_enabled)
return;
var manager = _rifle._cameraFovManager;
if (!manager)
{
@@ -137,14 +155,274 @@ public class RifleScope : SonsMod
continue;
_rifle = rifle;
_projectile = FindProjectileTransform(rifle);
if (!_overlay)
CreateOverlay();
HideRedDot(rifle);
_originalFireEvent ??= rifle._gunShotAudioEvent;
if (_enabled)
{
if (!_overlay)
CreateOverlay();
HideRedDot(rifle);
}
if (_suppressor)
SetSuppressor(rifle, true);
return true;
}
return false;
}
private static WeaponMod FindSuppressorMod()
{
foreach (var mod in Resources.FindObjectsOfTypeAll<WeaponMod>())
{
if (mod && mod.name == SuppressorModName)
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)
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
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("RifleScope.SuppressorAudio");
var handler = FindGameType("FMOD_AnimationEventHandler");
var animPrefix = new HarmonyMethod(typeof(RifleScope).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(RifleScope).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() =>
_suppressor && _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;
}
private static Transform FindProjectileTransform(RangedWeaponController rifle)
{
var constraint = rifle._projectileTransformConstraint;
@@ -308,7 +586,6 @@ public class RifleScope : SonsMod
settings._FieldOfViewTargetOffset_k__BackingField = original;
}
RestoreRedDot();
_rifle = null;
}
private static Material HiddenFor(Material original)
@@ -688,6 +965,24 @@ public class RifleScope : SonsMod
Say($"RifleScope is {(_enabled ? "on" : "off")}. Usage: scope on or scope off");
}
[DebugCommand("suppressor")]
private static void SuppressorCommand(string args)
{
args = (args ?? string.Empty).Trim().ToLowerInvariant();
if (args != "on" && args != "off")
{
Say($"Rifle suppressor is {(_suppressor ? "on" : "off")}. Usage: suppressor on or suppressor off");
return;
}
_suppressor = args == "on";
Save();
if (_rifle)
SetSuppressor(_rifle, _suppressor);
Say($"Rifle suppressor {(_suppressor ? "on" : "off")}");
}
[DebugCommand("scopezoom")]
private static void ScopeZoomCommand(string args)
{
@@ -790,6 +1085,8 @@ public class RifleScope : SonsMod
_windCm = wind;
if (parts.Length > 3)
_enabled = parts[3] != "0";
if (parts.Length > 4)
_suppressor = parts[4] == "1";
}
catch (Exception e)
{
@@ -801,7 +1098,7 @@ public class RifleScope : SonsMod
{
try
{
File.WriteAllText(_configPath, $"{_zoom.ToString(Inv)} {_trimCm.ToString(Inv)} {_windCm.ToString(Inv)} {(_enabled ? "1" : "0")}");
File.WriteAllText(_configPath, $"{_zoom.ToString(Inv)} {_trimCm.ToString(Inv)} {_windCm.ToString(Inv)} {(_enabled ? "1" : "0")} {(_suppressor ? "1" : "0")}");
}
catch (Exception e)
{
+2 -2
View File
@@ -6,7 +6,7 @@
<LangVersion>latest</LangVersion>
<AssemblyName>RifleScope</AssemblyName>
<RootNamespace>RifleScope</RootNamespace>
<Version>1.0.1</Version>
<Version>1.1.0</Version>
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
</PropertyGroup>
@@ -34,4 +34,4 @@
<EmbeddedResource Include="manifest.json" />
</ItemGroup>
</Project>
</Project>
+2 -2
View File
@@ -3,9 +3,9 @@
"id": "RifleScope",
"name": "RifleScope",
"author": "JustinOros",
"version": "1.0.1",
"version": "1.1.0",
"description": "Stronger rifle scope magnification and a crosshair reticle",
"gameVersion": "1.0.0",
"platform": "Client",
"type": "Mod"
}
}