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:
87
walker_hashes.py
Normal file
87
walker_hashes.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reproduce SAND walker hashes offline.
|
||||
|
||||
All walker hashes = MD5(UTF8(JsonConvert.SerializeObject(obj))).hexUPPER, where the
|
||||
default JsonConvert settings use a global StringEnumConverter (enums -> NAME strings),
|
||||
NullValueHandling.Include, compact formatting, member declaration order.
|
||||
|
||||
Verified against all local walkers:
|
||||
CompartmentHash (per part) = md5_json(placement from CompartmentsDatabase) [compartment_hashes.py]
|
||||
CompartmentsHash (walker) = md5_json( Compartments list ) <- here
|
||||
ConnectionsHash (walker) = md5_json( Connections list ) <- here
|
||||
DefinitionsHash (walker) = md5_json( Compartments.Select(->CompartmentDefinitionDto) )
|
||||
^ needs the server-sourced CompartmentDefinitionDto objects;
|
||||
not reproducible offline. Copy from source walker (it only
|
||||
changes if the set/order of part definitions changes).
|
||||
DefinitionHash (per part) = md5_json(CompartmentDefinitionDto) -> also server-sourced.
|
||||
|
||||
Enum name tables (from dump.cs):
|
||||
ConnectionSlotType: 0 DOOR,1 HATCH,2 STRUCTURE,3 BALCONY,4 DECK
|
||||
ConnectionState(MasterserverDtos): 0 DEFAULT,1 DOOR,2 OPEN
|
||||
ConnectionsCount: 0 FULL,1 PARTIAL,2 ERROR
|
||||
|
||||
DecorationsInfo nesting is serialized as KeyValuePair ARRAYS (not JSON objects):
|
||||
Sockets: Dict<CellCoordinate, Dict<ConnectionSlotType, {state,count}>>
|
||||
-> {"Sockets":[{"Key":{x,y,z},"Value":[{"Key":"<slot>","Value":{"state":"<st>","count":"<cnt>"}}]}]}
|
||||
"""
|
||||
import json, hashlib
|
||||
|
||||
SLOT = {0: 'DOOR', 1: 'HATCH', 2: 'STRUCTURE', 3: 'BALCONY', 4: 'DECK'}
|
||||
STATE = {0: 'DEFAULT', 1: 'DOOR', 2: 'OPEN'}
|
||||
COUNT = {0: 'FULL', 1: 'PARTIAL', 2: 'ERROR'}
|
||||
|
||||
def _md5(s): return hashlib.md5(s.encode()).hexdigest().upper()
|
||||
def _coord(v): return '{"x":%d,"y":%d,"z":%d}' % (v['x'], v['y'], v['z'])
|
||||
def _dbl(x): return f'{int(x)}.0' if float(x) == int(x) else repr(float(x))
|
||||
def _stinfo(d): return '{"state":"%s","count":"%s"}' % (STATE[d['state']], COUNT[d['count']])
|
||||
|
||||
def _deco(d):
|
||||
if d is None:
|
||||
return 'null'
|
||||
socks = ','.join(
|
||||
'{"Key":%s,"Value":[%s]}' % (
|
||||
_coord(kv['Key']),
|
||||
','.join('{"Key":"%s","Value":%s}' % (SLOT[iv['Key']], _stinfo(iv['Value']))
|
||||
for iv in kv['Value']))
|
||||
for kv in d['Sockets'])
|
||||
return '{"Sockets":[%s]}' % socks
|
||||
|
||||
def compartment_json(c):
|
||||
p = [('Id', str(c['Id'])), ('EpbId', json.dumps(c['EpbId'])),
|
||||
('CellCoordinate', _coord(c['CellCoordinate'])),
|
||||
('DecorationsInfo', _deco(c['DecorationsInfo'])),
|
||||
('Rotation', _dbl(c['Rotation'])),
|
||||
('CompartmentHash', json.dumps(c['CompartmentHash'])),
|
||||
('DefinitionHash', json.dumps(c['DefinitionHash']))]
|
||||
return '{' + ','.join(f'"{k}":{v}' for k, v in p) + '}'
|
||||
|
||||
def connection_json(c):
|
||||
p = [('Id', str(c['Id'])),
|
||||
('EpbId', json.dumps(c['EpbId']) if c['EpbId'] is not None else 'null'),
|
||||
('ActualDirection', _coord(c['ActualDirection'])),
|
||||
('GridCoordinate', _coord(c['GridCoordinate'])),
|
||||
('SlotType', '"%s"' % SLOT[c['SlotType']]),
|
||||
('State', '"%s"' % STATE[c['State']])]
|
||||
return '{' + ','.join(f'"{k}":{v}' for k, v in p) + '}'
|
||||
|
||||
def compartments_hash(compartments):
|
||||
"""CompartmentsHash from walker['Compartments'] (the BSON-decoded list)."""
|
||||
return _md5('[' + ','.join(compartment_json(c) for c in compartments) + ']')
|
||||
|
||||
def connections_hash(connections):
|
||||
"""ConnectionsHash from walker['Connections']."""
|
||||
return _md5('[' + ','.join(connection_json(c) for c in connections) + ']')
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys, gzip, glob
|
||||
from bson import decode
|
||||
KEY = bytes.fromhex('70dd1f2a0b4a'); CHUNK = 0xA000
|
||||
files = sys.argv[1:] or glob.glob(
|
||||
'/home/downloadpizza/sand_tools/Walkers/*.wbt')
|
||||
for f in sorted(files):
|
||||
raw = gzip.decompress(open(f, 'rb').read())
|
||||
w = decode(bytes(raw[i] ^ KEY[(i % CHUNK) % 6] for i in range(len(raw))))['walker']
|
||||
okC = compartments_hash(w['Compartments']) == w['CompartmentsHash']
|
||||
okN = connections_hash(w['Connections']) == w['ConnectionsHash']
|
||||
print(f"{f.split('/')[-1][:8]} CompartmentsHash={'OK' if okC else 'FAIL'} "
|
||||
f"ConnectionsHash={'OK' if okN else 'FAIL'}")
|
||||
Reference in New Issue
Block a user