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
+78 -17
View File
@@ -669,23 +669,24 @@ void
}
//
// Pop a "destroyed by <killer>" status message onto the pilot HUD.
// Gated on a real attacker player (+ the status pool, a NULL stub in
// bring-up), so the ownerless-dummy solo path skips it (no player to name).
// Pop a "destroyed" status message naming the victim onto the comm
// ticker (MessageBoard feed, LIVE 2026-07-12). ALLOCATION NOTE: the
// binary drew from the MemoryBlock pool @00512f6c and spliced the BT
// vtable; our engine's StatusMessageUpdate reclaims each expired
// message with Unregister_Object + plain delete (PLAYER.cpp:864), so
// the matching allocation here is the engine ctor + Register_Object
// -- same record {playerInvolved, messageType 0, 6.0s}, same queue.
// Gated on a real named player (the ownerless dummy has none).
//
if (sender_owner && StatusMessagePool)
if (sender_owner)
{
StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c)
if (status)
{
new (status) Player__StatusMessage(
sender_owner, // *(sender+0x190)
0,
STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f
);
*(int *)((char *)status + 0x18) = 0; // status[6] = 0
AddStatusMessage(status); // FUN_0042e580
}
Player__StatusMessage *status = new Player__StatusMessage(
sender_owner, // *(sender+0x190) -- the victim's player
0, // message strip 0 (the kill cell)
STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f
);
Register_Object(status);
AddStatusMessage(status); // FUN_0042e580
}
//
@@ -1359,7 +1360,41 @@ int BTPilotKills(void *pilot)
DEBUG_STREAM << "[score] gauge read: pilot=" << pilot << " kills=" << k << "\n" << std::flush;
return k;
}
int BTPilotDeaths(void *pilot) { return pilot ? ((BTPlayer *)pilot)->GetDeaths() : 0; }
// DEATHS display (MP DEATHS fix): the engine seeds Player::deathCount at -2
// (PLAYER.cpp:759) and only the LOCAL vehicle-acquire path zeroes it -- a REMOTE
// player's copy on this node keeps the raw pre-acquire value (-2/-1) forever,
// which the scoreboard showed verbatim ("DEATHS -1", user-reported). Those
// negatives are the engine's internal spawn-cycle convention, not real deaths;
// clamp them to the observable truth: no deaths yet = 0.
int BTPilotDeaths(void *pilot)
{
if (pilot == 0)
return 0;
int deaths = ((BTPlayer *)pilot)->GetDeaths();
return (deaths < 0) ? 0 : deaths;
}
// OBSERVED-DEATH tally (MP DEATHS fix): each node maintains every player's
// score from LOCALLY OBSERVED events -- exactly how the KILLS credit already
// works (the victim's ScoreMessage handler ++s the SHOOTER's local Player
// copy). This is the symmetric half: when a REPLICANT mech dies here (the
// once-per-death transition runs on every node), tally the death onto its
// owning player's LOCAL copy. The owner's own node counts through its real
// VehicleDead(-1) path, so the caller gates this to replicant mechs -- each
// node then counts every pilot self-consistently. Normalize the -2/-1
// pre-acquire seed first so death #1 reads 1.
void BTPlayerCountObservedDeath(void *player)
{
if (player == 0)
return;
BTPlayer *p = (BTPlayer *)player;
if (p->deathCount < 0)
p->deathCount = 0;
++p->deathCount;
if (getenv("BT_SCORE_LOG"))
DEBUG_STREAM << "[score] observed death -> pilot " << player
<< " deaths=" << p->deathCount << "\n" << std::flush;
}
// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row
// highlight). Computed here via accessors -- the PilotList's raw-offset version
@@ -1388,11 +1423,37 @@ int BTPilotIsSelected(void *pilot, void *local_player)
// compiled BTPlayer/Player__StatusMessage members (decode AddStatusMessage @0042e580).
// A real (stub) definition is REQUIRED or the /FORCE link AVs MessageBoard::Execute's call.
class BitMap;
// MessageBoard feed (LIVE 2026-07-12): the board is the LOCAL cockpit's comm
// ticker -- read the mission player's engine status queue. statusMessagePointer
// is maintained per frame by Player::StatusMessageUpdate [T0 PLAYER.cpp:520/822]:
// it holds the newest queued Player__StatusMessage until its 6s displayTime runs
// out. The name bitmap resolves through the SAME small-raster chain the radar
// labels use (Mission::GetSmallNameBitmap keyed by the involved player's egg
// bitmapindex).
Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap **nameBitmap)
{
*messageId = -1;
*nameBitmap = 0;
return False;
BTPlayer *local_player = (application != 0)
? (BTPlayer *)application->GetMissionPlayer() : 0;
if (local_player == 0)
{
return False;
}
Player::StatusMessage *m = local_player->statusMessagePointer;
if (m == 0)
{
return False;
}
*messageId = m->messageType;
Player *involved = m->playerInvolved;
if (involved != 0)
{
Mission *mission = application->GetCurrentMission();
if (mission != 0)
*nameBitmap = mission->GetSmallNameBitmap(involved->playerBitmapIndex);
}
return True;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~