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.
This commit is contained in:
73
extract_loot.py
Normal file
73
extract_loot.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user