#!/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))
