Add remote players, components, animator clips and states, rigassets and rigwatch for v0.4.0
This commit is contained in:
1 parent
48ce86589f
commit
1028903e49
4 files changed
+560
-23
No files matched your search
@@ -4,11 +4,12 @@ A RedLoader research mod for Sons of the Forest that dumps character data to JSO
|
|||||||
|
|
||||||
## What it collects
|
## What it collects
|
||||||
|
|
||||||
For the local player, the bodies the player can carry, every actor type prefab and every live actor:
|
For the local player, remote players, the bodies the player can carry, every actor type prefab and every live actor:
|
||||||
|
|
||||||
- Rig: every bone with its parent, local position, rotation and scale
|
- Rig: every bone with its parent, local position, rotation and scale
|
||||||
- Skinned meshes: mesh name, vertex count, bone list, bind pose rest position and rotation per bone, blendshape names, materials with shader and texture names
|
- Skinned meshes: mesh name, vertex count, render layer, shadow mode, bone list, bind pose rest position and rotation per bone, blendshape names, materials with shader and texture names
|
||||||
- Animators: controller, avatar, humanoid flag, human bone map, parameters
|
- Components: script types on the root, its children, rig bones and animator objects
|
||||||
|
- Animators: controller, avatar, humanoid flag, human bone map, layer names and weights, parameters with hashes, clips with length, loop flag and events, discovered states, current state per layer
|
||||||
- Rig families: characters grouped by identical bone sets, with bone name overlap against the player rig
|
- Rig families: characters grouped by identical bone sets, with bone name overlap against the player rig
|
||||||
- Player only: current race, all 8 race entries with head and arms Addressable GUIDs, every clothing piece with item id, slot, renderable GUID, default and worn flags
|
- Player only: current race, all 8 race entries with head and arms Addressable GUIDs, every clothing piece with item id, slot, renderable GUID, default and worn flags
|
||||||
|
|
||||||
@@ -18,9 +19,12 @@ Run these in the in-game console.
|
|||||||
|
|
||||||
| Command | What it does |
|
| Command | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `rigprobe` | Dump the player, carry bodies, all actor prefabs and live actors |
|
| `rigprobe` | Dump the player, remote players, carry bodies, all actor prefabs and live actors, plus all loaded clips and controllers |
|
||||||
| `rigprobe <filter>` | Same, limited to actor types containing the filter, e.g. `rigprobe female` |
|
| `rigprobe <filter>` | Same, limited to actor types containing the filter, e.g. `rigprobe female` |
|
||||||
| `rigscene` | Dump every character in the loaded scenes (posers, cutscene characters, props with skinned meshes), one file per character |
|
| `rigscene` | Dump every character in the loaded scenes (posers, cutscene characters, props with skinned meshes), one file per character |
|
||||||
|
| `rigassets [filter]` | Dump the Addressables catalog (keys, internal ids, types), optionally filtered, e.g. `rigassets female` |
|
||||||
|
| `rigwatch [player\|robby\|virginia]` | Log every animator state change on the target while you play |
|
||||||
|
| `rigwatch off` | Stop watching and write a summary of every state seen |
|
||||||
| `rigspawn <Type> [variation]` | Spawn an actor in front of you so its live variant gets dumped, e.g. `rigspawn Virginia` |
|
| `rigspawn <Type> [variation]` | Spawn an actor in front of you so its live variant gets dumped, e.g. `rigspawn Virginia` |
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
@@ -28,11 +32,14 @@ Run these in the in-game console.
|
|||||||
Written to `Sons Of The Forest\UserData\RigProbe\`:
|
Written to `Sons Of The Forest\UserData\RigProbe\`:
|
||||||
|
|
||||||
- `characters\index.tsv` one line per character with rig family and player bone match
|
- `characters\index.tsv` one line per character with rig family and player bone match
|
||||||
|
- `characters\clips.tsv` every loaded animation clip, `characters\controllers.tsv` every loaded animator controller and who uses it
|
||||||
|
- `assets.tsv` or `assets_<filter>.tsv` from `rigassets`
|
||||||
|
- `watch\<target>_<time>.log` and `_states.tsv` from `rigwatch`
|
||||||
- `characters\rigfamilies.txt` characters grouped by shared skeleton
|
- `characters\rigfamilies.txt` characters grouped by shared skeleton
|
||||||
- `characters\<source>_<name>.json` full data per character
|
- `characters\<source>_<name>.json` full data per character
|
||||||
- `scene\` the same layout for `rigscene`
|
- `scene\` the same layout for `rigscene`
|
||||||
|
|
||||||
Each run replaces the previous output for that command.
|
`rigprobe`, `rigscene` and `rigassets` replace their previous output. `rigwatch` files are timestamped.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|||||||
+546
-16
@@ -8,11 +8,16 @@ using System.Text.Json;
|
|||||||
using RedLoader;
|
using RedLoader;
|
||||||
using RedLoader.Utils;
|
using RedLoader.Utils;
|
||||||
using Sons.Ai.Vail;
|
using Sons.Ai.Vail;
|
||||||
|
using Sons.Multiplayer.Client;
|
||||||
using Sons.Wearable.Clothing;
|
using Sons.Wearable.Clothing;
|
||||||
|
using Sons.Wearable.Race;
|
||||||
using SonsSdk;
|
using SonsSdk;
|
||||||
using SonsSdk.Attributes;
|
using SonsSdk.Attributes;
|
||||||
using TheForest.Utils;
|
using TheForest.Utils;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.AddressableAssets;
|
||||||
|
using UnityEngine.AddressableAssets.ResourceLocators;
|
||||||
|
using UnityEngine.ResourceManagement.ResourceLocations;
|
||||||
|
|
||||||
namespace RigProbe;
|
namespace RigProbe;
|
||||||
|
|
||||||
@@ -21,9 +26,29 @@ public class RigProbe : SonsMod
|
|||||||
private static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
|
||||||
private static HashSet<string> _playerBoneNames = new();
|
private static HashSet<string> _playerBoneNames = new();
|
||||||
|
|
||||||
|
private static readonly string[] EmoteNames =
|
||||||
|
{
|
||||||
|
"HappyThumbsUp", "ThumbsUp", "Nod", "HappyFistPump", "FistPump", "Confused", "HitHeadSmall", "HitHeadBig",
|
||||||
|
"ShakeHead", "NoHandUp", "Sad", "Happy", "Laugh", "WagFingerNo", "SkunkReact", "OnPlayerNod",
|
||||||
|
"OnPlayerCrash", "OnPlayerSmallHit", "Wave", "Point", "Cheer", "Dance", "Salute", "Clap", "Shrug"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Animator _watchAnimator;
|
||||||
|
private static string _watchTarget;
|
||||||
|
private static int[] _watchLast;
|
||||||
|
private static StreamWriter _watchLog;
|
||||||
|
private static Dictionary<int, string> _watchNames;
|
||||||
|
private static readonly Dictionary<string, WatchEntry> WatchSeen = new();
|
||||||
|
private static string _watchStatesPath;
|
||||||
|
|
||||||
|
public RigProbe()
|
||||||
|
{
|
||||||
|
OnUpdateCallback = OnUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnSdkInitialized()
|
protected override void OnSdkInitialized()
|
||||||
{
|
{
|
||||||
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigspawn <TypeName> [variation]");
|
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigassets [filter], rigwatch [player|robby|virginia|off], rigspawn <TypeName> [variation]");
|
||||||
}
|
}
|
||||||
|
|
||||||
[DebugCommand("rigprobe")]
|
[DebugCommand("rigprobe")]
|
||||||
@@ -52,6 +77,32 @@ public class RigProbe : SonsMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigassets")]
|
||||||
|
private static void AssetsCommand(string args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DumpAddressables((args ?? string.Empty).Trim());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigassets failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigwatch")]
|
||||||
|
private static void WatchCommand(string args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StartWatch((args ?? string.Empty).Trim().ToLowerInvariant());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigwatch failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DebugCommand("rigspawn")]
|
[DebugCommand("rigspawn")]
|
||||||
private static void SpawnCommand(string args)
|
private static void SpawnCommand(string args)
|
||||||
{
|
{
|
||||||
@@ -106,6 +157,16 @@ public class RigProbe : SonsMod
|
|||||||
|
|
||||||
if (!sceneMode)
|
if (!sceneMode)
|
||||||
{
|
{
|
||||||
|
foreach (var setup in Resources.FindObjectsOfTypeAll<CoopPlayerRemoteSetup>())
|
||||||
|
{
|
||||||
|
if (!setup || !setup.gameObject.scene.IsValid() || IsUnder(setup.transform, player.transform))
|
||||||
|
continue;
|
||||||
|
var go = setup.gameObject;
|
||||||
|
var d = DumpObject(go, "remotePlayer", $"{go.name}_{go.GetInstanceID()}", null);
|
||||||
|
AddPlayerData(d, go);
|
||||||
|
dumps.Add(d);
|
||||||
|
}
|
||||||
|
|
||||||
if (carryRoot)
|
if (carryRoot)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < carryRoot.childCount; i++)
|
for (int i = 0; i < carryRoot.childCount; i++)
|
||||||
@@ -156,6 +217,8 @@ public class RigProbe : SonsMod
|
|||||||
dumps.Add(d);
|
dumps.Add(d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WriteClips(outDir);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -227,6 +290,7 @@ public class RigProbe : SonsMod
|
|||||||
AddWithAncestors(r.rootBone, root, rigIds);
|
AddWithAncestors(r.rootBone, root, rigIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var animatorIds = new HashSet<int>();
|
||||||
foreach (var a in go.GetComponentsInChildren<Animator>(true))
|
foreach (var a in go.GetComponentsInChildren<Animator>(true))
|
||||||
{
|
{
|
||||||
if (!a || (exclude && IsUnder(a.transform, exclude)))
|
if (!a || (exclude && IsUnder(a.transform, exclude)))
|
||||||
@@ -234,21 +298,34 @@ public class RigProbe : SonsMod
|
|||||||
if (only != null && a.transform != root && !rigIds.Contains(a.transform.GetInstanceID()))
|
if (only != null && a.transform != root && !rigIds.Contains(a.transform.GetInstanceID()))
|
||||||
continue;
|
continue;
|
||||||
AddWithAncestors(a.transform, root, rigIds);
|
AddWithAncestors(a.transform, root, rigIds);
|
||||||
|
animatorIds.Add(a.transform.GetInstanceID());
|
||||||
dump.Animators.Add(DumpAnimator(a, root));
|
dump.Animators.Add(DumpAnimator(a, root));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var t in go.GetComponentsInChildren<Transform>(true))
|
foreach (var t in go.GetComponentsInChildren<Transform>(true))
|
||||||
{
|
{
|
||||||
if (!rigIds.Contains(t.GetInstanceID()))
|
var id = t.GetInstanceID();
|
||||||
continue;
|
var inRig = rigIds.Contains(id);
|
||||||
dump.Rig.Add(new BoneInfo
|
if (inRig)
|
||||||
{
|
{
|
||||||
Path = RelPath(t, root),
|
dump.Rig.Add(new BoneInfo
|
||||||
Parent = t.parent && t != root ? RelPath(t.parent, root) : string.Empty,
|
{
|
||||||
LocalPosition = V3(t.localPosition),
|
Path = RelPath(t, root),
|
||||||
LocalRotation = Q(t.localRotation),
|
Parent = t.parent && t != root ? RelPath(t.parent, root) : string.Empty,
|
||||||
LocalScale = V3(t.localScale)
|
LocalPosition = V3(t.localPosition),
|
||||||
});
|
LocalRotation = Q(t.localRotation),
|
||||||
|
LocalScale = V3(t.localScale)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclude && IsUnder(t, exclude))
|
||||||
|
continue;
|
||||||
|
if (inRig || animatorIds.Contains(id) || t == root || t.parent == root)
|
||||||
|
{
|
||||||
|
var types = ComponentTypes(t);
|
||||||
|
if (types.Length > 0)
|
||||||
|
dump.Components.Add($"{RelPath(t, root)}: {types}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var r in smrs)
|
foreach (var r in smrs)
|
||||||
@@ -266,6 +343,29 @@ public class RigProbe : SonsMod
|
|||||||
return dump;
|
return dump;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ComponentTypes(Transform t)
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var c in t.GetComponents<Component>())
|
||||||
|
{
|
||||||
|
if (!c)
|
||||||
|
continue;
|
||||||
|
string n;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
n = c.GetIl2CppType().FullName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
n = c.GetType().FullName;
|
||||||
|
}
|
||||||
|
if (n == "UnityEngine.Transform")
|
||||||
|
continue;
|
||||||
|
names.Add(n);
|
||||||
|
}
|
||||||
|
return string.Join(", ", names);
|
||||||
|
}
|
||||||
|
|
||||||
private static SmrInfo DumpSmr(SkinnedMeshRenderer r, Transform root)
|
private static SmrInfo DumpSmr(SkinnedMeshRenderer r, Transform root)
|
||||||
{
|
{
|
||||||
var info = new SmrInfo
|
var info = new SmrInfo
|
||||||
@@ -273,6 +373,9 @@ public class RigProbe : SonsMod
|
|||||||
Path = RelPath(r.transform, root),
|
Path = RelPath(r.transform, root),
|
||||||
Enabled = r.enabled,
|
Enabled = r.enabled,
|
||||||
Active = r.gameObject.activeInHierarchy,
|
Active = r.gameObject.activeInHierarchy,
|
||||||
|
Layer = r.gameObject.layer,
|
||||||
|
LayerName = LayerMask.LayerToName(r.gameObject.layer),
|
||||||
|
Shadow = r.shadowCastingMode.ToString(),
|
||||||
RootBone = r.rootBone ? RelPath(r.rootBone, root) : string.Empty
|
RootBone = r.rootBone ? RelPath(r.rootBone, root) : string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -347,9 +450,12 @@ public class RigProbe : SonsMod
|
|||||||
Path = RelPath(a.transform, root),
|
Path = RelPath(a.transform, root),
|
||||||
Enabled = a.enabled
|
Enabled = a.enabled
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RuntimeAnimatorController controller = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
info.Controller = a.runtimeAnimatorController ? a.runtimeAnimatorController.name : string.Empty;
|
controller = a.runtimeAnimatorController;
|
||||||
|
info.Controller = controller ? controller.name : string.Empty;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -375,22 +481,391 @@ public class RigProbe : SonsMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clipNames = new List<string>();
|
||||||
|
if (controller)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>();
|
||||||
|
foreach (var clip in controller.animationClips)
|
||||||
|
{
|
||||||
|
if (!clip || !seen.Add(clip.name))
|
||||||
|
continue;
|
||||||
|
clipNames.Add(clip.name);
|
||||||
|
info.Clips.Add(DescribeClip(clip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
info.Layers = a.layerCount;
|
info.Initialized = a.isInitialized;
|
||||||
foreach (var p in a.parameters)
|
|
||||||
info.Parameters.Add($"{p.name}:{p.type}");
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!info.Initialized)
|
||||||
|
return info;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
info.LayerCount = a.layerCount;
|
||||||
|
for (int i = 0; i < a.layerCount; i++)
|
||||||
|
info.Layers.Add(new LayerInfo { Index = i, Name = a.GetLayerName(i), Weight = R(a.GetLayerWeight(i)) });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var p in a.parameters)
|
||||||
|
info.Parameters.Add($"{p.name}:{p.type}:{p.nameHash}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates = clipNames.Concat(EmoteNames).Distinct().ToList();
|
||||||
|
for (int layer = 0; layer < info.Layers.Count; layer++)
|
||||||
|
{
|
||||||
|
foreach (var name in candidates)
|
||||||
|
{
|
||||||
|
var hash = Animator.StringToHash(name);
|
||||||
|
bool has;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
has = a.HasState(layer, hash);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
has = false;
|
||||||
|
}
|
||||||
|
if (has)
|
||||||
|
info.States.Add(new StateHit { Layer = layer, LayerName = info.Layers[layer].Name, Name = name, ShortHash = hash });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var st = a.GetCurrentAnimatorStateInfo(layer);
|
||||||
|
var clips = new List<string>();
|
||||||
|
foreach (var ci in a.GetCurrentAnimatorClipInfo(layer))
|
||||||
|
{
|
||||||
|
var c = ci.clip;
|
||||||
|
if (c)
|
||||||
|
clips.Add(c.name);
|
||||||
|
}
|
||||||
|
info.Current.Add(new CurrentState
|
||||||
|
{
|
||||||
|
Layer = layer,
|
||||||
|
ShortHash = st.shortNameHash,
|
||||||
|
FullHash = st.fullPathHash,
|
||||||
|
NormalizedTime = R(st.normalizedTime),
|
||||||
|
Clips = string.Join("|", clips)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ClipInfo DescribeClip(AnimationClip clip)
|
||||||
|
{
|
||||||
|
var ci = new ClipInfo
|
||||||
|
{
|
||||||
|
Name = clip.name,
|
||||||
|
Length = R(clip.length),
|
||||||
|
Loop = clip.isLooping,
|
||||||
|
FrameRate = R(clip.frameRate),
|
||||||
|
Legacy = clip.legacy,
|
||||||
|
HumanMotion = clip.humanMotion
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var events = clip.events;
|
||||||
|
if (events != null)
|
||||||
|
ci.Events = events.Where(e => e != null).Select(e => $"{R(e.time)}:{e.functionName}").ToList();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return ci;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteClips(string outDir)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("name\tlength\tloop\tframeRate\tlegacy\thumanMotion\tevents");
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
foreach (var clip in Resources.FindObjectsOfTypeAll<AnimationClip>())
|
||||||
|
{
|
||||||
|
if (!clip || !seen.Add(clip.GetInstanceID()))
|
||||||
|
continue;
|
||||||
|
var ci = DescribeClip(clip);
|
||||||
|
sb.AppendLine($"{ci.Name}\t{ci.Length}\t{ci.Loop}\t{ci.FrameRate}\t{ci.Legacy}\t{ci.HumanMotion}\t{string.Join(" ", ci.Events)}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(Path.Combine(outDir, "clips.tsv"), sb.ToString());
|
||||||
|
|
||||||
|
var cb = new StringBuilder();
|
||||||
|
cb.AppendLine("controller\tclips\tusedBy");
|
||||||
|
var users = new Dictionary<string, HashSet<string>>();
|
||||||
|
foreach (var a in Resources.FindObjectsOfTypeAll<Animator>())
|
||||||
|
{
|
||||||
|
if (!a)
|
||||||
|
continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var c = a.runtimeAnimatorController;
|
||||||
|
if (!c)
|
||||||
|
continue;
|
||||||
|
if (!users.TryGetValue(c.name, out var set))
|
||||||
|
users[c.name] = set = new HashSet<string>();
|
||||||
|
if (set.Count < 20)
|
||||||
|
set.Add(a.transform.root.name);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var c in Resources.FindObjectsOfTypeAll<RuntimeAnimatorController>())
|
||||||
|
{
|
||||||
|
if (!c)
|
||||||
|
continue;
|
||||||
|
int count = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
count = c.animationClips.Length;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
users.TryGetValue(c.name, out var set);
|
||||||
|
cb.AppendLine($"{c.name}\t{count}\t{(set == null ? string.Empty : string.Join(", ", set))}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(Path.Combine(outDir, "controllers.tsv"), cb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpAddressables(string filter)
|
||||||
|
{
|
||||||
|
var outDir = Path.Combine(LoaderEnvironment.UserDataDirectory, "RigProbe");
|
||||||
|
Directory.CreateDirectory(outDir);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("locator\tkey\tprimaryKey\tinternalId\ttype\tprovider");
|
||||||
|
int rows = 0;
|
||||||
|
|
||||||
|
var locators = Addressables.ResourceLocators;
|
||||||
|
var en = locators.GetEnumerator();
|
||||||
|
var move = en.Cast<Il2CppSystem.Collections.IEnumerator>();
|
||||||
|
while (move.MoveNext())
|
||||||
|
{
|
||||||
|
var locator = en.Current;
|
||||||
|
if (locator == null)
|
||||||
|
continue;
|
||||||
|
var map = locator.TryCast<ResourceLocationMap>();
|
||||||
|
if (map == null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{locator.LocatorId}\t(not a ResourceLocationMap)\t\t\t\t");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kv in map.Locations)
|
||||||
|
{
|
||||||
|
var key = kv.Key != null ? kv.Key.ToString() : string.Empty;
|
||||||
|
var list = kv.Value?.TryCast<Il2CppSystem.Collections.Generic.List<IResourceLocation>>();
|
||||||
|
if (list == null)
|
||||||
|
continue;
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var loc = list[i];
|
||||||
|
if (loc == null)
|
||||||
|
continue;
|
||||||
|
var internalId = loc.InternalId ?? string.Empty;
|
||||||
|
if (filter.Length > 0
|
||||||
|
&& key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
&& internalId.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0
|
||||||
|
&& (loc.PrimaryKey ?? string.Empty).IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
continue;
|
||||||
|
string type;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
type = loc.ResourceType?.FullName ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
type = string.Empty;
|
||||||
|
}
|
||||||
|
sb.AppendLine($"{map.LocatorId}\t{key}\t{loc.PrimaryKey}\t{internalId}\t{type}\t{loc.ProviderId}");
|
||||||
|
rows++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = filter.Length > 0 ? $"assets_{Sanitize(filter)}.tsv" : "assets.tsv";
|
||||||
|
var path = Path.Combine(outDir, name);
|
||||||
|
File.WriteAllText(path, sb.ToString());
|
||||||
|
Say($"RigProbe: {rows} addressable locations written to {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void StartWatch(string target)
|
||||||
|
{
|
||||||
|
StopWatch();
|
||||||
|
if (target == "off")
|
||||||
|
{
|
||||||
|
Say("rigwatch off");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.Length == 0)
|
||||||
|
target = "player";
|
||||||
|
|
||||||
|
GameObject go = null;
|
||||||
|
if (target == "player")
|
||||||
|
go = LocalPlayer.GameObject;
|
||||||
|
else if (target == "robby")
|
||||||
|
go = ActorTools.GetRobby()?.gameObject;
|
||||||
|
else if (target == "virginia")
|
||||||
|
go = ActorTools.GetActors(VailActorTypeId.Virginia)?.FirstOrDefault(a => a)?.gameObject;
|
||||||
|
|
||||||
|
if (!go)
|
||||||
|
{
|
||||||
|
Say($"rigwatch: no {target} found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animator best = null;
|
||||||
|
foreach (var a in go.GetComponentsInChildren<Animator>(true))
|
||||||
|
{
|
||||||
|
if (!a || !a.isInitialized || !a.runtimeAnimatorController)
|
||||||
|
continue;
|
||||||
|
if (!best || a.layerCount > best.layerCount)
|
||||||
|
best = a;
|
||||||
|
}
|
||||||
|
if (!best)
|
||||||
|
{
|
||||||
|
Say($"rigwatch: no initialized animator on {target}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_watchAnimator = best;
|
||||||
|
_watchTarget = target;
|
||||||
|
_watchLast = Enumerable.Repeat(int.MinValue, best.layerCount).ToArray();
|
||||||
|
WatchSeen.Clear();
|
||||||
|
|
||||||
|
_watchNames = new Dictionary<int, string>();
|
||||||
|
var layerNames = Enumerable.Range(0, best.layerCount).Select(best.GetLayerName).ToList();
|
||||||
|
foreach (var clip in best.runtimeAnimatorController.animationClips)
|
||||||
|
{
|
||||||
|
if (!clip)
|
||||||
|
continue;
|
||||||
|
_watchNames[Animator.StringToHash(clip.name)] = clip.name;
|
||||||
|
foreach (var ln in layerNames)
|
||||||
|
_watchNames[Animator.StringToHash($"{ln}.{clip.name}")] = $"{ln}.{clip.name}";
|
||||||
|
}
|
||||||
|
foreach (var n in EmoteNames)
|
||||||
|
{
|
||||||
|
_watchNames[Animator.StringToHash(n)] = n;
|
||||||
|
foreach (var ln in layerNames)
|
||||||
|
_watchNames[Animator.StringToHash($"{ln}.{n}")] = $"{ln}.{n}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = Path.Combine(LoaderEnvironment.UserDataDirectory, "RigProbe", "watch");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
_watchLog = new StreamWriter(Path.Combine(dir, $"{target}_{stamp}.log")) { AutoFlush = true };
|
||||||
|
_watchStatesPath = Path.Combine(dir, $"{target}_{stamp}_states.tsv");
|
||||||
|
_watchLog.WriteLine("time\tlayer\tlayerName\tweight\tshortHash\tfullHash\tresolved\tclips");
|
||||||
|
Say($"rigwatch: watching {target} animator {best.name} ({best.layerCount} layers). rigwatch off to stop.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void StopWatch()
|
||||||
|
{
|
||||||
|
if (_watchLog == null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("layer\tlayerName\tshortHash\tfullHash\tresolved\tclips\tcount");
|
||||||
|
foreach (var e in WatchSeen.Values.OrderBy(e => e.Layer).ThenByDescending(e => e.Count))
|
||||||
|
sb.AppendLine($"{e.Layer}\t{e.LayerName}\t{e.ShortHash}\t{e.FullHash}\t{e.Resolved}\t{e.Clips}\t{e.Count}");
|
||||||
|
File.WriteAllText(_watchStatesPath, sb.ToString());
|
||||||
|
_watchLog.Dispose();
|
||||||
|
Say($"rigwatch: {WatchSeen.Count} states written to {_watchStatesPath}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigwatch stop failed: {e.Message}");
|
||||||
|
}
|
||||||
|
_watchLog = null;
|
||||||
|
_watchAnimator = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnUpdate()
|
||||||
|
{
|
||||||
|
if (_watchLog == null)
|
||||||
|
return;
|
||||||
|
var a = _watchAnimator;
|
||||||
|
if (!a)
|
||||||
|
{
|
||||||
|
StopWatch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _watchLast.Length; i++)
|
||||||
|
{
|
||||||
|
var st = a.GetCurrentAnimatorStateInfo(i);
|
||||||
|
if (st.fullPathHash == _watchLast[i])
|
||||||
|
continue;
|
||||||
|
_watchLast[i] = st.fullPathHash;
|
||||||
|
|
||||||
|
var clips = new List<string>();
|
||||||
|
foreach (var ci in a.GetCurrentAnimatorClipInfo(i))
|
||||||
|
{
|
||||||
|
var c = ci.clip;
|
||||||
|
if (c)
|
||||||
|
clips.Add(c.name);
|
||||||
|
}
|
||||||
|
var clipText = string.Join("|", clips);
|
||||||
|
var layerName = a.GetLayerName(i);
|
||||||
|
var resolved = _watchNames.TryGetValue(st.fullPathHash, out var fn) ? fn
|
||||||
|
: _watchNames.TryGetValue(st.shortNameHash, out var sn) ? sn : string.Empty;
|
||||||
|
|
||||||
|
_watchLog.WriteLine($"{Time.time:F2}\t{i}\t{layerName}\t{R(a.GetLayerWeight(i))}\t{st.shortNameHash}\t{st.fullPathHash}\t{resolved}\t{clipText}");
|
||||||
|
|
||||||
|
var key = $"{i}:{st.fullPathHash}";
|
||||||
|
if (!WatchSeen.TryGetValue(key, out var entry))
|
||||||
|
{
|
||||||
|
entry = new WatchEntry
|
||||||
|
{
|
||||||
|
Layer = i,
|
||||||
|
LayerName = layerName,
|
||||||
|
ShortHash = st.shortNameHash,
|
||||||
|
FullHash = st.fullPathHash,
|
||||||
|
Resolved = resolved,
|
||||||
|
Clips = clipText
|
||||||
|
};
|
||||||
|
WatchSeen[key] = entry;
|
||||||
|
}
|
||||||
|
entry.Count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigwatch update failed: {e.Message}");
|
||||||
|
StopWatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void AddPlayerData(CharacterDump dump, GameObject player)
|
private static void AddPlayerData(CharacterDump dump, GameObject player)
|
||||||
{
|
{
|
||||||
var race = LocalPlayer.RaceSystem;
|
var race = player.GetComponentInChildren<PlayerRaceSystem>(true);
|
||||||
if (race)
|
if (race)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -655,6 +1130,7 @@ public class CharacterDump
|
|||||||
public List<RaceInfo> Races { get; set; } = new();
|
public List<RaceInfo> Races { get; set; } = new();
|
||||||
public List<ClothingInfo> Clothing { get; set; } = new();
|
public List<ClothingInfo> Clothing { get; set; } = new();
|
||||||
public List<AnimatorInfo> Animators { get; set; } = new();
|
public List<AnimatorInfo> Animators { get; set; } = new();
|
||||||
|
public List<string> Components { get; set; } = new();
|
||||||
public List<BoneInfo> Rig { get; set; } = new();
|
public List<BoneInfo> Rig { get; set; } = new();
|
||||||
public List<SmrInfo> SkinnedMeshes { get; set; } = new();
|
public List<SmrInfo> SkinnedMeshes { get; set; } = new();
|
||||||
public List<string> AttachedMeshes { get; set; } = new();
|
public List<string> AttachedMeshes { get; set; } = new();
|
||||||
@@ -675,6 +1151,9 @@ public class SmrInfo
|
|||||||
public string Mesh { get; set; }
|
public string Mesh { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
public bool Active { get; set; }
|
public bool Active { get; set; }
|
||||||
|
public int Layer { get; set; }
|
||||||
|
public string LayerName { get; set; }
|
||||||
|
public string Shadow { get; set; }
|
||||||
public bool Readable { get; set; }
|
public bool Readable { get; set; }
|
||||||
public int Vertices { get; set; }
|
public int Vertices { get; set; }
|
||||||
public int SubMeshes { get; set; }
|
public int SubMeshes { get; set; }
|
||||||
@@ -703,13 +1182,64 @@ public class AnimatorInfo
|
|||||||
{
|
{
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
public bool Initialized { get; set; }
|
||||||
public string Controller { get; set; } = string.Empty;
|
public string Controller { get; set; } = string.Empty;
|
||||||
public string Avatar { get; set; } = string.Empty;
|
public string Avatar { get; set; } = string.Empty;
|
||||||
public bool IsHuman { get; set; }
|
public bool IsHuman { get; set; }
|
||||||
public bool IsValid { get; set; }
|
public bool IsValid { get; set; }
|
||||||
public int Layers { get; set; }
|
public int LayerCount { get; set; }
|
||||||
|
public List<LayerInfo> Layers { get; set; } = new();
|
||||||
public Dictionary<string, string> HumanBones { get; set; } = new();
|
public Dictionary<string, string> HumanBones { get; set; } = new();
|
||||||
public List<string> Parameters { get; set; } = new();
|
public List<string> Parameters { get; set; } = new();
|
||||||
|
public List<ClipInfo> Clips { get; set; } = new();
|
||||||
|
public List<StateHit> States { get; set; } = new();
|
||||||
|
public List<CurrentState> Current { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LayerInfo
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public float Weight { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClipInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public float Length { get; set; }
|
||||||
|
public bool Loop { get; set; }
|
||||||
|
public float FrameRate { get; set; }
|
||||||
|
public bool Legacy { get; set; }
|
||||||
|
public bool HumanMotion { get; set; }
|
||||||
|
public List<string> Events { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StateHit
|
||||||
|
{
|
||||||
|
public int Layer { get; set; }
|
||||||
|
public string LayerName { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int ShortHash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CurrentState
|
||||||
|
{
|
||||||
|
public int Layer { get; set; }
|
||||||
|
public int ShortHash { get; set; }
|
||||||
|
public int FullHash { get; set; }
|
||||||
|
public float NormalizedTime { get; set; }
|
||||||
|
public string Clips { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WatchEntry
|
||||||
|
{
|
||||||
|
public int Layer { get; set; }
|
||||||
|
public string LayerName { get; set; }
|
||||||
|
public int ShortHash { get; set; }
|
||||||
|
public int FullHash { get; set; }
|
||||||
|
public string Resolved { get; set; }
|
||||||
|
public string Clips { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RaceInfo
|
public class RaceInfo
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblyName>RigProbe</AssemblyName>
|
<AssemblyName>RigProbe</AssemblyName>
|
||||||
<RootNamespace>RigProbe</RootNamespace>
|
<RootNamespace>RigProbe</RootNamespace>
|
||||||
<Version>0.3.0</Version>
|
<Version>0.4.0</Version>
|
||||||
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"id": "RigProbe",
|
"id": "RigProbe",
|
||||||
"name": "RigProbe",
|
"name": "RigProbe",
|
||||||
"author": "JustinOros",
|
"author": "JustinOros",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "Dumps rig, mesh, material and animator data for every Sons of the Forest character to JSON for mod development",
|
"description": "Dumps rig, mesh, material and animator data for every Sons of the Forest character to JSON for mod development",
|
||||||
"gameVersion": "1.0.0",
|
"gameVersion": "1.0.0",
|
||||||
"platform": "Client",
|
"platform": "Client",
|
||||||
|
|||||||
Reference in new issue
Block a user