MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker

The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-12 17:24:15 -05:00
co-authored by Claude Fable 5
parent dd27238ceb
commit bb795e2805
30 changed files with 1006 additions and 97 deletions
+123 -19
View File
@@ -127,7 +127,8 @@ static bool DevGaugeDocked()
// render-state save/restore is needed (the next frame resets device state).
//===========================================================================//
void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
int monoTint, float dstX, float dstY, float dstW, float dstH)
int monoTint, float dstX, float dstY, float dstW, float dstH,
int rotateCCW)
{
int w = pixelBuffer.Data.Size.x; // 640
int h = pixelBuffer.Data.Size.y; // 480
@@ -181,6 +182,10 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
}
// Draw the surface quad at (dstX,dstY,dstW,dstH) in the current render target.
// rotateCCW: the pod's SECONDARY screen was a physically ROTATED portrait
// CRT -- the game authors its content sideways in the landscape buffer and
// the cabinet monitor unrotated it. On the dev panel WE unrotate: display
// the texture turned 90 degrees so the radar/secondary reads upright.
struct InsetVert { float x, y, z, rhw, u, v; };
InsetVert quad[4] =
{
@@ -189,8 +194,26 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
{ dstX + dstW, dstY + dstH, 0.0f, 1.0f, 1.0f, 1.0f },
{ dstX, dstY + dstH, 0.0f, 1.0f, 0.0f, 1.0f },
};
if (rotateCCW == 1) // screen corner <- texture corner, turned CCW
{
quad[0].u = 1.0f; quad[0].v = 0.0f; // TL <- TR
quad[1].u = 1.0f; quad[1].v = 1.0f; // TR <- BR
quad[2].u = 0.0f; quad[2].v = 1.0f; // BR <- BL
quad[3].u = 0.0f; quad[3].v = 0.0f; // BL <- TL
}
else if (rotateCCW == 3) // the other direction (CW), env-selectable
{
quad[0].u = 0.0f; quad[0].v = 1.0f; // TL <- BL
quad[1].u = 0.0f; quad[1].v = 0.0f; // TR <- TL
quad[2].u = 1.0f; quad[2].v = 0.0f; // BR <- TR
quad[3].u = 1.0f; quad[3].v = 1.0f; // BL <- BR
}
device->SetTexture(0, tex);
device->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
// LINEAR sampling: the cells scale the 8-bit surfaces (sec runs 0.75x at
// scale 1.0); point sampling drops the radar's 1px strokes entirely.
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
@@ -218,18 +241,27 @@ struct BTGaugeSurfaceDesc
int monoTint; // -1 = palette (sec); else R5G6B5 tint
float cellX, cellY; // normalized cell origin within the panel (y down)
float cellW, cellH; // normalized cell size
int rotateCCW; // 1 = unrotate (portrait secondary CRT)
};
// PANEL LAYOUT (2026-07-12 rework): the old 3x2 equal grid DOWNSCALED every
// 640x480 surface into a 320x192 cell (blurry) and showed the SECONDARY
// (radar) sideways -- on the pod that screen was a physically ROTATED
// portrait CRT. New layout, logical 1320x480: a 2x2 block of mono MFDs on
// the left at 320x240 (correct 4:3), the secondary UNROTATED in a portrait
// 360x480 cell in the middle, Comm top-right. Scale the whole window with
// BT_GAUGE_SCALE (e.g. 1.5) for more pixels -- content is a 1995 8-bit
// buffer, so past ~1.5x you magnify, not sharpen.
static const float kBTPanelW = 1320.0f;
static const float kBTPanelH = 480.0f;
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[6] =
{
// top row: Heat (UL) | Mfd2 (UC) | Comm (UR)
{ "Heat", 0xFFFF, 0.0f / 3, 0.0f, 1.0f / 3, 0.5f },
{ "Mfd2", 0xFFFF, 1.0f / 3, 0.0f, 1.0f / 3, 0.5f },
{ "Comm", 0xFFFF, 2.0f / 3, 0.0f, 1.0f / 3, 0.5f },
// bottom row: Mfd1 (LL) | sec/radar (center) | Mfd3 (LR)
{ "Mfd1", 0xFFFF, 0.0f / 3, 0.5f, 1.0f / 3, 0.5f },
{ "sec", -1, 1.0f / 3, 0.5f, 1.0f / 3, 0.5f },
{ "Mfd3", 0xFFFF, 2.0f / 3, 0.5f, 1.0f / 3, 0.5f },
{ "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
{ "Mfd2", 0xFFFF, 320.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
{ "Mfd1", 0xFFFF, 0.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 },
{ "Mfd3", 0xFFFF, 320.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 },
{ "sec", -1, 640.0f / kBTPanelW, 0.0f, 360.0f / kBTPanelW, 1.0f, 1 },
{ "Comm", 0xFFFF, 1000.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
};
//
@@ -243,6 +275,16 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
GaugeRenderer *gr = application ? application->GetGaugeRenderer() : NULL;
if (gr == NULL) return;
// DEVICE-STATE ISOLATION (the "floating cavern rocks" bug): the per-surface
// quad draw disables Z (D3DRS_ZENABLE=FALSE), culling and lighting, binds a
// pre-transformed FVF and rewrites texture stage 0 -- and the world pass
// does NOT re-assert these each frame, so the NEXT frame's world rendered
// WITHOUT DEPTH TESTING (geometry resolved in submission order; rock
// columns drew "floating / unconnected"). Snapshot the whole device state
// and restore it after the composite -- dev-only path, cost irrelevant.
IDirect3DStateBlock9 *stateBlock = NULL;
device->CreateStateBlock(D3DSBT_ALL, &stateBlock);
// Reach the shared display once (any present port shares the same SVGA16/pixelBuffer).
SVGA16 *svga = NULL;
for (int i = 0; i < 6 && svga == NULL; i++)
@@ -257,9 +299,30 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
const BTGaugeSurfaceDesc &d = kBTGaugeSurfaces[i];
L4GraphicsPort *port = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(d.portName));
if (port == NULL) continue; // this surface's port isn't configured -> skip
// Secondary-CRT unrotation direction is env-flippable (BT_GAUGE_SEC_ROT=3
// for the other way) in case the assumed pod mounting is mirrored.
int rot = d.rotateCCW;
if (rot != 0)
{
static int s_secRot = -1;
if (s_secRot < 0)
{
// Default 3 (CW): user-verified upright (the first guess, 1,
// showed the secondary upside down). Env still overrides.
const char *rv = getenv("BT_GAUGE_SEC_ROT");
s_secRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
}
rot = s_secRot;
}
svga->DrawDevSurface(
device, i, port->GetBitMask(), port->paletteID, d.monoTint,
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph);
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph, rot);
}
if (stateBlock != NULL)
{
stateBlock->Apply();
stateBlock->Release();
}
}
@@ -273,7 +336,8 @@ void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
{
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
// DOCKED mode only: draw into the main backbuffer (this runs before EndScene).
BTDrawGaugeSurfaces(device, 2.0f, 300.0f, 540.0f, 296.0f);
// Keep the new panel's 1320:480 aspect (2.75) so nothing distorts.
BTDrawGaugeSurfaces(device, 2.0f, 400.0f, 540.0f, 540.0f / 2.75f);
}
//===========================================================================//
@@ -286,13 +350,32 @@ void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
//===========================================================================//
static HWND s_gaugeHwnd = NULL;
static IDirect3DSwapChain9 *s_gaugeSwap = NULL;
static const int s_gaugeW = 960;
static const int s_gaugeH = 384;
// Window = the logical 1320x480 panel (see kBTGaugeSurfaces) x BT_GAUGE_SCALE.
// Resolved once at first ensure; default 1.0 (surfaces at/near native pixels).
static int s_gaugeW = 1320;
static int s_gaugeH = 480;
static bool s_gaugeSized = false;
static void BTGaugeResolveSize()
{
if (s_gaugeSized) return;
s_gaugeSized = true;
const char *sv = getenv("BT_GAUGE_SCALE");
float scale = (sv != NULL) ? (float)atof(sv) : 1.0f;
if (scale < 0.5f || scale > 3.0f) scale = 1.0f;
s_gaugeW = (int)(1320.0f * scale);
s_gaugeH = (int)(480.0f * scale);
}
static LRESULT CALLBACK BTGaugeWndProc(HWND h, UINT m, WPARAM w, LPARAM l)
{
if (m == WM_CLOSE) { ShowWindow(h, SW_HIDE); return 0; } // hide, never quit the app
return DefWindowProc(h, m, w, l);
// Fully-WIDE window (class + create + proc all W; the class/create below
// are RegisterClassW/CreateWindowExW): mixed A/W pairings mangled the
// title -- A-class + W-proc painted the ANSI bytes as UTF-16 (CJK-looking
// mojibake), W-class + A-proc truncated at the first thunked NUL ("B").
// One consistent character set ends the thunk roulette.
return DefWindowProcW(h, m, w, l);
}
// Lazily create the window + its additional swap chain (on the MAIN device).
@@ -301,26 +384,47 @@ static bool BTGaugeWindowEnsure(LPDIRECT3DDEVICE9 device)
if (s_gaugeSwap != NULL) return true;
if (device == NULL) return false;
BTGaugeResolveSize(); // BT_GAUGE_SCALE -> window dims
HINSTANCE hInst = GetModuleHandle(NULL);
static bool s_classReg = false;
if (!s_classReg)
{
WNDCLASSA wc;
// WIDE APIs throughout: this TU compiles UNICODE, so BTGaugeWndProc's
// DefWindowProc resolves to the W variant -- registering the class
// with RegisterClassA made Windows reinterpret the ANSI title bytes
// as UTF-16 (the "Korean garbage title" bug: byte-pairs of
// "BattleTech - Cockpit MFDs" land in the CJK plane, only the
// trailing 's'+NUL survives readable).
WNDCLASSW wc;
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = BTGaugeWndProc;
wc.hInstance = hInst;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
wc.lpszClassName = "BTGaugeWnd";
RegisterClassA(&wc);
wc.lpszClassName = L"BTGaugeWnd";
RegisterClassW(&wc);
s_classReg = true;
}
if (s_gaugeHwnd == NULL)
{
RECT r = { 0, 0, s_gaugeW, s_gaugeH };
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
s_gaugeHwnd = CreateWindowExA(
0, "BTGaugeWnd", "BattleTech - Cockpit MFDs", WS_OVERLAPPEDWINDOW,
// Window identity (MP dev): tag with the -net port so two side-by-side
// nodes' MFD windows are distinguishable (matches the main window tag).
wchar_t gaugeTitle[64];
{
int netPort = 0;
const char *cmd = GetCommandLineA();
const char *np = (cmd != NULL) ? strstr(cmd, "-net") : NULL;
if (np != NULL)
sscanf(np + 4, " %d", &netPort);
if (netPort > 0)
swprintf(gaugeTitle, 64, L"BattleTech MFDs \x2014 node %d", netPort);
else
wcscpy(gaugeTitle, L"BattleTech - Cockpit MFDs");
}
s_gaugeHwnd = CreateWindowExW(
0, L"BTGaugeWnd", gaugeTitle, WS_OVERLAPPEDWINDOW,
40, 40, r.right - r.left, r.bottom - r.top, NULL, NULL, hInst, NULL);
if (s_gaugeHwnd == NULL) return false;
ShowWindow(s_gaugeHwnd, SW_SHOWNOACTIVATE);