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
+120 -3
View File
@@ -68,8 +68,49 @@
// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp, a
// complete-Mech TU with the render + damage path). A fired missile becomes a tracked
// projectile that flies to the target + delivers this weapon's damage on impact.
// Missile-arc wave: launch_velocity = the authored MuzzleVelocity VECTOR in the
// shooter's frame (up-tilted rack launch); damage <= 0 = a VISUAL-ONLY round.
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
const Point3D &targetPos, Scalar speed, Scalar damage);
const Point3D &targetPos, Scalar speed, Scalar damage,
const Vector3D *launch_velocity = 0, int guided = 1);
//###########################################################################
// Port-side salvo replication state (missile-visibility wave).
//
// The launcher OBJECT is byte-locked at 0x44C (factory alloc) -- salvo state
// cannot live on it. Keyed by launcher identity: on a MASTER, `fired` counts
// FireWeapon salvos (serialized in the extended update record); on a
// REPLICANT, `seen` tracks the last counter applied (-1 = unsynced, so a
// freshly-joined peer does not replay the master's historical count).
//###########################################################################
namespace {
struct BTSalvoState
{
const void *owner;
int fired; // master: salvos launched
int seen; // replicant: last counter applied (-1 = unsynced)
Point3D target; // master: the last salvo's aim point
};
BTSalvoState gSalvoTable[64];
BTSalvoState &BTSalvoOf(const void *launcher)
{
int freeSlot = -1;
for (int i = 0; i < 64; ++i)
{
if (gSalvoTable[i].owner == launcher)
return gSalvoTable[i];
if (gSalvoTable[i].owner == 0 && freeSlot < 0)
freeSlot = i;
}
BTSalvoState &s = gSalvoTable[(freeSlot >= 0) ? freeSlot : 0];
s.owner = launcher;
s.fired = 0;
s.seen = -1;
s.target = Point3D(0.0f, 0.0f, 0.0f);
return s;
}
}
//###########################################################################
//###########################################################################
@@ -258,15 +299,91 @@ void MissileLauncher::FireWeapon()
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
// Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target
// slot isn't populated (bring-up); missileCount missiles per salvo.
// slot isn't populated (bring-up); missileCount missiles per salvo. The authored
// MuzzleVelocity VECTOR rides along (missile-arc wave): the round leaves the rack
// on the authored up-tilt, then the seeker loft + steering arc it onto the target.
int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
for (int i = 0; i < nmiss; ++i)
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount);
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
&launchVelocity);
// Salvo replication (missile-visibility wave): bump the fire counter + stamp
// the aim point; the extended update record carries both to peer nodes.
{
BTSalvoState &s = BTSalvoOf(this);
++s.fired;
s.target = targetPos;
}
simulationFlags |= 0x1; // replication-dirty
// ENQUEUE the record (visibility fix): the dirty flag alone never
// serializes -- ForceUpdate sets the updateModel bit that makes the next
// PerformAndWatch write THIS subsystem's record into the mech's update
// stream (the exact mechanism the Emitter's beam replication uses,
// emitter.cpp:512). Without it the peer never saw a salvo record at all.
ForceUpdate();
Check_Fpu();
}
//#############################################################################
// WriteUpdateRecord / ReadUpdateRecord (PORT record extension -- missile-
// visibility wave; see the mislanch.hpp banner). The master serializes the
// salvo counter + aim point after the MechWeapon base fields; the replicant
// edge-detects the counter and mirrors the salvo as VISUAL-ONLY rounds
// (damage 0 -- the master's own rounds deliver the damage cross-pod).
//
void
MissileLauncher::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
{
MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields)
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
BTSalvoState &s = BTSalvoOf(this);
rec->recordLength = sizeof(MissileLauncher__UpdateRecord);
rec->salvoCounter = s.fired;
rec->salvoRounds = missileCount;
rec->salvoTarget = s.target;
}
void
MissileLauncher::ReadUpdateRecord(Simulation__UpdateRecord *message)
{
MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply)
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
BTSalvoState &s = BTSalvoOf(this);
if (s.seen < 0)
{
// first sync after spawn/join: adopt the master's count silently --
// no replay of salvos fired before we could see them.
s.seen = rec->salvoCounter;
}
else if (rec->salvoCounter != s.seen)
{
s.seen = rec->salvoCounter;
// Mirror ONE salvo (a missed record collapses to one -- catch-up
// replays would draw stale phantom volleys). Rounds fly the same
// authored launch vector + seeker-loft arc as the master's, from
// this replicant's own resolved rack ports, damage 0.
Point3D mz;
GetMuzzlePoint(mz); // @004b9948
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
int n = rec->salvoRounds;
if (n < 1 || n > 40)
n = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
for (int i = 0; i < n; ++i)
BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/,
rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity);
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
<< "," << rec->salvoTarget.z << ")\n" << std::flush;
}
}
//#############################################################################
// TakeDamage / DeathReset
//