Replace in-game rigassets with offline catalog decoder and add decoded catalog for v0.5.0
This commit is contained in:
@@ -22,8 +22,6 @@ Run these in the in-game console.
|
||||
| `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` |
|
||||
| `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`, and resolve every race and clothing GUID |
|
||||
| `rigassets key:<key>` | Resolve a single Addressables key or GUID |
|
||||
| `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` |
|
||||
@@ -34,13 +32,19 @@ Written to `Sons Of The Forest\UserData\RigProbe\`:
|
||||
|
||||
- `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_locators.tsv`, `assets_known.tsv` (race and clothing GUIDs resolved), `assets_keycounts.tsv` and `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\<source>_<name>.json` full data per character
|
||||
- `scene\` the same layout for `rigscene`
|
||||
|
||||
`rigprobe`, `rigscene` and `rigassets` replace their previous output. `rigwatch` files are timestamped.
|
||||
|
||||
## Addressables catalog
|
||||
|
||||
Walking the catalog from inside the game crashes it, so the catalog is decoded offline instead. `tools/decode_catalog.py` reads `SonsOfTheForest_Data\StreamingAssets\aa\catalog.json` and writes every key with its asset path, type and provider as TSV. Use it to resolve any AssetReference GUID to its asset.
|
||||
|
||||
```
|
||||
python tools\decode_catalog.py "C:\Program Files (x86)\Steam\steamapps\common\Sons Of The Forest\SonsOfTheForest_Data\StreamingAssets\aa\catalog.json" data\assets.tsv
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
+1
-230
@@ -15,8 +15,6 @@ using SonsSdk;
|
||||
using SonsSdk.Attributes;
|
||||
using TheForest.Utils;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.ResourceLocations;
|
||||
|
||||
namespace RigProbe;
|
||||
|
||||
@@ -47,7 +45,7 @@ public class RigProbe : SonsMod
|
||||
|
||||
protected override void OnSdkInitialized()
|
||||
{
|
||||
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigassets [filter], rigwatch [player|robby|virginia|off], rigspawn <TypeName> [variation]");
|
||||
RLog.Msg("RigProbe loaded. Commands: rigprobe [filter], rigscene, rigwatch [player|robby|virginia|off], rigspawn <TypeName> [variation]");
|
||||
}
|
||||
|
||||
[DebugCommand("rigprobe")]
|
||||
@@ -76,19 +74,6 @@ 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)
|
||||
{
|
||||
@@ -651,220 +636,6 @@ public class RigProbe : SonsMod
|
||||
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 locators = new List<UnityEngine.AddressableAssets.ResourceLocators.IResourceLocator>();
|
||||
foreach (var locator in Iterate(Addressables.ResourceLocators))
|
||||
if (locator != null)
|
||||
locators.Add(locator);
|
||||
RLog.Msg($"rigassets: {locators.Count} locators");
|
||||
|
||||
if (filter.StartsWith("key:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var key = filter.Substring(4).Trim();
|
||||
var one = new StringBuilder();
|
||||
one.AppendLine("label\tlocator\tkey\tprimaryKey\tinternalId\ttype\tprovider");
|
||||
var found = ResolveKey(one, locators, key, key);
|
||||
var onePath = Path.Combine(outDir, $"assets_key_{Sanitize(key)}.tsv");
|
||||
File.WriteAllText(onePath, one.ToString());
|
||||
Say($"RigProbe: {found} locations for {key} written to {onePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var locSb = new StringBuilder();
|
||||
locSb.AppendLine("locatorId\ttype\tkeys");
|
||||
foreach (var locator in locators)
|
||||
{
|
||||
string locType;
|
||||
try
|
||||
{
|
||||
locType = locator.Cast<Il2CppSystem.Object>().GetIl2CppType().FullName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
locType = "?";
|
||||
}
|
||||
locSb.AppendLine($"{locator.LocatorId}\t{locType}\t?");
|
||||
RLog.Msg($"rigassets: locator {locator.LocatorId} type {locType}");
|
||||
}
|
||||
var locPath = Path.Combine(outDir, "assets_locators.tsv");
|
||||
File.WriteAllText(locPath, locSb.ToString());
|
||||
|
||||
var known = new StringBuilder();
|
||||
known.AppendLine("label\tlocator\tkey\tprimaryKey\tinternalId\ttype\tprovider");
|
||||
int knownRows = 0;
|
||||
foreach (var (label, key) in KnownKeys())
|
||||
{
|
||||
RLog.Msg($"rigassets: resolving {label} {key}");
|
||||
knownRows += ResolveKey(known, locators, label, key);
|
||||
}
|
||||
File.WriteAllText(Path.Combine(outDir, "assets_known.tsv"), known.ToString());
|
||||
RLog.Msg($"rigassets: {knownRows} known GUID locations");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("locator\tkey\tprimaryKey\tinternalId\ttype\tprovider");
|
||||
int rows = 0;
|
||||
var counts = new StringBuilder();
|
||||
counts.AppendLine("locatorId\tkeys");
|
||||
foreach (var locator in locators)
|
||||
{
|
||||
RLog.Msg($"rigassets: walking keys of {locator.LocatorId}");
|
||||
int keyCount = 0;
|
||||
foreach (var keyObj in Iterate(locator.Keys))
|
||||
{
|
||||
if (keyObj == null)
|
||||
continue;
|
||||
keyCount++;
|
||||
var key = keyObj.ToString();
|
||||
Il2CppSystem.Collections.Generic.IList<IResourceLocation> locs = null;
|
||||
bool ok;
|
||||
try
|
||||
{
|
||||
ok = locator.Locate(keyObj, null, out locs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
if (!ok || locs == null)
|
||||
continue;
|
||||
foreach (var loc in Iterate(locs.Cast<Il2CppSystem.Collections.Generic.IEnumerable<IResourceLocation>>()))
|
||||
{
|
||||
if (loc == null || !Matches(filter, key, loc))
|
||||
continue;
|
||||
sb.AppendLine($"{locator.LocatorId}\t{key}\t{LocationRow(loc)}");
|
||||
rows++;
|
||||
}
|
||||
}
|
||||
counts.AppendLine($"{locator.LocatorId}\t{keyCount}");
|
||||
RLog.Msg($"rigassets: {locator.LocatorId} had {keyCount} keys");
|
||||
}
|
||||
|
||||
var suffix = filter.Length > 0 ? $"_{Sanitize(filter)}" : string.Empty;
|
||||
File.WriteAllText(Path.Combine(outDir, $"assets{suffix}.tsv"), sb.ToString());
|
||||
File.WriteAllText(Path.Combine(outDir, "assets_keycounts.tsv"), counts.ToString());
|
||||
Say($"RigProbe: {rows} catalog locations and {knownRows} known GUID locations written to {outDir}");
|
||||
}
|
||||
|
||||
private static List<(string, string)> KnownKeys()
|
||||
{
|
||||
var keys = new List<(string, string)>();
|
||||
var player = LocalPlayer.GameObject;
|
||||
if (!player)
|
||||
return keys;
|
||||
|
||||
var race = player.GetComponentInChildren<PlayerRaceSystem>(true);
|
||||
if (race && race._races != null)
|
||||
{
|
||||
for (int i = 0; i < race._races.Count; i++)
|
||||
{
|
||||
var r = race._races[i];
|
||||
if (!r)
|
||||
continue;
|
||||
var head = r.HeadAsset?.AssetGUID;
|
||||
var arms = r.ArmsAsset?.AssetGUID;
|
||||
if (!string.IsNullOrEmpty(head))
|
||||
keys.Add(($"race{i} {r.name} head", head));
|
||||
if (!string.IsNullOrEmpty(arms))
|
||||
keys.Add(($"race{i} {r.name} arms", arms));
|
||||
}
|
||||
}
|
||||
|
||||
var clothing = player.GetComponentInChildren<PlayerClothingSystem>(true);
|
||||
if (clothing && clothing._allClothing != null)
|
||||
{
|
||||
foreach (var c in clothing._allClothing)
|
||||
{
|
||||
if (!c)
|
||||
continue;
|
||||
var guid = c.Renderable?.AssetGUID;
|
||||
if (!string.IsNullOrEmpty(guid))
|
||||
keys.Add(($"clothing {c.ItemId} {c.name}", guid));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static int ResolveKey(StringBuilder sb, List<UnityEngine.AddressableAssets.ResourceLocators.IResourceLocator> locators, string label, string key)
|
||||
{
|
||||
int rows = 0;
|
||||
Il2CppSystem.Object keyObj;
|
||||
try
|
||||
{
|
||||
keyObj = new Il2CppSystem.Object(Il2CppInterop.Runtime.IL2CPP.ManagedStringToIl2Cpp(key));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sb.AppendLine($"{label}\t\t{key}\terror: {e.Message}\t\t\t");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var locator in locators)
|
||||
{
|
||||
Il2CppSystem.Collections.Generic.IList<IResourceLocation> locs = null;
|
||||
bool ok;
|
||||
try
|
||||
{
|
||||
ok = locator.Locate(keyObj, null, out locs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
if (!ok || locs == null)
|
||||
continue;
|
||||
foreach (var loc in Iterate(locs.Cast<Il2CppSystem.Collections.Generic.IEnumerable<IResourceLocation>>()))
|
||||
{
|
||||
if (loc == null)
|
||||
continue;
|
||||
sb.AppendLine($"{label}\t{locator.LocatorId}\t{key}\t{LocationRow(loc)}");
|
||||
rows++;
|
||||
}
|
||||
}
|
||||
|
||||
if (rows == 0)
|
||||
sb.AppendLine($"{label}\t\t{key}\tnot found\t\t\t");
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static bool Matches(string filter, string key, IResourceLocation loc)
|
||||
{
|
||||
if (filter.Length == 0)
|
||||
return true;
|
||||
return key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (loc.InternalId ?? string.Empty).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (loc.PrimaryKey ?? string.Empty).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static string LocationRow(IResourceLocation loc)
|
||||
{
|
||||
string type;
|
||||
try
|
||||
{
|
||||
type = loc.ResourceType?.FullName ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
type = string.Empty;
|
||||
}
|
||||
return $"{loc.PrimaryKey}\t{loc.InternalId}\t{type}\t{loc.ProviderId}";
|
||||
}
|
||||
|
||||
private static IEnumerable<T> Iterate<T>(Il2CppSystem.Collections.Generic.IEnumerable<T> source)
|
||||
{
|
||||
if (source == null)
|
||||
yield break;
|
||||
var en = source.GetEnumerator();
|
||||
if (en == null)
|
||||
yield break;
|
||||
var move = en.Cast<Il2CppSystem.Collections.IEnumerator>();
|
||||
while (move.MoveNext())
|
||||
yield return en.Current;
|
||||
}
|
||||
|
||||
private static void StartWatch(string target)
|
||||
{
|
||||
StopWatch();
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>RigProbe</AssemblyName>
|
||||
<RootNamespace>RigProbe</RootNamespace>
|
||||
<Version>0.4.2</Version>
|
||||
<Version>0.5.0</Version>
|
||||
<NoWarn>CS0436;CS1701;CS1702;MSB3277;MSB3106</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
+9305
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -3,7 +3,7 @@
|
||||
"id": "RigProbe",
|
||||
"name": "RigProbe",
|
||||
"author": "JustinOros",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"description": "Dumps rig, mesh, material and animator data for every Sons of the Forest character to JSON for mod development",
|
||||
"gameVersion": "1.0.0",
|
||||
"platform": "Client",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import json, base64, struct, sys
|
||||
d = json.load(open(sys.argv[1], encoding='utf-8-sig'))
|
||||
kd = base64.b64decode(d['m_KeyDataString'])
|
||||
bd = base64.b64decode(d['m_BucketDataString'])
|
||||
ed = base64.b64decode(d['m_EntryDataString'])
|
||||
ids = d['m_InternalIds']
|
||||
provs = d['m_ProviderIds']
|
||||
types = [t['m_ClassName'] for t in d['m_resourceTypes']]
|
||||
|
||||
def read_key(off):
|
||||
t = kd[off]; off += 1
|
||||
if t in (0, 1):
|
||||
n = struct.unpack_from('<i', kd, off)[0]; off += 4
|
||||
return kd[off:off+n].decode('ascii' if t == 0 else 'utf-16-le', 'replace')
|
||||
if t == 2: return struct.unpack_from('<H', kd, off)[0]
|
||||
if t == 3: return struct.unpack_from('<I', kd, off)[0]
|
||||
if t == 4: return struct.unpack_from('<i', kd, off)[0]
|
||||
if t == 5:
|
||||
n = kd[off]; return kd[off+1:off+1+n].decode('ascii', 'replace')
|
||||
return f'<type{t}>'
|
||||
|
||||
bc = struct.unpack_from('<i', bd, 0)[0]
|
||||
buckets = []
|
||||
o = 4
|
||||
for _ in range(bc):
|
||||
doff, ec = struct.unpack_from('<ii', bd, o); o += 8
|
||||
ents = struct.unpack_from(f'<{ec}i', bd, o); o += 4 * ec
|
||||
buckets.append((doff, ents))
|
||||
keys = [read_key(b[0]) for b in buckets]
|
||||
|
||||
ec = struct.unpack_from('<i', ed, 0)[0]
|
||||
entries = [struct.unpack_from('<7i', ed, 4 + i * 28) for i in range(ec)]
|
||||
|
||||
def row(e):
|
||||
iid, prov, depk, deph, data, pk, rt = e
|
||||
return ids[iid], provs[prov], keys[pk] if 0 <= pk < len(keys) else pk, types[rt] if 0 <= rt < len(types) else rt, keys[depk] if depk >= 0 else ''
|
||||
|
||||
out = open(sys.argv[2], 'w', encoding='utf-8')
|
||||
out.write('key\tprimaryKey\tinternalId\ttype\tprovider\tdependencyKey\n')
|
||||
for k, (doff, ents) in zip(keys, buckets):
|
||||
for ei in ents:
|
||||
iid, prov, pk, rt, dep = row(entries[ei])
|
||||
out.write(f'{k}\t{pk}\t{iid}\t{rt}\t{prov.split(".")[-1]}\t{dep}\n')
|
||||
out.close()
|
||||
print(len(keys), 'keys', ec, 'entries')
|
||||
Reference in New Issue
Block a user