The recovered system: fire channels = LBE4ControlsManager buttonGroups (0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port already called -- it needed only the TriggerState attribute (id 0x13 PINNED to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an input feed. Keyboard/harness now push press/release edges into the button groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger = 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through the authentic chain (12 hits vs ~36 pre-groups). Config-mode session (regrouping UI) = the remaining stage, KB-scoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
5.0 KiB
Python
103 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Robust BGF (DIV-BIZ2) parser for the cockpit-shell reconstruction harness.
|
|
Handles the vertex-format chunks the engine loader (bgfload.cpp) does:
|
|
0x80 XYZ(12) 0x81 XYZ_N(24) 0x82 XYZ_RGBA(28)
|
|
0x88 XYZ_UV(20) 0x89 XYZ_N_UV(32) 0x8A XYZ_RGBA_UV(36)
|
|
Faces: 0x47 index list (u32 tri indices), 0x4d poly list (ppf-prefixed u32).
|
|
Tree: 0x40 container / 0x41 object / 0x42 patch / 0x46 mesh; 0x2037 SV_SPECIAL
|
|
(carries "PUNCH"); 0x2030 material name. Face indices are patch-local (base=patch
|
|
vert start). Returns per-patch geometry so we see frame vs glass vs punch."""
|
|
import struct
|
|
|
|
VSTRIDE = {0x80:12, 0x81:24, 0x82:28, 0x88:20, 0x89:32, 0x8A:36}
|
|
VHASN = {0x81, 0x89}
|
|
VHASUV = {0x88, 0x89, 0x8A}
|
|
CONTAINERS = {0x40,0x41,0x42,0x46,0x48,0x70,0x10,0x20,0x30,0x3}
|
|
|
|
def parse(path):
|
|
d = open(path,"rb").read()
|
|
patches = [] # each: dict(material, punch, verts[list (x,y,z,has_n,nx,ny,nz,u,v)], faces[list (a,b,c)])
|
|
cur = {"material":"", "punch":False}
|
|
def read_chunk(p, end):
|
|
if p+2 > end: return None
|
|
tw = struct.unpack_from("<H", d, p)[0]
|
|
cid = tw & 0x2fff
|
|
lw = 4 if (tw & 0x8000) else (2 if (tw & 0x4000) else 1)
|
|
if p+2+lw > end: return None
|
|
if lw==1: ln = d[p+2]
|
|
elif lw==2: ln = struct.unpack_from("<H", d, p+2)[0]
|
|
else: ln = struct.unpack_from("<I", d, p+2)[0]
|
|
body = p+2+lw
|
|
return cid, body, ln, body+ln
|
|
def walk(p, end, patch):
|
|
while p < end:
|
|
r = read_chunk(p, end)
|
|
if not r: break
|
|
cid, body, ln, nxt = r
|
|
if cid == 0x42: # new patch
|
|
patch = {"material":"", "punch":False, "verts":[], "faces":[]}
|
|
patches.append(patch)
|
|
walk(body, nxt, patch)
|
|
elif cid == 0x2030: # material name (string)
|
|
if patch is not None:
|
|
patch["material"] = d[body:body+ln].split(b'\0')[0].decode('latin1','ignore')
|
|
elif cid == 0x2037: # SV_SPECIAL -> PUNCH?
|
|
if patch is not None and b"PUNCH" in d[body:body+ln]:
|
|
patch["punch"] = True
|
|
elif cid in VSTRIDE: # vertex chunk -> this submesh's face indices are
|
|
st = VSTRIDE[cid]; hasn = cid in VHASN; hasuv = cid in VHASUV
|
|
patch["_base"] = len(patch["verts"]) # local to THIS chunk (base offset)
|
|
n = ln // st
|
|
for i in range(n):
|
|
o = body + i*st
|
|
x,y,z = struct.unpack_from("<fff", d, o)
|
|
nx=ny=nz=0.0; u=v=0.0; off=12
|
|
if hasn:
|
|
nx,ny,nz = struct.unpack_from("<fff", d, o+off); off+=12
|
|
if hasuv:
|
|
u,v = struct.unpack_from("<ff", d, o+off)
|
|
patch["verts"].append((x,y,z,hasn,nx,ny,nz,u,v))
|
|
elif cid == 0x47: # tri index list (indices local to last vertex chunk)
|
|
b = patch.get("_base",0); n = ln//4
|
|
idx = [struct.unpack_from("<I", d, body+i*4)[0] for i in range(n)]
|
|
for f in range(0,(n//3)*3,3):
|
|
patch["faces"].append((b+idx[f],b+idx[f+1],b+idx[f+2]))
|
|
elif cid == 0x4d: # poly list (ppf-prefixed)
|
|
if ln>=1:
|
|
b = patch.get("_base",0); ppf = d[body]; n=(ln-1)//4
|
|
idx=[struct.unpack_from("<I",d,body+1+i*4)[0] for i in range(n)]
|
|
for f in range(0,(n//ppf)*ppf,ppf):
|
|
poly=[idx[f+j] for j in range(ppf)]
|
|
for k in range(1,ppf-1):
|
|
patch["faces"].append((b+poly[0],b+poly[k],b+poly[k+1]))
|
|
elif cid in CONTAINERS:
|
|
walk(body, nxt, patch)
|
|
p = nxt
|
|
walk(8, len(d), None)
|
|
return [pt for pt in patches if pt["verts"] and pt["faces"]]
|
|
|
|
if __name__ == "__main__":
|
|
import sys, numpy as np
|
|
from collections import Counter
|
|
path = sys.argv[1] if len(sys.argv)>1 else "content/VIDEO/GEO/MAX_COP.BGF"
|
|
P = parse(path)
|
|
print(f"{path}: {len(P)} patches")
|
|
allV=[]; ec=Counter(); tot_faces=0; punch_faces=0
|
|
for i,pt in enumerate(P):
|
|
V=np.array([(v[0],v[1],v[2]) for v in pt["verts"]])
|
|
hasuv = any(v[7]!=0 or v[8]!=0 for v in pt["verts"])
|
|
hasn = any(v[3] for v in pt["verts"])
|
|
F=pt["faces"]
|
|
lo=V.min(0); hi=V.max(0); cen=V.mean(0)
|
|
for a,b,c in F:
|
|
if max(a,b,c)>=len(V): continue
|
|
for e in ((a,b),(b,c),(c,a)): ec[tuple(sorted(e))]+=1
|
|
tot_faces+=len(F)
|
|
if pt["punch"]: punch_faces+=len(F)
|
|
print(f" patch{i}: mtl='{pt['material'][:30]}' punch={pt['punch']} "
|
|
f"verts={len(V)} faces={len(F)} uv={hasuv} n={hasn} "
|
|
f"cen=({cen[0]:.2f},{cen[1]:.2f},{cen[2]:.2f}) "
|
|
f"bbox=[{lo[0]:.2f},{lo[1]:.2f},{lo[2]:.2f}]..[{hi[0]:.2f},{hi[1]:.2f},{hi[2]:.2f}]")
|
|
bound=sum(1 for k,vv in ec.items() if vv==1)
|
|
print(f"TOTAL faces={tot_faces} punch_faces={punch_faces} boundary_edges={bound}/{len(ec)}")
|