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>
127 lines
5.6 KiB
Python
127 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Render every mech's *_COP canopy from its AUTHORED eye position (SKL [jointeye]
|
|
translation, same torso-local frame as the _cop mesh), single-sided with per-face
|
|
INWARD winding -- the port's exact treatment -- with faces coloured by patch +
|
|
punch flag so we can see WHICH geometry blocks each mech's view.
|
|
|
|
frame colours: punch patches = warm (red/orange/yellow), non-punch = cool (blue/teal)
|
|
world (holes) = lavender background
|
|
Env: FWD=+z|-z (look direction, default -z), CULL=0 disables single-siding,
|
|
DROP_PUNCH=1 drops punch patches entirely.
|
|
"""
|
|
import re, os, math, sys
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("bgf_parse", os.path.join(os.path.dirname(__file__), "bgf_parse.py"))
|
|
bgf = importlib.util.module_from_spec(spec); spec.loader.exec_module(bgf)
|
|
|
|
ROOT = r"C:\git\bt411"
|
|
MECHS = { # model: (outside skl prefix, X-variant cop prefix)
|
|
"madcat": ("MAD", "MAX"), "bhk1": ("BLH", "BLX"), "thor": ("THR", "THX"),
|
|
"owens": ("OWN", "OWX"), "sunder": ("SND", "SNX"), "loki": ("LOK", "LOX"),
|
|
"vulture": ("VUL", "VUX"), "avatar": ("AVA", "AVX"),
|
|
}
|
|
W, H = 300, 225
|
|
PUNCH_COLS = [(200,60,50),(230,140,40),(230,210,60)] # warm = punch
|
|
FRAME_COLS = [(70,90,200),(60,170,180),(120,80,200)] # cool = non-punch
|
|
|
|
def jointeye(skl_path):
|
|
t = open(skl_path, encoding="latin1").read()
|
|
m = re.search(r"\[jointeye\]", t)
|
|
if not m: return None
|
|
blk = t[m.start():m.start()+400]
|
|
g = lambda k: float((re.search(rf"{k}=([-\d.e]+)", blk) or [None,"0"])[1])
|
|
return (g("tranx"), g("trany"), g("tranz"))
|
|
|
|
def render(model, out_path, fwd_sign=-1.0, cull=True, drop_punch=False):
|
|
pfx, xpfx = MECHS[model]
|
|
eye = jointeye(os.path.join(ROOT, "content", "VIDEO", pfx + ".SKL"))
|
|
P = bgf.parse(os.path.join(ROOT, "content", "VIDEO", "GEO", xpfx + "_COP.BGF"))
|
|
allv = np.array([v[:3] for pt in P for v in pt["verts"]])
|
|
cen = allv.mean(0)
|
|
e = np.array(eye, float)
|
|
f = np.array([0.0, 0.0, fwd_sign]); u = np.array([0.0, 1.0, 0.0])
|
|
r = np.cross(u, f); r /= np.linalg.norm(r); u2 = np.cross(f, r)
|
|
tanf = math.tan(math.radians(75)/2); aspect = W/H
|
|
|
|
img = np.zeros((H, W, 3), np.uint8)
|
|
for y in range(H): # lavender world bg
|
|
t = y/H
|
|
img[y,:] = (150,148,205) if t < 0.5 else (185,178,200)
|
|
zb = np.full((H, W), 1e9)
|
|
|
|
npi = nfi = 0
|
|
for pi, pt in enumerate(P):
|
|
V = pt["verts"]
|
|
if pt["punch"]:
|
|
col = PUNCH_COLS[npi % 3]; npi += 1
|
|
if drop_punch: continue
|
|
else:
|
|
col = FRAME_COLS[nfi % 3]; nfi += 1
|
|
for (a, b, c) in pt["faces"]:
|
|
if max(a, b, c) >= len(V): continue
|
|
tri = np.array([V[a][:3], V[b][:3], V[c][:3]])
|
|
n = np.cross(tri[1]-tri[0], tri[2]-tri[0]); nl = np.linalg.norm(n)
|
|
if nl < 1e-9: continue
|
|
n /= nl
|
|
# per-face INWARD orientation (the loader's rule): normal toward mesh centroid
|
|
fc = tri.mean(0)
|
|
inward = n if np.dot(n, cen - fc) >= 0 else -n
|
|
if cull and np.dot(inward, e - fc) <= 0: # single-sided: cull faces facing away from the eye
|
|
continue
|
|
s = []; ok = True
|
|
for p in tri:
|
|
d = p - e; cx = np.dot(d, r); cy = np.dot(d, u2); cz = np.dot(d, f)
|
|
if cz <= 0.02: ok = False; break
|
|
s.append((cx/(cz*tanf*aspect)*0.5*W + 0.5*W, -cy/(cz*tanf)*0.5*H + 0.5*H, cz))
|
|
if not ok: continue
|
|
shade = 0.55 + 0.45*abs(np.dot(inward, f))
|
|
cc = tuple(min(255, int(ch*shade)) for ch in col)
|
|
raster(img, zb, s, cc)
|
|
im = Image.new("RGB", (W, H+22), (18,18,22)); im.paste(Image.fromarray(img), (0,22))
|
|
d = ImageDraw.Draw(im)
|
|
d.text((4,5), f"{model} eye=({eye[0]:+.2f},{eye[1]:.2f},{eye[2]:+.2f}) fwd={'-Z' if fwd_sign<0 else '+Z'}", fill=(235,235,240))
|
|
im.save(out_path)
|
|
return eye
|
|
|
|
def raster(img, zb, s, col):
|
|
(ax,ay,az),(bx,by,bz),(cx,cy,cz) = s
|
|
x0=max(0,int(min(ax,bx,cx))); x1=min(W-1,int(max(ax,bx,cx)))
|
|
y0=max(0,int(min(ay,by,cy))); y1=min(H-1,int(max(ay,by,cy)))
|
|
den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
|
|
if abs(den)<1e-6 or x1<x0 or y1<y0: return
|
|
for py in range(y0,y1+1):
|
|
for px in range(x0,x1+1):
|
|
w0=((by-cy)*(px+.5-cx)+(cx-bx)*(py+.5-cy))/den
|
|
w1=((cy-ay)*(px+.5-cx)+(ax-cx)*(py+.5-cy))/den
|
|
w2=1-w0-w1
|
|
if w0<0 or w1<0 or w2<0: continue
|
|
z=w0*az+w1*bz+w2*cz
|
|
if z<zb[py,px]: zb[py,px]=z; img[py,px]=col
|
|
|
|
if __name__ == "__main__":
|
|
fwd = -1.0 if os.getenv("FWD","-z") == "-z" else 1.0
|
|
cull = os.getenv("CULL","1") != "0"
|
|
dropp = os.getenv("DROP_PUNCH","0") == "1"
|
|
tag = sys.argv[1] if len(sys.argv) > 1 else "eyeview"
|
|
tiles = []
|
|
for m in MECHS:
|
|
p = os.path.join(ROOT, "scratchpad", f"ev_{m}.png")
|
|
render(m, p, fwd, cull, dropp)
|
|
tiles.append(np.array(Image.open(p)))
|
|
g = np.full((tiles[0].shape[0], 4, 3), 255, np.uint8)
|
|
rows = []
|
|
for i in range(0, len(tiles), 4):
|
|
row = []
|
|
for t in tiles[i:i+4]: row.append(t); row.append(g)
|
|
rows.append(np.concatenate(row[:-1], axis=1))
|
|
wmax = max(r.shape[1] for r in rows)
|
|
rows = [np.pad(r, ((0,0),(0,wmax-r.shape[1]),(0,0)), constant_values=18) for r in rows]
|
|
vg = np.full((4, wmax, 3), 255, np.uint8)
|
|
out = []
|
|
for r in rows: out.append(r); out.append(vg)
|
|
outp = os.path.join(ROOT, "scratchpad", f"{tag}.png")
|
|
Image.fromarray(np.concatenate(out[:-1], axis=0)).save(outp)
|
|
print("saved", outp)
|