Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b2481742 | ||
|
|
6bef73f148 | ||
|
|
2b7cd08f0c | ||
|
|
d0303817b8 | ||
|
|
1301a1c632 | ||
|
|
cb1768163a |
No files matched your search
+258
-14
@@ -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))
|
||||
{
|
||||
var body = cart.GetComponentInParent<Rigidbody>();
|
||||
if (body == null) body = cart.GetComponentInChildren<Rigidbody>();
|
||||
if (body == null || !IsInCart(body)) continue;
|
||||
|
||||
_cart = cart;
|
||||
_body = body;
|
||||
AttachCamera();
|
||||
AttachScreen();
|
||||
return true;
|
||||
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)) return false;
|
||||
|
||||
_cart = cart;
|
||||
_body = body;
|
||||
AttachCamera();
|
||||
AttachScreen();
|
||||
AttachRearLight();
|
||||
return true;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# BackupCam
|
||||
|
||||
Shows a backup camera on screen while you reverse the golf cart in Sons of the
|
||||
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
|
||||
|
||||
@@ -53,9 +53,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 +67,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
@@ -3,7 +3,7 @@
|
||||
"id": "BackupCam",
|
||||
"name": "BackupCam",
|
||||
"author": "JustinOros",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Shows a backup camera when reversing the golf cart",
|
||||
"gameVersion": "1.0.0",
|
||||
"platform": "Client",
|
||||
|
||||
Reference in new issue
Block a user