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:
DownloadPizza
2026-06-16 00:35:17 +02:00
parent 3df0797acc
commit fc6b270fa8
29 changed files with 61574 additions and 0 deletions

View 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>"))