#!/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> -> {"Sockets":[{"Key":{x,y,z},"Value":[{"Key":"","Value":{"state":"","count":""}}]}]} """ 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'}")