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:
95
reverse/render_trampler.py
Normal file
95
reverse/render_trampler.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
# Strictly data-derived: footprints (CompartmentsDatabase cells + blueprint rotation/origin),
|
||||
# doors/hatches (blueprint Connections), single-tile guns. NO firing arcs: the facing/aim is
|
||||
# NOT present in the data (no forward/aim/angle field), so it is intentionally omitted.
|
||||
import json, collections, os, re
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
bp=json.load(open(ROOT+"/extracted/host_trampler_blueprint.json"))
|
||||
db={c["entityId"]:c for c in json.load(open(ROOT+"/extracted/CompartmentsDatabase.json"))["compartments"]}
|
||||
def rot(x,z,deg):
|
||||
deg%=360; return {0:(x,z),90:(z,-x),180:(-x,-z),270:(-z,x)}[deg]
|
||||
def cat(e):
|
||||
m=re.match(r"walker_comp([A-Za-z]+)_",e or ""); return m.group(1) if m else "?"
|
||||
CC={"Chassis":(90,90,100),"Reactor":(255,140,0),"Engine":(210,60,45),"Weapon":(170,40,60),
|
||||
"Armor":(70,110,165),"Cargo":(210,170,60),"Crew":(70,165,85),"CaptainCrew":(30,110,55),
|
||||
"Deck":(205,200,185),"Corridor":(160,205,228),"Special":(150,80,190),"Crafting":(150,100,60),
|
||||
"Steering":(40,165,165),"Structure":(150,150,160),"Balcony":(190,170,140)}
|
||||
LET={k:k[0] for k in CC}; LET.update({"CaptainCrew":"K","Corridor":"+","Crew":"c","Crafting":"F","Steering":"T","Chassis":"#","Weapon":"W"})
|
||||
parts=[bp["Chassis"]]+bp["Compartments"]
|
||||
def is_turret(e): return e.startswith("walker_compWeapon_TurretSlot")
|
||||
def is_ram(e): return "BattleRam" in e
|
||||
floors=collections.defaultdict(dict); labels=collections.defaultdict(list); turrets=[]; cargo_n=0
|
||||
for p in parts:
|
||||
e=db.get(p["EpbId"]);
|
||||
if not e: continue
|
||||
eid=p["EpbId"]; ox,oy,oz=p["CellCoordinate"]["x"],p["CellCoordinate"]["y"],p["CellCoordinate"]["z"]; deg=int(p.get("Rotation",0)); c=cat(eid)
|
||||
if is_turret(eid):
|
||||
floors[oy][(ox,oz)]=(c,True); labels[oy].append((ox,oz,"W"))
|
||||
base=135 if "Corner" in eid else 90 # rot0: corner=SW, straight=S (your calibration)
|
||||
turrets.append((oy,ox,oz,(base-deg)%360)); continue
|
||||
cells=[]
|
||||
for cl in e["cells"]:
|
||||
rx,rz=rot(cl["position"]["x"],cl["position"]["z"],deg); wy=oy+cl["position"]["y"]
|
||||
floors[wy].setdefault((ox+rx,oz+rz),(c,bool(cl.get("volumeOccupied"))))
|
||||
if cl.get("volumeOccupied") and wy==oy: cells.append((ox+rx,oz+rz))
|
||||
if is_ram(eid):
|
||||
for (x,z) in cells: floors[oy][(x,z)]=(c,True)
|
||||
cx=sum(x for x,z in cells)/max(1,len(cells)); cz=sum(z for x,z in cells)/max(1,len(cells)); labels[oy].append((cx,cz,"R"))
|
||||
elif c=="Cargo":
|
||||
cargo_n+=1; labels[oy].append((ox,oz,"C%d"%cargo_n))
|
||||
else:
|
||||
labels[oy].append((ox,oz,LET.get(c,"?")))
|
||||
doors=collections.defaultdict(list); hatches=collections.defaultdict(list)
|
||||
for cn in bp["Connections"]:
|
||||
g=cn["GridCoordinate"]; d=cn["ActualDirection"]; sl=cn["SlotType"]; st=cn["State"]
|
||||
if sl=="DOOR" and st in ("DOOR","OPEN") and d["y"]==0: doors[g["y"]].append((g["x"],g["z"],d["x"],d["z"],st))
|
||||
if sl=="HATCH" and st=="DOOR": hatches[g["y"]].append((g["x"],g["z"],d["y"]))
|
||||
allc=[(x,z) for y in floors if -1<=y<=3 for (x,z) in floors[y]]
|
||||
xmin=min(x for x,z in allc); xmax=max(x for x,z in allc); zmin=min(z for x,z in allc); zmax=max(z for x,z in allc)
|
||||
W=xmax-xmin+1; H=zmax-zmin+1; CELL=46; PAD=14; GAP=22; TITLEH=24
|
||||
order=[y for y in sorted(floors,reverse=True) if -1<=y<=3]
|
||||
FN={3:"deck 3",2:"deck 2",1:"deck 1",0:"deck 0",-1:"base / hull"}
|
||||
panelW=W*CELL; panelH=H*CELL; cols=len(order)
|
||||
imgW=PAD+cols*(panelW+GAP); imgH=PAD+TITLEH*2+panelH+120
|
||||
img=Image.new("RGBA",(imgW,imgH),(245,245,248,255)); ov=Image.new("RGBA",img.size,(0,0,0,0)); d=ImageDraw.Draw(img); od=ImageDraw.Draw(ov)
|
||||
f=ImageFont.load_default(size=18); fs=ImageFont.load_default(size=12); ft=ImageFont.load_default(size=15)
|
||||
d.text((PAD,4),"Host trampler - data-derived: footprints, doors/hatches, single-tile guns (W). Gun arc=rotation field (corner=45/straight=cardinal, +-90). N=up",fill=(20,20,30),font=ft)
|
||||
for i,y in enumerate(order):
|
||||
ox0=PAD+i*(panelW+GAP); oy0=PAD+TITLEH*2
|
||||
d.text((ox0,oy0-18),FN.get(y,"y%d"%y),fill=(20,20,30),font=ft)
|
||||
def px(x,z): return ox0+(x-xmin)*CELL, oy0+(z-zmin)*CELL
|
||||
for zi in range(H):
|
||||
for xi in range(W):
|
||||
X0=ox0+xi*CELL; Y0=oy0+zi*CELL; d.rectangle([X0,Y0,X0+CELL,Y0+CELL],outline=(225,225,228))
|
||||
for (x,z),(c,solid) in floors[y].items():
|
||||
X0,Y0=px(x,z); col=CC.get(c,(120,120,120))
|
||||
if solid: d.rectangle([X0+1,Y0+1,X0+CELL-1,Y0+CELL-1],fill=col,outline=(40,40,40))
|
||||
else: d.rectangle([X0+4,Y0+4,X0+CELL-4,Y0+CELL-4],fill=tuple(min(255,v+70) for v in col),outline=col)
|
||||
for (cx,cz,let) in labels[y]:
|
||||
X0,Y0=px(cx,cz); d.text((X0+CELL/2-5*len(let),Y0+CELL/2-9),let,fill=(255,255,255),font=f)
|
||||
for (x,z,dx,dz,st) in doors.get(y,[]):
|
||||
X0,Y0=px(x,z); col=(60,200,90) if st=="OPEN" else (130,80,30)
|
||||
if dx==1: d.line([X0+CELL,Y0+8,X0+CELL,Y0+CELL-8],fill=col,width=5)
|
||||
if dx==-1: d.line([X0,Y0+8,X0,Y0+CELL-8],fill=col,width=5)
|
||||
if dz==1: d.line([X0+8,Y0+CELL,X0+CELL-8,Y0+CELL],fill=col,width=5)
|
||||
if dz==-1: d.line([X0+8,Y0,X0+CELL-8,Y0],fill=col,width=5)
|
||||
for (x,z,dy) in hatches.get(y,[]):
|
||||
X0,Y0=px(x,z); cx,cy=X0+CELL/2,Y0+CELL/2; d.ellipse([cx-8,cy-8,cx+8,cy+8],outline=(20,20,140),width=2)
|
||||
d.text((cx-3,cy-7),"^" if dy==1 else "v",fill=(20,20,140),font=fs)
|
||||
for (ty,tx,tz,face) in turrets:
|
||||
if ty!=y: continue
|
||||
X0,Y0=px(tx,tz); cx,cy=X0+CELL/2,Y0+CELL/2; R=CELL*0.75
|
||||
od.pieslice([cx-R,cy-R,cx+R,cy+R],face-90,face+90,fill=(255,210,40,75),outline=(230,160,0,200))
|
||||
img=Image.alpha_composite(img,ov); d=ImageDraw.Draw(img)
|
||||
ly=PAD+TITLEH*2+panelH+10; d.text((PAD,ly-2),"LEGEND:",fill=(20,20,30),font=ft); ly+=18; lx=PAD
|
||||
for k in CC:
|
||||
d.rectangle([lx,ly,lx+20,ly+20],fill=CC[k],outline=(30,30,30)); d.text((lx+4,ly+4),LET.get(k,"?"),fill=(255,255,255),font=fs)
|
||||
d.text((lx+26,ly+4),k,fill=(20,20,30),font=fs); lx+=26+len(k)*7+18
|
||||
if lx>imgW-150: lx=PAD; ly+=26
|
||||
ly+=8
|
||||
d.line([PAD,ly,PAD+40,ly],fill=(130,80,30),width=5); d.text((PAD+48,ly-7),"door",fill=(20,20,30),font=fs)
|
||||
d.line([PAD+100,ly,PAD+140,ly],fill=(60,200,90),width=5); d.text((PAD+148,ly-7),"open door",fill=(20,20,30),font=fs)
|
||||
d.ellipse([PAD+230,ly-8,PAD+246,ly+8],outline=(20,20,140),width=2); d.text((PAD+254,ly-7),"hatch (^/v) W=gun R=ram yellow wedge=firing arc (+-90) faded=non-solid",fill=(20,20,30),font=fs)
|
||||
img.convert("RGB").save(ROOT+"/extracted/host_trampler_nice.png"); print("saved with rotation arcs, turrets=%d"%len(turrets))
|
||||
Reference in New Issue
Block a user