Initial commit of RigProbe skeleton comparison mod
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
dist/
|
||||||
|
*.zip
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Endnight.Animation;
|
||||||
|
using RedLoader;
|
||||||
|
using RedLoader.Utils;
|
||||||
|
using Sons.Ai.Vail;
|
||||||
|
using SonsSdk;
|
||||||
|
using SonsSdk.Attributes;
|
||||||
|
using TheForest.Utils;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace RigProbe;
|
||||||
|
|
||||||
|
public class RigProbe : SonsMod
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> PlayerParents = new();
|
||||||
|
private static readonly Dictionary<string, string> PlayerPaths = new();
|
||||||
|
|
||||||
|
protected override void OnSdkInitialized()
|
||||||
|
{
|
||||||
|
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigspawn <TypeName> [variation]");
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigprobe")]
|
||||||
|
private static void ProbeCommand(string args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Probe((args ?? string.Empty).Trim());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"RigProbe failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigspawn")]
|
||||||
|
private static void SpawnCommand(string args)
|
||||||
|
{
|
||||||
|
var parts = (args ?? string.Empty).Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length == 0 || !Enum.TryParse<VailActorTypeId>(parts[0], true, out var id))
|
||||||
|
{
|
||||||
|
Say("Usage: rigspawn <TypeName> [variation] e.g. rigspawn Brandy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var variation = parts.Length > 1 && int.TryParse(parts[1], out var v) ? v : 0;
|
||||||
|
var player = LocalPlayer.GameObject;
|
||||||
|
if (!player)
|
||||||
|
{
|
||||||
|
Say("Load into a game first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var t = player.transform;
|
||||||
|
var actor = ActorTools.Spawn(id, t.position + t.forward * 4f, variation);
|
||||||
|
Say(actor ? $"Spawned {id} variation {variation}" : $"Spawn returned null for {id}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigspawn {id} failed: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Probe(string filter)
|
||||||
|
{
|
||||||
|
var player = LocalPlayer.GameObject;
|
||||||
|
var race = LocalPlayer.RaceSystem;
|
||||||
|
if (!player || !race)
|
||||||
|
{
|
||||||
|
Say("Load into a game first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = race._animationRoot;
|
||||||
|
if (!root)
|
||||||
|
root = player.transform.Find("PlayerAnimator/Root");
|
||||||
|
if (!root)
|
||||||
|
{
|
||||||
|
Say("Could not find the player animation root");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildPlayerRig(root);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"RigProbe {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||||
|
sb.AppendLine($"Player animation root: {HierPath(root)}");
|
||||||
|
sb.AppendLine($"Player rig transforms: {PlayerPaths.Count}");
|
||||||
|
sb.AppendLine($"Current race: {race.CurrentRace}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sb.AppendLine($"ExpressionBlendsStartIndex: {race.GetExpressionBlendsStartIndex()}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"ExpressionBlendsStartIndex: error {e.Message}");
|
||||||
|
}
|
||||||
|
sb.AppendLine($"Head: {Name(race.GetHead())} LeftArm: {Name(race.GetLeftArm())} RightArm: {Name(race.GetRightArm())}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("==== PLAYER RENDERERS ====");
|
||||||
|
DumpObject(sb, "Player", player, true);
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
sb.AppendLine("==== ACTORS ====");
|
||||||
|
var ids = Enum.GetValues(typeof(VailActorTypeId)).Cast<VailActorTypeId>()
|
||||||
|
.Where(i => i != VailActorTypeId.None)
|
||||||
|
.Where(i => filter.Length == 0 || i.ToString().IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int probed = 0;
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
VailActor prefab = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
prefab = ActorTools.GetPrefab(id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"[{id} prefab] error {e.Message}");
|
||||||
|
}
|
||||||
|
if (prefab)
|
||||||
|
{
|
||||||
|
DumpObject(sb, $"{id} prefab", prefab.gameObject, false);
|
||||||
|
probed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
VailActor live = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
live = ActorTools.GetActors(id)?.FirstOrDefault(a => a);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"[{id} live] error {e.Message}");
|
||||||
|
}
|
||||||
|
if (live)
|
||||||
|
{
|
||||||
|
DumpObject(sb, $"{id} live", live.gameObject, false);
|
||||||
|
probed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = LoaderEnvironment.UserDataDirectory;
|
||||||
|
var outPath = Path.Combine(dir, "RigProbe.txt");
|
||||||
|
File.WriteAllText(outPath, sb.ToString());
|
||||||
|
|
||||||
|
var rig = new StringBuilder();
|
||||||
|
foreach (var kv in PlayerPaths.OrderBy(k => k.Value))
|
||||||
|
rig.AppendLine(kv.Value);
|
||||||
|
File.WriteAllText(Path.Combine(dir, "RigProbe_PlayerRig.txt"), rig.ToString());
|
||||||
|
|
||||||
|
Say($"RigProbe: {probed} actor objects written to {outPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildPlayerRig(Transform root)
|
||||||
|
{
|
||||||
|
PlayerParents.Clear();
|
||||||
|
PlayerPaths.Clear();
|
||||||
|
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
if (PlayerPaths.ContainsKey(t.name))
|
||||||
|
continue;
|
||||||
|
PlayerPaths[t.name] = RelPath(t, root);
|
||||||
|
PlayerParents[t.name] = t.parent ? t.parent.name : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpObject(StringBuilder sb, string label, GameObject go, bool allBlendShapes)
|
||||||
|
{
|
||||||
|
var renderers = go.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||||
|
var swaps = go.GetComponentsInChildren<Sons.Ai.SwapSkinnedMeshWithPrefab>(true);
|
||||||
|
|
||||||
|
int totalBones = 0, totalName = 0, totalParent = 0, remapCount = 0;
|
||||||
|
var body = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var r in renderers)
|
||||||
|
{
|
||||||
|
var a = Analyze(r, allBlendShapes);
|
||||||
|
totalBones += a.Bones;
|
||||||
|
totalName += a.NameMatch;
|
||||||
|
totalParent += a.ParentMatch;
|
||||||
|
if (a.HasRemap)
|
||||||
|
remapCount++;
|
||||||
|
body.Append(a.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = $"[{label}] go={go.name} renderers={renderers.Length} bones={totalBones} " +
|
||||||
|
$"nameMatch={Pct(totalName, totalBones)} parentMatch={Pct(totalParent, totalBones)} " +
|
||||||
|
$"remapCaches={remapCount} meshSwappers={swaps.Length}";
|
||||||
|
sb.AppendLine(summary);
|
||||||
|
foreach (var s in swaps)
|
||||||
|
{
|
||||||
|
string asset;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
asset = s._meshAsset ? s._meshAsset.name : "null";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
asset = "?";
|
||||||
|
}
|
||||||
|
sb.AppendLine($" swapper {HierPath(s.transform)} meshAsset={asset}");
|
||||||
|
}
|
||||||
|
sb.Append(body);
|
||||||
|
sb.AppendLine();
|
||||||
|
RLog.Msg(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Result
|
||||||
|
{
|
||||||
|
public int Bones;
|
||||||
|
public int NameMatch;
|
||||||
|
public int ParentMatch;
|
||||||
|
public bool HasRemap;
|
||||||
|
public string Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result Analyze(SkinnedMeshRenderer r, bool allBlendShapes)
|
||||||
|
{
|
||||||
|
var res = new Result();
|
||||||
|
var missing = new List<string>();
|
||||||
|
var parentDiff = new List<string>();
|
||||||
|
int nulls = 0;
|
||||||
|
|
||||||
|
var bones = r.bones;
|
||||||
|
if (bones != null)
|
||||||
|
{
|
||||||
|
foreach (var b in bones)
|
||||||
|
{
|
||||||
|
if (!b)
|
||||||
|
{
|
||||||
|
nulls++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
res.Bones++;
|
||||||
|
if (PlayerParents.TryGetValue(b.name, out var expectedParent))
|
||||||
|
{
|
||||||
|
res.NameMatch++;
|
||||||
|
var actualParent = b.parent ? b.parent.name : string.Empty;
|
||||||
|
if (actualParent == expectedParent)
|
||||||
|
res.ParentMatch++;
|
||||||
|
else
|
||||||
|
parentDiff.Add($"{b.name}({actualParent}!={expectedParent})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
missing.Add(b.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var mesh = r.sharedMesh;
|
||||||
|
int blendCount = mesh ? mesh.blendShapeCount : 0;
|
||||||
|
|
||||||
|
sb.AppendLine($" SMR {HierPath(r.transform)} active={r.gameObject.activeInHierarchy} mesh={(mesh ? mesh.name : "null")} " +
|
||||||
|
$"verts={(mesh ? mesh.vertexCount : 0)} bones={res.Bones} null={nulls} nameMatch={res.NameMatch} " +
|
||||||
|
$"parentMatch={res.ParentMatch} rootBone={(r.rootBone ? r.rootBone.name : "null")} blendShapes={blendCount} " +
|
||||||
|
$"materials={string.Join(",", r.sharedMaterials.Where(m => m).Select(m => m.name))}");
|
||||||
|
|
||||||
|
var remap = r.GetComponent<SkinnedMeshBoneRemapCache>();
|
||||||
|
if (remap)
|
||||||
|
{
|
||||||
|
res.HasRemap = true;
|
||||||
|
sb.AppendLine($" remap compat={remap._isCompatible} rootPath={remap._rootBonePath} " +
|
||||||
|
$"paths={(remap._bonePaths != null ? remap._bonePaths.Count : 0)} " +
|
||||||
|
$"srcBase={remap._sourceTransformBasePath} base={remap._transformBasePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.Count > 0)
|
||||||
|
sb.AppendLine($" missing({missing.Count}): {string.Join(" ", missing.Take(60))}");
|
||||||
|
if (parentDiff.Count > 0)
|
||||||
|
sb.AppendLine($" parentDiff({parentDiff.Count}): {string.Join(" ", parentDiff.Take(30))}");
|
||||||
|
|
||||||
|
if (mesh && blendCount > 0)
|
||||||
|
{
|
||||||
|
var take = allBlendShapes ? blendCount : Math.Min(blendCount, 15);
|
||||||
|
var names = new List<string>();
|
||||||
|
for (int i = 0; i < take; i++)
|
||||||
|
names.Add($"{i}:{mesh.GetBlendShapeName(i)}");
|
||||||
|
sb.AppendLine($" blendShapes: {string.Join(" ", names)}{(take < blendCount ? " ..." : string.Empty)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Text = sb.ToString();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Pct(int a, int b)
|
||||||
|
{
|
||||||
|
return b == 0 ? "n/a" : $"{a}/{b} ({a * 100 / b}%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Name(GameObject go)
|
||||||
|
{
|
||||||
|
return go ? go.name : "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HierPath(Transform t)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
while (t)
|
||||||
|
{
|
||||||
|
parts.Add(t.name);
|
||||||
|
t = t.parent;
|
||||||
|
}
|
||||||
|
parts.Reverse();
|
||||||
|
return string.Join("/", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RelPath(Transform t, Transform root)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
while (t && t != root)
|
||||||
|
{
|
||||||
|
parts.Add(t.name);
|
||||||
|
t = t.parent;
|
||||||
|
}
|
||||||
|
parts.Reverse();
|
||||||
|
return string.Join("/", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Say(string text)
|
||||||
|
{
|
||||||
|
SonsTools.ShowMessage(text, 6f);
|
||||||
|
RLog.Msg(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<AssemblyName>RigProbe</AssemblyName>
|
||||||
|
<RootNamespace>RigProbe</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,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\RigProbe.dll'
|
||||||
|
|
||||||
|
if ($Install) {
|
||||||
|
$modsDir = Join-Path $GameDir 'Mods'
|
||||||
|
$modDataDir = Join-Path $modsDir 'RigProbe'
|
||||||
|
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 'RigProbe.zip'
|
||||||
|
|
||||||
|
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'RigProbe') | Out-Null
|
||||||
|
Copy-Item $dll -Destination $stage -Force
|
||||||
|
Copy-Item (Join-Path $PSScriptRoot 'manifest.json') -Destination (Join-Path $stage 'RigProbe') -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": "RigProbe",
|
||||||
|
"name": "RigProbe",
|
||||||
|
"author": "JustinOros",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Dumps NPC skinned mesh bones and compares them to the player rig",
|
||||||
|
"gameVersion": "1.0.0",
|
||||||
|
"platform": "Client",
|
||||||
|
"type": "Mod"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user