Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce33139fe | ||
|
|
d8b9995924 | ||
|
|
7fa11aafe8 | ||
|
|
f2d4cc9a26 | ||
|
|
51fc824e0a | ||
|
|
0eff6cf70b | ||
|
|
66f3398455 | ||
|
|
f186743171 | ||
|
|
59f196262b | ||
|
|
a8af75721c | ||
|
|
76da33cc6d | ||
|
|
39055569ad | ||
|
|
3e744a9a74 |
+55
-7
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using RedLoader;
|
using RedLoader;
|
||||||
using RedLoader.Utils;
|
using RedLoader.Utils;
|
||||||
using Sons.Wearable.Race;
|
using Sons.Wearable.Race;
|
||||||
using SonsSdk;
|
using SonsSdk;
|
||||||
|
using SonsSdk.Attributes;
|
||||||
using TheForest.Utils;
|
using TheForest.Utils;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ public class CharacterSelect : SonsMod
|
|||||||
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "CharacterSelect.txt");
|
_configPath = Path.Combine(LoaderEnvironment.UserDataDirectory, "CharacterSelect.txt");
|
||||||
Load();
|
Load();
|
||||||
SdkEvents.OnAfterSpawn.Subscribe(OnSpawned);
|
SdkEvents.OnAfterSpawn.Subscribe(OnSpawned);
|
||||||
RLog.Msg($"CharacterSelect loaded. Saved race: {_index} ({(PlayerRace.Race)_index}). F9 cycles.");
|
RLog.Msg($"CharacterSelect loaded. Saved character: {_index} ({(PlayerRace.Race)_index}). Press F9 or type: character");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSpawned()
|
private void OnSpawned()
|
||||||
@@ -37,32 +39,78 @@ public class CharacterSelect : SonsMod
|
|||||||
|
|
||||||
private void OnUpdate()
|
private void OnUpdate()
|
||||||
{
|
{
|
||||||
var system = LocalPlayer.RaceSystem;
|
if (!LocalPlayer.RaceSystem)
|
||||||
if (!system)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_applied)
|
if (!_applied)
|
||||||
{
|
{
|
||||||
_applied = true;
|
_applied = true;
|
||||||
Apply(system, false);
|
ApplyIndex(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Input.GetKeyDown(KeyCode.F9))
|
if (Input.GetKeyDown(KeyCode.F9))
|
||||||
{
|
{
|
||||||
_index = (_index + 1) % RaceCount;
|
_index = (_index + 1) % RaceCount;
|
||||||
Apply(system, true);
|
ApplyIndex(true);
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Apply(PlayerRaceSystem system, bool announce)
|
[DebugCommand("character")]
|
||||||
|
private static void CharacterCommand(string args)
|
||||||
{
|
{
|
||||||
|
args = (args ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
ShowList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Equals("next", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_index = (_index + 1) % RaceCount;
|
||||||
|
ApplyIndex(true);
|
||||||
|
Save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(args, out var value) && value >= 0 && value < RaceCount)
|
||||||
|
{
|
||||||
|
_index = value;
|
||||||
|
ApplyIndex(true);
|
||||||
|
Save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SonsTools.ShowMessage("Usage: character [0-7] or character next", 5f);
|
||||||
|
RLog.Msg("Usage: character [0-7] or character next");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowList()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"Current: {_index} ({(PlayerRace.Race)_index})");
|
||||||
|
for (int i = 0; i < RaceCount; i++)
|
||||||
|
sb.AppendLine($"{i}: {(PlayerRace.Race)i}");
|
||||||
|
|
||||||
|
var text = sb.ToString().TrimEnd();
|
||||||
|
SonsTools.ShowMessage(text, 8f);
|
||||||
|
RLog.Msg(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyIndex(bool announce)
|
||||||
|
{
|
||||||
|
var system = LocalPlayer.RaceSystem;
|
||||||
|
if (!system)
|
||||||
|
return;
|
||||||
|
|
||||||
var race = (PlayerRace.Race)_index;
|
var race = (PlayerRace.Race)_index;
|
||||||
system.ApplyRace(race);
|
system.ApplyRace(race);
|
||||||
if (announce)
|
if (announce)
|
||||||
SonsTools.ShowMessage($"{_index}: {race}");
|
SonsTools.ShowMessage($"{_index}: {race}");
|
||||||
RLog.Msg($"Applied race {_index} ({race})");
|
RLog.Msg($"Applied character {_index} ({race})");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Load()
|
private static void Load()
|
||||||
|
|||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$GameDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '====================================' -ForegroundColor Green
|
||||||
|
Write-Host ' Character Select installer' -ForegroundColor Green
|
||||||
|
Write-Host '====================================' -ForegroundColor Green
|
||||||
|
|
||||||
|
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 ' Find the folder that contains SonsOfTheForest.exe and paste the path 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
|
||||||
|
|
||||||
|
$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 Character Select'
|
||||||
|
$modZip = Join-Path $env:TEMP 'CharacterSelect.zip'
|
||||||
|
Get-ReleaseZip -Repo 'JustinOros/sotf-character-select' -Pattern '^CharacterSelect\.zip$' -OutFile $modZip | Out-Null
|
||||||
|
|
||||||
|
Write-Step 'Installing Character Select'
|
||||||
|
$modsDir = Join-Path $GameDir 'Mods'
|
||||||
|
New-Item -ItemType Directory -Force -Path $modsDir | Out-Null
|
||||||
|
Remove-Item (Join-Path $modsDir 'CharacterSelect.dll') -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item (Join-Path $modsDir 'CharacterSelect') -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Expand-Archive -Path $modZip -DestinationPath $modsDir -Force
|
||||||
|
Remove-Item $modZip -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $modsDir 'CharacterSelect.dll'))) {
|
||||||
|
throw 'Something went wrong, CharacterSelect.dll is not in the Mods folder'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path (Join-Path $modsDir 'CharacterSelect\manifest.json'))) {
|
||||||
|
throw 'Something went wrong, manifest.json is not in the Mods\CharacterSelect 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 CharacterSelect'
|
||||||
|
Write-Host ' 2. Load into a game'
|
||||||
|
Write-Host ' 3. Press F9 to change your appearance'
|
||||||
|
Write-Host ''
|
||||||
@@ -1,34 +1,75 @@
|
|||||||
# Character Select
|
# Character Select
|
||||||
|
|
||||||
Pick your player appearance in Sons of the Forest and keep it across sessions.
|
Choose 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,
|
Sons of the Forest gives you a random appearance out of eight every time you join
|
||||||
and only remembers it if the world gets saved. This mod lets you choose, and
|
a server, and there is no way to choose. This mod lets you pick the one you want
|
||||||
reapplies your choice every time you spawn.
|
and automatically reapplies it every time your player spawns, so you stay the
|
||||||
|
same character.
|
||||||
|
|
||||||
## Notes
|
## Multiplayer
|
||||||
|
|
||||||
Client side only. Nothing needs to be installed on a dedicated server.
|
Client side only. Nothing gets installed on a dedicated server.
|
||||||
|
|
||||||
Your appearance replicates through normal game networking, so other players see
|
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
|
your choice even if they do not have the mod installed.
|
||||||
change their own appearance without it.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
RedLoader
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the latest release
|
### Step 1: Install RedLoader
|
||||||
2. Extract into your Sons of the Forest folder so you end up with:
|
|
||||||
- `Mods/CharacterSelect.dll`
|
[RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest) is the mod
|
||||||
- `Mods/CharacterSelect/manifest.json`
|
loader for Sons of the Forest. The game cannot load any mod without it, including
|
||||||
|
this one, 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. You should now see `version.dll` and a `_Redloader` folder sitting next to
|
||||||
|
`SonsOfTheForest.exe`
|
||||||
|
4. 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
|
||||||
|
5. Check that `MODS` appears on the main menu, then quit
|
||||||
|
|
||||||
|
Ignore `RedManager.exe` if you see it. It is only a convenience installer and
|
||||||
|
you do not need it.
|
||||||
|
|
||||||
|
### Step 2: Install Character Select
|
||||||
|
|
||||||
|
1. Download `CharacterSelect.zip` from the
|
||||||
|
[latest release](https://github.com/JustinOros/sotf-character-select/releases/latest)
|
||||||
|
2. Open the `Mods` folder inside your Sons of the Forest install, usually
|
||||||
|
`C:\Program Files (x86)\Steam\steamapps\common\Sons Of The Forest\Mods`
|
||||||
|
3. Extract the contents of the zip into that `Mods` folder
|
||||||
|
|
||||||
|
When you are done it should look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Sons Of The Forest/
|
||||||
|
Mods/
|
||||||
|
CharacterSelect.dll
|
||||||
|
CharacterSelect/
|
||||||
|
manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Press F9 to cycle through the eight appearances. Your choice is saved to
|
Once you are in the world, press F9 to cycle through the eight appearances. The
|
||||||
`_RedLoader/UserData/CharacterSelect.txt` and applied automatically on every spawn.
|
current one is shown on screen as you cycle.
|
||||||
|
|
||||||
|
If your keyboard has no F keys, or you would rather pick one directly, open the
|
||||||
|
in game console and use the `character` command:
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `character` | Shows your current appearance and the full list |
|
||||||
|
| `character next` | Moves to the next appearance |
|
||||||
|
| `character 3` | Switches straight to that appearance |
|
||||||
|
|
||||||
|
Your choice is saved to `UserData/CharacterSelect.txt` in your Sons of the Forest
|
||||||
|
folder and applied automatically every time you spawn.
|
||||||
|
|
||||||
| Value | Name |
|
| Value | Name |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -43,5 +84,28 @@ Press F9 to cycle through the eight appearances. Your choice is saved to
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires the .NET 8 SDK and a RedLoader install that has been launched once, so
|
This section is only for those who want to modify the source or build it
|
||||||
the interop assemblies exist. The script offers to install both if they are missing.
|
yourself.
|
||||||
|
|
||||||
|
Requires the [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) and a
|
||||||
|
[RedLoader](https://github.com/ToniMacaroni/RedLoader/releases/latest) install
|
||||||
|
that has been launched at least once, so the interop assemblies exist. The build
|
||||||
|
script offers to install both if they are missing.
|
||||||
|
|
||||||
|
```
|
||||||
|
.\build.ps1 -Install
|
||||||
|
```
|
||||||
|
|
||||||
|
The game folder is found through Steam. Override it with `-GameDir "path"` or the
|
||||||
|
`SOTF_PATH` environment variable.
|
||||||
|
|
||||||
|
To produce a release zip:
|
||||||
|
|
||||||
|
```
|
||||||
|
.\build.ps1 -Package
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Built against RedLoader by Toni Macaroni. The player race system was found by
|
||||||
|
reading the source of ArmorMod by clairenheit.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/ToniMacaroni/RedLoader/main/MetadataSchema.json",
|
"$schema": "https://raw.githubusercontent.com/ToniMacaroni/RedLoader/main/MetadataSchema.json",
|
||||||
"id": "CharacterSelect",
|
"id": "CharacterSelect",
|
||||||
"name": "CharacterSelect",
|
"name": "CharacterSelect",
|
||||||
|
|||||||
Reference in New Issue
Block a user