commit 048bf77543bc1e42d5f6300456680869f9c805d6 Author: Justin Oros Date: Tue Sep 22 22:40:11 2026 -0700 BackupCam 1.1.0, backup camera on the golf cart GPS screen diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ce79dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +dist/ +*.zip diff --git a/BackupCam.cs b/BackupCam.cs new file mode 100644 index 0000000..4cc199d --- /dev/null +++ b/BackupCam.cs @@ -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.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.sortingOrder = 500; + + var imgGo = new GameObject("BackupCamImage"); + imgGo.transform.SetParent(_canvasGo.transform, false); + _image = imgGo.AddComponent(); + _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()) + { + var body = cart.GetComponentInParent(); + if (body == null) body = cart.GetComponentInChildren(); + 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(); + _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(); + 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().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 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()) + { + 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()) + { + 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(); + if (canvas != null) sb.Append(" renderMode=").Append(canvas.renderMode.ToString()); + + var raw = c.TryCast(); + if (raw != null && raw.texture != null) + sb.Append(" texture=").Append(raw.texture.name).Append(" (").Append(raw.texture.GetIl2CppType().Name).Append(')'); + + var cam = c.TryCast(); + if (cam != null && cam.targetTexture != null) + sb.Append(" targetTexture=").Append(cam.targetTexture.name); + + sb.AppendLine(); + + var r = c.TryCast(); + 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); + } + } + } +} \ No newline at end of file diff --git a/BackupCam.csproj b/BackupCam.csproj new file mode 100644 index 0000000..897a9cf --- /dev/null +++ b/BackupCam.csproj @@ -0,0 +1,37 @@ + + + + net6 + enable + latest + BackupCam + BackupCam + 1.0.0 + CS0436;CS1701;CS1702;MSB3277;MSB3106 + + + + $(SOTF_PATH) + + + + + + + + + + + + + + + + + + + + + diff --git a/Install.ps1 b/Install.ps1 new file mode 100644 index 0000000..9faa7d5 --- /dev/null +++ b/Install.ps1 @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..81fe2ab --- /dev/null +++ b/README.md @@ -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. diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..2e20e61 --- /dev/null +++ b/build.ps1 @@ -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 +} + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..13dcca3 --- /dev/null +++ b/manifest.json @@ -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" +} \ No newline at end of file