Files
SandTools/bundle/dump_loot_bytes.py
DownloadPizza a44e4db1c3 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.
2026-06-11 14:49:33 +02:00

48 lines
1.9 KiB
Python

#!/usr/bin/env python3
"""Dump the raw Odin SerializedBytes for the two LootTablesConfig assets, and do a
first-pass analysis: hexdump head, and list ASCII strings (>=3 chars) found."""
import os, sys, json, re, 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"
gen = TypeTreeGenerator("6000.0.40f1")
gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
env = UnityPy.load(os.path.join(BD, "configuration_assets_all.bundle"),
os.path.join(BD, "sand_monoscripts.bundle"))
for o in env.objects:
if o.type.name != "MonoBehaviour":
continue
try:
d = o.read(); nm = getattr(d, "m_Name", "") or ""
except Exception:
continue
if "LootTables" not in nm:
continue
script = d.m_Script.read()
full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName
nodes = json.loads(gen.get_nodes_as_json(script.m_AssemblyName, full))
tree = o.read_typetree(nodes)
sb = tree["serializationData"]["SerializedBytes"]
data = bytes(sb)
out_bin = os.path.join(OUT, f"{nm}.odin.bin")
open(out_bin, "wb").write(data)
print(f"\n=== {nm}: {len(data)} bytes -> {out_bin}")
print("head hex:", data[:64].hex())
# ascii strings
strings = re.findall(rb"[\x20-\x7e]{3,}", data)
uniq = []
seen = set()
for s in strings:
t = s.decode()
if t not in seen:
seen.add(t); uniq.append(t)
print(f"{len(strings)} ascii runs, {len(uniq)} unique. First 60 unique:")
for t in uniq[:60]:
print(" ", repr(t))