Files
BT411/scratchpad/fxshot.py
T
arcattackandClaude Fable 5 065c114590 Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed
The "we're off the rails" reinvestigation -- a 5-thread evidence workflow
(logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware
decode, then surgical fixes.  Every claim tiered; the wrong turns are on the
record in the KB alongside the corrections.

THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]:
rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch
(data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698,
instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board
frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 =
the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b
x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap;
y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance",
INI authors "varience").  The 13 descriptors (PPCHit/LaserHit/MissileHit/
Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx
layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps
GRAYSCALE (authored colours: DNBOOM orange, DDAM gray).

HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11]
canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical.
Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx.

"SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5
relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the
emitter trusted rate -> 5 shells at exactly 1Hz.  Emission rate now always
maxIssue/releasePeriod.

PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own
ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12
fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only --
the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the
phantom "4th gold beam"); replicant salvos detonate too (launcher index rides
the visual push).  Missile damage bundles through the shooter's messmgr with
the launcher's subsystemID (mslhit fires at the consolidated point; the
binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record
claim REFUTED by direct decomp read).

DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on
any damage rise (the binary's changed-flag coalescing) -- a mauled mech under
fire smokes/burns per hit; bands 3/4 are authored fire plumes.

THE RAM ECONOMY -- CLOSED (3 layers, measured live):
 1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140;
    the [zone-armor] BT_DMG_LOG dump);
 2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts
    @10m/s; the binary dispatches it RAW but pod MP dropped it on the local
    replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our
    task-#47 replicant forwarding surfaced it as a one-shot.  Port
    normalizes x1e-3 to the point economy [T3];
 3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made
    separation implicit; our gait-derived velocity re-priced full rams at
    60Hz (the respawn explode-loop).  One bump = one hit; pressed = BLOCK.

PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3];
particle pool 2048->8192 (missile traffic starved laser bursts -- the
"intermittent effects"); replicant beam aging (a lost beam-END record pinned
a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap);
no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py
self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry).

KB: rendering.md (specialfx engine + census + closeout), combat-damage.md
(ram economy + band semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 08:14:12 -05:00

57 lines
2.1 KiB
Python

"""Capture timed screenshots of a btl4 window via PrintWindow (works occluded).
Usage: python fxshot.py [count] [interval_s] [outprefix] [nodesubstr]
"""
import ctypes, ctypes.wintypes as wt, sys, time
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
count = int(sys.argv[1]) if len(sys.argv) > 1 else 8
interval = float(sys.argv[2]) if len(sys.argv) > 2 else 0.4
prefix = sys.argv[3] if len(sys.argv) > 3 else 'fx'
want = sys.argv[4] if len(sys.argv) > 4 else '1501'
titles = []
def enum_cb(hwnd, lparam):
n = user32.GetWindowTextLengthW(hwnd)
if n > 0:
buf = ctypes.create_unicode_buffer(n + 1)
user32.GetWindowTextW(hwnd, buf, n + 1)
if buf.value.startswith('BattleTech') and want in buf.value:
titles.append((hwnd, buf.value))
return True
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wt.HWND, wt.LPARAM)
user32.EnumWindows(WNDENUMPROC(enum_cb), 0)
if not titles:
print('no matching BattleTech window'); sys.exit(1)
hwnd, title = titles[0]
print('capturing:', title)
rect = wt.RECT()
user32.GetClientRect(hwnd, ctypes.byref(rect))
w, h = rect.right, rect.bottom
hdc = user32.GetDC(0)
mem = gdi32.CreateCompatibleDC(hdc)
bmp = gdi32.CreateCompatibleBitmap(hdc, w, h)
gdi32.SelectObject(mem, bmp)
class BMIH(ctypes.Structure):
_fields_ = [('biSize', wt.DWORD), ('biWidth', wt.LONG), ('biHeight', wt.LONG),
('biPlanes', wt.WORD), ('biBitCount', wt.WORD), ('biCompression', wt.DWORD),
('biSizeImage', wt.DWORD), ('biXPelsPerMeter', wt.LONG), ('biYPelsPerMeter', wt.LONG),
('biClrUsed', wt.DWORD), ('biClrImportant', wt.DWORD)]
from PIL import Image
for i in range(count):
# PW_RENDERFULLCONTENT (2) captures D3D content
user32.PrintWindow(hwnd, mem, 2 | 1) # +PW_CLIENTONLY
bmi = BMIH(ctypes.sizeof(BMIH), w, -h, 1, 32, 0, 0, 0, 0, 0, 0)
buf = ctypes.create_string_buffer(w * h * 4)
gdi32.GetDIBits(mem, bmp, 0, h, buf, ctypes.byref(bmi), 0)
img = Image.frombuffer('RGB', (w, h), buf.raw, 'raw', 'BGRX', 0, 1)
img.save(r'C:\git\bt411\scratchpad\%s_%d.png' % (prefix, i))
time.sleep(interval)
print('saved %d shots %dx%d' % (count, w, h))