Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them. The peer's replicant emitters never learned the master fired. THE AUTHENTIC PIPE (decomp-verified): - FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call) does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the app+0x34 manager -- our per-weapon render walk already plays that role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's updateModel / ForceUpdate(). - Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message: the roster walk already hands the entity's stream to every subsystem's PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested updateModel bit; Entity::UpdateMessageHandler routes received records by subsystemID to the subsystem's ReadUpdateRecord. All engine machinery -- the missing pieces were the Emitter's serialize/apply pair + the triggers. CORRECTIONS to the dormant task-33-era transcriptions (never exercised -- nothing ever set updateModel -- so the latent misreads never surfaced): - The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord, 7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry + body semantics; @004ba568 resolves an EntityID at rec+0x30 through the entity index -- record semantics, not Damage). Renamed across MechWeapon / Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is undecoded (inherits MechWeapon for now). - Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour -- the old `CopyColor(targetEntity+0x184)` was also a databinding trap. - OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing UpdateRecord typedef as the param type, silently NOT overriding the engine virtual (the base ran instead; nothing would ever have serialized). Base-typed params (Simulation__UpdateRecord*), casts inside. - Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields. - ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one final record at beam end (turns the peer's beam off). - Mech::DrawWeaponBeams extracted from the player-only drive block so the walk runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics would have served the player's segment pointers as the replicant's muzzles). VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all 225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun ports). Solo un-regressed (150 beams, kill chain, no crash). Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record types decoded (pose/alarm/leg-state+heat with the body-channel write-through re-sync, knockdown, death, impact, movementMode) -- transcription deferred; it replicates remote knockdown/death/heat and was not needed for beams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a110041a70
commit
29035028fd
+105
-82
@@ -1237,6 +1237,11 @@ void
|
||||
if (legCycleSpeed < -100.0f || legCycleSpeed > 200.0f)
|
||||
legCycleSpeed = 0.0f;
|
||||
(void)AdvanceLegAnimation(dt); // joints only; travel = DeadReckon
|
||||
|
||||
// REPLICANT BEAMS (task #51): the emitters carry live replicated
|
||||
// discharge state (Emitter::ReadUpdateRecord); draw them with the
|
||||
// same per-weapon walk the master uses.
|
||||
DrawWeaponBeams(dt);
|
||||
}
|
||||
|
||||
if (getenv("BT_REPL_LOG"))
|
||||
@@ -2461,88 +2466,7 @@ void
|
||||
// PPCs) instead of the old hardcoded single-look stagger, so every
|
||||
// mech type fires like its data says. (Old stagger/keepalive block
|
||||
// removed; see the git history for the bring-up scaffolding.)
|
||||
{
|
||||
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
|
||||
const Scalar ttl1 = (dt > 1e-4f) ? dt : 1e-4f; // one-frame life (redrawn while on)
|
||||
static int s_beamStateLog = 0;
|
||||
int energyOrdinal = -1; // Nth energy weapon (port assignment)
|
||||
for (int wi = 0; wi < GetSubsystemCount(); ++wi)
|
||||
{
|
||||
Subsystem *ws = GetSubsystem(wi);
|
||||
if (ws == 0)
|
||||
continue;
|
||||
// EXACT class filter: Emitter (0xBC8=3016) or PPC (0xBD4=3028).
|
||||
// The derivation check matched too broadly here (the recon
|
||||
// derivation chains are shared stubs for some subsystems --
|
||||
// a Sensor and the MissileLauncher passed and drew garbage
|
||||
// from misinterpreted offsets).
|
||||
const int wcid = (int)ws->GetClassID();
|
||||
if (wcid != 3016 && wcid != 3028)
|
||||
continue;
|
||||
++energyOrdinal;
|
||||
Emitter *em = (Emitter *)ws;
|
||||
if (!em->BeamOn())
|
||||
continue;
|
||||
Point3D mz;
|
||||
em->MuzzlePoint(mz); // LIVE muzzle (tracks the gun)
|
||||
// MOUNT FALLBACK: when the weapon's mount segment doesn't
|
||||
// resolve, GetMuzzlePoint returns the mech ORIGIN (feet).
|
||||
// Assign this weapon its own gun-port segment by roster
|
||||
// ordinal (the same port set the old visual used) so each
|
||||
// energy weapon keeps a stable muzzle on the arms.
|
||||
if (mz.y - localOrigin.linearPosition.y < 1.0f)
|
||||
{
|
||||
static const char *const kGunPorts[] =
|
||||
{ "siterugunport", "sitelugunport", "siterdgunport",
|
||||
"siteldgunport", "siterbgunport", "sitelbgunport" };
|
||||
static EntitySegment *s_portCache[64];
|
||||
static int s_portTried[64];
|
||||
if (energyOrdinal >= 0 && energyOrdinal < 64)
|
||||
{
|
||||
if (!s_portTried[energyOrdinal])
|
||||
{
|
||||
s_portTried[energyOrdinal] = 1;
|
||||
s_portCache[energyOrdinal] = GetSegment(
|
||||
CString(kGunPorts[energyOrdinal % 6]));
|
||||
}
|
||||
if (s_portCache[energyOrdinal] != 0)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(s_portCache[energyOrdinal]->GetSegmentToEntity(),
|
||||
localToWorld);
|
||||
mz = mw; // Point3D = matrix translation
|
||||
}
|
||||
}
|
||||
}
|
||||
const Point3D &bend = em->BeamEndpoint(); // the fire's world hit point
|
||||
// authored per-weapon colour; unset (-1) -> the ER-laser red
|
||||
RGBColor pc = em->PipColor();
|
||||
float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue;
|
||||
if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; }
|
||||
// PPC (classID 0xBD4 = 3028): a thicker, brighter bolt than a laser tube.
|
||||
const int isPPC = ((int)em->GetClassID() == 3028);
|
||||
// ONE draw per beam, at the model's NATURAL width: the weapon's
|
||||
// own tube (ERMLASER radius 0.22u, PPC bolt 0.62u) IS the beam
|
||||
// -- the old inflated two-layer glow/core (3.0x + 0.9x widths)
|
||||
// drew fat cartoon cylinders 13x the authored size. The tint
|
||||
// modulates the scrolling grit; thin natural tubes stay under
|
||||
// saturation without a hand-dimmed core.
|
||||
extern void BTPushBeamKind(float,float,float, float,float,float,
|
||||
unsigned, float, float, int);
|
||||
unsigned tint =
|
||||
(((unsigned)(40.0f + r * 215.0f) & 0xFF) << 16) |
|
||||
(((unsigned)(40.0f + g * 215.0f) & 0xFF) << 8) |
|
||||
((unsigned)(40.0f + b * 215.0f) & 0xFF);
|
||||
BTPushBeamKind(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
|
||||
tint, ttl1, 1.0f /* natural model width */,
|
||||
isPPC ? 1 : 0 /* ppc.bgf : ermlaser.bgf */);
|
||||
if (getenv("BT_BEAM_LOG") && (s_beamStateLog++ % 31) == 0) // 31: coprime with the 5-beam volley (a %30 sampler aliased to one weapon)
|
||||
DEBUG_STREAM << "[beam] " << (isPPC ? "PPC" : "laser") << " #" << wi
|
||||
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z
|
||||
<< ") end=(" << bend.x << "," << bend.y << "," << bend.z
|
||||
<< ") rgb=(" << r << "," << g << "," << b << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
DrawWeaponBeams(dt); // task #51: extracted (runs for replicants too)
|
||||
}
|
||||
|
||||
// --- TARGETING: the WORLD-PICK model (task #41, the reconciliation of
|
||||
@@ -3538,3 +3462,102 @@ static const DamageStateEntry kDamageStateTable[] =
|
||||
//===========================================================================//
|
||||
// End of recovered mech4.cpp slice.
|
||||
//===========================================================================//
|
||||
|
||||
//
|
||||
// DrawWeaponBeams (task #51 extraction) -- the per-weapon beam render walk
|
||||
// (task #33), extracted from the player-only drive block so it runs for
|
||||
// EVERY mech: the local player, the solo dummy, and MP REPLICANTS (whose
|
||||
// emitters now carry live replicated discharge state via the Emitter update
|
||||
// records; without this the peer's beams applied but never drew).
|
||||
//
|
||||
void
|
||||
Mech::DrawWeaponBeams(Scalar dt)
|
||||
{
|
||||
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
|
||||
const Scalar ttl1 = (dt > 1e-4f) ? dt : 1e-4f; // one-frame life (redrawn while on)
|
||||
static int s_beamStateLog = 0;
|
||||
int energyOrdinal = -1; // Nth energy weapon (port assignment)
|
||||
for (int wi = 0; wi < GetSubsystemCount(); ++wi)
|
||||
{
|
||||
Subsystem *ws = GetSubsystem(wi);
|
||||
if (ws == 0)
|
||||
continue;
|
||||
// EXACT class filter: Emitter (0xBC8=3016) or PPC (0xBD4=3028).
|
||||
// The derivation check matched too broadly here (the recon
|
||||
// derivation chains are shared stubs for some subsystems --
|
||||
// a Sensor and the MissileLauncher passed and drew garbage
|
||||
// from misinterpreted offsets).
|
||||
const int wcid = (int)ws->GetClassID();
|
||||
if (wcid != 3016 && wcid != 3028)
|
||||
continue;
|
||||
++energyOrdinal;
|
||||
Emitter *em = (Emitter *)ws;
|
||||
if (!em->BeamOn())
|
||||
continue;
|
||||
Point3D mz;
|
||||
em->MuzzlePoint(mz); // LIVE muzzle (tracks the gun)
|
||||
// MOUNT FALLBACK: when the weapon's mount segment doesn't
|
||||
// resolve, GetMuzzlePoint returns the mech ORIGIN (feet).
|
||||
// Assign this weapon its own gun-port segment by roster
|
||||
// ordinal (the same port set the old visual used) so each
|
||||
// energy weapon keeps a stable muzzle on the arms.
|
||||
if (mz.y - localOrigin.linearPosition.y < 1.0f)
|
||||
{
|
||||
static const char *const kGunPorts[] =
|
||||
{ "siterugunport", "sitelugunport", "siterdgunport",
|
||||
"siteldgunport", "siterbgunport", "sitelbgunport" };
|
||||
// PER-MECH cache (task #51): the walk now runs for EVERY mech
|
||||
// (player + replicants); the old process-wide statics would serve
|
||||
// the PLAYER's segment pointers as the replicant's muzzles. Key
|
||||
// the slot by owner and re-resolve on mismatch.
|
||||
static EntitySegment *s_portCache[64];
|
||||
static int s_portTried[64];
|
||||
static Mech *s_portOwner[64];
|
||||
if (energyOrdinal >= 0 && energyOrdinal < 64)
|
||||
{
|
||||
if (!s_portTried[energyOrdinal]
|
||||
|| s_portOwner[energyOrdinal] != this)
|
||||
{
|
||||
s_portTried[energyOrdinal] = 1;
|
||||
s_portOwner[energyOrdinal] = this;
|
||||
s_portCache[energyOrdinal] = GetSegment(
|
||||
CString(kGunPorts[energyOrdinal % 6]));
|
||||
}
|
||||
if (s_portCache[energyOrdinal] != 0)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(s_portCache[energyOrdinal]->GetSegmentToEntity(),
|
||||
localToWorld);
|
||||
mz = mw; // Point3D = matrix translation
|
||||
}
|
||||
}
|
||||
}
|
||||
const Point3D &bend = em->BeamEndpoint(); // the fire's world hit point
|
||||
// authored per-weapon colour; unset (-1) -> the ER-laser red
|
||||
RGBColor pc = em->PipColor();
|
||||
float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue;
|
||||
if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; }
|
||||
// PPC (classID 0xBD4 = 3028): a thicker, brighter bolt than a laser tube.
|
||||
const int isPPC = ((int)em->GetClassID() == 3028);
|
||||
// ONE draw per beam, at the model's NATURAL width: the weapon's
|
||||
// own tube (ERMLASER radius 0.22u, PPC bolt 0.62u) IS the beam
|
||||
// -- the old inflated two-layer glow/core (3.0x + 0.9x widths)
|
||||
// drew fat cartoon cylinders 13x the authored size. The tint
|
||||
// modulates the scrolling grit; thin natural tubes stay under
|
||||
// saturation without a hand-dimmed core.
|
||||
extern void BTPushBeamKind(float,float,float, float,float,float,
|
||||
unsigned, float, float, int);
|
||||
unsigned tint =
|
||||
(((unsigned)(40.0f + r * 215.0f) & 0xFF) << 16) |
|
||||
(((unsigned)(40.0f + g * 215.0f) & 0xFF) << 8) |
|
||||
((unsigned)(40.0f + b * 215.0f) & 0xFF);
|
||||
BTPushBeamKind(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
|
||||
tint, ttl1, 1.0f /* natural model width */,
|
||||
isPPC ? 1 : 0 /* ppc.bgf : ermlaser.bgf */);
|
||||
if (getenv("BT_BEAM_LOG") && (s_beamStateLog++ % 31) == 0) // 31: coprime with the 5-beam volley (a %30 sampler aliased to one weapon)
|
||||
DEBUG_STREAM << "[beam] " << (isPPC ? "PPC" : "laser") << " #" << wi
|
||||
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z
|
||||
<< ") end=(" << bend.x << "," << bend.y << "," << bend.z
|
||||
<< ") rgb=(" << r << "," << g << "," << b << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user