5 Commits
5 changed files with 384 additions and 14 deletions

No files matched your search

-2
View File
@@ -117,7 +117,6 @@ try {
Write-Host 'Start Sons of the Forest, then:' Write-Host 'Start Sons of the Forest, then:'
Write-Host ' 1. Check that MODS appears on the main menu and lists RifleScope' Write-Host ' 1. Check that MODS appears on the main menu and lists RifleScope'
Write-Host ' 2. Load into a game, equip the rifle and aim down the scope' Write-Host ' 2. Load into a game, equip the rifle and aim down the scope'
Write-Host ' 3. Press F1 and type scopezoom to see or change the zoom'
Write-Host '' Write-Host ''
} }
catch { catch {
@@ -129,4 +128,3 @@ catch {
Write-Host '' Write-Host ''
} }
Read-Host 'Press Enter to close' | Out-Null
+20 -3
View File
@@ -2,7 +2,8 @@
A stronger rifle scope for Sons of the Forest. Aiming down the rifle scope zooms 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 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. 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.
## Multiplayer ## Multiplayer
@@ -67,15 +68,31 @@ Press F1 to open the console, then use these commands:
| Command | Action | | Command | Action |
| --- | --- | | --- | --- |
| `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` | Show the current zoom |
| `scopezoom 5` | Set the zoom as a multiple of the vanilla scope, 1 to 50. The default is 5 | | `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` | 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` | Show the current windage |
| `scopewind 20` | Set the windage, -200 to 200. Positive moves hits right, negative moves hits left. The default is 0 | | `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 | | `scopedebug` | Toggle a debug readout above the range |
The zoom, trim and windage are saved to `UserData\RifleScope.txt` in your game ### 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, 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, folder. Setting the zoom to 1 keeps vanilla zoom and the normal scope view,
without the red dot. without the red dot.
+362 -7
View File
@@ -2,9 +2,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using RedLoader; using RedLoader;
using RedLoader.Utils; using RedLoader.Utils;
using Sons.FieldOfView; using Sons.FieldOfView;
using Sons.Items.Core;
using Sons.Weapon; using Sons.Weapon;
using SonsSdk; using SonsSdk;
using SonsSdk.Attributes; using SonsSdk.Attributes;
@@ -20,9 +26,16 @@ public class RifleScope : SonsMod
private const float DefaultZoom = 5f; private const float DefaultZoom = 5f;
private const float MinZoom = 1f; private const float MinZoom = 1f;
private const float MaxZoom = 50f; private const float MaxZoom = 50f;
private const float DefaultTrimCm = 50f; private const float DefaultTrimCm = 60f;
private const float MaxTrimCm = 200f; private const float MaxTrimCm = 200f;
private const float MaxWindCm = 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 int MilDotCount = 10;
private const float MilDotSize = 0.4f; private const float MilDotSize = 0.4f;
private const float MinDotPx = 7f; private const float MinDotPx = 7f;
@@ -42,6 +55,14 @@ public class RifleScope : SonsMod
private static float _trimCm = DefaultTrimCm; private static float _trimCm = DefaultTrimCm;
private static float _windCm; private static float _windCm;
private static bool _debug; 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; private static string _configPath;
private static RifleAnimatorController _rifle; private static RifleAnimatorController _rifle;
@@ -109,6 +130,9 @@ public class RifleScope : SonsMod
return; return;
} }
if (!_enabled)
return;
var manager = _rifle._cameraFovManager; var manager = _rifle._cameraFovManager;
if (!manager) if (!manager)
{ {
@@ -131,14 +155,274 @@ public class RifleScope : SonsMod
continue; continue;
_rifle = rifle; _rifle = rifle;
_projectile = FindProjectileTransform(rifle); _projectile = FindProjectileTransform(rifle);
if (!_overlay) _originalFireEvent ??= rifle._gunShotAudioEvent;
CreateOverlay(); if (_enabled)
HideRedDot(rifle); {
if (!_overlay)
CreateOverlay();
HideRedDot(rifle);
}
if (_suppressor)
SetSuppressor(rifle, true);
return true; return true;
} }
return false; 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) private static Transform FindProjectileTransform(RangedWeaponController rifle)
{ {
var constraint = rifle._projectileTransformConstraint; var constraint = rifle._projectileTransformConstraint;
@@ -196,8 +480,9 @@ public class RifleScope : SonsMod
} }
CenterPoint(cam, out var point); CenterPoint(cam, out var point);
point -= cam.transform.up * (_trimCm * 0.01f); var scale = Vector3.Distance(cam.transform.position, point) / TrimReferenceRange;
point += cam.transform.right * (_windCm * 0.01f); point -= cam.transform.up * (_trimCm * 0.01f * scale);
point += cam.transform.right * (_windCm * 0.01f * scale);
var dir = point - _projectile.position; var dir = point - _projectile.position;
if (dir.sqrMagnitude < 0.0001f) if (dir.sqrMagnitude < 0.0001f)
return; return;
@@ -267,6 +552,7 @@ public class RifleScope : SonsMod
var mat = mats[i]; var mat = mats[i];
if (!mat || HiddenIds.Contains(mat.GetInstanceID()) || !mat.shader || mat.shader.name != RedDotShader) if (!mat || HiddenIds.Contains(mat.GetInstanceID()) || !mat.shader || mat.shader.name != RedDotShader)
continue; continue;
RedDotSlots.Add((renderer, i, mat));
mats[i] = HiddenFor(mat); mats[i] = HiddenFor(mat);
changed = true; changed = true;
} }
@@ -275,6 +561,33 @@ public class RifleScope : SonsMod
} }
} }
private static void RestoreRedDot()
{
foreach (var (renderer, slot, original) in RedDotSlots)
{
if (!renderer || !original)
continue;
var mats = renderer.sharedMaterials;
if (slot >= mats.Length || !mats[slot] || !HiddenIds.Contains(mats[slot].GetInstanceID()))
continue;
mats[slot] = original;
renderer.sharedMaterials = mats;
}
RedDotSlots.Clear();
}
private static void Disable()
{
SetZoomed(false, null);
if (_rifle)
{
var settings = _rifle._fovChangeSettings;
if (settings != null && OriginalOffsets.TryGetValue(settings.Pointer, out var original))
settings._FieldOfViewTargetOffset_k__BackingField = original;
}
RestoreRedDot();
}
private static Material HiddenFor(Material original) private static Material HiddenFor(Material original)
{ {
var id = original.GetInstanceID(); var id = original.GetInstanceID();
@@ -632,6 +945,44 @@ public class RifleScope : SonsMod
return tex; return tex;
} }
[DebugCommand("scope")]
private static void ScopeCommand(string args)
{
args = (args ?? string.Empty).Trim().ToLowerInvariant();
if (args == "on" || args == "off")
{
_enabled = args == "on";
Save();
if (_enabled)
_rifle = null;
else
Disable();
Say($"RifleScope {(_enabled ? "on" : "off")}");
return;
}
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")] [DebugCommand("scopezoom")]
private static void ScopeZoomCommand(string args) private static void ScopeZoomCommand(string args)
{ {
@@ -732,6 +1083,10 @@ public class RifleScope : SonsMod
_trimCm = trim; _trimCm = trim;
if (parts.Length > 2 && float.TryParse(parts[2], NumberStyles.Float, Inv, out var wind) && wind >= -MaxWindCm && wind <= MaxWindCm) if (parts.Length > 2 && float.TryParse(parts[2], NumberStyles.Float, Inv, out var wind) && wind >= -MaxWindCm && wind <= MaxWindCm)
_windCm = wind; _windCm = wind;
if (parts.Length > 3)
_enabled = parts[3] != "0";
if (parts.Length > 4)
_suppressor = parts[4] == "1";
} }
catch (Exception e) catch (Exception e)
{ {
@@ -743,7 +1098,7 @@ public class RifleScope : SonsMod
{ {
try try
{ {
File.WriteAllText(_configPath, $"{_zoom.ToString(Inv)} {_trimCm.ToString(Inv)} {_windCm.ToString(Inv)}"); File.WriteAllText(_configPath, $"{_zoom.ToString(Inv)} {_trimCm.ToString(Inv)} {_windCm.ToString(Inv)} {(_enabled ? "1" : "0")} {(_suppressor ? "1" : "0")}");
} }
catch (Exception e) catch (Exception e)
{ {
+1 -1
View File
@@ -6,7 +6,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<AssemblyName>RifleScope</AssemblyName> <AssemblyName>RifleScope</AssemblyName>
<RootNamespace>RifleScope</RootNamespace> <RootNamespace>RifleScope</RootNamespace>
<Version>1.0.0</Version> <Version>1.1.0</Version>
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn> <NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
</PropertyGroup> </PropertyGroup>
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "RifleScope", "id": "RifleScope",
"name": "RifleScope", "name": "RifleScope",
"author": "JustinOros", "author": "JustinOros",
"version": "1.0.0", "version": "1.1.0",
"description": "Stronger rifle scope magnification and a crosshair reticle", "description": "Stronger rifle scope magnification and a crosshair reticle",
"gameVersion": "1.0.0", "gameVersion": "1.0.0",
"platform": "Client", "platform": "Client",