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
+10
View File
@@ -579,7 +579,17 @@ Logical
if (s_devGaugesUpd)
{
if (!GuardedExecute())
{
// LOUD KILL (2026-07-12): the silent version made a faulted
// gauge simply FREEZE on screen (user-hit: a weapon panel's
// dial + lamp died mid-session with no trace). Name the
// casualty so the log convicts the faulting Execute.
DEBUG_STREAM << "[gauge-fault] '"
<< (identificationString ? identificationString : "?")
<< "' Execute FAULTED -> gauge DISABLED (this="
<< (void *)this << ")\n" << std::flush;
Disable(True);
}
}
else
{
+32 -4
View File
@@ -3628,15 +3628,24 @@ void
// Radar contacts are the DYNAMIC vehicles (mechs), not the props/terrain/
// projectiles that AllEntityIterator would flood the grid with -- iterate the
// dynamic masters (the local + AI/dummy vehicles) plus the dynamic replicants
// (peer vehicles in multiplayer). Static beacons/props (a separate
// classification) are left for a follow-up.
// (peer vehicles in multiplayer).
// CLASSIFICATION (the phantom-red-pip fix, 2026-07-12): the dynamic
// iterators ALSO surface non-Mover world entities (the map's CulturalIcon
// props, class 0x5E, register dynamic on this port) -- unsorted, they drew
// through the radar's MOVING loop as RED contacts ("multiple pips with one
// player"). Sort by Mover derivation: vehicles into the moving grid (red
// pips), everything else into the static grid (the dim silhouette layer
// DrawStatic draws) -- the follow-up the old note deferred.
{
HostManager::DynamicMasterEntityIterator master_iterator(host_manager);
master_iterator.First();
Entity *entity;
while ((entity = master_iterator.ReadAndNext()) != NULL)
{
movingEntities.Add(entity);
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
movingEntities.Add(entity);
else
staticEntities.Add(entity);
}
}
{
@@ -3645,7 +3654,10 @@ void
Entity *entity;
while ((entity = replicant_iterator.ReadAndNext()) != NULL)
{
movingEntities.Add(entity);
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
movingEntities.Add(entity);
else
staticEntities.Add(entity);
}
}
Check_Fpu();
@@ -3665,6 +3677,22 @@ void
Check(application->GetModeManager());
ModeMask
current_mode_mask = application->GetModeManager()->GetModeMask();
// DEV-COMPOSITE (2026-07-12, the frozen-dial fix): page-gated gauges (mode
// = their MFD page bit) freeze when their page isn't mode-active -- on the
// pod you FLIP pages, but the dev window shows every page surface at once,
// so six of seven weapon dials sat frozen on screen (user-hit; the arcs
// executed exactly 8 times at startup then never again). Under
// BT_DEV_GAUGES force the 15 MFD page-plane bits (BTL4 mode bits 0..14 =
// MFD1/2/3 Quad+Eng1-4) active so every visible page runs. The secondary
// trio (bits 18-20) stays authentic -- those views SHARE pixels and are
// exclusive by design.
{
static int s_devAllPages = -1;
if (s_devAllPages < 0)
s_devAllPages = (getenv("BT_DEV_GAUGES") != NULL) ? 1 : 0;
if (s_devAllPages)
current_mode_mask |= (ModeMask)0x7FFF;
}
ModeMask
previous_mode_mask = application->GetModeManager()->GetPreviousModeMask();
ModeMask
+13
View File
@@ -912,6 +912,19 @@ void
break;
}
}
if (getenv("BT_MAP_LOG"))
{
static int s_gi = 0;
if ((s_gi++ % 30) == 0)
{
DEBUG_STREAM << "[gauima] lodVal=" << LOD_value
<< " lodCount=" << LODCount << " chosen=" << lod_index
<< " scales={";
for (int gi = 0; gi < LODCount && gi < 4; ++gi)
DEBUG_STREAM << LODScales[gi] << (gi + 1 < LODCount ? "," : "");
DEBUG_STREAM << "} verts=" << vertexCount << "\n" << std::flush;
}
}
//-------------------------------------------------------------
// Continue if the index is within range
//-------------------------------------------------------------
+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);
+4 -1
View File
@@ -300,8 +300,11 @@ public:
// dstW,dstH). monoTint < 0 -> palette-expand (sec/radar, case-0 LUT); monoTint >= 0
// -> mono bit-plane expand ((word & mask) ? tint : 0) for an MFD plane. See
// BTDrawGaugeSurfaces (L4VB16.cpp).
// rotateCCW: 0 = as-is; 1/3 = display the texture turned 90 degrees (the
// pod's portrait-mounted secondary CRT; the dev panel unrotates it).
void 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 = 0);
};
//########################################################################