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
+64 -8
View File
@@ -679,15 +679,23 @@ void
if (currentPositionPointer != NULL)
{
Point3D center = *currentPositionPointer; // FUN_00408440(local_5c, ...)
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center)
// TRANSCRIPTION FIX #2 (the invisible-pip bug): FUN_0040adec writes
// ONLY the 3x3 rotation elements ([0..2],[4..6],[8..10]) -- it never
// touches the translation row. The binary therefore ends with
// view = [R(q) | center]: rotation SET, center PRESERVED. Our old
// `view *= yaw` was the engine's full COMPOSE, which also rotated the
// translation (view = [R | center x R]) -- the subsequent Invert then
// mapped center x R to the origin instead of the viewer, so every
// blip landed hundreds of pixels off-scope. Rotation first
// (rotation-only assignment), translation row LAST.
if (currentAngularPointer != NULL)
{
if (!rockAndRoll)
{
//
// Top-down: keep heading only. Extract the yaw, build a
// yaw-only quaternion and concatenate it.
// yaw-only quaternion, SET the rotation block from it.
//
EulerAngles euler;
euler = *currentAngularPointer; // FUN_0040954c
@@ -695,21 +703,27 @@ void
SinCosPair sc;
sc = Radian(heading); // FUN_00408328
Quaternion yaw(0.0f, sc.sine, 0.0f, sc.cosine); // FUN_00409948
view *= yaw; // FUN_0040adec
view = yaw; // rotation-only (FUN_0040adec semantics)
}
else
{
//
// "Rock and roll": use the full orientation matrix.
// "Rock and roll": use the full orientation.
//
AffineMatrix full;
full = *currentAngularPointer; // FUN_00409968
view *= full; // FUN_0040adec
view = *currentAngularPointer; // rotation-only build
}
}
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center) -- translation LAST
}
worldToView = view; // FUN_0040b244(this+0x324, view)
// TRANSCRIPTION FIX (the invisible-pip bug, 2026-07-12): FUN_0040b244 is
// the full affine INVERSE (cofactor expansion + determinant divide,
// part_001.c:172), not a copy. `view` above is the OPERATOR's local->
// world pose (translate to the mech + yaw); the radar needs its inverse
// -- world->scope, center subtracted, heading unrotated. Read as a copy,
// every pip/name transformed to (world + center) x ppm = thousands of
// pixels off-scope: the scope drew empty at all times.
worldToView.Invert(view); // FUN_0040b244(this+0x324, view)
//
// Bake the metres->pixels scale into the matrix.
@@ -717,6 +731,18 @@ void
Vector3D scale;
scale.x = scale.y = scale.z = pixelsPerMeter;
worldToView.Multiply(worldToView, scale); // FUN_0040b374
if (getenv("BT_MAP_LOG"))
{
static int s_m = 0;
if ((s_m++ % 30) == 0)
DEBUG_STREAM << "[map] w2v ppm=" << pixelsPerMeter
<< " halfW=" << halfWidth << " scale=" << currentScale
<< " m=[" << worldToView(0,0) << "," << worldToView(0,1) << "," << worldToView(0,2)
<< " | " << worldToView(1,0) << "," << worldToView(1,1) << "," << worldToView(1,2)
<< " | " << worldToView(2,0) << "," << worldToView(2,1) << "," << worldToView(2,2)
<< " | t " << worldToView(3,0) << "," << worldToView(3,1) << "," << worldToView(3,2)
<< "]\n" << std::flush;
}
BuildRadarShadow(worldToView); // FUN_004c228c
@@ -803,6 +829,14 @@ void
{
continue;
}
// VISIBLE-RADIUS cull (port): the capability radius above (RadarPercent
// x 4000 m) exceeds the SCOPE radius (currentScale/2 at the current
// zoom); an out-of-scope draw lands beyond the port rect on the shared
// gauge surface. Skip what the scope can't show.
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
{
continue;
}
Pip *pip = (pips != NULL)
? pips->LookUpPip(entity->GetResourceID()) // FUN_004688ce(pips+0x58,entity+0x1bc,0)
@@ -888,10 +922,23 @@ void
{
continue;
}
// VISIBLE-RADIUS cull (port; see DrawStatic): don't draw past the scope.
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
{
continue;
}
Pip *pip = (pips != NULL)
? pips->LookUpPip(entity->GetResourceID())
: NULL;
if (mlog)
{
Point3D sp;
sp.Multiply(EntityPosition(entity), worldToView);
DEBUG_STREAM << "[map] pip=" << (void*)pip
<< " screen=(" << sp.x << "," << sp.y << "," << sp.z << ")"
<< " lod=" << LODIndex << " mpp=" << metersPerPixel << "\n" << std::flush;
}
if (pip != NULL)
{
AffineMatrix blipXform;
@@ -987,6 +1034,15 @@ void
// "MapDisplay::DrawNames -- GetSize() / iterator mismatch"
// "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x408
}
else
{
// VISIBLE-RADIUS cull (port; see DrawStatic): a label for a
// contact beyond the scope would blit outside the port rect.
Vector3D d;
d.Subtract(EntityPosition(entity), viewingPosition);
if (d.x*d.x + d.z*d.z > currentScale * currentScale * 0.25f)
entity = NULL; // extract guards NULL -> no label
}
nameArray[i].ExtractFromEntity(entity, worldToView, owner, target); // FUN_004c19fc
}