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/extract_data.py
Normal file
89
bundle/extract_data.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract SAND MonoBehaviour data (loot tables, crafting graph, …) using IL2CPP typetrees.
|
||||
|
||||
Builds a UnityPy TypeTreeGenerator from GameAssembly.dll + global-metadata.dat (Unity
|
||||
6000.0.40f1), loads the relevant bundles into one environment so PPtr references resolve,
|
||||
reads each MonoBehaviour against its generated typetree, and rewrites PPtrs as the target's
|
||||
m_Name where known. Output: structured JSON in extracted/.
|
||||
"""
|
||||
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"
|
||||
UNITY = "6000.0.40f1"
|
||||
|
||||
def build_generator():
|
||||
g = TypeTreeGenerator(UNITY)
|
||||
g.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
|
||||
return g
|
||||
|
||||
_node_cache = {}
|
||||
def nodes_for(gen, script):
|
||||
key = (script.m_AssemblyName, script.m_Namespace, script.m_ClassName)
|
||||
if key not in _node_cache:
|
||||
full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName
|
||||
try:
|
||||
_node_cache[key] = gen.get_nodes(script.m_AssemblyName, full)
|
||||
except Exception as e:
|
||||
_node_cache[key] = None
|
||||
return _node_cache[key]
|
||||
|
||||
def load_env(*bundles):
|
||||
paths = [os.path.join(BD, b) for b in bundles]
|
||||
return UnityPy.load(*paths)
|
||||
|
||||
def build_name_index(env):
|
||||
"""path_id -> m_Name for every object we can cheaply name (GameObjects, named MBs)."""
|
||||
idx = {}
|
||||
for o in env.objects:
|
||||
try:
|
||||
if o.type.name in ("GameObject",):
|
||||
idx[o.path_id] = o.read().m_Name
|
||||
except Exception:
|
||||
pass
|
||||
return idx
|
||||
|
||||
def read_mb(gen, obj, name_idx=None):
|
||||
"""Read a MonoBehaviour into a plain dict; resolve PPtr dicts to {'->': name|pathid}."""
|
||||
d = obj.read()
|
||||
try:
|
||||
script = d.m_Script.read()
|
||||
except Exception:
|
||||
return None
|
||||
nodes = nodes_for(gen, script)
|
||||
if not nodes:
|
||||
return None
|
||||
tree = obj.read_typetree(nodes)
|
||||
return _clean(tree, name_idx, obj)
|
||||
|
||||
def _clean(v, name_idx, obj):
|
||||
if isinstance(v, dict):
|
||||
# PPtr shape
|
||||
if set(v.keys()) >= {"m_FileID", "m_PathID"} and len(v) <= 3:
|
||||
pid = v["m_PathID"]
|
||||
if pid == 0:
|
||||
return None
|
||||
nm = name_idx.get(pid) if name_idx else None
|
||||
return {"ref": nm or pid}
|
||||
return {k: _clean(x, name_idx, obj) for k, x in v.items()}
|
||||
if isinstance(v, list):
|
||||
return [_clean(x, name_idx, obj) for x in v]
|
||||
return v
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("building generator…", flush=True)
|
||||
gen = build_generator()
|
||||
print("generator ready", flush=True)
|
||||
# quick validation on one loot table
|
||||
env = load_env("lootsets_assets_all.bundle", "sand_monoscripts.bundle",
|
||||
"epb_assets_all.bundle", "configuration_assets_all.bundle")
|
||||
name_idx = build_name_index(env)
|
||||
print("name index:", len(name_idx), "entries", flush=True)
|
||||
o = next(x for x in env.objects if x.type.name == "MonoBehaviour"
|
||||
and x.read().m_Name == "POIShipMediumWeapons")
|
||||
import pprint
|
||||
pprint.pprint(read_mb(gen, o, name_idx))
|
||||
Reference in New Issue
Block a user