#!/usr/bin/env python3 """Convert a master-server WalkerDto (e.g. ExpeditionDto.Trampler, from GetExpedition / GetExpeditionWalker) into a loadable WalkerBlueprintDto, recomputing the 3 top-level hashes. WHY a transform is needed: the WS channel serializes enums as INTEGERS and omits null EpbId on connections; the blueprint/hash ("storage") form uses enum NAME strings and includes EpbId:null. ObjectToByteArray hashes the storage form (verified in trampler_hashes.py). So WS-form -> storage-form before hashing. Enum maps (from il2cpp/dump.cs): ConnectionSlotType: DOOR=0 HATCH=1 STRUCTURE=2 BALCONY=3 DECK=4 ConnectionState : DEFAULT=0 DOOR=1 OPEN=2 (MasterserverDtos.ConnectionState, 35406) ConnectionsCount : FULL=0 PARTIAL=1 ERROR=2 Self-verifies offline via round-trip on the known sample (storage->WS->storage is identity and reproduces the stored CompartmentsHash/ConnectionsHash). """ import json, os, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from trampler_hashes import compartments_hash, connections_hash, definitions_hash SLOT = ["DOOR", "HATCH", "STRUCTURE", "BALCONY", "DECK"] # ConnectionSlotType STATE = ["DEFAULT", "DOOR", "OPEN"] # ConnectionState COUNT = ["FULL", "PARTIAL", "ERROR"] # ConnectionsCount SLOT_I = {n: i for i, n in enumerate(SLOT)} STATE_I = {n: i for i, n in enumerate(STATE)} COUNT_I = {n: i for i, n in enumerate(COUNT)} def _od(*pairs): """build a dict in explicit (declaration) order.""" d = {} for k, v in pairs: d[k] = v return d def _decor_i2s(decor): """DecorationsInfo: socket enums int->name (storage form).""" if decor is None: return None socks = [] for s in decor.get("Sockets", []): vals = [] for kv in s.get("Value", []): v = kv.get("Value") or {} vals.append(_od(("Key", SLOT[kv["Key"]]), ("Value", _od(("state", STATE[v["state"]]), ("count", COUNT[v["count"]]))))) socks.append(_od(("Key", s["Key"]), ("Value", vals))) return _od(("Sockets", socks)) def _decor_s2i(decor): """inverse, for round-trip self-test only.""" if decor is None: return None socks = [] for s in decor.get("Sockets", []): vals = [] for kv in s.get("Value", []): v = kv.get("Value") or {} vals.append(_od(("Key", SLOT_I[kv["Key"]]), ("Value", _od(("state", STATE_I[v["state"]]), ("count", COUNT_I[v["count"]]))))) socks.append(_od(("Key", s["Key"]), ("Value", vals))) return _od(("Sockets", socks)) def comp_i2s(bp): """one CompartmentBlueprintDto WS->storage (CompartmentBlueprintDto field order).""" return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]), ("DecorationsInfo", _decor_i2s(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]), ("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash"))) def comp_s2i(bp): return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]), ("DecorationsInfo", _decor_s2i(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]), ("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash"))) def conn_i2s(bp): """one ConnectionBlueprintDto WS->storage: add EpbId:null, enums int->name (declaration order).""" return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("ActualDirection", bp["ActualDirection"]), ("GridCoordinate", bp["GridCoordinate"]), ("SlotType", SLOT[bp["SlotType"]]), ("State", STATE[bp["State"]])) def conn_s2i(c): """inverse: drop EpbId, enums name->int (round-trip self-test only).""" return _od(("Id", c["Id"]), ("ActualDirection", c["ActualDirection"]), ("GridCoordinate", c["GridCoordinate"]), ("SlotType", SLOT_I[c["SlotType"]]), ("State", STATE_I[c["State"]])) def walkerdto_to_blueprint(walker, definitions=None, version=1): """ExpeditionDto.Trampler / WalkerDto -> WalkerBlueprintDto with recomputed hashes.""" chassis = comp_i2s(walker["Chassis"]["Blueprint"]) comps = [comp_i2s(c["Blueprint"]) for c in walker["Compartments"]] conns = [conn_i2s(c["Blueprint"]) for c in walker["Connections"]] bp = _od(("Id", walker.get("BlueprintId")), ("UniqueId", walker.get("BlueprintUniqueId")), ("Version", version), ("Chassis", chassis), ("Compartments", comps), ("Connections", conns), ("CompartmentsHash", compartments_hash(comps)), ("DefinitionsHash", ""), ("ConnectionsHash", connections_hash(conns))) if definitions is not None: bp["DefinitionsHash"] = definitions_hash(definitions) return bp def _verify_roundtrip(): """storage -> WS -> storage must be identity (and reproduce stored hashes) on the sample.""" p = os.path.join(os.path.dirname(__file__), "..", "extracted", "playfab_titledata_TramplerBlueprint3.json") s = json.load(open(p)) comps_ws = [comp_s2i(c) for c in s["Compartments"]] conns_ws = [conn_s2i(c) for c in s["Connections"]] comps_back = [comp_i2s(c) for c in comps_ws] conns_back = [conn_i2s(c) for c in conns_ws] cj = lambda o: json.dumps(o, separators=(",", ":")) assert cj(comps_back) == cj(s["Compartments"]), "compartment round-trip not identity" assert cj(conns_back) == cj(s["Connections"]), "connection round-trip not identity" assert compartments_hash(comps_back) == s["CompartmentsHash"], "CompartmentsHash mismatch" assert connections_hash(conns_back) == s["ConnectionsHash"], "ConnectionsHash mismatch" print("round-trip OK: storage->WS->storage is byte-identical; #1 & #3 hashes reproduce.") if __name__ == "__main__": _verify_roundtrip() exp = os.path.join(os.path.dirname(__file__), "..", "extracted", "master_GetExpedition.json") if os.path.exists(exp): tr = json.load(open(exp)).get("Trampler") if tr: bp = walkerdto_to_blueprint(tr) out = os.path.join(os.path.dirname(__file__), "..", "extracted", "host_trampler_blueprint.json") json.dump(bp, open(out, "w"), indent=1, ensure_ascii=False) print("converted host trampler -> %s" % out) print(" chassis=%s comps=%d conns=%d" % (bp["Chassis"]["EpbId"], len(bp["Compartments"]), len(bp["Connections"]))) print(" CompartmentsHash=%s" % bp["CompartmentsHash"]) print(" ConnectionsHash =%s" % bp["ConnectionsHash"]) print(" DefinitionsHash =%s (provisional - needs live verify)" % (bp["DefinitionsHash"] or ""))