Files
SandTools/harvest_hashes.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

108 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Harvest per-part hashes from SAND walker saves (100% offline, no injection/proxy).
Every compartment placed in the in-game editor writes its DefinitionHash (and
CompartmentHash) into the .wbt save. This scans all saves, decodes them, and
merges every (EpbId -> {DefinitionHash, CompartmentHash}) into a JSON table,
deduping and keeping a history of any older hash values seen per part.
Re-run it any time after building/saving new walkers to grow the table.
Usage:
harvest_hashes.py # scan the live Walkers dir + snapshots, update table
harvest_hashes.py <dir> ... # scan extra dirs of .wbt too
"""
import sys, gzip, json, glob, os, hashlib
KEY = bytes.fromhex('70dd1f2a0b4a'); CHUNK = 0xA000
WBT_DIR = '/home/downloadpizza/sand_tools/Walkers' # symlink -> live game saves (*.wbt)
EXTRACTED = '/home/downloadpizza/sand_tools/extracted'
OUT = os.path.join(EXTRACTED, 'definition_hashes_known.json')
COMPDB = os.path.join(EXTRACTED, 'CompartmentsDatabase.json')
try:
from bson import decode
except ImportError:
sys.exit("need pymongo: ~/sand_tools/venv/bin/pip install pymongo")
def decode_wbt(path):
raw = gzip.decompress(open(path, 'rb').read())
dec = bytes(raw[i] ^ KEY[(i % CHUNK) % 6] for i in range(len(raw)))
return decode(dec)
def main():
dirs = [WBT_DIR, os.path.join(os.path.expanduser('~'), 'sand_tools', 'snapshots')] + sys.argv[1:]
files = []
for d in dirs:
files += glob.glob(os.path.join(d, '*.wbt'))
# snapshots are decoded already (.dec) -> handle separately
dec_files = []
for d in dirs:
dec_files += glob.glob(os.path.join(d, '*.dec'))
# rebuild fresh from saves+snapshots (the authoritative source); snapshots persist
# history even if live .wbt files are later deleted.
table = {}
def note(epb, comp, deff):
e = table.setdefault(epb, {'DefinitionHash': None, 'DefinitionHash_history': [],
'CompartmentHash': None, 'CompartmentHash_history': []})
for field, hist, val in [('DefinitionHash', 'DefinitionHash_history', deff),
('CompartmentHash', 'CompartmentHash_history', comp)]:
if not val:
continue
if e[field] and e[field] != val and e[field] not in e[hist]:
e[hist].append(e[field])
e[field] = val
scanned = 0
def walkers_from(doc):
w = doc['walker']
return [w['Chassis']] + w['Compartments']
for f in sorted(set(files)):
try:
doc = decode_wbt(f); scanned += 1
except Exception as ex:
print(f"skip {os.path.basename(f)}: {ex}"); continue
for c in walkers_from(doc):
note(c['EpbId'], c.get('CompartmentHash'), c.get('DefinitionHash'))
for f in sorted(set(dec_files)):
try:
doc = decode(open(f, 'rb').read()); scanned += 1
except Exception:
continue
try:
for c in walkers_from(doc):
note(c['EpbId'], c.get('CompartmentHash'), c.get('DefinitionHash'))
except Exception:
pass
# cross-check CompartmentHash against CompartmentsDatabase (authoritative current value)
total_parts = None
if os.path.exists(COMPDB):
db = json.load(open(COMPDB))
total_parts = len(db['compartments'])
calc = {c['entityId']: hashlib.md5(json.dumps(c, separators=(',', ':')).encode()).hexdigest().upper()
for c in db['compartments']}
for epb, e in table.items():
if epb in calc:
e['CompartmentHash_computed'] = calc[epb]
out = {
'_note': ("Per-part hashes harvested from walker saves. CompartmentHash is also computed "
"offline from CompartmentsDatabase (CompartmentHash_computed, authoritative). "
"DefinitionHash is server-sourced and only known for parts that appear in a save — "
"build a part in-game and re-run this to capture it."),
'definition_known': sum(1 for e in table.values() if e.get('DefinitionHash')),
'total_parts_in_db': total_parts,
'parts': dict(sorted(table.items())),
}
os.makedirs(os.path.dirname(OUT), exist_ok=True)
json.dump(out, open(OUT, 'w'), indent=2)
print(f"scanned {scanned} saves; {out['definition_known']} parts with DefinitionHash"
+ (f" / {total_parts} total" if total_parts else "") + f"\nwrote {OUT}")
if __name__ == '__main__':
main()