8 Commits
3 changed files with 274 additions and 22 deletions
+253 -9
View File
@@ -7,6 +7,7 @@ using SonsSdk;
using SonsSdk.Attributes;
using TheForest.Utils;
using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.UI;
using Object = UnityEngine.Object;
@@ -16,9 +17,14 @@ public class BackupCam : SonsMod
{
const float ReverseSpeed = -0.4f;
const float HideDelay = 0.6f;
const float SearchInterval = 1f;
const float SearchInterval = 0.5f;
const float SearchRadius = 2.5f;
const string ScreenPath = "GolfCartScreen/GolfCartGps/Canvas/ScreenMask";
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 =
{
@@ -34,6 +40,9 @@ public class BackupCam : SonsMod
GameObject _canvasGo;
RawImage _image;
GameObject _screenGo;
GameObject _rearLightGo;
Light _headLight;
Light _rearLight;
float _screenAspect;
float _searchTimer;
float _lastReverse = -10f;
@@ -43,6 +52,15 @@ public class BackupCam : SonsMod
float _camZ = -1.3f;
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()
{
_instance = this;
@@ -51,7 +69,7 @@ public class BackupCam : SonsMod
protected override void OnSdkInitialized()
{
RLog.Msg("BackupCam 1.1.0 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()
@@ -106,6 +124,8 @@ public class BackupCam : SonsMod
if (!FindCart()) return;
}
if (_clipping) UpdateClipping();
var localVel = _body.transform.InverseTransformDirection(_body.velocity);
if (localVel.z < ReverseSpeed) _lastReverse = Time.time;
SetShowing(Time.time - _lastReverse < HideDelay);
@@ -113,26 +133,43 @@ public class BackupCam : SonsMod
bool FindCart()
{
RestoreCollisions();
_cart = null;
_body = null;
_screenGo = null;
_rearLightGo = null;
_headLight = null;
_rearLight = null;
foreach (var cart in Object.FindObjectsOfType<GolfCartController>())
var player = LocalPlayer.Transform;
var root = player.root;
if (root != player && TryUseCart(root.GetComponentInChildren<GolfCartController>())) return true;
foreach (var col in Physics.OverlapSphere(player.position, SearchRadius, ~0, QueryTriggerInteraction.Ignore))
{
if (col == null) continue;
if (TryUseCart(col.GetComponentInParent<GolfCartController>())) return true;
}
return false;
}
bool TryUseCart(GolfCartController cart)
{
if (cart == null) return false;
var body = cart.GetComponentInParent<Rigidbody>();
if (body == null) body = cart.GetComponentInChildren<Rigidbody>();
if (body == null || !IsInCart(body)) continue;
if (body == null || !IsInCart(body)) return false;
_cart = cart;
_body = body;
AttachCamera();
AttachScreen();
AttachRearLight();
return true;
}
return false;
}
bool IsInCart(Rigidbody body)
{
var player = LocalPlayer.Transform;
@@ -151,14 +188,36 @@ public class BackupCam : SonsMod
_cam.farClipPlane = 200f;
_cam.targetTexture = _rt;
_cam.enabled = false;
var main = Camera.main;
if (main != null) _cam.cullingMask = main.cullingMask;
MatchMainCamera();
}
_cam.transform.SetParent(_body.transform, false);
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()
{
var mask = _cart.transform.root.Find(ScreenPath);
@@ -200,6 +259,141 @@ public class BackupCam : SonsMod
_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()
{
if (_cam == null) return;
@@ -215,6 +409,7 @@ public class BackupCam : SonsMod
var onScreen = _screenGo != null;
if (_screenGo != null) _screenGo.SetActive(show);
if (_canvasGo != null) _canvasGo.SetActive(show && !onScreen);
if (_rearLightGo != null) _rearLightGo.SetActive(show);
if (_cam == null) return;
if (onScreen && _screenAspect > 0f) _cam.aspect = _screenAspect;
@@ -222,6 +417,34 @@ public class BackupCam : SonsMod
_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")]
static void OffsetCommand(string args)
{
@@ -244,6 +467,27 @@ public class BackupCam : SonsMod
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")]
static void DumpCommand(string args)
{
+14 -6
View File
@@ -1,7 +1,9 @@
# BackupCam
Shows a backup camera on screen while you reverse the golf cart in Sons of the
Forest.
[![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)
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
@@ -53,9 +55,13 @@ Mods\BackupCam\manifest.json
## Usage
Get in a golf cart and drive backward. The camera feed appears at the bottom of
the screen, mirrored like a real backup camera, and hides shortly after you stop
reversing.
Get in a golf cart and drive backward. The camera feed replaces the map on the
cart's GPS screen, mirrored like a real backup camera, and the map comes back
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:
@@ -63,7 +69,9 @@ Press F1 to open the console, then use these commands:
| --- | --- |
| `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 |
| `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
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "BackupCam",
"name": "BackupCam",
"author": "JustinOros",
"version": "1.1.0",
"version": "1.3.1",
"description": "Shows a backup camera when reversing the golf cart",
"gameVersion": "1.0.0",
"platform": "Client",