Files
SandTools/extract_loot.py
DownloadPizza e2a2984925 tools: SAND .wbt + game-data extraction scripts
Python tooling for decoding walker saves and mining game data:
sand.py / build_wbt.py / walker_hashes.py / harvest_hashes.py (.wbt
codec + hashes), extract_*/loot_probe/odin_read/unitybundle (asset
parsing), make_*_wiki + render_wiki (wiki generation), recover_key.
Paths point at the local extracted/, wiki/, and Walkers symlink.
2026-06-11 14:43:57 +02:00

74 lines
2.9 KiB
Python

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