Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07b8ebb2e7 | ||
|
|
ad9987cac0 | ||
|
|
af00ddb866 |
@@ -1,9 +1,14 @@
|
||||
# Rifle Scope
|
||||
|
||||
[](https://github.com/JustinOros/sotf-riflescope/releases) [](https://github.com/JustinOros/sotf-riflescope/releases/latest)
|
||||
|
||||
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.
|
||||
|
||||
Want a suppressed rifle too? Install
|
||||
[RifleSuppressor](https://github.com/JustinOros/sotf-riflesuppressor). It used to
|
||||
be part of this mod and is now its own mod, so you can use either one or both.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
@@ -69,7 +74,7 @@ Press F1 to open the console, then use these commands:
|
||||
| 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 off` | Go back to the vanilla scope |
|
||||
| `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 |
|
||||
@@ -77,22 +82,12 @@ Press F1 to open the console, then use these commands:
|
||||
| `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, suppressor, zoom, trim and windage are saved to `UserData\RifleScope.txt` in your game
|
||||
The on or off setting, 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.
|
||||
|
||||
|
||||
+841
@@ -0,0 +1,841 @@
|
||||
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 RifleScope;
|
||||
|
||||
public class RifleScope : SonsMod
|
||||
{
|
||||
private const float DefaultZoom = 5f;
|
||||
private const float MinZoom = 1f;
|
||||
private const float MaxZoom = 50f;
|
||||
private const float DefaultTrimCm = 60f;
|
||||
private const float MaxTrimCm = 200f;
|
||||
private const float MaxWindCm = 200f;
|
||||
private const float TrimReferenceRange = 50f;
|
||||
private const int MilDotCount = 10;
|
||||
private const float MilDotSize = 0.4f;
|
||||
private const float MinDotPx = 7f;
|
||||
private const float MinDotSpacingPx = 14f;
|
||||
private const float ThinLinePx = 2f;
|
||||
private const float PostPx = 8f;
|
||||
private const string RedDotShader = "Sons/RedDot HLSL";
|
||||
private const int MaskSize = 1024;
|
||||
private const float MaxRayDistance = 2000f;
|
||||
|
||||
private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
|
||||
private static readonly Dictionary<IntPtr, float> OriginalOffsets = new();
|
||||
private static readonly Dictionary<int, Material> HiddenMaterials = new();
|
||||
private static readonly HashSet<int> HiddenIds = new();
|
||||
|
||||
private static float _zoom = DefaultZoom;
|
||||
private static float _trimCm = DefaultTrimCm;
|
||||
private static float _windCm;
|
||||
private static bool _debug;
|
||||
private static bool _enabled = true;
|
||||
private static readonly List<(Renderer Renderer, int Slot, Material Original)> RedDotSlots = new();
|
||||
private static string _configPath;
|
||||
|
||||
private static RifleAnimatorController _rifle;
|
||||
private static RifleAnimatorController _pendingRifle;
|
||||
private static float _hipFov;
|
||||
private static float _vanillaAimFov = 35f;
|
||||
private static float _targetAimFov;
|
||||
private static int _heldLayer = -1;
|
||||
private static Camera _hiddenCam;
|
||||
private static Transform _projectile;
|
||||
private static ProjectileTransformConstraint _steeredConstraint;
|
||||
private static Quaternion _originalShotOffset;
|
||||
private static bool _steered;
|
||||
|
||||
private static GameObject _overlay;
|
||||
private static RectTransform _scopeRect;
|
||||
private static RectTransform _crosshairRect;
|
||||
private static RectTransform _lineH;
|
||||
private static RectTransform _lineV;
|
||||
private static readonly List<(RectTransform Rect, Vector2 Dir)> Posts = new();
|
||||
private static float _layoutH = -1f;
|
||||
private static readonly List<(RectTransform Rect, Vector2 Dir)> MilDots = new();
|
||||
private static RectTransform _leftBar;
|
||||
private static RectTransform _rightBar;
|
||||
private static TextMeshProUGUI _rangeText;
|
||||
private static string _lastRange;
|
||||
private static int _milsPerDot = 1;
|
||||
|
||||
public RifleScope()
|
||||
{
|
||||
OnUpdateCallback = OnUpdate;
|
||||
HarmonyPatchAll = true;
|
||||
}
|
||||
|
||||
protected override void OnSdkInitialized()
|
||||
{
|
||||
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "RifleScope.txt");
|
||||
Load();
|
||||
RLog.Msg($"RifleScope loaded. Zoom {Fmt(_zoom)}x the vanilla scope. Change it with: scopezoom <{Fmt(MinZoom)}-{Fmt(MaxZoom)}>");
|
||||
}
|
||||
|
||||
private void OnUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
Tick();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"RifleScope error: {e.Message}");
|
||||
_rifle = null;
|
||||
SetZoomed(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Tick()
|
||||
{
|
||||
if (!_rifle || !_rifle.gameObject.activeInHierarchy)
|
||||
{
|
||||
if (_rifle || _hiddenCam)
|
||||
{
|
||||
_rifle = null;
|
||||
SetZoomed(false, null);
|
||||
}
|
||||
|
||||
var pending = _pendingRifle;
|
||||
_pendingRifle = null;
|
||||
if (!pending || !pending.gameObject.activeInHierarchy)
|
||||
return;
|
||||
AdoptRifle(pending);
|
||||
}
|
||||
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
var manager = _rifle._cameraFovManager;
|
||||
if (!manager)
|
||||
{
|
||||
SetZoomed(false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateOffset(manager);
|
||||
|
||||
var cam = manager._targetCamera;
|
||||
var zoomed = _rifle.IsAiming && cam && _zoom > 1.01f && cam.fieldOfView < _vanillaAimFov - 0.5f;
|
||||
SetZoomed(zoomed, cam);
|
||||
}
|
||||
|
||||
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;
|
||||
_projectile = FindProjectileTransform(rifle);
|
||||
if (_enabled)
|
||||
{
|
||||
if (!_overlay)
|
||||
CreateOverlay();
|
||||
HideRedDot(rifle);
|
||||
}
|
||||
}
|
||||
|
||||
private static Transform FindProjectileTransform(RangedWeaponController rifle)
|
||||
{
|
||||
var constraint = rifle._projectileTransformConstraint;
|
||||
if (constraint)
|
||||
return constraint.transform;
|
||||
foreach (var t in rifle.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
if (t.name == "ProjectileTransform")
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool CenterPoint(Camera cam, out Vector3 point)
|
||||
{
|
||||
var origin = cam.transform.position;
|
||||
var dir = cam.transform.forward;
|
||||
var mask = _rifle ? _rifle._aimHitMask.value : 0;
|
||||
if (mask == 0)
|
||||
mask = Physics.DefaultRaycastLayers;
|
||||
|
||||
if (Physics.Raycast(origin, dir, out var hit, MaxRayDistance, mask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
point = hit.point;
|
||||
return true;
|
||||
}
|
||||
|
||||
point = origin + dir * MaxRayDistance;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static float Range(Camera cam)
|
||||
{
|
||||
if (!cam || !CenterPoint(cam, out var point))
|
||||
return -1f;
|
||||
return Vector3.Distance(cam.transform.position, point);
|
||||
}
|
||||
|
||||
private static void SteerShot(Camera cam)
|
||||
{
|
||||
var constraint = _rifle._projectileTransformConstraint;
|
||||
if (!constraint || !_projectile)
|
||||
return;
|
||||
|
||||
var goal = constraint._target;
|
||||
if (!goal)
|
||||
return;
|
||||
|
||||
if (!_steered || _steeredConstraint.Pointer != constraint.Pointer)
|
||||
{
|
||||
RestoreShot();
|
||||
_steeredConstraint = constraint;
|
||||
_originalShotOffset = constraint._offset;
|
||||
_steered = true;
|
||||
}
|
||||
|
||||
CenterPoint(cam, out var point);
|
||||
var scale = Vector3.Distance(cam.transform.position, point) / TrimReferenceRange;
|
||||
point -= cam.transform.up * (_trimCm * 0.01f * scale);
|
||||
point += cam.transform.right * (_windCm * 0.01f * scale);
|
||||
var dir = point - _projectile.position;
|
||||
if (dir.sqrMagnitude < 0.0001f)
|
||||
return;
|
||||
|
||||
var desired = Quaternion.LookRotation(dir, goal.up);
|
||||
constraint._offset = Quaternion.Inverse(goal.rotation) * desired;
|
||||
}
|
||||
|
||||
private static string DebugLine(Camera cam)
|
||||
{
|
||||
if (!_projectile || !CenterPoint(cam, out var point))
|
||||
return $"trim {Fmt(_trimCm)} cm\nno target";
|
||||
|
||||
var r = Vector3.Distance(_projectile.position, point);
|
||||
var barrel = _projectile.position + _projectile.forward * r;
|
||||
var up = Vector3.Dot(barrel - point, cam.transform.up) * 100f;
|
||||
var side = Vector3.Dot(barrel - point, cam.transform.right) * 100f;
|
||||
var eye = Vector3.Dot(_projectile.position - cam.transform.position, cam.transform.up) * 100f;
|
||||
return $"trim {Fmt(_trimCm)} wind {Fmt(_windCm)} steer {(_steered ? "on" : "off")}\nbarrel up {up:+0.0;-0.0} cm side {side:+0.0;-0.0} cm\nbarrel above eye {eye:+0.0;-0.0} cm";
|
||||
}
|
||||
|
||||
private static void RestoreShot()
|
||||
{
|
||||
if (_steered && _steeredConstraint)
|
||||
_steeredConstraint._offset = _originalShotOffset;
|
||||
_steered = false;
|
||||
_steeredConstraint = null;
|
||||
}
|
||||
|
||||
private static void UpdateOffset(FovManager manager)
|
||||
{
|
||||
var settings = _rifle._fovChangeSettings;
|
||||
if (settings == null)
|
||||
return;
|
||||
|
||||
if (!OriginalOffsets.TryGetValue(settings.Pointer, out var original))
|
||||
{
|
||||
original = settings._FieldOfViewTargetOffset_k__BackingField;
|
||||
OriginalOffsets[settings.Pointer] = original;
|
||||
}
|
||||
|
||||
if (!_rifle.IsAiming && manager._activeSource == null)
|
||||
_hipFov = manager._currentTargetFieldOfView;
|
||||
if (_hipFov <= 1f)
|
||||
_hipFov = manager._defaultFov;
|
||||
|
||||
_vanillaAimFov = Mathf.Clamp(_hipFov + original, 1f, 179f);
|
||||
|
||||
if (_rifle.IsAiming)
|
||||
return;
|
||||
|
||||
var target = 2f * Mathf.Atan(Mathf.Tan(_vanillaAimFov * 0.5f * Mathf.Deg2Rad) / _zoom) * Mathf.Rad2Deg;
|
||||
_targetAimFov = target;
|
||||
var offset = target - _hipFov;
|
||||
if (Mathf.Abs(settings._FieldOfViewTargetOffset_k__BackingField - offset) > 0.001f)
|
||||
settings._FieldOfViewTargetOffset_k__BackingField = offset;
|
||||
}
|
||||
|
||||
private static void HideRedDot(RifleAnimatorController rifle)
|
||||
{
|
||||
foreach (var renderer in rifle.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
var mats = renderer.sharedMaterials;
|
||||
var changed = false;
|
||||
for (var i = 0; i < mats.Length; i++)
|
||||
{
|
||||
var mat = mats[i];
|
||||
if (!mat || HiddenIds.Contains(mat.GetInstanceID()) || !mat.shader || mat.shader.name != RedDotShader)
|
||||
continue;
|
||||
RedDotSlots.Add((renderer, i, mat));
|
||||
mats[i] = HiddenFor(mat);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
renderer.sharedMaterials = mats;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var id = original.GetInstanceID();
|
||||
if (HiddenMaterials.TryGetValue(id, out var cached) && cached)
|
||||
return cached;
|
||||
|
||||
var hidden = new Material(original)
|
||||
{
|
||||
name = original.name + "_Hidden",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
hidden.SetColor("_Color", Color.clear);
|
||||
if (hidden.HasProperty("_EmissionColor"))
|
||||
hidden.SetColor("_EmissionColor", Color.clear);
|
||||
|
||||
HiddenMaterials[id] = hidden;
|
||||
HiddenIds.Add(hidden.GetInstanceID());
|
||||
return hidden;
|
||||
}
|
||||
|
||||
private static void SetZoomed(bool zoomed, Camera cam)
|
||||
{
|
||||
if (zoomed)
|
||||
{
|
||||
if (_heldLayer < 0)
|
||||
{
|
||||
_heldLayer = LayerMask.NameToLayer("Held");
|
||||
if (_heldLayer < 0)
|
||||
_heldLayer = 15;
|
||||
}
|
||||
|
||||
if (_hiddenCam && _hiddenCam != cam)
|
||||
ShowHeld();
|
||||
if (!_hiddenCam)
|
||||
{
|
||||
cam.cullingMask &= ~(1 << _heldLayer);
|
||||
_hiddenCam = cam;
|
||||
}
|
||||
|
||||
SteerShot(cam);
|
||||
ShowOverlay(true, Range(cam), _debug ? DebugLine(cam) : null, _targetAimFov > 0f ? _targetAimFov : cam.fieldOfView);
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreShot();
|
||||
if (_hiddenCam)
|
||||
ShowHeld();
|
||||
_hiddenCam = null;
|
||||
ShowOverlay(false, -1f, null, 0f);
|
||||
}
|
||||
|
||||
private static void ShowHeld()
|
||||
{
|
||||
if (_hiddenCam)
|
||||
_hiddenCam.cullingMask |= 1 << _heldLayer;
|
||||
_hiddenCam = null;
|
||||
}
|
||||
|
||||
private static void ShowOverlay(bool show, float range, string debug, float fov)
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
if (_overlay && _overlay.activeSelf)
|
||||
_overlay.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_overlay)
|
||||
CreateOverlay();
|
||||
if (!_overlay.activeSelf)
|
||||
_overlay.SetActive(true);
|
||||
|
||||
var h = (float)Screen.height;
|
||||
if (!Mathf.Approximately(h, _layoutH))
|
||||
{
|
||||
_layoutH = h;
|
||||
LayoutReticle(h);
|
||||
}
|
||||
PlaceMilDots(h, fov);
|
||||
|
||||
if (_rangeText)
|
||||
{
|
||||
var label = range < 0f ? "--- m" : $"{Mathf.RoundToInt(range)} m";
|
||||
if (debug != null)
|
||||
label = debug + "\n" + label;
|
||||
if (label != _lastRange)
|
||||
{
|
||||
_lastRange = label;
|
||||
_rangeText.text = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void LayoutReticle(float h)
|
||||
{
|
||||
var radius = h * 0.495f;
|
||||
var postStart = radius * 0.55f;
|
||||
var postLength = h - postStart;
|
||||
|
||||
_scopeRect.sizeDelta = new Vector2(h, h);
|
||||
_crosshairRect.sizeDelta = new Vector2(h * 2f, h * 2f);
|
||||
_crosshairRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
_lineH.sizeDelta = new Vector2(h * 2f, ThinLinePx);
|
||||
_lineV.sizeDelta = new Vector2(ThinLinePx, h * 2f);
|
||||
|
||||
foreach (var (rect, dir) in Posts)
|
||||
{
|
||||
if (!rect)
|
||||
continue;
|
||||
var center = postStart + postLength * 0.5f;
|
||||
rect.anchoredPosition = dir * center;
|
||||
rect.sizeDelta = dir.x != 0f ? new Vector2(postLength, PostPx) : new Vector2(PostPx, postLength);
|
||||
}
|
||||
|
||||
if (_rangeText)
|
||||
{
|
||||
var rt = _rangeText.rectTransform;
|
||||
rt.anchoredPosition = new Vector2(-0.34f * h, -0.31f * h);
|
||||
rt.sizeDelta = new Vector2(0.3f * h, 0.2f * h);
|
||||
_rangeText.fontSize = 0.035f * h;
|
||||
}
|
||||
|
||||
_leftBar.offsetMax = new Vector2(-h * 0.5f + 1f, 0f);
|
||||
_rightBar.offsetMin = new Vector2(h * 0.5f - 1f, 0f);
|
||||
}
|
||||
|
||||
private static void CreateOverlay()
|
||||
{
|
||||
_overlay = new GameObject("RifleScopeOverlay");
|
||||
UnityEngine.Object.DontDestroyOnLoad(_overlay);
|
||||
_overlay.hideFlags = HideFlags.HideAndDontSave;
|
||||
|
||||
var canvas = _overlay.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = -100;
|
||||
|
||||
var crosshairGo = new GameObject("Crosshair");
|
||||
crosshairGo.hideFlags = HideFlags.HideAndDontSave;
|
||||
crosshairGo.transform.SetParent(_overlay.transform, false);
|
||||
_crosshairRect = crosshairGo.AddComponent<RectTransform>();
|
||||
Center(_crosshairRect);
|
||||
|
||||
_lineH = CreateImage("LineH", Texture2D.whiteTexture, Color.black, _crosshairRect).rectTransform;
|
||||
Center(_lineH);
|
||||
_lineV = CreateImage("LineV", Texture2D.whiteTexture, Color.black, _crosshairRect).rectTransform;
|
||||
Center(_lineV);
|
||||
|
||||
Posts.Clear();
|
||||
foreach (var dir in new[] { Vector2.up, Vector2.down, Vector2.left, Vector2.right })
|
||||
{
|
||||
var post = CreateImage($"Post{dir}", Texture2D.whiteTexture, Color.black, _crosshairRect).rectTransform;
|
||||
Center(post);
|
||||
Posts.Add((post, dir));
|
||||
}
|
||||
|
||||
CreateMilDots();
|
||||
|
||||
var scope = CreateImage("Scope", BuildMaskTexture(), Color.white, _overlay.transform);
|
||||
_scopeRect = scope.rectTransform;
|
||||
Center(_scopeRect);
|
||||
|
||||
_rangeText = CreateRangeText(scope.transform);
|
||||
|
||||
_leftBar = CreateImage("Left", Texture2D.whiteTexture, Color.black, _overlay.transform).rectTransform;
|
||||
_leftBar.anchorMin = new Vector2(0f, 0f);
|
||||
_leftBar.anchorMax = new Vector2(0.5f, 1f);
|
||||
_leftBar.offsetMin = Vector2.zero;
|
||||
|
||||
_rightBar = CreateImage("Right", Texture2D.whiteTexture, Color.black, _overlay.transform).rectTransform;
|
||||
_rightBar.anchorMin = new Vector2(0.5f, 0f);
|
||||
_rightBar.anchorMax = new Vector2(1f, 1f);
|
||||
_rightBar.offsetMax = Vector2.zero;
|
||||
|
||||
_layoutH = -1f;
|
||||
_overlay.SetActive(false);
|
||||
}
|
||||
|
||||
private static void Center(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt.anchoredPosition = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void CreateMilDots()
|
||||
{
|
||||
MilDots.Clear();
|
||||
var tex = BuildDotTexture();
|
||||
foreach (var dir in new[] { Vector2.up, Vector2.down, Vector2.left, Vector2.right })
|
||||
{
|
||||
for (var k = 1; k <= MilDotCount; k++)
|
||||
{
|
||||
var rt = CreateImage($"MilDot{dir}{k}", tex, Color.black, _crosshairRect).rectTransform;
|
||||
Center(rt);
|
||||
MilDots.Add((rt, dir * k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceMilDots(float h, float fov)
|
||||
{
|
||||
if (fov <= 0f)
|
||||
return;
|
||||
|
||||
var pxPerMil = h * 0.5f / Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad) * 0.001f;
|
||||
_milsPerDot = 1;
|
||||
foreach (var step in new[] { 1, 2, 5, 10 })
|
||||
{
|
||||
_milsPerDot = step;
|
||||
if (pxPerMil * step >= MinDotSpacingPx)
|
||||
break;
|
||||
}
|
||||
|
||||
var spacing = pxPerMil * _milsPerDot;
|
||||
var size = Math.Max(MinDotPx, MilDotSize * spacing);
|
||||
var limit = h * 0.495f * 0.95f;
|
||||
|
||||
foreach (var (rect, dir) in MilDots)
|
||||
{
|
||||
if (!rect)
|
||||
continue;
|
||||
var pos = dir * spacing;
|
||||
var visible = pos.magnitude <= limit;
|
||||
if (rect.gameObject.activeSelf != visible)
|
||||
rect.gameObject.SetActive(visible);
|
||||
if (!visible)
|
||||
continue;
|
||||
rect.anchoredPosition = pos;
|
||||
rect.sizeDelta = new Vector2(size, size);
|
||||
}
|
||||
}
|
||||
|
||||
private static Texture2D BuildDotTexture()
|
||||
{
|
||||
const int size = 64;
|
||||
var pixels = new Color32[size * size];
|
||||
var c = (size - 1) * 0.5f;
|
||||
var radius = size * 0.5f - 1f;
|
||||
for (var y = 0; y < size; y++)
|
||||
{
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var dx = x - c;
|
||||
var dy = y - c;
|
||||
var d = MathF.Sqrt(dx * dx + dy * dy);
|
||||
var alpha = Math.Clamp(radius - d + 0.5f, 0f, 1f);
|
||||
pixels[y * size + x] = new Color32(255, 255, 255, (byte)(alpha * 255f + 0.5f));
|
||||
}
|
||||
}
|
||||
return MakeTexture("RifleScopeMilDot", size, pixels);
|
||||
}
|
||||
|
||||
private static TextMeshProUGUI CreateRangeText(Transform parent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var go = new GameObject("Range");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
go.transform.SetParent(parent, false);
|
||||
var text = go.AddComponent<TextMeshProUGUI>();
|
||||
var font = FindFont();
|
||||
if (font)
|
||||
text.font = font;
|
||||
text.color = new Color(0.9f, 0.08f, 0.08f, 1f);
|
||||
text.alignment = TextAlignmentOptions.BottomLeft;
|
||||
text.enableWordWrapping = false;
|
||||
text.raycastTarget = false;
|
||||
text.text = string.Empty;
|
||||
|
||||
var rt = text.rectTransform;
|
||||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rt.pivot = Vector2.zero;
|
||||
return text;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"RifleScope could not create the range readout: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static TMP_FontAsset FindFont()
|
||||
{
|
||||
try
|
||||
{
|
||||
var font = TMP_Settings.defaultFontAsset;
|
||||
if (font)
|
||||
return font;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
foreach (var font in Resources.FindObjectsOfTypeAll<TMP_FontAsset>())
|
||||
{
|
||||
if (font)
|
||||
return font;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RawImage CreateImage(string name, Texture texture, Color color, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
go.transform.SetParent(parent, false);
|
||||
var image = go.AddComponent<RawImage>();
|
||||
image.texture = texture;
|
||||
image.color = color;
|
||||
image.raycastTarget = false;
|
||||
return image;
|
||||
}
|
||||
|
||||
private static Texture2D BuildMaskTexture()
|
||||
{
|
||||
var size = MaskSize;
|
||||
var pixels = new Color32[size * size];
|
||||
var c = (size - 1) * 0.5f;
|
||||
var radius = size * 0.495f;
|
||||
var fadeStart = radius * 0.88f;
|
||||
|
||||
for (var y = 0; y < size; y++)
|
||||
{
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var dx = x - c;
|
||||
var dy = y - c;
|
||||
var d = MathF.Sqrt(dx * dx + dy * dy);
|
||||
var alpha = Math.Clamp((d - radius) * 0.5f + 0.5f, 0f, 1f);
|
||||
var t = Math.Clamp((d - fadeStart) / (radius - fadeStart), 0f, 1f);
|
||||
var fade = t * t * (3f - 2f * t) * 0.55f;
|
||||
if (fade > alpha)
|
||||
alpha = fade;
|
||||
pixels[y * size + x] = new Color32(0, 0, 0, (byte)(alpha * 255f + 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
return MakeTexture("RifleScopeMask", size, pixels);
|
||||
}
|
||||
|
||||
private static Texture2D MakeTexture(string name, int size, Color32[] pixels)
|
||||
{
|
||||
var tex = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
||||
{
|
||||
name = name,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
tex.SetPixels32(pixels);
|
||||
tex.Apply(false, false);
|
||||
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("scopezoom")]
|
||||
private static void ScopeZoomCommand(string args)
|
||||
{
|
||||
args = (args ?? string.Empty).Trim();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Say($"Scope zoom is {Fmt(_zoom)}x the vanilla scope. Usage: scopezoom <{Fmt(MinZoom)}-{Fmt(MaxZoom)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!float.TryParse(args, NumberStyles.Float, Inv, out var value) || value < MinZoom || value > MaxZoom)
|
||||
{
|
||||
Say($"Usage: scopezoom <{Fmt(MinZoom)}-{Fmt(MaxZoom)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
_zoom = value;
|
||||
Save();
|
||||
Say($"Scope zoom set to {Fmt(_zoom)}x the vanilla scope. Applies the next time you aim.");
|
||||
}
|
||||
|
||||
[DebugCommand("scopetrim")]
|
||||
private static void ScopeTrimCommand(string args)
|
||||
{
|
||||
args = (args ?? string.Empty).Trim();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Say($"Scope trim is {Fmt(_trimCm)}. Positive moves hits down, negative moves hits up. Usage: scopetrim <-{Fmt(MaxTrimCm)} to {Fmt(MaxTrimCm)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!float.TryParse(args, NumberStyles.Float, Inv, out var value) || value < -MaxTrimCm || value > MaxTrimCm)
|
||||
{
|
||||
Say($"Usage: scopetrim <-{Fmt(MaxTrimCm)} to {Fmt(MaxTrimCm)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
_trimCm = value;
|
||||
Save();
|
||||
Say($"Scope trim set to {Fmt(_trimCm)}.");
|
||||
}
|
||||
|
||||
[DebugCommand("scopewind")]
|
||||
private static void ScopeWindCommand(string args)
|
||||
{
|
||||
args = (args ?? string.Empty).Trim();
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Say($"Scope windage is {Fmt(_windCm)}. Positive moves hits right, negative moves hits left. Usage: scopewind <-{Fmt(MaxWindCm)} to {Fmt(MaxWindCm)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!float.TryParse(args, NumberStyles.Float, Inv, out var value) || value < -MaxWindCm || value > MaxWindCm)
|
||||
{
|
||||
Say($"Usage: scopewind <-{Fmt(MaxWindCm)} to {Fmt(MaxWindCm)}>");
|
||||
return;
|
||||
}
|
||||
|
||||
_windCm = value;
|
||||
Save();
|
||||
Say($"Scope windage set to {Fmt(_windCm)}.");
|
||||
}
|
||||
|
||||
[DebugCommand("scopedebug")]
|
||||
private static void ScopeDebugCommand(string args)
|
||||
{
|
||||
_debug = !_debug;
|
||||
Say($"Scope debug readout {(_debug ? "on" : "off")}");
|
||||
}
|
||||
|
||||
private static void Say(string text)
|
||||
{
|
||||
RLog.Msg(text);
|
||||
try
|
||||
{
|
||||
SonsTools.ShowMessage(text, 5f);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string Fmt(float v) => v.ToString("0.##", Inv);
|
||||
|
||||
private static void Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_configPath))
|
||||
return;
|
||||
var parts = File.ReadAllText(_configPath).Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 0 && float.TryParse(parts[0], NumberStyles.Float, Inv, out var zoom) && zoom >= MinZoom && zoom <= MaxZoom)
|
||||
_zoom = zoom;
|
||||
if (parts.Length > 1 && float.TryParse(parts[1], NumberStyles.Float, Inv, out var trim) && trim >= -MaxTrimCm && trim <= MaxTrimCm)
|
||||
_trimCm = trim;
|
||||
if (parts.Length > 2 && float.TryParse(parts[2], NumberStyles.Float, Inv, out var wind) && wind >= -MaxWindCm && wind <= MaxWindCm)
|
||||
_windCm = wind;
|
||||
if (parts.Length > 3)
|
||||
_enabled = parts[3] != "0";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"RifleScope could not read config: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(_configPath, $"{_zoom.ToString(Inv)} {_trimCm.ToString(Inv)} {_windCm.ToString(Inv)} {(_enabled ? "1" : "0")}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"RifleScope could not write config: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(RangedWeaponController), nameof(RangedWeaponController.CheckFireInput))]
|
||||
internal static class CheckFireInputPatch
|
||||
{
|
||||
private static void Postfix(RangedWeaponController __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
global::RifleScope.RifleScope.OnWeaponTick(__instance);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>RifleScope</AssemblyName>
|
||||
<RootNamespace>RifleScope</RootNamespace>
|
||||
<Version>1.1.1</Version>
|
||||
<Version>1.2.0</Version>
|
||||
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"id": "RifleScope",
|
||||
"name": "RifleScope",
|
||||
"author": "JustinOros",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"description": "Stronger rifle scope magnification and a crosshair reticle",
|
||||
"gameVersion": "1.0.0",
|
||||
"platform": "Client",
|
||||
|
||||
Reference in New Issue
Block a user