master-server replay + trampler RE: protocol, hashes, footprints, map renderer
- master_scrape.py: live master-server (ger.hologryph.com) ClientMessage replay over the two-socket /login + /connect handshake (PlayFab ticket auth). Pulled compartment defs, shop prices, research tree, storage, characters, expedition -> extracted/master_*.json - PlayFab confirmed auth-only for this title (Economy disabled); docs corrected - trampler_hashes.py: blueprint hash algo MD5(UTF8(compact-JSON)); CompartmentsHash(#1) and ConnectionsHash(#3) verified & generatable from scratch - walkerdto_to_blueprint.py: WalkerDto(expedition) -> WalkerBlueprintDto, enum int<->name, verified by storage->WS->storage round-trip - render_trampler.py: per-floor map from CompartmentsDatabase cell footprints (rotation solved via overlap check) + doors/hatches from Connections + turret arcs + cargo C1-C8 in game order - docs/MASTER_SERVER.md, docs/TRAMPLER.md; ghidra address-offset bug fixed (no -0x1000)
This commit is contained in:
138
reverse/walkerdto_to_blueprint.py
Normal file
138
reverse/walkerdto_to_blueprint.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/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 "<empty>"))
|
||||
Reference in New Issue
Block a user