- 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)
77 lines
3.5 KiB
Python
77 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the SAND trampler/walker blueprint hashes FROM SCRATCH (no harvesting needed).
|
|
|
|
RE'd from GameAssembly.dll (2026-06-15 build):
|
|
MD5Utility.ComputeHash(obj) = MD5( UTF8( JsonConvert.SerializeObject(obj) ) ), formatted as
|
|
uppercase hex with no separators (each byte -> ToString("X2")). [ghidra: MD5Utility$$ObjectToByteArray,
|
|
$$ComputeHash]
|
|
|
|
The three top-level WalkerBlueprintDto hashes are MD5 of the *compact* JSON (Newtonsoft default:
|
|
no whitespace, PascalCase keys in C# declaration order, nulls included, enums as STRING names,
|
|
CellCoordinate as {x,y,z}) of:
|
|
CompartmentsHash = ComputeHash(blueprint.Compartments) [List<CompartmentBlueprintDto>] VERIFIED
|
|
ConnectionsHash = ComputeHash(blueprint.Connections) [List<ConnectionBlueprintDto>] VERIFIED <- "hash #3"
|
|
DefinitionsHash = ComputeHash(<the CompartmentDefinitionDto list for the compartments>) PROVISIONAL
|
|
(hashes the actual definitions, not blueprint-internal fields; verify live
|
|
against a fresh GetTrampler + GetCompartmentDefinitions once unfrozen.)
|
|
|
|
To match Newtonsoft byte-for-byte, build the dicts with keys already in declaration order and
|
|
serialize with json.dumps(obj, separators=(",",":"), ensure_ascii=False). Do NOT sort keys.
|
|
"""
|
|
import json, hashlib
|
|
|
|
|
|
def md5_hash(obj) -> str:
|
|
"""MD5Utility.ComputeHash(obj): MD5 of compact-JSON(obj), uppercase hex."""
|
|
blob = json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
|
|
return hashlib.md5(blob.encode("utf-8")).hexdigest().upper()
|
|
|
|
|
|
def compartments_hash(compartments: list) -> str:
|
|
"""#1 CompartmentsHash = MD5(JSON(Compartments list)). VERIFIED."""
|
|
return md5_hash(compartments)
|
|
|
|
|
|
def connections_hash(connections: list) -> str:
|
|
"""#3 ConnectionsHash = MD5(JSON(Connections list)). VERIFIED.
|
|
|
|
Each connection must serialize as (declaration order):
|
|
{"Id": int, "EpbId": str|None, "ActualDirection": {"x":,"y":,"z":},
|
|
"GridCoordinate": {"x":,"y":,"z":}, "SlotType": "<name>", "State": "<name>"}
|
|
"""
|
|
return md5_hash(connections)
|
|
|
|
|
|
def definitions_hash(definitions: list) -> str:
|
|
"""#2 DefinitionsHash = MD5(JSON(<CompartmentDefinitionDto list>)). PROVISIONAL — verify live."""
|
|
return md5_hash(definitions)
|
|
|
|
|
|
def apply_hashes(blueprint: dict, definitions: list = None) -> dict:
|
|
"""Set the three top-level hashes on a blueprint dict in place. Returns it.
|
|
definitions: ordered CompartmentDefinitionDto list for #2 (optional until verified)."""
|
|
blueprint["CompartmentsHash"] = compartments_hash(blueprint["Compartments"])
|
|
blueprint["ConnectionsHash"] = connections_hash(blueprint["Connections"])
|
|
if definitions is not None:
|
|
blueprint["DefinitionsHash"] = definitions_hash(definitions)
|
|
return blueprint
|
|
|
|
|
|
def _selftest():
|
|
import os
|
|
p = os.path.join(os.path.dirname(__file__), "..", "extracted",
|
|
"playfab_titledata_TramplerBlueprint3.json")
|
|
b = json.load(open(p))
|
|
c1 = compartments_hash(b["Compartments"])
|
|
c3 = connections_hash(b["Connections"])
|
|
assert c1 == b["CompartmentsHash"], "CompartmentsHash %s != %s" % (c1, b["CompartmentsHash"])
|
|
assert c3 == b["ConnectionsHash"], "ConnectionsHash %s != %s" % (c3, b["ConnectionsHash"])
|
|
print("selftest OK:")
|
|
print(" CompartmentsHash #1:", c1, "== stored")
|
|
print(" ConnectionsHash #3:", c3, "== stored <- generatable from scratch")
|
|
print(" DefinitionsHash #2: provisional (verify live vs current GetTrampler).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_selftest()
|