Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e832898636 | ||
|
|
fd8f0c9dac |
+132
@@ -0,0 +1,132 @@
|
|||||||
|
# RigProbe findings
|
||||||
|
|
||||||
|
Captured with RigProbe v0.3.0 on 2026-09-24.
|
||||||
|
|
||||||
|
## Player
|
||||||
|
|
||||||
|
- Animation root: `LocalPlayer/PlayerAnimator/Root`, skeleton starts at `Hips`
|
||||||
|
- Animator is Generic, not Humanoid (`PlayerAControllerGenerated`, 12 layers, 271 parameters). No character in the game uses a Humanoid avatar, so Unity retargeting is not available and body swaps need a manual bone map.
|
||||||
|
- The visible body is head + arms (race system) + clothing (clothing system). There is no base body mesh.
|
||||||
|
- Head has 2 expression blendshapes: 0 Angry, 1 Scared. `ExpressionBlendsStartIndex` is 0.
|
||||||
|
- `SkinnedMeshBoneRemapCache` (Endnight.Animation) retargets meshes onto the player rig by bone path.
|
||||||
|
|
||||||
|
## Races
|
||||||
|
|
||||||
|
`PlayerRaceSystem._races` holds 8 entries, applied by list index. Entry 6 (asset `BlackBRace`) has its race field set to `BlackA`, but it has its own head and works correctly in game.
|
||||||
|
|
||||||
|
| Index | Race | Asset | Head GUID | Arms GUID |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 0 | White | WhiteRace | c6b5d71936b7773418409c28fbedc0a4 | 3886900e0d13c3d4f97a4795c8d0f0bb |
|
||||||
|
| 1 | Black | BlackRace | 091aa8cb2ce206441b1ff4d1a3c02ab9 | 7b273beb092763143bab57f876dce75d |
|
||||||
|
| 2 | Latin | LatinRace | c60e9927531d33745b61950677621768 | dca5d9c41251d324e9af4c916f91a049 |
|
||||||
|
| 3 | Asian | AsianRace | a90a307a8c0607c40b68d376074715df | 3ce89e7484ac88244b1df4b6ae8a4c6f |
|
||||||
|
| 4 | BlackA | BlackARace | ff992ec27e7740142a305730e65ec46a | 7b273beb092763143bab57f876dce75d |
|
||||||
|
| 5 | WhiteA | WhiteARace | c67c50b0bbf49e244bb0d2ac839d9b69 | 3886900e0d13c3d4f97a4795c8d0f0bb |
|
||||||
|
| 6 | BlackB | BlackBRace | 2a66a9d5f2b53cc4185d37f4b8a59b37 | b1e878151ef75294687db0e618ec4844 |
|
||||||
|
| 7 | LatinA | LatinARace | 65c86538006018140835657ca528e0c1 | b1cd63fc76d99f94694fe1fc73e67f9a |
|
||||||
|
|
||||||
|
## Clothing
|
||||||
|
|
||||||
|
Defaults are TacticalJacket, TacticalPants, TacticalBoots and Backpack.
|
||||||
|
|
||||||
|
| Item | Asset | Slots |
|
||||||
|
|---|---|---|
|
||||||
|
| 402 | Backpack | Back |
|
||||||
|
| 444 | TacticalRebreather | Back |
|
||||||
|
| 487 | SilkPyjamas | Torso, Legs, Feet, Overcoat |
|
||||||
|
| 489 | TacticalPants | Legs |
|
||||||
|
| 490 | Hoodie | Torso, Overcoat |
|
||||||
|
| 491 | OldJacket | Torso, Overcoat |
|
||||||
|
| 492 | Tuxedo | Torso, Legs, Feet, Overcoat |
|
||||||
|
| 493 | LeatherJacket | Torso, Overcoat |
|
||||||
|
| 495 | TacticalJacket | Torso, Overcoat |
|
||||||
|
| 499 | Wetsuit | Torso, Legs, Feet, Overcoat |
|
||||||
|
| 500 | PuffyJacket | Overcoat |
|
||||||
|
| 501 | TacticalBoots | Feet |
|
||||||
|
| 572 | GoldenArmour | Head, Torso, Legs, Hands, Feet, Overcoat |
|
||||||
|
| 639 | SpaceSuit | Torso, Legs, Feet, Overcoat |
|
||||||
|
| 703 | PriestOutfit | Torso, Legs, Feet, Overcoat |
|
||||||
|
| 749 | FlightAttendantUniform | Torso, Legs, Feet, Overcoat |
|
||||||
|
|
||||||
|
## Rig families
|
||||||
|
|
||||||
|
Bone name overlap is against the player rig.
|
||||||
|
|
||||||
|
| Family | Members | Bones | Player match |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3485f157 | Player | 205 | 100% |
|
||||||
|
| a240987b | Player cutscene rigs | 122 | 96% |
|
||||||
|
| 09b012a0 | TacticalSoldier A/B/D, BossMutantChopper | 125 | 93% |
|
||||||
|
| 02a67127 | Robby cutscene | 132 | 88% |
|
||||||
|
| 1b02206c | Robby (Kelvin) | 93 | 86% |
|
||||||
|
| db3f3ed0 | WorkerDude posers | 94 | 86% |
|
||||||
|
| c59ddba4 | GoldArmourSkeletonPosed | 60 | 75% |
|
||||||
|
| 007bf83f | TimmysDad | 141 | 70% |
|
||||||
|
| d31a5f4b | TacticoolCaucasianPoserRig | 158 | 65% |
|
||||||
|
| 18772f39 | Male cannibals, male carry body | 91 | 0% |
|
||||||
|
| e5ab1cff | Female cannibals (Angel, Brandy, Crystal, Destiny, Elise, MuddyFemale), female carry body | 67 | 0% |
|
||||||
|
| fe742020 | DeadMaleA/B/C, DeadFemaleC | 89 | 0% |
|
||||||
|
| 19b9a8ac | DeadFemaleB (bar, pool, gym) | 87 | 0% |
|
||||||
|
| fcd908ad | HeavyMale, FacelessMale | 86 | 0% |
|
||||||
|
| e53f4f23 | FatMale, FatFemale | 90 | 0% |
|
||||||
|
| f8c8bc7e | Mr/Miss Puffy, Puffton bosses, puffy carry bodies | 63 | 0% |
|
||||||
|
| f7fc845b | Virginia | 161 | 0% |
|
||||||
|
|
||||||
|
## Rig naming
|
||||||
|
|
||||||
|
Player style: `Hips`, `Spine`, `Spine1`, `Spine2`, `Neck`, `Neck1`, `Head`, `LeftShoulder`, `LeftArm`, `LeftForeArm`, `LeftHand`, `LeftHandIndex1`, `LeftUpLeg`, `LeftLeg`, `LeftFoot`, `LeftToeBase`.
|
||||||
|
|
||||||
|
`C1:` style (cannibals, dead civilians, puffies): `C1:c_hip_SC`, `C1:C_spine0_SC`, `C1:C_neck0_SC`, `C1:C_head0_SC`, `C1:L_armPrnt0_SC`, `C1:L_arm0_SC`, `C1:L_arm1_SC`, `C1:L_hand00_SC`, `C1:l_handAFingerA0_SC`, `C1:L_leg0_SC`, `C1:L_leg1_SC`, `C1:L_foot00_SC`. Civilian dead bodies add twist and helper joints (`_JNT`).
|
||||||
|
|
||||||
|
Virginia uses a separate `C1:` rig with an `A` suffix (`C1:C_spineAHip_SC`, `C1:R_armA0_SC`) and face bones.
|
||||||
|
|
||||||
|
## Female assets
|
||||||
|
|
||||||
|
- `LocalPlayer/CarryBody/FemaleCannibal` exists on every player, vanilla included. Main mesh `C1:Female_lp`: 14209 verts, 67 bones, blendshapes 0 scared and 1 angry.
|
||||||
|
- Civilian women exist only as scene posers: `DeadFemaleBodyB/C` with separate `DeadFemaleHeadB/C`, outfits for pool, gym and bar, and `RichWomanCostume`.
|
||||||
|
- Virginia: `C1:Virginia_body`, 129 bones, 20 blendshapes, outfits camosuit, dress, leather suit, tracksuit, tutu.
|
||||||
|
|
||||||
|
## Addressables catalog
|
||||||
|
|
||||||
|
Decoded from `catalog.json` into `data/assets.tsv` (5523 keys, 2917 entries). Race and clothing GUIDs resolve to prefabs, for example `65c86538006018140835657ca528e0c1` is `Assets/Sons/Characters/PlayerA/Variations/LatinA/LatinAHead.prefab` and `c84bc6d4586c5f04b9eabc8d26ad54fe` is `Assets/Sons/Wearables/Clothing/TacticalJacket.prefab`.
|
||||||
|
|
||||||
|
- Loadable from anywhere: all player race heads and arms (including extra variants like `WhiteHeadBalaclava`, `LatinArmsFull`, `AsianHeadNoHair`), every clothing piece, Kelvin's full wardrobe (`Assets/Sons/Characters/Robby/Robby*Addressable.prefab`), Timmy and TimmysDad parts, `FatFemaleCannibalSkin`, `FingersSkinAddressable`, `TwinsSkinAddressable`, `PlayerA/Poses/CultRobe*`, `playerARenderRig.fbx`, `VirginiaRig.fbx`.
|
||||||
|
- Not addressable: the dead civilian women (`DeadFemaleBody/Head`) and the female cannibal body meshes. They only exist when their scene or actor is loaded. The female carry body on the player is the reliable source.
|
||||||
|
- In-game `Locate` with a wrapped string key returns nothing and walking the main catalog crashes the game, so use the offline decode.
|
||||||
|
|
||||||
|
## Emotes
|
||||||
|
|
||||||
|
The player has no emote system. `IPlayerState` has no emote field, but the Bolt event `updateMecanimRemoteState` (layer, state hash, normalizedTime) tells other clients to play a state on a remote player, so any state that already exists in the player controller can be shown to vanilla clients.
|
||||||
|
|
||||||
|
Player controller `PlayerAControllerGenerated`: Generic avatar, 12 layers (Base Layer, AdditiveArms, upperBody, UpperBodyReactions, fullBodyActions, leftArm, spineAddititve2, ClientPredictionLayer, Inventory, InventoryUpperBody, Face, Blink), 1205 clips, 271 parameters. Pose and gesture states that exist on the player:
|
||||||
|
|
||||||
|
| State | Layer |
|
||||||
|
|---|---|
|
||||||
|
| couchIdle, seatedRockIdle | fullBodyActions |
|
||||||
|
| injuredLoop, standUpIdle, standup, wakeUp | fullBodyActions |
|
||||||
|
| checkArms, coldIdleVar1 | upperBody |
|
||||||
|
| PlayerALighterLookAt | Base Layer |
|
||||||
|
| PlayerAGuitar clips | upperBody |
|
||||||
|
| PlayerAAngry, PlayerAScared | Face |
|
||||||
|
|
||||||
|
Kelvin (`RobbyController`, 86% bone match with the player) plays his emotes as full body states on Base Layer. Captured with `rigwatch robby`:
|
||||||
|
|
||||||
|
| State | Clip | Short hash | Full hash |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ThumbsUp | ThumbsUp | 1712013045 | -1646602863 |
|
||||||
|
| ShakeHead | ShakeHead | -1770187688 | 27107153 |
|
||||||
|
| (unnamed) | RobbyEmoteYes | 1721109824 | 1053072325 |
|
||||||
|
| IdleScratchHead | IdleScratchHead | -628397148 | 1736991184 |
|
||||||
|
| IdleSwatFly | IdleSwatFly | -331262521 | 476507385 |
|
||||||
|
| IdleLookVar1 | IdleLookVar1 | 336950523 | -712749335 |
|
||||||
|
|
||||||
|
Other Kelvin emote clips: NodHead, RobbyEmoteHappy, RobbyEmoteSad, RobbyEmoteNo, RobbyEmoteConfused, RobbyPointIntro, RobbyPointLoop, RobbyPointOutro, HitHeadSmall, HitHeadBig, ShakeHeadStepBack. Virginia (`VirginiaController`): LaughWithPlayer, ThumbsUpReaction, PointStanding, VirginiaEmoteHappy, VirginiaEmoteSad, VirginiaEmoteYes, VirginiaEmoteNo, VirginiaEmoteConfused.
|
||||||
|
|
||||||
|
Kelvin and Virginia clips are not in the player controller, so playing them on a player (for example with a PlayableGraph clip overriding the player Animator) is only visible to modded clients.
|
||||||
|
|
||||||
|
## Data layout
|
||||||
|
|
||||||
|
- `characters/` full `rigprobe` output: player, carry bodies, every actor prefab, live actors
|
||||||
|
- `scene/index.tsv` and `scene/rigfamilies.txt` from `rigscene`
|
||||||
|
- `scene/samples/` one JSON per scene rig family
|
||||||
|
- `assets.tsv` decoded Addressables catalog
|
||||||
@@ -25,6 +25,10 @@ Run these in the in-game console.
|
|||||||
| `rigwatch [player\|robby\|virginia]` | Log every animator state change on the target while you play |
|
| `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 |
|
| `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` |
|
||||||
|
| `rigplay <layer> <state\|hash> [fade]` | CrossFade the local player into an animator state, fade 0 uses Play, e.g. `rigplay fullBodyActions couchIdle` |
|
||||||
|
| `rigparam <name> [value]` | Set a local player animator parameter, e.g. `rigparam couchBool 1` |
|
||||||
|
| `rigweight <layer> <weight>` | Set a local player animator layer weight |
|
||||||
|
| `rignet` | Hook the `updateMecanimRemoteState` Bolt event, its receivers and any method with RemoteState or Mecanim in its name, and log every call |
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
@@ -32,7 +36,8 @@ 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
|
- `characters\clips.tsv` every loaded animation clip, `characters\controllers.tsv` every loaded animator controller and who uses it
|
||||||
- `watch\<target>_<time>.log` and `_states.tsv` from `rigwatch`
|
- `watch\<target>_<time>.log` and `_states.tsv` from `rigwatch`. The log has a kind column: `state`, `weight` (layer weight changes), `param` (bool, int and trigger changes) and `net` (`rignet` lines while both run)
|
||||||
|
- `net\net_<time>.log` from `rignet`
|
||||||
- `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`
|
||||||
|
|||||||
+536
-21
@@ -1,10 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using HarmonyLib;
|
||||||
using RedLoader;
|
using RedLoader;
|
||||||
using RedLoader.Utils;
|
using RedLoader.Utils;
|
||||||
using Sons.Ai.Vail;
|
using Sons.Ai.Vail;
|
||||||
@@ -27,9 +30,13 @@ public class RigProbe : SonsMod
|
|||||||
{
|
{
|
||||||
"HappyThumbsUp", "ThumbsUp", "Nod", "HappyFistPump", "FistPump", "Confused", "HitHeadSmall", "HitHeadBig",
|
"HappyThumbsUp", "ThumbsUp", "Nod", "HappyFistPump", "FistPump", "Confused", "HitHeadSmall", "HitHeadBig",
|
||||||
"ShakeHead", "NoHandUp", "Sad", "Happy", "Laugh", "WagFingerNo", "SkunkReact", "OnPlayerNod",
|
"ShakeHead", "NoHandUp", "Sad", "Happy", "Laugh", "WagFingerNo", "SkunkReact", "OnPlayerNod",
|
||||||
"OnPlayerCrash", "OnPlayerSmallHit", "Wave", "Point", "Cheer", "Dance", "Salute", "Clap", "Shrug"
|
"OnPlayerCrash", "OnPlayerSmallHit", "Wave", "Point", "Cheer", "Dance", "Salute", "Clap", "Shrug",
|
||||||
|
"NodHead", "IdleScratchHead", "IdleSwatFly", "IdleLookVar1", "couchIdle", "seatedRockIdle", "injuredLoop",
|
||||||
|
"standUpIdle", "standup", "wakeUp", "checkArms", "coldIdleVar1", "PlayerALighterLookAt", "PlayerAAngry", "PlayerAScared"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private const BindingFlags AllDeclared = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
private static Animator _watchAnimator;
|
private static Animator _watchAnimator;
|
||||||
private static string _watchTarget;
|
private static string _watchTarget;
|
||||||
private static int[] _watchLast;
|
private static int[] _watchLast;
|
||||||
@@ -37,6 +44,17 @@ public class RigProbe : SonsMod
|
|||||||
private static Dictionary<int, string> _watchNames;
|
private static Dictionary<int, string> _watchNames;
|
||||||
private static readonly Dictionary<string, WatchEntry> WatchSeen = new();
|
private static readonly Dictionary<string, WatchEntry> WatchSeen = new();
|
||||||
private static string _watchStatesPath;
|
private static string _watchStatesPath;
|
||||||
|
private static float[] _watchWeights;
|
||||||
|
private static List<(string Name, int Hash, AnimatorControllerParameterType Type)> _watchParams;
|
||||||
|
private static int[] _watchParamLast;
|
||||||
|
|
||||||
|
private static bool _netHooked;
|
||||||
|
private static StreamWriter _netLog;
|
||||||
|
private static Type _netEventType;
|
||||||
|
private static Type _il2cppStackType;
|
||||||
|
private static HarmonyLib.Harmony _harmony;
|
||||||
|
private static Dictionary<int, string> _netNames = new();
|
||||||
|
private static readonly Dictionary<string, int> NetCallCounts = new();
|
||||||
|
|
||||||
public RigProbe()
|
public RigProbe()
|
||||||
{
|
{
|
||||||
@@ -45,7 +63,7 @@ public class RigProbe : SonsMod
|
|||||||
|
|
||||||
protected override void OnSdkInitialized()
|
protected override void OnSdkInitialized()
|
||||||
{
|
{
|
||||||
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigwatch [player|robby|virginia|off], rigspawn <TypeName> [variation]");
|
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigwatch [player|robby|virginia|off], rigspawn <TypeName> [variation], rigplay <layer> <state|hash> [fade], rigparam <name> [value], rigweight <layer> <weight>, rignet");
|
||||||
}
|
}
|
||||||
|
|
||||||
[DebugCommand("rigprobe")]
|
[DebugCommand("rigprobe")]
|
||||||
@@ -117,6 +135,166 @@ public class RigProbe : SonsMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigplay")]
|
||||||
|
private static void PlayCommand(string args)
|
||||||
|
{
|
||||||
|
var parts = SplitArgs(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
Say("Usage: rigplay <layer> <state|hash> [fade] e.g. rigplay fullBodyActions couchIdle 0.25");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var a = PlayerAnimator();
|
||||||
|
if (!a)
|
||||||
|
{
|
||||||
|
Say("Load into a game first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = ResolveLayer(a, parts[0]);
|
||||||
|
if (layer < 0)
|
||||||
|
{
|
||||||
|
Say($"rigplay: no layer {parts[0]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ln = a.GetLayerName(layer);
|
||||||
|
if (!int.TryParse(parts[1], out var hash))
|
||||||
|
{
|
||||||
|
var sh = Animator.StringToHash(parts[1]);
|
||||||
|
var fh = Animator.StringToHash($"{ln}.{parts[1]}");
|
||||||
|
hash = SafeHasState(a, layer, sh) ? sh : SafeHasState(a, layer, fh) ? fh : 0;
|
||||||
|
}
|
||||||
|
if (hash == 0 || !SafeHasState(a, layer, hash))
|
||||||
|
{
|
||||||
|
Say($"rigplay: {ln} has no state {parts[1]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fade = parts.Length > 2 && float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var f) ? f : 0.25f;
|
||||||
|
if (fade <= 0f)
|
||||||
|
a.Play(hash, layer, 0f);
|
||||||
|
else
|
||||||
|
a.CrossFadeInFixedTime(hash, fade, layer);
|
||||||
|
|
||||||
|
Say($"rigplay: {ln} ({layer}) hash {hash} fade {fade:F2}, layer weight {a.GetLayerWeight(layer):F2}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigplay failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigparam")]
|
||||||
|
private static void ParamCommand(string args)
|
||||||
|
{
|
||||||
|
var parts = SplitArgs(args);
|
||||||
|
if (parts.Length < 1)
|
||||||
|
{
|
||||||
|
Say("Usage: rigparam <name> [value] e.g. rigparam couchBool 1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var a = PlayerAnimator();
|
||||||
|
if (!a)
|
||||||
|
{
|
||||||
|
Say("Load into a game first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatorControllerParameter found = null;
|
||||||
|
foreach (var p in a.parameters)
|
||||||
|
{
|
||||||
|
if (string.Equals(p.name, parts[0], StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
found = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found is null)
|
||||||
|
{
|
||||||
|
Say($"rigparam: no parameter {parts[0]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v = parts.Length > 1 ? parts[1] : "1";
|
||||||
|
switch (found.type)
|
||||||
|
{
|
||||||
|
case AnimatorControllerParameterType.Trigger:
|
||||||
|
a.SetTrigger(found.nameHash);
|
||||||
|
break;
|
||||||
|
case AnimatorControllerParameterType.Bool:
|
||||||
|
a.SetBool(found.nameHash, v == "1" || v.Equals("true", StringComparison.OrdinalIgnoreCase));
|
||||||
|
break;
|
||||||
|
case AnimatorControllerParameterType.Int:
|
||||||
|
a.SetInteger(found.nameHash, int.Parse(v, CultureInfo.InvariantCulture));
|
||||||
|
break;
|
||||||
|
case AnimatorControllerParameterType.Float:
|
||||||
|
a.SetFloat(found.nameHash, float.Parse(v, CultureInfo.InvariantCulture));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Say($"rigparam: {found.name} ({found.type}) = {ParamValue(a, found.nameHash, found.type)}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigparam failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rigweight")]
|
||||||
|
private static void WeightCommand(string args)
|
||||||
|
{
|
||||||
|
var parts = SplitArgs(args);
|
||||||
|
if (parts.Length < 2 || !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var w))
|
||||||
|
{
|
||||||
|
Say("Usage: rigweight <layer> <weight> e.g. rigweight fullBodyActions 1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var a = PlayerAnimator();
|
||||||
|
if (!a)
|
||||||
|
{
|
||||||
|
Say("Load into a game first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var layer = ResolveLayer(a, parts[0]);
|
||||||
|
if (layer < 0)
|
||||||
|
{
|
||||||
|
Say($"rigweight: no layer {parts[0]}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.SetLayerWeight(layer, w);
|
||||||
|
Say($"rigweight: {a.GetLayerName(layer)} ({layer}) = {a.GetLayerWeight(layer):F2}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rigweight failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebugCommand("rignet")]
|
||||||
|
private static void NetCommand(string args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HookNet();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
RLog.Error($"rignet failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void RunProbe(string filter, bool sceneMode)
|
private static void RunProbe(string filter, bool sceneMode)
|
||||||
{
|
{
|
||||||
var player = LocalPlayer.GameObject;
|
var player = LocalPlayer.GameObject;
|
||||||
@@ -678,31 +856,23 @@ public class RigProbe : SonsMod
|
|||||||
_watchAnimator = best;
|
_watchAnimator = best;
|
||||||
_watchTarget = target;
|
_watchTarget = target;
|
||||||
_watchLast = Enumerable.Repeat(int.MinValue, best.layerCount).ToArray();
|
_watchLast = Enumerable.Repeat(int.MinValue, best.layerCount).ToArray();
|
||||||
|
_watchWeights = Enumerable.Repeat(-1f, best.layerCount).ToArray();
|
||||||
|
_watchParams = new();
|
||||||
|
foreach (var p in best.parameters)
|
||||||
|
{
|
||||||
|
if (p.type != AnimatorControllerParameterType.Float)
|
||||||
|
_watchParams.Add((p.name, p.nameHash, p.type));
|
||||||
|
}
|
||||||
|
_watchParamLast = Enumerable.Repeat(int.MinValue, _watchParams.Count).ToArray();
|
||||||
WatchSeen.Clear();
|
WatchSeen.Clear();
|
||||||
|
_watchNames = BuildNames(best);
|
||||||
_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");
|
var dir = Path.Combine(LoaderEnvironment.UserDataDirectory, "RigProbe", "watch");
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
_watchLog = new StreamWriter(Path.Combine(dir, $"{target}_{stamp}.log")) { AutoFlush = true };
|
_watchLog = new StreamWriter(Path.Combine(dir, $"{target}_{stamp}.log")) { AutoFlush = true };
|
||||||
_watchStatesPath = Path.Combine(dir, $"{target}_{stamp}_states.tsv");
|
_watchStatesPath = Path.Combine(dir, $"{target}_{stamp}_states.tsv");
|
||||||
_watchLog.WriteLine("time\tlayer\tlayerName\tweight\tshortHash\tfullHash\tresolved\tclips");
|
_watchLog.WriteLine("time\tkind\tlayer\tlayerName\tweight\tshortHash\tfullHash\tresolved\tclips");
|
||||||
Say($"rigwatch: watching {target} animator {best.name} ({best.layerCount} layers). rigwatch off to stop.");
|
Say($"rigwatch: watching {target} animator {best.name} ({best.layerCount} layers). rigwatch off to stop.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,6 +913,13 @@ public class RigProbe : SonsMod
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < _watchLast.Length; i++)
|
for (int i = 0; i < _watchLast.Length; i++)
|
||||||
{
|
{
|
||||||
|
var w = a.GetLayerWeight(i);
|
||||||
|
if (Mathf.Abs(w - _watchWeights[i]) >= 0.25f || (w != _watchWeights[i] && (w == 0f || w == 1f)))
|
||||||
|
{
|
||||||
|
_watchWeights[i] = w;
|
||||||
|
_watchLog.WriteLine($"{Time.time:F2}\tweight\t{i}\t{a.GetLayerName(i)}\t{R(w)}");
|
||||||
|
}
|
||||||
|
|
||||||
var st = a.GetCurrentAnimatorStateInfo(i);
|
var st = a.GetCurrentAnimatorStateInfo(i);
|
||||||
if (st.fullPathHash == _watchLast[i])
|
if (st.fullPathHash == _watchLast[i])
|
||||||
continue;
|
continue;
|
||||||
@@ -760,7 +937,7 @@ public class RigProbe : SonsMod
|
|||||||
var resolved = _watchNames.TryGetValue(st.fullPathHash, out var fn) ? fn
|
var resolved = _watchNames.TryGetValue(st.fullPathHash, out var fn) ? fn
|
||||||
: _watchNames.TryGetValue(st.shortNameHash, out var sn) ? sn : string.Empty;
|
: _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}");
|
_watchLog.WriteLine($"{Time.time:F2}\tstate\t{i}\t{layerName}\t{R(a.GetLayerWeight(i))}\t{st.shortNameHash}\t{st.fullPathHash}\t{resolved}\t{clipText}");
|
||||||
|
|
||||||
var key = $"{i}:{st.fullPathHash}";
|
var key = $"{i}:{st.fullPathHash}";
|
||||||
if (!WatchSeen.TryGetValue(key, out var entry))
|
if (!WatchSeen.TryGetValue(key, out var entry))
|
||||||
@@ -778,6 +955,18 @@ public class RigProbe : SonsMod
|
|||||||
}
|
}
|
||||||
entry.Count++;
|
entry.Count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _watchParams.Count; i++)
|
||||||
|
{
|
||||||
|
var p = _watchParams[i];
|
||||||
|
var v = p.Type == AnimatorControllerParameterType.Int ? a.GetInteger(p.Hash) : a.GetBool(p.Hash) ? 1 : 0;
|
||||||
|
if (v == _watchParamLast[i])
|
||||||
|
continue;
|
||||||
|
var first = _watchParamLast[i] == int.MinValue;
|
||||||
|
_watchParamLast[i] = v;
|
||||||
|
if (!first)
|
||||||
|
_watchLog.WriteLine($"{Time.time:F2}\tparam\t{p.Name}\t{p.Type}\t{v}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -1032,6 +1221,332 @@ public class RigProbe : SonsMod
|
|||||||
return string.Join("/", parts);
|
return string.Join("/", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void HookNet()
|
||||||
|
{
|
||||||
|
if (_netHooked)
|
||||||
|
{
|
||||||
|
Say("rignet: already hooked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir = Path.Combine(LoaderEnvironment.UserDataDirectory, "RigProbe", "net");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var path = Path.Combine(dir, $"net_{DateTime.Now:yyyyMMdd_HHmmss}.log");
|
||||||
|
_netLog = new StreamWriter(path) { AutoFlush = true };
|
||||||
|
var pa = PlayerAnimator();
|
||||||
|
if (pa)
|
||||||
|
_netNames = BuildNames(pa);
|
||||||
|
|
||||||
|
var types = GameTypes();
|
||||||
|
foreach (var t in types.Where(t => t.Name.IndexOf("mecanim", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
|
NetLine($"type\t{t.Assembly.GetName().Name}\t{t.FullName}\t{t.BaseType?.FullName}");
|
||||||
|
|
||||||
|
_netEventType = types.FirstOrDefault(t => t.Name == "updateMecanimRemoteState");
|
||||||
|
if (_netEventType == null)
|
||||||
|
{
|
||||||
|
Say($"rignet: updateMecanimRemoteState not found, see {path}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_harmony ??= new HarmonyLib.Harmony("RigProbe.Net");
|
||||||
|
var patched = 0;
|
||||||
|
|
||||||
|
foreach (var p in _netEventType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
NetLine($"prop\t{p.Name}\t{p.PropertyType.FullName}");
|
||||||
|
var set = p.GetSetMethod();
|
||||||
|
if (set != null && NetPatch(set, null, nameof(NetOnSet)))
|
||||||
|
patched++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var m in _netEventType.GetMethods(AllDeclared))
|
||||||
|
{
|
||||||
|
if (m.IsSpecialName)
|
||||||
|
continue;
|
||||||
|
NetLine($"method\t{DescribeMethod(m)}");
|
||||||
|
if ((m.Name == "Create" || m.Name == "Send") && NetPatch(m, null, nameof(NetOnCreateOrSend)))
|
||||||
|
patched++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var named = 0;
|
||||||
|
foreach (var t in types)
|
||||||
|
{
|
||||||
|
if (t == _netEventType || t.IsInterface)
|
||||||
|
continue;
|
||||||
|
MethodInfo[] ms;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ms = t.GetMethods(AllDeclared);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var m in ms)
|
||||||
|
{
|
||||||
|
bool takesEvent;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
takesEvent = m.GetParameters().Any(p => p.ParameterType == _netEventType);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNamed = !m.IsSpecialName && (m.Name.IndexOf("RemoteState", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| m.Name.IndexOf("Mecanim", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||||
|
if (!takesEvent && !isNamed)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
NetLine($"{(takesEvent ? "receiver" : "named")}\t{t.FullName}\t{DescribeMethod(m)}");
|
||||||
|
if (!takesEvent && ++named > 60)
|
||||||
|
continue;
|
||||||
|
if (NetPatch(m, nameof(NetOnCall), null))
|
||||||
|
patched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_netHooked = true;
|
||||||
|
Say($"rignet: {patched} hooks installed, log {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool NetPatch(MethodInfo m, string prefix, string postfix)
|
||||||
|
{
|
||||||
|
if (m.IsAbstract || m.ContainsGenericParameters)
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_harmony.Patch(m,
|
||||||
|
prefix: prefix == null ? null : new HarmonyMethod(typeof(RigProbe).GetMethod(prefix, BindingFlags.NonPublic | BindingFlags.Static)),
|
||||||
|
postfix: postfix == null ? null : new HarmonyMethod(typeof(RigProbe).GetMethod(postfix, BindingFlags.NonPublic | BindingFlags.Static)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
NetLine($"patchfail\t{m.DeclaringType?.FullName}.{m.Name}\t{e.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NetOnSet(MethodBase __originalMethod, object[] __args)
|
||||||
|
{
|
||||||
|
var v = __args != null && __args.Length > 0 ? __args[0] : null;
|
||||||
|
var extra = v is int h && _netNames.TryGetValue(h, out var n) ? $"\t{n}" : string.Empty;
|
||||||
|
NetLine($"set\t{__originalMethod.Name.Substring(4)}\t{Fmt(v)}{extra}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NetOnCreateOrSend(MethodBase __originalMethod, object[] __args)
|
||||||
|
{
|
||||||
|
NetLine($"{__originalMethod.Name}\t{string.Join(", ", (__args ?? Array.Empty<object>()).Select(Fmt))}");
|
||||||
|
NetLine($"stack\t{Il2CppStack()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NetOnCall(MethodBase __originalMethod, object[] __args)
|
||||||
|
{
|
||||||
|
var key = $"{__originalMethod.DeclaringType?.FullName}.{__originalMethod.Name}";
|
||||||
|
NetCallCounts.TryGetValue(key, out var n);
|
||||||
|
n++;
|
||||||
|
NetCallCounts[key] = n;
|
||||||
|
if (n > 20 && n % 200 != 0)
|
||||||
|
return;
|
||||||
|
var parts = (__args ?? Array.Empty<object>()).Select(x => x != null && _netEventType != null && _netEventType.IsInstanceOfType(x) ? DumpNetEvent(x) : Fmt(x));
|
||||||
|
NetLine($"call#{n}\t{key}\t{string.Join(", ", parts)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DumpNetEvent(object e)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder("{");
|
||||||
|
for (var t = e.GetType(); t != null && t != typeof(object) && t.FullName != "Il2CppSystem.Object" && t.Namespace?.StartsWith("Il2CppInterop") != true; t = t.BaseType)
|
||||||
|
{
|
||||||
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
if (p.GetIndexParameters().Length > 0 || p.GetGetMethod() == null)
|
||||||
|
continue;
|
||||||
|
object v;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
v = p.GetValue(e);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
v = "?";
|
||||||
|
}
|
||||||
|
var extra = v is int h && _netNames.TryGetValue(h, out var n) ? $"({n})" : string.Empty;
|
||||||
|
sb.Append($"{p.Name}={Fmt(v)}{extra} ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.Append('}').ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Il2CppStack()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_il2cppStackType ??= AppDomain.CurrentDomain.GetAssemblies().Select(x => x.GetType("Il2CppSystem.Diagnostics.StackTrace")).FirstOrDefault(x => x != null);
|
||||||
|
if (_il2cppStackType == null)
|
||||||
|
return "unavailable";
|
||||||
|
var s = Activator.CreateInstance(_il2cppStackType)?.ToString() ?? string.Empty;
|
||||||
|
return string.Join(" | ", s.Split('\n').Select(x => x.Trim()).Where(x => x.Length > 0).Take(15));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return "unavailable: " + e.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Type> GameTypes()
|
||||||
|
{
|
||||||
|
var list = new List<Type>();
|
||||||
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
string loc;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
loc = asm.Location;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(loc) || loc.Replace('/', '\\').IndexOf(@"_RedLoader\Game", StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
continue;
|
||||||
|
var name = asm.GetName().Name ?? string.Empty;
|
||||||
|
if (name.StartsWith("Il2Cppmscorlib") || name.StartsWith("Il2CppSystem") || name.StartsWith("UnityEngine") || name.StartsWith("Unity."))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Type[] ts;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ts = asm.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException e)
|
||||||
|
{
|
||||||
|
ts = e.Types.Where(x => x != null).ToArray();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list.AddRange(ts);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NetLine(string line)
|
||||||
|
{
|
||||||
|
var t = Time.time.ToString("F2", CultureInfo.InvariantCulture);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_netLog?.WriteLine($"{t}\t{line}");
|
||||||
|
_watchLog?.WriteLine($"{t}\tnet\t{line}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<int, string> BuildNames(Animator a)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<int, string>();
|
||||||
|
var layers = Enumerable.Range(0, a.layerCount).Select(a.GetLayerName).ToList();
|
||||||
|
var names = new List<string>(EmoteNames);
|
||||||
|
foreach (var clip in a.runtimeAnimatorController.animationClips)
|
||||||
|
{
|
||||||
|
if (clip)
|
||||||
|
names.Add(clip.name);
|
||||||
|
}
|
||||||
|
foreach (var n in names)
|
||||||
|
{
|
||||||
|
map[Animator.StringToHash(n)] = n;
|
||||||
|
foreach (var ln in layers)
|
||||||
|
map[Animator.StringToHash($"{ln}.{n}")] = $"{ln}.{n}";
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Animator PlayerAnimator()
|
||||||
|
{
|
||||||
|
var go = LocalPlayer.GameObject;
|
||||||
|
if (!go)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var t = go.transform.Find("PlayerAnimator");
|
||||||
|
if (t)
|
||||||
|
{
|
||||||
|
var pa = t.GetComponent<Animator>();
|
||||||
|
if (pa && pa.isInitialized)
|
||||||
|
return pa;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResolveLayer(Animator a, string s)
|
||||||
|
{
|
||||||
|
if (int.TryParse(s, out var i))
|
||||||
|
return i >= 0 && i < a.layerCount ? i : -1;
|
||||||
|
for (int l = 0; l < a.layerCount; l++)
|
||||||
|
{
|
||||||
|
if (string.Equals(a.GetLayerName(l), s, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SafeHasState(Animator a, int layer, int hash)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return a.HasState(layer, hash);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParamValue(Animator a, int hash, AnimatorControllerParameterType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
AnimatorControllerParameterType.Float => a.GetFloat(hash).ToString("F3", CultureInfo.InvariantCulture),
|
||||||
|
AnimatorControllerParameterType.Int => a.GetInteger(hash).ToString(CultureInfo.InvariantCulture),
|
||||||
|
_ => a.GetBool(hash) ? "1" : "0"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DescribeMethod(MethodInfo m)
|
||||||
|
{
|
||||||
|
var ps = string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"));
|
||||||
|
return $"{(m.IsStatic ? "static " : string.Empty)}{m.ReturnType.Name} {m.Name}({ps})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Fmt(object o)
|
||||||
|
{
|
||||||
|
return o switch
|
||||||
|
{
|
||||||
|
null => "null",
|
||||||
|
float f => f.ToString("F3", CultureInfo.InvariantCulture),
|
||||||
|
UnityEngine.Object u => u ? $"{u.GetType().Name}:{u.name}" : "destroyed",
|
||||||
|
_ => o.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] SplitArgs(string args)
|
||||||
|
{
|
||||||
|
return (args ?? string.Empty).Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
}
|
||||||
|
|
||||||
private static void Say(string text)
|
private static void Say(string text)
|
||||||
{
|
{
|
||||||
SonsTools.ShowMessage(text, 6f);
|
SonsTools.ShowMessage(text, 6f);
|
||||||
|
|||||||
+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.5.0</Version>
|
<Version>0.6.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.5.0",
|
"version": "0.6.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