2026-09-22 22:40:11 -07:00
using System.Globalization ;
using System.Text ;
using RedLoader ;
using RedLoader.Utils ;
using Sons.Gameplay ;
using SonsSdk ;
using SonsSdk.Attributes ;
using TheForest.Utils ;
using UnityEngine ;
2026-09-25 23:10:17 -07:00
using UnityEngine.Rendering.HighDefinition ;
2026-09-22 22:40:11 -07:00
using UnityEngine.UI ;
using Object = UnityEngine . Object ;
namespace BackupCam ;
public class BackupCam : SonsMod
{
const float ReverseSpeed = - 0.4f ;
const float HideDelay = 0.6f ;
2026-09-22 22:43:57 -07:00
const float SearchInterval = 0.5f ;
const float SearchRadius = 2.5f ;
2026-09-22 22:40:11 -07:00
const string ScreenPath = "GolfCartScreen/GolfCartGps/Canvas/ScreenMask" ;
const string ScreenImageName = "BackupCamScreen" ;
2026-09-25 19:34:35 -07:00
const string HeadLightPath = "LightsGroup/HeadLights" ;
const string RearLightName = "BackupCamRearLight" ;
2026-09-25 20:01:07 -07:00
const float ClipScanInterval = 0.2f ;
const float ClipScanRadius = 8f ;
2026-09-22 22:40:11 -07:00
static readonly string [] TextureProps =
{
"_MainTex" , "_BaseMap" , "_BaseColorMap" , "_EmissiveColorMap" , "_EmissionMap" , "_UnlitColorMap" , "_ColorMap"
};
static BackupCam _instance ;
GolfCartController _cart ;
Rigidbody _body ;
Camera _cam ;
RenderTexture _rt ;
GameObject _canvasGo ;
RawImage _image ;
GameObject _screenGo ;
2026-09-25 19:34:35 -07:00
GameObject _rearLightGo ;
2026-09-25 19:44:36 -07:00
Light _headLight ;
Light _rearLight ;
2026-09-22 22:40:11 -07:00
float _screenAspect ;
float _searchTimer ;
float _lastReverse = - 10f ;
bool _showing ;
float _camY = 1.1f ;
float _camZ = - 1.3f ;
float _camPitch = 20f ;
2026-09-25 19:44:36 -07:00
float _lightIntensity = 3f ;
float _lightRange = 1.5f ;
2026-09-25 20:01:07 -07:00
bool _clipping ;
float _clipTimer ;
readonly List < Collider > _cartColliders = new ();
readonly List < Collider > _ignored = new ();
readonly HashSet < int > _ignoredIds = new ();
2026-09-22 22:40:11 -07:00
public BackupCam ()
{
_instance = this ;
OnUpdateCallback = Tick ;
}
protected override void OnSdkInitialized ()
{
2026-09-25 23:10:17 -07:00
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" );
2026-09-22 22:40:11 -07:00
}
protected override void OnGameStart ()
{
BuildOverlay ();
}
void BuildOverlay ()
{
if ( _canvasGo != null ) return ;
_rt = new RenderTexture ( 640 , 360 , 24 );
_rt . Create ();
_canvasGo = new GameObject ( "BackupCamCanvas" );
Object . DontDestroyOnLoad ( _canvasGo );
var canvas = _canvasGo . AddComponent < Canvas >();
canvas . renderMode = RenderMode . ScreenSpaceOverlay ;
canvas . sortingOrder = 500 ;
var imgGo = new GameObject ( "BackupCamImage" );
imgGo . transform . SetParent ( _canvasGo . transform , false );
_image = imgGo . AddComponent < RawImage >();
_image . texture = _rt ;
_image . uvRect = new Rect ( 1f , 0f , - 1f , 1f );
_image . raycastTarget = false ;
var rect = _image . rectTransform ;
rect . anchorMin = new Vector2 ( 0.5f , 0f );
rect . anchorMax = new Vector2 ( 0.5f , 0f );
rect . pivot = new Vector2 ( 0.5f , 0f );
rect . anchoredPosition = new Vector2 ( 0f , 40f );
rect . sizeDelta = new Vector2 ( 480f , 270f );
_canvasGo . SetActive ( false );
}
void Tick ()
{
if ( _canvasGo == null || LocalPlayer . Transform == null )
{
SetShowing ( false );
return ;
}
if ( _cart == null || _body == null || ! IsInCart ( _body ))
{
SetShowing ( false );
_searchTimer -= Time . deltaTime ;
if ( _searchTimer > 0f ) return ;
_searchTimer = SearchInterval ;
if (! FindCart ()) return ;
}
2026-09-25 20:01:07 -07:00
if ( _clipping ) UpdateClipping ();
2026-09-22 22:40:11 -07:00
var localVel = _body . transform . InverseTransformDirection ( _body . velocity );
if ( localVel . z < ReverseSpeed ) _lastReverse = Time . time ;
SetShowing ( Time . time - _lastReverse < HideDelay );
}
bool FindCart ()
{
2026-09-25 20:01:07 -07:00
RestoreCollisions ();
2026-09-22 22:40:11 -07:00
_cart = null ;
_body = null ;
_screenGo = null ;
2026-09-25 19:34:35 -07:00
_rearLightGo = null ;
2026-09-25 19:44:36 -07:00
_headLight = null ;
_rearLight = null ;
2026-09-22 22:40:11 -07:00
2026-09-22 22:43:57 -07:00
var player = LocalPlayer . Transform ;
var root = player . root ;
if ( root != player && TryUseCart ( root . GetComponentInChildren < GolfCartController >())) return true ;
2026-09-22 22:40:11 -07:00
2026-09-22 22:43:57 -07:00
foreach ( var col in Physics . OverlapSphere ( player . position , SearchRadius , ~ 0 , QueryTriggerInteraction . Ignore ))
{
if ( col == null ) continue ;
if ( TryUseCart ( col . GetComponentInParent < GolfCartController >())) return true ;
2026-09-22 22:40:11 -07:00
}
return false ;
}
2026-09-22 22:43:57 -07:00
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 ();
2026-09-25 19:34:35 -07:00
AttachRearLight ();
2026-09-22 22:43:57 -07:00
return true ;
}
2026-09-22 22:40:11 -07:00
bool IsInCart ( Rigidbody body )
{
var player = LocalPlayer . Transform ;
if ( player . IsChildOf ( body . transform . root )) return true ;
return Vector3 . Distance ( player . position , body . position ) < 1.8f ;
}
void AttachCamera ()
{
if ( _cam == null )
{
var go = new GameObject ( "BackupCam" );
_cam = go . AddComponent < Camera >();
_cam . fieldOfView = 100f ;
_cam . nearClipPlane = 0.05f ;
_cam . farClipPlane = 200f ;
_cam . targetTexture = _rt ;
_cam . enabled = false ;
2026-09-25 23:10:17 -07:00
MatchMainCamera ();
2026-09-22 22:40:11 -07:00
}
_cam . transform . SetParent ( _body . transform , false );
ApplyOffset ();
}
2026-09-25 23:10:17 -07:00
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}" );
}
2026-09-22 22:40:11 -07:00
void AttachScreen ()
{
var mask = _cart . transform . root . Find ( ScreenPath );
if ( mask == null )
{
RLog . Msg ( "BackupCam: GPS screen not found on this cart, using the screen overlay" );
return ;
}
var existing = mask . Find ( ScreenImageName );
GameObject go ;
if ( existing != null )
{
go = existing . gameObject ;
}
else
{
go = new GameObject ( ScreenImageName );
go . layer = mask . gameObject . layer ;
go . transform . SetParent ( mask , false );
var raw = go . AddComponent < RawImage >();
raw . texture = _rt ;
raw . uvRect = new Rect ( 1f , 0f , - 1f , 1f );
raw . raycastTarget = false ;
var rect = raw . rectTransform ;
rect . anchorMin = Vector2 . zero ;
rect . anchorMax = Vector2 . one ;
rect . offsetMin = Vector2 . zero ;
rect . offsetMax = Vector2 . zero ;
}
go . transform . SetAsLastSibling ();
go . SetActive ( false );
_screenGo = go ;
var maskRect = mask . GetComponent < RectTransform >(). rect ;
_screenAspect = maskRect . height > 0f ? maskRect . width / maskRect . height : 0f ;
}
2026-09-25 19:34:35 -07:00
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 ;
}
2026-09-25 19:44:36 -07:00
_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 ;
}
2026-09-25 19:34:35 -07:00
go . SetActive ( false );
_rearLightGo = go ;
2026-09-25 19:44:36 -07:00
_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 );
2026-09-25 19:34:35 -07:00
}
2026-09-25 20:01:07 -07:00
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 ))
{
2026-09-25 23:10:17 -07:00
if ( other == null || ClipSkipReason ( other , root ) != null ) continue ;
2026-09-25 20:01:07 -07:00
if (! _ignoredIds . Add ( other . GetInstanceID ())) continue ;
foreach ( var mine in _cartColliders )
if ( mine != null ) Physics . IgnoreCollision ( mine , other , true );
_ignored . Add ( other );
}
}
2026-09-25 23:10:17 -07:00
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}" );
}
2026-09-25 20:01:07 -07:00
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 ();
}
2026-09-22 22:40:11 -07:00
void ApplyOffset ()
{
if ( _cam == null ) return ;
_cam . transform . localPosition = new Vector3 ( 0f , _camY , _camZ );
_cam . transform . localRotation = Quaternion . Euler ( _camPitch , 180f , 0f );
}
void SetShowing ( bool show )
{
if ( show == _showing ) return ;
_showing = show ;
var onScreen = _screenGo != null ;
if ( _screenGo != null ) _screenGo . SetActive ( show );
if ( _canvasGo != null ) _canvasGo . SetActive ( show && ! onScreen );
2026-09-25 19:34:35 -07:00
if ( _rearLightGo != null ) _rearLightGo . SetActive ( show );
2026-09-22 22:40:11 -07:00
if ( _cam == null ) return ;
if ( onScreen && _screenAspect > 0f ) _cam . aspect = _screenAspect ;
else _cam . ResetAspect ();
_cam . enabled = show ;
}
2026-09-25 20:01:07 -07:00
[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 );
2026-09-25 23:10:17 -07:00
if ( parts . Length == 2 && parts [ 0 ] == "clipping" )
2026-09-25 20:01:07 -07:00
{
2026-09-25 23:10:17 -07:00
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 ;
}
2026-09-25 20:01:07 -07:00
}
2026-09-25 23:10:17 -07:00
RLog . Msg ( $"backupcam clipping <on|off|scan> current: {(_clipping ? " on " : " off ")}" );
2026-09-25 20:01:07 -07:00
}
2026-09-22 22:40:11 -07:00
[DebugCommand("backupcamoffset")]
static void OffsetCommand ( string args )
{
if ( _instance == null ) return ;
_instance . SetOffset ( args );
}
void SetOffset ( string args )
{
var parts = ( args ?? "" ). Split ( ' ' , StringSplitOptions . RemoveEmptyEntries );
if ( parts . Length != 3 )
{
RLog . Msg ( $"backupcamoffset <y> <z> <pitch> current: {_camY} {_camZ} {_camPitch}" );
return ;
}
_camY = float . Parse ( parts [ 0 ], CultureInfo . InvariantCulture );
_camZ = float . Parse ( parts [ 1 ], CultureInfo . InvariantCulture );
_camPitch = float . Parse ( parts [ 2 ], CultureInfo . InvariantCulture );
ApplyOffset ();
RLog . Msg ( $"BackupCam offset set to {_camY} {_camZ} {_camPitch}" );
}
2026-09-25 19:44:36 -07:00
[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" );
}
2026-09-22 22:40:11 -07:00
[DebugCommand("backupcamdump")]
static void DumpCommand ( string args )
{
if ( _instance == null ) return ;
_instance . WriteDump ();
}
void WriteDump ()
{
RLog . Msg ( "BackupCam dump starting" );
var sb = new StringBuilder ();
var path = Path . Combine ( LoaderEnvironment . UserDataDirectory , "BackupCamDump.txt" );
try
{
var cart = _cart != null ? _cart : NearestCart ();
if ( cart == null )
{
RLog . Msg ( "No golf cart found" );
return ;
}
sb . Append ( "GolfCartController on: " ). AppendLine ( cart . gameObject . name );
Dump ( cart . transform . root , 0 , sb );
}
catch ( Exception e )
{
sb . AppendLine ( "DUMP FAILED: " + e );
RLog . Error ( "BackupCam dump failed: " + e );
}
File . WriteAllText ( path , sb . ToString ());
RLog . Msg ( $"BackupCam dump written to {path}" );
}
GolfCartController NearestCart ()
{
GolfCartController best = null ;
var bestDist = float . MaxValue ;
var from = LocalPlayer . Transform != null ? LocalPlayer . Transform . position : Vector3 . zero ;
foreach ( var cart in Object . FindObjectsOfType < GolfCartController >())
{
var d = Vector3 . Distance ( from , cart . transform . position );
if ( d < bestDist )
{
bestDist = d ;
best = cart ;
}
}
return best ;
}
void Dump ( Transform t , int depth , StringBuilder sb )
{
var pad = new string ( ' ' , depth * 2 );
sb . Append ( pad ). Append ( t . name );
if (! t . gameObject . activeInHierarchy ) sb . Append ( " (inactive)" );
sb . AppendLine ();
foreach ( var c in t . GetComponents < Component >())
{
if ( c == null ) continue ;
try
{
DumpComponent ( c , pad , sb );
}
catch ( Exception e )
{
sb . Append ( pad ). Append ( " ! error: " ). AppendLine ( e . Message );
}
}
for ( int i = 0 ; i < t . childCount ; i ++) Dump ( t . GetChild ( i ), depth + 1 , sb );
}
void DumpComponent ( Component c , string pad , StringBuilder sb )
{
sb . Append ( pad ). Append ( " - " ). Append ( c . GetIl2CppType (). Name );
var canvas = c . TryCast < Canvas >();
if ( canvas != null ) sb . Append ( " renderMode=" ). Append ( canvas . renderMode . ToString ());
var raw = c . TryCast < RawImage >();
if ( raw != null && raw . texture != null )
sb . Append ( " texture=" ). Append ( raw . texture . name ). Append ( " (" ). Append ( raw . texture . GetIl2CppType (). Name ). Append ( ')' );
var cam = c . TryCast < Camera >();
if ( cam != null && cam . targetTexture != null )
sb . Append ( " targetTexture=" ). Append ( cam . targetTexture . name );
sb . AppendLine ();
var r = c . TryCast < Renderer >();
if ( r != null )
foreach ( var m in r . sharedMaterials )
AppendMaterial ( m , pad + " " , sb );
}
void AppendMaterial ( Material m , string pad , StringBuilder sb )
{
if ( m == null )
{
sb . Append ( pad ). AppendLine ( "material null" );
return ;
}
sb . Append ( pad ). Append ( "material " ). Append ( m . name ). Append ( " / " ). Append ( m . shader . name ). AppendLine ();
foreach ( var prop in TextureProps )
{
try
{
if (! m . HasProperty ( prop )) continue ;
var tex = m . GetTexture ( prop );
if ( tex == null ) continue ;
sb . Append ( pad ). Append ( " " ). Append ( prop ). Append ( " = " ). Append ( tex . name )
. Append ( " (" ). Append ( tex . GetIl2CppType (). Name ). Append ( ')' ). AppendLine ();
}
catch ( Exception e )
{
sb . Append ( pad ). Append ( " ! " ). Append ( prop ). Append ( " error: " ). AppendLine ( e . Message );
}
}
}
2026-09-25 19:34:35 -07:00
}