7 Commits
3 changed files with 250 additions and 11 deletions

No files matched your search

+235 -4
View File
@@ -7,6 +7,7 @@ using SonsSdk;
using SonsSdk.Attributes; using SonsSdk.Attributes;
using TheForest.Utils; using TheForest.Utils;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.UI; using UnityEngine.UI;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
@@ -20,6 +21,10 @@ public class BackupCam : SonsMod
const float SearchRadius = 2.5f; const float SearchRadius = 2.5f;
const string ScreenPath = "GolfCartScreen/GolfCartGps/Canvas/ScreenMask"; const string ScreenPath = "GolfCartScreen/GolfCartGps/Canvas/ScreenMask";
const string ScreenImageName = "BackupCamScreen"; const string ScreenImageName = "BackupCamScreen";
const string HeadLightPath = "LightsGroup/HeadLights";
const string RearLightName = "BackupCamRearLight";
const float ClipScanInterval = 0.2f;
const float ClipScanRadius = 8f;
static readonly string[] TextureProps = static readonly string[] TextureProps =
{ {
@@ -35,6 +40,9 @@ public class BackupCam : SonsMod
GameObject _canvasGo; GameObject _canvasGo;
RawImage _image; RawImage _image;
GameObject _screenGo; GameObject _screenGo;
GameObject _rearLightGo;
Light _headLight;
Light _rearLight;
float _screenAspect; float _screenAspect;
float _searchTimer; float _searchTimer;
float _lastReverse = -10f; float _lastReverse = -10f;
@@ -44,6 +52,15 @@ public class BackupCam : SonsMod
float _camZ = -1.3f; float _camZ = -1.3f;
float _camPitch = 20f; float _camPitch = 20f;
float _lightIntensity = 3f;
float _lightRange = 1.5f;
bool _clipping;
float _clipTimer;
readonly List<Collider> _cartColliders = new();
readonly List<Collider> _ignored = new();
readonly HashSet<int> _ignoredIds = new();
public BackupCam() public BackupCam()
{ {
_instance = this; _instance = this;
@@ -52,7 +69,7 @@ public class BackupCam : SonsMod
protected override void OnSdkInitialized() protected override void OnSdkInitialized()
{ {
RLog.Msg("BackupCam 1.1.1 loaded. Reverse the golf cart to show the camera on its GPS screen. Console: backupcamoffset, backupcamdump"); RLog.Msg("BackupCam 1.3.1 loaded. Reverse the golf cart to show the camera on its GPS screen and turn on rear lights. Console: backupcam, backupcamoffset, backupcamlight, backupcamdump");
} }
protected override void OnGameStart() protected override void OnGameStart()
@@ -107,6 +124,8 @@ public class BackupCam : SonsMod
if (!FindCart()) return; if (!FindCart()) return;
} }
if (_clipping) UpdateClipping();
var localVel = _body.transform.InverseTransformDirection(_body.velocity); var localVel = _body.transform.InverseTransformDirection(_body.velocity);
if (localVel.z < ReverseSpeed) _lastReverse = Time.time; if (localVel.z < ReverseSpeed) _lastReverse = Time.time;
SetShowing(Time.time - _lastReverse < HideDelay); SetShowing(Time.time - _lastReverse < HideDelay);
@@ -114,9 +133,13 @@ public class BackupCam : SonsMod
bool FindCart() bool FindCart()
{ {
RestoreCollisions();
_cart = null; _cart = null;
_body = null; _body = null;
_screenGo = null; _screenGo = null;
_rearLightGo = null;
_headLight = null;
_rearLight = null;
var player = LocalPlayer.Transform; var player = LocalPlayer.Transform;
var root = player.root; var root = player.root;
@@ -143,6 +166,7 @@ public class BackupCam : SonsMod
_body = body; _body = body;
AttachCamera(); AttachCamera();
AttachScreen(); AttachScreen();
AttachRearLight();
return true; return true;
} }
@@ -164,14 +188,36 @@ public class BackupCam : SonsMod
_cam.farClipPlane = 200f; _cam.farClipPlane = 200f;
_cam.targetTexture = _rt; _cam.targetTexture = _rt;
_cam.enabled = false; _cam.enabled = false;
var main = Camera.main; MatchMainCamera();
if (main != null) _cam.cullingMask = main.cullingMask;
} }
_cam.transform.SetParent(_body.transform, false); _cam.transform.SetParent(_body.transform, false);
ApplyOffset(); ApplyOffset();
} }
void MatchMainCamera()
{
var main = Camera.main;
if (main == null) return;
_cam.cullingMask = main.cullingMask;
var mainHd = main.GetComponent<HDAdditionalCameraData>();
if (mainHd == null) return;
var hd = _cam.GetComponent<HDAdditionalCameraData>();
if (hd == null) hd = _cam.gameObject.AddComponent<HDAdditionalCameraData>();
hd.volumeLayerMask = mainHd.volumeLayerMask;
hd.probeLayerMask = mainHd.probeLayerMask;
hd.antialiasing = mainHd.antialiasing;
hd.dithering = mainHd.dithering;
hd.stopNaNs = mainHd.stopNaNs;
hd.clearColorMode = mainHd.clearColorMode;
hd.backgroundColorHDR = mainHd.backgroundColorHDR;
RLog.Msg($"BackupCam: matched main camera volume mask {mainHd.volumeLayerMask.value}");
}
void AttachScreen() void AttachScreen()
{ {
var mask = _cart.transform.root.Find(ScreenPath); var mask = _cart.transform.root.Find(ScreenPath);
@@ -213,6 +259,141 @@ public class BackupCam : SonsMod
_screenAspect = maskRect.height > 0f ? maskRect.width / maskRect.height : 0f; _screenAspect = maskRect.height > 0f ? maskRect.width / maskRect.height : 0f;
} }
void AttachRearLight()
{
var root = _cart.transform.root;
var head = root.Find(HeadLightPath);
if (head == null)
{
RLog.Msg("BackupCam: headlights not found on this cart, no rear lights");
return;
}
_headLight = head.GetComponent<Light>();
var existing = root.Find(RearLightName);
GameObject go;
if (existing != null)
{
go = existing.gameObject;
}
else
{
go = Object.Instantiate(head.gameObject, root);
go.name = RearLightName;
foreach (var c in go.GetComponents<Component>())
if (c != null && c.GetIl2CppType().Name == "UL_FastGI")
Object.Destroy(c);
var localPos = root.InverseTransformPoint(head.position);
var localRot = Quaternion.Inverse(root.rotation) * head.rotation;
go.transform.localPosition = new Vector3(localPos.x, localPos.y, -localPos.z);
go.transform.localRotation = Quaternion.AngleAxis(180f, Vector3.up) * localRot;
}
go.SetActive(false);
_rearLightGo = go;
_rearLight = go.GetComponent<Light>();
ApplyLight();
}
void ApplyLight()
{
if (_rearLight == null || _headLight == null) return;
_rearLight.enabled = true;
_rearLight.intensity = _headLight.intensity * _lightIntensity;
_rearLight.range = _headLight.range * _lightRange;
if (_rearLight.type == LightType.Spot)
_rearLight.spotAngle = Mathf.Min(_headLight.spotAngle * 1.25f, 150f);
}
void UpdateClipping()
{
_clipTimer -= Time.deltaTime;
if (_clipTimer > 0f) return;
_clipTimer = ClipScanInterval;
var root = _body.transform.root;
if (_cartColliders.Count == 0)
{
foreach (var c in root.GetComponentsInChildren<Collider>(true))
if (c != null && !c.isTrigger && c.TryCast<WheelCollider>() == null)
_cartColliders.Add(c);
}
foreach (var other in Physics.OverlapSphere(_body.position, ClipScanRadius, ~0, QueryTriggerInteraction.Ignore))
{
if (other == null || ClipSkipReason(other, root) != null) continue;
if (!_ignoredIds.Add(other.GetInstanceID())) continue;
foreach (var mine in _cartColliders)
if (mine != null) Physics.IgnoreCollision(mine, other, true);
_ignored.Add(other);
}
}
string ClipSkipReason(Collider other, Transform root)
{
if (other.isTrigger) return "trigger";
if (other.transform.IsChildOf(root)) return "part of this cart";
if (other.TryCast<TerrainCollider>() != null) return "terrain";
if (other.TryCast<CharacterController>() != null) return "player";
var rb = other.attachedRigidbody;
if (rb != null && !rb.isKinematic) return "physics object";
return null;
}
void WriteClipScan()
{
if (_body == null)
{
RLog.Msg("BackupCam: get in a golf cart first");
return;
}
var root = _body.transform.root;
var sb = new StringBuilder();
sb.AppendLine($"clipping={(_clipping ? "on" : "off")} cartColliders={_cartColliders.Count} ignored={_ignored.Count}");
var found = new List<(float dist, string line)>();
foreach (var other in Physics.OverlapSphere(_body.position, ClipScanRadius, ~0, QueryTriggerInteraction.Collide))
{
if (other == null) continue;
try
{
var rb = other.attachedRigidbody;
var rbText = rb == null ? "none" : rb.isKinematic ? "kinematic" : "dynamic";
var reason = ClipSkipReason(other, root) ?? (_ignoredIds.Contains(other.GetInstanceID()) ? "IGNORED" : "would ignore");
var dist = Vector3.Distance(_body.position, other.ClosestPoint(_body.position));
var line = $"{dist:F1}m {other.transform.root.name}/{other.name} type={other.GetIl2CppType().Name} layer={LayerMask.LayerToName(other.gameObject.layer)} rb={rbText} -> {reason}";
found.Add((dist, line));
}
catch (Exception e)
{
found.Add((999f, $"error on {other.name}: {e.Message}"));
}
}
foreach (var f in found.OrderBy(f => f.dist)) sb.AppendLine(f.line);
var path = Path.Combine(LoaderEnvironment.UserDataDirectory, "BackupCamClipScan.txt");
File.WriteAllText(path, sb.ToString());
RLog.Msg($"BackupCam clip scan written to {path}");
}
void RestoreCollisions()
{
foreach (var other in _ignored)
{
if (other == null) continue;
foreach (var mine in _cartColliders)
if (mine != null) Physics.IgnoreCollision(mine, other, false);
}
_ignored.Clear();
_ignoredIds.Clear();
_cartColliders.Clear();
}
void ApplyOffset() void ApplyOffset()
{ {
if (_cam == null) return; if (_cam == null) return;
@@ -228,6 +409,7 @@ public class BackupCam : SonsMod
var onScreen = _screenGo != null; var onScreen = _screenGo != null;
if (_screenGo != null) _screenGo.SetActive(show); if (_screenGo != null) _screenGo.SetActive(show);
if (_canvasGo != null) _canvasGo.SetActive(show && !onScreen); if (_canvasGo != null) _canvasGo.SetActive(show && !onScreen);
if (_rearLightGo != null) _rearLightGo.SetActive(show);
if (_cam == null) return; if (_cam == null) return;
if (onScreen && _screenAspect > 0f) _cam.aspect = _screenAspect; if (onScreen && _screenAspect > 0f) _cam.aspect = _screenAspect;
@@ -235,6 +417,34 @@ public class BackupCam : SonsMod
_cam.enabled = show; _cam.enabled = show;
} }
[DebugCommand("backupcam")]
static void MainCommand(string args)
{
if (_instance == null) return;
_instance.HandleMain(args);
}
void HandleMain(string args)
{
var parts = (args ?? "").ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 && parts[0] == "clipping")
{
if (parts[1] == "on" || parts[1] == "off")
{
_clipping = parts[1] == "on";
if (!_clipping) RestoreCollisions();
RLog.Msg(_clipping ? "BackupCam clipping on: the cart drives through trees and objects" : "BackupCam clipping off");
return;
}
if (parts[1] == "scan")
{
WriteClipScan();
return;
}
}
RLog.Msg($"backupcam clipping <on|off|scan> current: {(_clipping ? "on" : "off")}");
}
[DebugCommand("backupcamoffset")] [DebugCommand("backupcamoffset")]
static void OffsetCommand(string args) static void OffsetCommand(string args)
{ {
@@ -257,6 +467,27 @@ public class BackupCam : SonsMod
RLog.Msg($"BackupCam offset set to {_camY} {_camZ} {_camPitch}"); RLog.Msg($"BackupCam offset set to {_camY} {_camZ} {_camPitch}");
} }
[DebugCommand("backupcamlight")]
static void LightCommand(string args)
{
if (_instance == null) return;
_instance.SetLight(args);
}
void SetLight(string args)
{
var parts = (args ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
RLog.Msg($"backupcamlight <intensity multiplier> <range multiplier> current: {_lightIntensity} {_lightRange}");
return;
}
_lightIntensity = float.Parse(parts[0], CultureInfo.InvariantCulture);
_lightRange = float.Parse(parts[1], CultureInfo.InvariantCulture);
ApplyLight();
RLog.Msg($"BackupCam rear light set to {_lightIntensity}x intensity, {_lightRange}x range");
}
[DebugCommand("backupcamdump")] [DebugCommand("backupcamdump")]
static void DumpCommand(string args) static void DumpCommand(string args)
{ {
@@ -379,4 +610,4 @@ public class BackupCam : SonsMod
} }
} }
} }
} }
+14 -6
View File
@@ -1,7 +1,9 @@
# BackupCam # BackupCam
Shows a backup camera on screen while you reverse the golf cart in Sons of the [![Downloads](https://img.shields.io/github/downloads/JustinOros/sotf-backupcam/total?label=downloads)](https://github.com/JustinOros/sotf-backupcam/releases) [![Latest](https://img.shields.io/github/v/release/JustinOros/sotf-backupcam?label=latest)](https://github.com/JustinOros/sotf-backupcam/releases/latest)
Forest.
Shows a backup camera on the golf cart's GPS screen and turns on rear lights
while you reverse in Sons of the Forest.
## Multiplayer ## Multiplayer
@@ -53,9 +55,13 @@ Mods\BackupCam\manifest.json
## Usage ## Usage
Get in a golf cart and drive backward. The camera feed appears at the bottom of Get in a golf cart and drive backward. The camera feed replaces the map on the
the screen, mirrored like a real backup camera, and hides shortly after you stop cart's GPS screen, mirrored like a real backup camera, and the map comes back
reversing. shortly after you stop reversing. Rear lights also turn on while reversing so
the camera can see at night. If a cart has no GPS screen, the feed is shown at
the bottom of your screen instead.
The rear lights and camera are only visible to players who have the mod.
Press F1 to open the console, then use these commands: Press F1 to open the console, then use these commands:
@@ -63,7 +69,9 @@ Press F1 to open the console, then use these commands:
| --- | --- | | --- | --- |
| `backupcamoffset` | Show the current camera height, distance and tilt | | `backupcamoffset` | Show the current camera height, distance and tilt |
| `backupcamoffset 1.1 -1.3 20` | Set camera height, distance behind the cart, and downward tilt | | `backupcamoffset 1.1 -1.3 20` | Set camera height, distance behind the cart, and downward tilt |
| `backupcamdump` | Write the golf cart's object and material list to `_RedLoader\Latest.log` | | `backupcamlight` | Show the current rear light brightness and reach |
| `backupcamlight 3 1.5` | Set rear light brightness and reach as multiples of the headlights |
| `backupcamdump` | Write the golf cart's objects, components and textures to `UserData\BackupCamDump.txt` |
## Uninstall ## Uninstall
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "BackupCam", "id": "BackupCam",
"name": "BackupCam", "name": "BackupCam",
"author": "JustinOros", "author": "JustinOros",
"version": "1.1.1", "version": "1.3.1",
"description": "Shows a backup camera when reversing the golf cart", "description": "Shows a backup camera when reversing the golf cart",
"gameVersion": "1.0.0", "gameVersion": "1.0.0",
"platform": "Client", "platform": "Client",