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
@@ -72,6 +72,7 @@
|
||||
#if !defined(TESTBT_HPP)
|
||||
# include <testbt.hpp>
|
||||
#endif
|
||||
#include <hostmgr.hpp> // HostManager::GetEntityPointer -- the update-record target resolve (task #51)
|
||||
|
||||
// E8 (bring-up): the player fire input. The controls mapper that would normally write
|
||||
// the weapon's fireImpulse is bypassed (mech4.cpp), so EmitterSimulation reads this
|
||||
@@ -413,11 +414,21 @@ void
|
||||
beamFlag = 0; // 0x46c
|
||||
dischargeTimer = dischargeTime; // 0x440 = 0x43c
|
||||
SetDirty();
|
||||
// replicate the beam END: one final Emitter update record turns the
|
||||
// peer's copy off (the 1995 flush wrote idle-flagged subsystems too).
|
||||
ForceUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// keep the beam alive for another frame (re-send the beam message).
|
||||
// CROSS-FAMILY (messaging): SendMessage(PTR_LAB_00511e6c,..) -- FUN_0041c350.
|
||||
// keep the beam alive for another frame -- BEAM REPLICATION (task #51):
|
||||
// the 1995 keepalive (FUN_0041c350, template @0x511e6c) marked the
|
||||
// subsystem dirty + queued the local deferred beam-effect callback; the
|
||||
// local draw is our per-weapon render walk, and the dirty-mark maps to the
|
||||
// engine's updateModel bit -- the next PerformAndWatch serializes THIS
|
||||
// emitter's record (@004ba65c) into the mech's update stream, and the
|
||||
// peer's replicant emitter applies it (@004ba568) so its own walk draws
|
||||
// the beam.
|
||||
ForceUpdate();
|
||||
(void)PTR_LAB_00511e6c; (void)DAT_00511e70; (void)DAT_00511e74;
|
||||
}
|
||||
|
||||
@@ -449,7 +460,9 @@ void
|
||||
ResetFiringState(); // @004ba9a8
|
||||
return;
|
||||
}
|
||||
// re-send the beam-keepalive message (CROSS-FAMILY messaging, FUN_0041c350)
|
||||
// re-send the beam keepalive -- the engine's updateModel bit (task #51,
|
||||
// same mapping as ServiceDischarge above; 1995 template @0x511e78).
|
||||
ForceUpdate();
|
||||
(void)PTR_LAB_00511e78; (void)DAT_00511e7c; (void)DAT_00511e80;
|
||||
}
|
||||
}
|
||||
@@ -552,45 +565,52 @@ void
|
||||
//
|
||||
|
||||
//
|
||||
// @004ba568 -- slot 6. Resolve the incoming damage's source subsystem (the
|
||||
// VoltageSource link), then apply the base MechWeapon damage and replicate the
|
||||
// extra Emitter fields (firingActive, beam endpoint, target). A "no-source"
|
||||
// damage clears the link and sets the alarm to 2 (Loaded) / 0 (active).
|
||||
// @004ba568 -- slot 6 (TASK #51 RENAME: was mistranscribed as TakeDamage; the
|
||||
// body is unambiguous RECORD semantics -- it compares rec+0x30 against
|
||||
// EntityID::Null (DAT_00522524), resolves it through the entity index, and
|
||||
// applies alarm/beam fields). Apply an Emitter update record on the
|
||||
// REPLICANT: resolve the beam TARGET id (drop the record when a non-null id is
|
||||
// unknown on this host), chain the MechWeapon read (@004b964c, alarm apply),
|
||||
// then the beam-replication fields. The tail `this+0x10 = this+0x14` is the
|
||||
// 1995 Simulation lastUpdate=lastPerformance resync (the earlier transcription
|
||||
// misread it as rechargeLevel=effectiveRange) -- the engine base read already
|
||||
// maintains lastUpdate.
|
||||
//
|
||||
void
|
||||
Emitter::TakeDamage(Damage &damage)
|
||||
Emitter::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
||||
{
|
||||
int src = FUN_00421070(&damage, DAT_00522524); // damage source (damage+0x30)
|
||||
if (src == 0)
|
||||
Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message;
|
||||
|
||||
if (rec->targetID == EntityID::Null) // FUN_00421070 vs DAT_00522524
|
||||
{
|
||||
targetEntity = 0; // 0x474
|
||||
}
|
||||
else
|
||||
{
|
||||
// look up the named source through the global subsystem registry
|
||||
targetEntity = (Entity *)ResolveSource(&damage); // this+0x474
|
||||
targetEntity = (application != 0)
|
||||
? application->GetHostManager()->GetEntityPointer(rec->targetID)
|
||||
: 0;
|
||||
if (targetEntity == 0)
|
||||
{
|
||||
return;
|
||||
return; // unknown target -> drop
|
||||
}
|
||||
}
|
||||
|
||||
MechWeapon::TakeDamage(damage); // FUN_004b964c
|
||||
MechWeapon::ReadUpdateRecord(message); // FUN_004b964c (alarm apply)
|
||||
|
||||
// CROSS-FAMILY (replication): the shipped Emitter also pulled its beam state
|
||||
// (firingActive, beamFlag, beamEndpoint, beamColor, targetLocalFlag) out of the
|
||||
// networked damage record. Those fields are not present on the engine Damage;
|
||||
// they arrive through the update-record path (WriteUpdateRecord) instead.
|
||||
if (firingActive == 0)
|
||||
firingActive = rec->firingActive; // 0x418 <- rec+0x18
|
||||
weaponAlarm.SetLevel(firingActive != 0 ? 0 : 2); // 0=Firing / 2=Loaded
|
||||
beamFlag = rec->beamFlag; // 0x46c <- rec+0x28
|
||||
beamEndpoint = rec->beamEndpoint; // 0x460 <- rec+0x1c
|
||||
targetLocalFlag = rec->targetLocalFlag; // 0x470 <- rec+0x2c
|
||||
ClearDirty(); // this+0x28 &= ~2 (idle bit clear)
|
||||
if (getenv("BT_BEAM_LOG"))
|
||||
{
|
||||
weaponAlarm.SetLevel(2); // Loaded
|
||||
static int s_rd = 0;
|
||||
if ((s_rd++ % 60) == 0)
|
||||
DEBUG_STREAM << "[emit-rd] " << (void*)this << " firing=" << firingActive
|
||||
<< " beamFlag=" << beamFlag << "\n" << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
weaponAlarm.SetLevel(0); // Firing
|
||||
}
|
||||
ClearDirty(); // this+0x28 &= ~2
|
||||
rechargeLevel = effectiveRange; // this+0x10 = this+0x14 (best-effort)
|
||||
}
|
||||
|
||||
//
|
||||
@@ -599,23 +619,31 @@ void
|
||||
// endpoint, beamFlag and the target's colour tag.
|
||||
//
|
||||
void
|
||||
Emitter::WriteUpdateRecord(UpdateRecord *message, int update_model)
|
||||
Emitter::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
|
||||
{
|
||||
MechWeapon::WriteUpdateRecord(message, update_model); // FUN_004b9690
|
||||
|
||||
message->recordID = 0x38; // *message
|
||||
message->firingActive = firingActive; // message[6] <- 0x418
|
||||
message->targetLocalFlag= targetLocalFlag; // message[10] <- 0x470
|
||||
message->beamEndpoint = beamEndpoint; // message[7] <- 0x460
|
||||
message->beamFlag = beamFlag; // message[11] <- 0x46c
|
||||
|
||||
if (targetEntity == 0) // 0x474
|
||||
Emitter__UpdateRecord *rec = (Emitter__UpdateRecord *)message;
|
||||
// DISASM CORRECTIONS (task #51): `*param_2 = 0x38` writes the record
|
||||
// LENGTH, not recordID; and rec+0x30 receives the TARGET's EntityID
|
||||
// (FUN_00420ef4 copies from targetEntity+0x184 = the 1995 entityID --
|
||||
// the old `CopyColor(...targetEntity+0x184)` was both a misread AND a
|
||||
// databinding trap in our compiled Entity layout).
|
||||
rec->recordLength = sizeof(Emitter__UpdateRecord); // *message = 0x38
|
||||
rec->firingActive = firingActive; // rec+0x18 <- 0x418
|
||||
rec->beamEndpoint = beamEndpoint; // rec+0x1c <- 0x460
|
||||
rec->beamFlag = beamFlag; // rec+0x28 <- 0x46c
|
||||
rec->targetLocalFlag = targetLocalFlag; // rec+0x2c <- 0x470
|
||||
rec->targetID = (targetEntity != 0)
|
||||
? ((Entity *)targetEntity)->GetEntityID()
|
||||
: EntityID::Null; // rec+0x30
|
||||
if (getenv("BT_BEAM_LOG"))
|
||||
{
|
||||
CopyColor(&message->color, DAT_00522524); // FUN_00420ef4
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyColor(&message->color, (void*)((char*)targetEntity + 0x184));
|
||||
static int s_wr = 0;
|
||||
if ((s_wr++ % 60) == 0)
|
||||
DEBUG_STREAM << "[emit-wr] " << (void*)this << " firing=" << firingActive
|
||||
<< " beamFlag=" << beamFlag << " subsysID=" << (int)rec->subsystemID
|
||||
<< " len=" << (int)rec->recordLength << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user