refactor: group scripts into walker/ wikigen/ bundle/
Organize the 16 loose scripts by concern:
walker/ -- .wbt save tooling (sand, build_wbt, walker_hashes,
harvest_hashes, recover_key)
wikigen/ -- MediaWiki page generators (make_*_wiki, render_wiki)
bundle/ -- Unity/Odin asset extraction (unitybundle, odin_read,
extract_*, loot_probe, dump_loot_bytes)
The only cross-script imports (build_wbt->walker_hashes,
extract_loot->odin_read) live within the same folder, so each
script's dir on sys.path[0] keeps them resolving with no code
changes. All data paths are absolute, so the moves don't affect
I/O. Named the code dir wikigen/ to avoid colliding with the
generated wiki/ output dir; ignore the regenerable wiki_site/ render.
This commit is contained in:
89
bundle/loot_probe.py
Normal file
89
bundle/loot_probe.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read the configuration bundle's MonoBehaviours via IL2CPP typetree and find the
|
||||
loot tables. Determine definitively: are they plain (readable) or Odin-binary blobs?"""
|
||||
import os, sys, json, UnityPy
|
||||
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
|
||||
|
||||
GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest"
|
||||
BD = os.path.join(GAME, "Sand_Data/StreamingAssets/aa/StandaloneWindows64")
|
||||
META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat")
|
||||
DLL = os.path.join(GAME, "GameAssembly.dll")
|
||||
OUT = "/home/downloadpizza/sand_tools/extracted"
|
||||
|
||||
print("building generator...", flush=True)
|
||||
gen = TypeTreeGenerator("6000.0.40f1")
|
||||
gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
|
||||
print("ready", flush=True)
|
||||
|
||||
env = UnityPy.load(os.path.join(BD, "configuration_assets_all.bundle"),
|
||||
os.path.join(BD, "sand_monoscripts.bundle"))
|
||||
|
||||
_cache = {}
|
||||
def nodes_for(script):
|
||||
full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName
|
||||
key = (script.m_AssemblyName, full)
|
||||
if key not in _cache:
|
||||
try:
|
||||
_cache[key] = json.loads(gen.get_nodes_as_json(script.m_AssemblyName, full))
|
||||
except Exception as e:
|
||||
_cache[key] = None
|
||||
return _cache[key]
|
||||
|
||||
# list all MBs with class + whether they look loot/odin
|
||||
mbs = [o for o in env.objects if o.type.name == "MonoBehaviour"]
|
||||
print(f"{len(mbs)} MonoBehaviours in configuration bundle\n", flush=True)
|
||||
loot_objs = []
|
||||
for o in mbs:
|
||||
try:
|
||||
d = o.read(); nm = getattr(d, "m_Name", "") or ""
|
||||
script = d.m_Script.read(); cls = script.m_ClassName
|
||||
except Exception as e:
|
||||
nm, cls, script = "?", f"<{e}>", None
|
||||
raw = o.get_raw_data()
|
||||
has_loot = b"LootTable" in raw or b"Storm" in raw or b"Voyage" in raw
|
||||
mark = " <== LOOT" if has_loot else ""
|
||||
print(f" {nm:45s} {cls:35s} {len(raw):8d}B{mark}", flush=True)
|
||||
if has_loot or "Loot" in cls:
|
||||
loot_objs.append((o, nm, cls, script))
|
||||
|
||||
print(f"\n--- inspecting {len(loot_objs)} loot-ish objects ---", flush=True)
|
||||
for o, nm, cls, script in loot_objs:
|
||||
print(f"\n### {nm} ({cls})", flush=True)
|
||||
if script is None:
|
||||
print(" no script"); continue
|
||||
nodes = nodes_for(script)
|
||||
if not nodes:
|
||||
print(" no typetree nodes"); continue
|
||||
try:
|
||||
tree = o.read_typetree(nodes)
|
||||
except Exception as e:
|
||||
print(f" typetree read FAILED: {e}"); continue
|
||||
keys = list(tree.keys()) if isinstance(tree, dict) else type(tree)
|
||||
print(f" top-level keys: {keys}", flush=True)
|
||||
# Odin signature: only m_GameObject/m_Enabled/m_Script/m_Name + serializationData
|
||||
sd = tree.get("serializationData") if isinstance(tree, dict) else None
|
||||
if sd:
|
||||
print(f" ODIN serializationData fields: {list(sd.keys())}", flush=True)
|
||||
for f in ("SerializedBytes", "SerializedBytesString", "SerializationNodes"):
|
||||
v = sd.get(f)
|
||||
if v:
|
||||
print(f" {f}: len={len(v)}", flush=True)
|
||||
# dump a trimmed view
|
||||
def trim(v, depth=0):
|
||||
if depth > 3: return "..."
|
||||
if isinstance(v, dict):
|
||||
return {k: trim(x, depth+1) for k, x in list(v.items())[:8]}
|
||||
if isinstance(v, list):
|
||||
return [trim(x, depth+1) for x in v[:3]] + (["...+%d"%(len(v)-3)] if len(v) > 3 else [])
|
||||
if isinstance(v, (bytes, bytearray)):
|
||||
return f"<{len(v)} bytes>"
|
||||
return v
|
||||
import pprint
|
||||
pprint.pprint(trim(tree), width=120)
|
||||
# save full
|
||||
safe = "".join(c if c.isalnum() else "_" for c in nm)[:40]
|
||||
try:
|
||||
json.dump(tree, open(os.path.join(OUT, f"_loot_{safe}.json"), "w"),
|
||||
default=lambda x: f"<bytes {len(x)}>" if isinstance(x,(bytes,bytearray)) else str(x))
|
||||
except Exception as e:
|
||||
print(f" (save skipped: {e})")
|
||||
Reference in New Issue
Block a user