Character Select mod for Sons of the Forest
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
*.zip
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using RedLoader;
|
||||
using RedLoader.Utils;
|
||||
using Sons.Wearable.Race;
|
||||
using SonsSdk;
|
||||
using TheForest.Utils;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CharacterSelect;
|
||||
|
||||
public class CharacterSelect : SonsMod
|
||||
{
|
||||
private const int RaceCount = 8;
|
||||
|
||||
private static int _index;
|
||||
private static bool _applied;
|
||||
private static string _configPath;
|
||||
|
||||
public CharacterSelect()
|
||||
{
|
||||
OnUpdateCallback = OnUpdate;
|
||||
}
|
||||
|
||||
protected override void OnSdkInitialized()
|
||||
{
|
||||
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "CharacterSelect.txt");
|
||||
Load();
|
||||
SdkEvents.OnAfterSpawn.Subscribe(OnSpawned);
|
||||
RLog.Msg($"CharacterSelect loaded. Saved race: {_index} ({(PlayerRace.Race)_index}). F9 cycles.");
|
||||
}
|
||||
|
||||
private void OnSpawned()
|
||||
{
|
||||
_applied = false;
|
||||
}
|
||||
|
||||
private void OnUpdate()
|
||||
{
|
||||
var system = LocalPlayer.RaceSystem;
|
||||
if (!system)
|
||||
return;
|
||||
|
||||
if (!_applied)
|
||||
{
|
||||
_applied = true;
|
||||
Apply(system, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.F9))
|
||||
{
|
||||
_index = (_index + 1) % RaceCount;
|
||||
Apply(system, true);
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void Apply(PlayerRaceSystem system, bool announce)
|
||||
{
|
||||
var race = (PlayerRace.Race)_index;
|
||||
system.ApplyRace(race);
|
||||
if (announce)
|
||||
SonsTools.ShowMessage($"{_index}: {race}");
|
||||
RLog.Msg($"Applied race {_index} ({race})");
|
||||
}
|
||||
|
||||
private static void Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_configPath))
|
||||
return;
|
||||
if (int.TryParse(File.ReadAllText(_configPath).Trim(), out var v) && v >= 0 && v < RaceCount)
|
||||
_index = v;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"CharacterSelect could not read config: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(_configPath, _index.ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
RLog.Error($"CharacterSelect could not write config: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>CharacterSelect</AssemblyName>
|
||||
<RootNamespace>CharacterSelect</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>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Character Select
|
||||
|
||||
Pick your player appearance in Sons of the Forest and keep it across sessions.
|
||||
|
||||
The game assigns one of eight player appearances at random when you join a server,
|
||||
and only remembers it if the world gets saved. This mod lets you choose, and
|
||||
reapplies your choice every time you spawn.
|
||||
|
||||
## Notes
|
||||
|
||||
Client side only. Nothing needs to be installed on a dedicated server.
|
||||
|
||||
Your appearance replicates through normal game networking, so other players see
|
||||
your choice even if they do not have the mod installed. They will not be able to
|
||||
change their own appearance without it.
|
||||
|
||||
## Requirements
|
||||
|
||||
RedLoader
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the latest release
|
||||
2. Extract into your Sons of the Forest folder so you end up with:
|
||||
- `Mods/CharacterSelect.dll`
|
||||
- `Mods/CharacterSelect/manifest.json`
|
||||
|
||||
## Usage
|
||||
|
||||
Press F9 to cycle through the eight appearances. Your choice is saved to
|
||||
`_RedLoader/UserData/CharacterSelect.txt` and applied automatically on every spawn.
|
||||
|
||||
| Value | Name |
|
||||
| --- | --- |
|
||||
| 0 | White |
|
||||
| 1 | Black |
|
||||
| 2 | Latin |
|
||||
| 3 | Asian |
|
||||
| 4 | BlackA |
|
||||
| 5 | WhiteA |
|
||||
| 6 | BlackB |
|
||||
| 7 | LatinA |
|
||||
|
||||
## Building
|
||||
|
||||
Requires the .NET 8 SDK and a RedLoader install that has been launched once, so
|
||||
the interop assemblies exist. The script offers to install both if they are missing.
|
||||
@@ -0,0 +1,159 @@
|
||||
[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\CharacterSelect.dll'
|
||||
|
||||
if ($Install) {
|
||||
$modsDir = Join-Path $GameDir 'Mods'
|
||||
$modDataDir = Join-Path $modsDir 'CharacterSelect'
|
||||
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 'CharacterSelect.zip'
|
||||
|
||||
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'CharacterSelect') | Out-Null
|
||||
Copy-Item $dll -Destination $stage -Force
|
||||
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination (Join-Path $stage 'CharacterSelect') -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": "CharacterSelect",
|
||||
"name": "CharacterSelect",
|
||||
"author": "JustinOros",
|
||||
"version": "1.0.0",
|
||||
"description": "Pick your player appearance and keep it across sessions",
|
||||
"gameVersion": "1.0.0",
|
||||
"platform": "Client",
|
||||
"type": "Mod"
|
||||
}
|
||||
Reference in New Issue
Block a user