#!/usr/bin/env python3 """Extract SAND loot/drop tables from the two Odin-binary LootTablesConfig assets. Decodes conf_worldLootTablesStormConfig + conf_worldLootTablesVoyageConfig (Odin SerializedFormat=0 Binary) via odin_read, flattens to a clean dict: { region: { lootTableId: [ {itemBlueprint, countMin, countMax, ...}, ... ] } } Writes extracted/loot_tables.json (+ reports any unexpected fields / coverage). """ import os, sys, json import odin_read EX = "/home/downloadpizza/sand_tools/extracted" REGIONS = { "Storm": "conf_worldLootTablesStormConfig.odin.bin", "Voyage": "conf_worldLootTablesVoyageConfig.odin.bin", } def odin_list(node): """Unwrap an Odin List node -> python list of elements.""" if node is None: return [] if isinstance(node, list): return node items = node.get("$items", []) # List nodes serialize as one inner array: $items == [[...elements...]] out = [] for chunk in items: if isinstance(chunk, list): out.extend(chunk) else: out.append(chunk) return out def main(): result = {} extra_fields = set() item_ids = set() for region, fn in REGIONS.items(): path = os.path.join(EX, fn) data = open(path, "rb").read() parsed = odin_read.parse(data) assert parsed["consumed"] == parsed["total"], f"{region}: incomplete parse" tables = odin_list(parsed["roots"]["_lootTables"]) region_out = {} for t in tables: tid = t.get("lootTableId") items = odin_list(t.get("items")) rows = [] for it in items: row = {k: v for k, v in it.items() if k != "$type"} for k in row: if k not in ("itemBlueprint", "countMin", "countMax"): extra_fields.add(k) if "itemBlueprint" in row: item_ids.add(row["itemBlueprint"]) rows.append(row) region_out[tid] = rows result[region] = region_out print(f"{region}: {len(region_out)} loot tables, " f"{sum(len(v) for v in region_out.values())} drop rows") out = os.path.join(EX, "loot_tables.json") json.dump(result, open(out, "w"), indent=1, ensure_ascii=False) print(f"\nwrote {out}") print(f"unique item blueprints referenced: {len(item_ids)}") if extra_fields: print(f"NOTE extra (non count) fields present: {sorted(extra_fields)}") # how many drop-table item ids are NOT in our authoritative item registry? reg_path = os.path.join(EX, "items_registry.json") if os.path.exists(reg_path): reg = set(json.load(open(reg_path))["items"].keys()) unknown = sorted(i for i in item_ids if i not in reg) print(f"item blueprints not in items_registry: {len(unknown)}") for u in unknown[:40]: print(" ", u) if __name__ == "__main__": main()