#!/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"" if isinstance(x,(bytes,bytearray)) else str(x)) except Exception as e: print(f" (save skipped: {e})")