BackupCam 1.1.0, backup camera on the golf cart GPS screen
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
*.zip
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using RedLoader;
|
||||
using RedLoader.Utils;
|
||||
using Sons.Gameplay;
|
||||
using SonsSdk;
|
||||
using SonsSdk.Attributes;
|
||||
using TheForest.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace BackupCam;
|
||||
|
||||
public class BackupCam : SonsMod
|
||||
{
|
||||
const float ReverseSpeed = -0.4f;
|
||||
const float HideDelay = 0.6f;
|
||||
const float SearchInterval = 1f;
|
||||
const string ScreenPath = "GolfCartScreen/GolfCartGps/Canvas/ScreenMask";
|
||||
const string ScreenImageName = "BackupCamScreen";
|
||||
|
||||
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;
|
||||
float _screenAspect;
|
||||
float _searchTimer;
|
||||
float _lastReverse = -10f;
|
||||
bool _showing;
|
||||
|
||||
float _camY = 1.1f;
|
||||
float _camZ = -1.3f;
|
||||
float _camPitch = 20f;
|
||||
|
||||
public BackupCam()
|
||||
{
|
||||
_instance = this;
|
||||
OnUpdateCallback = Tick;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var localVel = _body.transform.InverseTransformDirection(_body.velocity);
|
||||
if (localVel.z < ReverseSpeed) _lastReverse = Time.time;
|
||||
SetShowing(Time.time - _lastReverse < HideDelay);
|
||||
}
|
||||
|
||||
bool FindCart()
|
||||
{
|
||||
_cart = null;
|
||||
_body = null;
|
||||
_screenGo = null;
|
||||
|
||||
foreach (var cart in Object.FindObjectsOfType<GolfCartController>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
var main = Camera.main;
|
||||
if (main != null) _cam.cullingMask = main.cullingMask;
|
||||
}
|
||||
|
||||
_cam.transform.SetParent(_body.transform, false);
|
||||
ApplyOffset();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (_cam == null) return;
|
||||
if (onScreen && _screenAspect > 0f) _cam.aspect = _screenAspect;
|
||||
else _cam.ResetAspect();
|
||||
_cam.enabled = show;
|
||||
}
|
||||
|
||||
[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}");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>BackupCam</AssemblyName>
|
||||
<RootNamespace>BackupCam</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(GameDir)' == ''">
|
||||
<GameDir>$(SOTF_PATH)</GameDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="CheckGameDir" BeforeTargets="BeforeBuild">
|
||||
<Error Condition="'$(GameDir)' == ''"
|
||||
Text="GameDir is not set. Run build.ps1, pass -p:GameDir="path", or set SOTF_PATH." />
|
||||
<Error Condition="!Exists('$(GameDir)\_RedLoader\Game\Sons.dll')"
|
||||
Text="No RedLoader interop assemblies at $(GameDir)\_RedLoader\Game. Install RedLoader and launch the game once." />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="$(GameDir)\_RedLoader\Game\*.dll" Private="false" />
|
||||
<Reference Include="$(GameDir)\_RedLoader\net6\RedLoader.dll" Private="false" />
|
||||
<Reference Include="$(GameDir)\_RedLoader\net6\SonsSdk.dll" Private="false" />
|
||||
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Runtime.dll" Private="false" />
|
||||
<Reference Include="$(GameDir)\_RedLoader\net6\Il2CppInterop.Common.dll" Private="false" />
|
||||
<Reference Include="$(GameDir)\_RedLoader\net6\0Harmony.dll" Private="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="manifest.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$GameDir,
|
||||
[switch]$Remove
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Text)
|
||||
Write-Host ''
|
||||
Write-Host $Text -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Find-GameDir {
|
||||
$steam = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath
|
||||
if (-not $steam) { return $null }
|
||||
$libs = @($steam)
|
||||
$vdf = Join-Path $steam 'steamapps\libraryfolders.vdf'
|
||||
if (Test-Path $vdf) {
|
||||
Select-String -Path $vdf -Pattern '"path"\s+"(.+?)"' -AllMatches |
|
||||
ForEach-Object { $_.Matches } |
|
||||
ForEach-Object { $libs += $_.Groups[1].Value.Replace('\\','\') }
|
||||
}
|
||||
foreach ($lib in $libs) {
|
||||
$candidate = Join-Path $lib 'steamapps\common\Sons Of The Forest'
|
||||
if (Test-Path (Join-Path $candidate 'SonsOfTheForest.exe')) { return $candidate }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-ReleaseZip {
|
||||
param([string]$Repo, [string]$Pattern, [string]$OutFile)
|
||||
$release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ 'User-Agent' = 'ps' }
|
||||
$asset = $release.assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1
|
||||
if (-not $asset) { throw "Could not find a download matching $Pattern in the latest $Repo release" }
|
||||
Write-Host " version $($release.tag_name)"
|
||||
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $OutFile -UseBasicParsing
|
||||
return $asset.name
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ''
|
||||
Write-Host '====================================' -ForegroundColor Green
|
||||
Write-Host ' BackupCam installer' -ForegroundColor Green
|
||||
Write-Host '====================================' -ForegroundColor Green
|
||||
|
||||
if (Get-Process -Name 'SonsOfTheForest' -ErrorAction SilentlyContinue) {
|
||||
throw 'Sons of the Forest is running. Close the game and run the installer again.'
|
||||
}
|
||||
|
||||
Write-Step 'Looking for Sons of the Forest'
|
||||
if (-not $GameDir) { $GameDir = Find-GameDir }
|
||||
if (-not $GameDir -or -not (Test-Path (Join-Path $GameDir 'SonsOfTheForest.exe'))) {
|
||||
Write-Host ' Could not find the game automatically.' -ForegroundColor Yellow
|
||||
Write-Host ' In Steam, right-click Sons Of The Forest, then Manage, then Browse local files.'
|
||||
Write-Host ' Copy the folder path from the address bar and paste it below.'
|
||||
$GameDir = (Read-Host ' Game folder').Trim('"')
|
||||
if (-not (Test-Path (Join-Path $GameDir 'SonsOfTheForest.exe'))) {
|
||||
throw "SonsOfTheForest.exe was not found in $GameDir"
|
||||
}
|
||||
}
|
||||
Write-Host " found: $GameDir" -ForegroundColor Green
|
||||
|
||||
if ($Remove) {
|
||||
Write-Step 'Removing BackupCam'
|
||||
$modsDir = Join-Path $GameDir 'Mods'
|
||||
$dll = Join-Path $modsDir 'BackupCam.dll'
|
||||
$dataDir = Join-Path $modsDir 'BackupCam'
|
||||
$found = (Test-Path $dll) -or (Test-Path $dataDir)
|
||||
Remove-Item $dll -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $dataDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if ($found) {
|
||||
Write-Host ' removed' -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host ' BackupCam was not installed' -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ''
|
||||
Write-Host 'RedLoader and your other mods were left alone.'
|
||||
Write-Host ''
|
||||
Read-Host 'Press Enter to close' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$needsFirstLaunch = $false
|
||||
|
||||
Write-Step 'Checking for RedLoader'
|
||||
if (Test-Path (Join-Path $GameDir '_RedLoader\net6\SonsSdk.dll')) {
|
||||
Write-Host ' already installed' -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host ' not installed, downloading it now'
|
||||
$rlZip = Join-Path $env:TEMP 'RedLoader.zip'
|
||||
Get-ReleaseZip -Repo 'ToniMacaroni/RedLoader' -Pattern '^RedLoader.*\.zip$' -OutFile $rlZip | Out-Null
|
||||
|
||||
$rlStage = Join-Path $env:TEMP 'redloader-stage'
|
||||
if (Test-Path $rlStage) { Remove-Item $rlStage -Recurse -Force }
|
||||
Expand-Archive -Path $rlZip -DestinationPath $rlStage -Force
|
||||
Copy-Item -Path (Join-Path $rlStage '*') -Destination $GameDir -Recurse -Force
|
||||
Remove-Item $rlZip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $rlStage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Host ' installed' -ForegroundColor Green
|
||||
$needsFirstLaunch = $true
|
||||
}
|
||||
|
||||
Write-Step 'Downloading BackupCam'
|
||||
$modZip = Join-Path $env:TEMP 'BackupCam.zip'
|
||||
Get-ReleaseZip -Repo 'JustinOros/sotf-backupcam' -Pattern '^BackupCam\.zip$' -OutFile $modZip | Out-Null
|
||||
|
||||
Write-Step 'Installing BackupCam'
|
||||
$modsDir = Join-Path $GameDir 'Mods'
|
||||
New-Item -ItemType Directory -Force -Path $modsDir | Out-Null
|
||||
Remove-Item (Join-Path $modsDir 'BackupCam.dll') -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item (Join-Path $modsDir 'BackupCam') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Expand-Archive -Path $modZip -DestinationPath $modsDir -Force
|
||||
Remove-Item $modZip -Force -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not (Test-Path (Join-Path $modsDir 'BackupCam.dll'))) {
|
||||
throw 'Something went wrong, BackupCam.dll is not in the Mods folder'
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $modsDir 'BackupCam\manifest.json'))) {
|
||||
throw 'Something went wrong, manifest.json is not in the Mods\BackupCam folder'
|
||||
}
|
||||
Write-Host ' installed' -ForegroundColor Green
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '====================================' -ForegroundColor Green
|
||||
Write-Host ' Done' -ForegroundColor Green
|
||||
Write-Host '====================================' -ForegroundColor Green
|
||||
Write-Host ''
|
||||
|
||||
if ($needsFirstLaunch) {
|
||||
Write-Host 'RedLoader was installed for the first time, so the next time you start'
|
||||
Write-Host 'the game it will take a few extra minutes to get ready. That is normal.'
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
Write-Host 'Start Sons of the Forest, then:'
|
||||
Write-Host ' 1. Check that MODS appears on the main menu and lists BackupCam'
|
||||
Write-Host ' 2. Load into a game and drive a golf cart'
|
||||
Write-Host ' 3. Reverse to show the backup camera'
|
||||
Write-Host ''
|
||||
Write-Host 'To uninstall, run this script again with -Remove'
|
||||
Write-Host ''
|
||||
}
|
||||
catch {
|
||||
Write-Host ''
|
||||
Write-Host "Install failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Message -match 'denied') {
|
||||
Write-Host 'Try again from PowerShell opened with Run as administrator.' -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
Read-Host 'Press Enter to close' | Out-Null
|
||||
@@ -0,0 +1,95 @@
|
||||
# BackupCam
|
||||
|
||||
Shows a backup camera on screen while you reverse the golf cart in Sons of the
|
||||
Forest.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
Client side only. Nothing gets installed on a dedicated server, and other
|
||||
players do not need the mod. Works on dedicated servers, on games hosted by a
|
||||
player, and in single player.
|
||||
|
||||
## Installation
|
||||
|
||||
### Easy install
|
||||
|
||||
Close the game, open PowerShell and paste:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/JustinOros/sotf-backupcam/main/Install.ps1 | iex
|
||||
```
|
||||
|
||||
It finds your game, installs RedLoader if needed, and installs the latest
|
||||
BackupCam. Run it again any time to update.
|
||||
|
||||
### Manual install
|
||||
|
||||
#### Step 1: Install RedLoader
|
||||
|
||||
[RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest) is the mod
|
||||
loader for Sons of the Forest. The game cannot load any mod without it, so
|
||||
install it first. You only have to do this once.
|
||||
|
||||
1. Download `RedLoader.zip` from the
|
||||
[latest RedLoader release](https://github.com/ToniMacaroni/RedLoader/releases/latest)
|
||||
2. Extract it into your Sons of the Forest folder, the one containing
|
||||
`SonsOfTheForest.exe`, usually
|
||||
`C:\Program Files (x86)\Steam\steamapps\common\Sons Of The Forest`
|
||||
3. Launch the game once and wait until you reach the main menu. The first launch
|
||||
takes a few minutes while RedLoader processes the game files
|
||||
4. Check that `MODS` appears on the main menu, then quit
|
||||
|
||||
#### Step 2: Install BackupCam
|
||||
|
||||
1. Download `BackupCam.zip` from the
|
||||
[latest release](https://github.com/JustinOros/sotf-backupcam/releases/latest)
|
||||
2. Extract it into the `Mods` folder inside your game folder
|
||||
3. You should end up with:
|
||||
|
||||
```
|
||||
Mods\BackupCam.dll
|
||||
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.
|
||||
|
||||
Press F1 to open the console, then use these commands:
|
||||
|
||||
| Command | Action |
|
||||
| --- | --- |
|
||||
| `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` |
|
||||
|
||||
## Uninstall
|
||||
|
||||
Close the game, open PowerShell and paste:
|
||||
|
||||
```powershell
|
||||
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/JustinOros/sotf-backupcam/main/Install.ps1))) -Remove
|
||||
```
|
||||
|
||||
Or from a clone of this repo, run `.\Install.ps1 -Remove`. This removes only
|
||||
BackupCam. RedLoader and your other mods are left alone.
|
||||
|
||||
## Building from source
|
||||
|
||||
Requires the .NET 8 SDK and RedLoader installed with its game assemblies
|
||||
generated.
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -Install
|
||||
```
|
||||
|
||||
Use `-Package` to build `BackupCam.zip` for a release. Pass `-GameDir "path"` if
|
||||
the game is not found automatically.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Check `_RedLoader\Latest.log` in your game folder. BackupCam logs a line when it
|
||||
loads. If the camera never shows, run `backupcamdump` while sitting in the cart
|
||||
and include the log when reporting the problem.
|
||||
@@ -0,0 +1,160 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$GameDir,
|
||||
[switch]$Install,
|
||||
[switch]$Package
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Test-Dotnet {
|
||||
$cmd = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) { return $false }
|
||||
$sdks = & dotnet --list-sdks 2>$null
|
||||
return ($LASTEXITCODE -eq 0 -and $sdks)
|
||||
}
|
||||
|
||||
function Update-Path {
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' +
|
||||
[System.Environment]::GetEnvironmentVariable('Path','User')
|
||||
$dotnetRoot = Join-Path $env:ProgramFiles 'dotnet'
|
||||
if ((Test-Path $dotnetRoot) -and ($env:Path -notlike "*$dotnetRoot*")) {
|
||||
$env:Path += ";$dotnetRoot"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Dotnet {
|
||||
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
||||
Write-Host 'Installing the .NET 8 SDK with winget' -ForegroundColor Cyan
|
||||
winget install --id Microsoft.DotNet.SDK.8 --accept-source-agreements --accept-package-agreements --silent
|
||||
Update-Path
|
||||
if (Test-Dotnet) { return }
|
||||
Write-Host ' winget install did not make dotnet available, falling back' -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host 'Downloading the official dotnet-install script' -ForegroundColor Cyan
|
||||
$script = Join-Path $env:TEMP 'dotnet-install.ps1'
|
||||
Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $script -UseBasicParsing
|
||||
|
||||
Write-Host 'Installing the .NET 8 SDK to your user profile' -ForegroundColor Cyan
|
||||
& $script -Channel 8.0 -InstallDir "$env:LOCALAPPDATA\Microsoft\dotnet"
|
||||
$userDotnet = "$env:LOCALAPPDATA\Microsoft\dotnet"
|
||||
$env:Path = "$userDotnet;$env:Path"
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
'Path',
|
||||
"$userDotnet;" + [Environment]::GetEnvironmentVariable('Path','User'),
|
||||
'User')
|
||||
Remove-Item $script -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Install-RedLoader {
|
||||
param([string]$Target)
|
||||
|
||||
Write-Host 'Looking up the latest RedLoader release' -ForegroundColor Cyan
|
||||
$release = Invoke-RestMethod 'https://api.github.com/repos/ToniMacaroni/RedLoader/releases/latest' -Headers @{ 'User-Agent' = 'ps' }
|
||||
$asset = $release.assets | Where-Object { $_.name -match '^RedLoader.*\.zip$' } | Select-Object -First 1
|
||||
if (-not $asset) { throw 'Could not find RedLoader.zip in the latest release' }
|
||||
Write-Host " $($release.tag_name) / $($asset.name)"
|
||||
|
||||
$zip = Join-Path $env:TEMP $asset.name
|
||||
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -UseBasicParsing
|
||||
|
||||
$stage = Join-Path $env:TEMP 'redloader-extract'
|
||||
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||
Expand-Archive -Path $zip -DestinationPath $stage -Force
|
||||
Copy-Item -Path (Join-Path $stage '*') -Destination $Target -Recurse -Force
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host ' RedLoader installed' -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Find-GameDir {
|
||||
if ($env:SOTF_PATH) { return $env:SOTF_PATH }
|
||||
$steam = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath
|
||||
if (-not $steam) { return $null }
|
||||
$libs = @($steam)
|
||||
$vdf = Join-Path $steam 'steamapps\libraryfolders.vdf'
|
||||
if (Test-Path $vdf) {
|
||||
Select-String -Path $vdf -Pattern '"path"\s+"(.+?)"' -AllMatches |
|
||||
ForEach-Object { $_.Matches } |
|
||||
ForEach-Object { $libs += $_.Groups[1].Value.Replace('\\','\') }
|
||||
}
|
||||
foreach ($lib in $libs) {
|
||||
$candidate = Join-Path $lib 'steamapps\common\Sons Of The Forest'
|
||||
if (Test-Path (Join-Path $candidate 'SonsOfTheForest.exe')) { return $candidate }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not (Test-Dotnet)) {
|
||||
Write-Host ''
|
||||
Write-Host 'The .NET 8 SDK is required to build this mod and was not found.' -ForegroundColor Yellow
|
||||
$answer = Read-Host 'Download and install it now? (y/N)'
|
||||
if ($answer -ne 'y') {
|
||||
throw 'Cannot build without the .NET SDK. Get it at https://dotnet.microsoft.com/download'
|
||||
}
|
||||
Install-Dotnet
|
||||
if (-not (Test-Dotnet)) {
|
||||
throw 'The SDK was installed but dotnet is still not on PATH. Reopen PowerShell and rerun this script.'
|
||||
}
|
||||
}
|
||||
Write-Host ("dotnet SDK: " + (dotnet --version)) -ForegroundColor Green
|
||||
|
||||
if (-not $GameDir) { $GameDir = Find-GameDir }
|
||||
if (-not $GameDir) {
|
||||
throw 'Could not find Sons Of The Forest. Pass -GameDir "path" or set SOTF_PATH.'
|
||||
}
|
||||
Write-Host "Game directory: $GameDir" -ForegroundColor Cyan
|
||||
|
||||
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\net6\SonsSdk.dll'))) {
|
||||
Write-Host ''
|
||||
Write-Host 'RedLoader is not installed in your game folder.' -ForegroundColor Yellow
|
||||
$answer = Read-Host 'Download and install the latest RedLoader release now? (y/N)'
|
||||
if ($answer -ne 'y') {
|
||||
throw 'RedLoader is required. Get it at https://github.com/ToniMacaroni/RedLoader/releases'
|
||||
}
|
||||
Install-RedLoader -Target $GameDir
|
||||
}
|
||||
|
||||
if (-not (Test-Path (Join-Path $GameDir '_RedLoader\Game\Sons.dll'))) {
|
||||
Write-Host ''
|
||||
Write-Host 'RedLoader is installed but has not generated its game assemblies yet.' -ForegroundColor Yellow
|
||||
Write-Host 'Launch Sons of the Forest once and wait for the main menu. The first'
|
||||
Write-Host 'launch takes a few minutes while it processes the game files.'
|
||||
Write-Host ''
|
||||
Write-Host 'Then quit the game and run this script again.'
|
||||
return
|
||||
}
|
||||
Write-Host 'RedLoader ready' -ForegroundColor Green
|
||||
|
||||
dotnet build -c Release --nologo -p:GameDir="$GameDir"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Build failed' }
|
||||
|
||||
$dll = Join-Path $PSScriptRoot 'bin\Release\net6\BackupCam.dll'
|
||||
|
||||
if ($Install) {
|
||||
$modsDir = Join-Path $GameDir 'Mods'
|
||||
$modDataDir = Join-Path $modsDir 'BackupCam'
|
||||
New-Item -ItemType Directory -Force -Path $modDataDir | Out-Null
|
||||
Copy-Item $dll -Destination $modsDir -Force
|
||||
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination $modDataDir -Force
|
||||
Write-Host "Installed to $modsDir" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($Package) {
|
||||
$stage = Join-Path $PSScriptRoot 'dist'
|
||||
$zip = Join-Path $PSScriptRoot 'BackupCam.zip'
|
||||
|
||||
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'BackupCam') | Out-Null
|
||||
Copy-Item $dll -Destination $stage -Force
|
||||
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination (Join-Path $stage 'BackupCam') -Force
|
||||
|
||||
if (Test-Path $zip) { Remove-Item $zip -Force }
|
||||
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zip
|
||||
Remove-Item $stage -Recurse -Force
|
||||
|
||||
Write-Host "Packaged $zip" -ForegroundColor Green
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToniMacaroni/RedLoader/main/MetadataSchema.json",
|
||||
"id": "BackupCam",
|
||||
"name": "BackupCam",
|
||||
"author": "JustinOros",
|
||||
"version": "1.1.0",
|
||||
"description": "Shows a backup camera when reversing the golf cart",
|
||||
"gameVersion": "1.0.0",
|
||||
"platform": "Client",
|
||||
"type": "Mod"
|
||||
}
|
||||
Reference in New Issue
Block a user