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:
arcattack
2026-07-09 19:04:34 -05:00
co-authored by Claude Fable 5
parent a110041a70
commit 29035028fd
11 changed files with 764 additions and 180 deletions
+22 -4
View File
@@ -181,10 +181,28 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
3. **Victim visuals on the shooter's screen**: the wreck swap gates on zone/graphic state the
REPLICANT copy may never see (zone damage may not replicate — only pose updates confirmed).
Check DamageZone/subsystem state in the update stream; else replicate the death as an event.
4. **Cross-pod beam visuals**: replicant emitters don't run the local trigger sim, so the peer's
beams never draw today. ⭐ The authentic path exists: ServiceDischarge/ContinueDischarge SEND
**beam-keepalive messages** (`FUN_0041c350`, templates @0x511e6c/@0x511e78, emitter.cpp already
stubs them) — decode + route those to drive replicant beam draws.
4. **Cross-pod beam visuals — DONE (task #51, 2026-07-09).** The keepalive theory needed one
correction: `FUN_0041c350` = (a) queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the
app+0x34 manager (our per-weapon render walk plays that role) AND (b) set the subsystem DIRTY bit —
which maps to the 2007 engine's `updateModel`/`ForceUpdate()`. The REPLICATION rides **subsystem
update records inside the mech's update message**: the roster walk passes the entity's stream to
every subsystem's `PerformAndWatch`; `Emitter::WriteUpdateRecord` (@004ba65c, corrected: record
LENGTH not recordID; rec+0x30 = the TARGET's **EntityID** not a colour) serializes
firingActive/beamEndpoint/beamFlag/targetID; the engine's `Entity::UpdateMessageHandler` routes
records by `subsystemID` to `Emitter::ReadUpdateRecord` (@004ba568 — was MIStranscribed as
TakeDamage; the whole weapon-family slot map was swapped: slot 6=Read, 7=Write, 9=TakeDamage — see
mechweap.hpp). `ServiceDischarge`/`ContinueDischarge` call `ForceUpdate()` per keepalive tick +
once at beam end. The render walk was extracted to `Mech::DrawWeaponBeams` (per-mech port cache)
and runs for REPLICANTS. ⚠ two silent-failure traps fixed: the overrides originally used the
class's own shadowing `UpdateRecord` typedef as the param type → never overrode the engine virtual
(base ran, nothing serialized); and `recordLength` must be each level's sizeof or the stream
framing misparses. VERIFIED 2-node: A fires 57 volleys → 225 records → B applies 225 → draws 414
replicant beams (PPC blue / laser red from A's replicant's gun ports). Solo un-regressed.
REMAINING (deferred): the MECH-level writer @0x4a0c2c (recovered + preserved:
`reference/decomp/mech_writeupdate_004a0c2c.disasm.txt`; 9 record types decoded — 0/1/4 pose
variants, 2 alarm, 3 leg-state+heat with body-channel write-through re-sync, 5 knockdown
(SetBodyAnimation(0x20) write-through), 6 death, 7 impact, 8 movementMode) — transcribing it
would replicate remote KNOCKDOWN/death/heat states; beams did not need it.
5. **2-window driving**: input gates on window focus (alternate windows; `BT_KEY_NOFOCUS` for
harness). DEATHS scoring should light via the existing BTPostKillScore MP branch. Respawn =
the P5 teardown debt (deferred until needed).
+67 -39
View File
@@ -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;
}
}
+24 -12
View File
@@ -97,12 +97,18 @@ class NotationFile;
struct Emitter__UpdateRecord:
public MechWeapon::UpdateRecord
{
int firingActive; // message[6]
int targetLocalFlag; // message[10]
Point3D beamEndpoint; // message[7]
int beamFlag; // message[11]
RGBColor color;
int payloadCount; // message+0xc (AdvanceSeekVoltage)
int firingActive; // rec+0x18 <- 0x418
Point3D beamEndpoint; // rec+0x1c <- 0x460
int beamFlag; // rec+0x28 <- 0x46c
int targetLocalFlag; // rec+0x2c <- 0x470
// DISASM CORRECTION (task #51, @004ba65c/@004ba568): rec+0x30 is the
// TARGET's EntityID -- FUN_00420ef4 (an EntityID copy) from
// targetEntity+0x184 (the 1995 Entity::entityID) or DAT_00522524
// (EntityID::Null) -- NOT an RGB colour. The reader resolves it
// through the entity index and DROPS the record when a non-null id
// is unknown on this host.
EntityID targetID; // rec+0x30
int payloadCount; // (AdvanceSeekVoltage)
};
typedef Emitter__UpdateRecord UpdateRecord;
@@ -160,9 +166,14 @@ class NotationFile;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem / Simulation virtual overrides (vtable @00512078)
//
// Slot indices verified against the MechWeapon vtable @00511d2c:
// slot 6 TakeDamage (MechWeapon base @004b964c)
// slot 7 WriteUpdateRecord (MechWeapon base @004b9690)
// SLOT MAP CORRECTED (task #51): slot 6 is ReadUpdateRecord, not TakeDamage
// (@004ba568 resolves an EntityID at rec+0x30 through the entity index and
// applies alarm/beam fields -- record semantics; it chains @004b964c, the
// MechWeapon slot-6 read). The real Emitter::TakeDamage is slot 9
// @004bafc8 (undecoded -- inherit MechWeapon::TakeDamage until recovered).
// slot 6 ReadUpdateRecord @004ba568 (MechWeapon base @004b964c)
// slot 7 WriteUpdateRecord @004ba65c (MechWeapon base @004b9690)
// slot 9 TakeDamage @004bafc8 (NOT yet reconstructed)
// slot 10 ResetToInitialState (MechWeapon base @004b96ec)
// slot 13 PrintState (MechWeapon base @004b9d00)
// slot 16 ReadyToDischarge (Simulation interface)
@@ -171,7 +182,7 @@ class NotationFile;
//
public:
void
TakeDamage(Damage &damage); // slot 6, @004ba568
ReadUpdateRecord(Simulation__UpdateRecord *message); // slot 6, @004ba568
void
ResetToInitialState(); // slot 10, @004ba4d0
void
@@ -184,12 +195,13 @@ class NotationFile;
int BeamOn() const { return beamFlag; } // 0x46c
const Point3D &BeamEndpoint() const { return beamEndpoint; } // 0x460
protected:
public:
void
WriteUpdateRecord( // slot 7, @004ba65c
UpdateRecord *message,
Simulation__UpdateRecord *message,
int update_model
);
protected:
// @004ba6e0 -- True when the supplied (or linked) voltage source can
// satisfy the current seekVoltage index and is in the "ready" state (2).
+1
View File
@@ -96,6 +96,7 @@ public:
void SetLevel(int n) { level = n; }
int GetLevel() const { return level; }
int Level() const { return level; }
int LevelCountB() const { return levelB; } // +0x10 (the weapon update-record "reset" source)
// ReconAlarm/AlarmIndicator API aliases (callers that predate the retype use these):
void SetState(unsigned n){ level = (int)n; }
unsigned GetState() const { return (unsigned)level; }
+5
View File
@@ -487,6 +487,11 @@ public:
// -> world hit point. Defined in mech.cpp.
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world);
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
// carry replicated discharge state). Defined in mech4.cpp.
void DrawWeaponBeams(Scalar dt);
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+105 -82
View File
@@ -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;
}
}
+38 -24
View File
@@ -233,59 +233,73 @@ Logical
//
//
// @004b964c -- slot 6. Apply damage through the base, then set the weapon's
// display alarm: a destroyed weapon (damage->destroyed flag, +0x14) clears the
// alarm to 0, otherwise the alarm tracks the incoming damage level (+0x10).
// @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6
// ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage,
// which dispatches on *param==4 == Damage::damageType -- damage semantics).
// The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet
// reconstructed; chain to the engine Subsystem base, and keep the damage-zone
// alarm tracking this port has shipped with (best-effort; the binary's alarm
// display on weapon damage flows through the unreconstructed @004b0efc chain).
//
void
MechWeapon::TakeDamage(Damage &damage)
{
Subsystem::TakeDamage(damage); // FUN_0041bd34
Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc)
// A destroyed weapon clears its display alarm; otherwise the alarm tracks
// the damage-zone state set by the base TakeDamage above. (The decomp read
// the post-damage destroyed/level flags out of the damage record; here we
// read them back off the resolved damage zone.)
// Read the ENGINE Subsystem::damageZone (the real built zone), qualified -- the
// nearer MechSubsystem::damageZone is a ReconDamageZone-shim SHADOW of it (exposed
// once HeatableSubsystem re-based onto MechSubsystem). TODO: remove that shadow.
// Port behavior (kept): a destroyed weapon clears its display alarm;
// otherwise the alarm tracks the resolved damage-zone state.
::DamageZone *dz = this->Subsystem::damageZone;
if (dz != 0
&& dz->GetGraphicState() == DamageZone::DestroyedGraphicState)
{
weaponAlarm.SetLevel(0); // FUN_0041bbd8(this+0xd4, 0)
weaponAlarm.SetLevel(0);
}
else
{
weaponAlarm.SetLevel(
(dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0
); // FUN_0041bbd8(this+0xd4, level)
);
}
}
//
// @004b9690 -- slot 7. Append the weapon's replication fields to the update
// record: record type 0x18, the alarm state (this+0x364), and a "ready/idle"
// flag derived from this+0x360.
// record. DISASM CORRECTIONS (task #51): `*message = 0x18` writes the record
// LENGTH (record[0] = recordLength), not recordID; and message[5] derives from
// this+0x360 == weaponAlarm+0x10 (LevelCountB), not from the level. BASE-TYPED
// param: with the class's own UpdateRecord typedef this silently failed to
// override the engine virtual, so the engine base ran and no weapon field
// ever serialized.
//
void
MechWeapon::WriteUpdateRecord(UpdateRecord *message, int update_model)
MechWeapon::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
{
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041c500 (header + subsystemID)
message->recordID = 0x18; // *message = 0x18
message->alarmState = weaponAlarm.GetLevel(); // message[4] = *(this+0x364)
message->weaponIdle = (weaponAlarm.GetLevel() == 0) ? 1 : 0; // message[5]
MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
rec->recordLength = sizeof(MechWeapon__UpdateRecord); // *message = 0x18
rec->alarmState = weaponAlarm.GetLevel(); // message[4] = *(this+0x364)
rec->weaponIdle = (weaponAlarm.LevelCountB() == 0) ? 1 : 0; // message[5] = (*(this+0x360)==0)
}
//
// @004b96d4 -- slot 9. MechWeapon adds nothing of its own; defers to the
// PoweredSubsystem implementation (paired with WriteUpdateRecord above).
// @004b964c -- slot 6 (TASK #51 RENAME: was mislabeled TakeDamage). Apply a
// weapon update record on the REPLICANT: chain the Simulation base
// (@0041bd34 -- lastUpdate + simulationState), then the weapon alarm --
// reset-through-0 first when the record's weaponIdle flag is set, then the
// replicated alarm level.
//
void
MechWeapon::ReadUpdateRecord(UpdateRecord *message)
MechWeapon::ReadUpdateRecord(Simulation__UpdateRecord *message)
{
PoweredSubsystem::ReadUpdateRecord(message); // FUN_004b0efc
Simulation::ReadUpdateRecord(message); // FUN_0041bd34
MechWeapon__UpdateRecord *rec = (MechWeapon__UpdateRecord *)message;
if (rec->weaponIdle != 0) // record+0x14
{
weaponAlarm.SetLevel(0);
}
weaponAlarm.SetLevel(rec->alarmState); // record+0x10
}
//
+18 -9
View File
@@ -159,30 +159,39 @@ class CockpitHud;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @00511d2c)
//
// Slot indices verified by diffing against the Subsystem vtable tail of
// @0050e170 and the PoweredSubsystem vtable @0050f9d8:
// slot 6 TakeDamage(Damage&) (Subsystem base @0041bd34)
// slot 7 WriteUpdateRecord(...) (Subsystem base @0041c500)
// slot 9 ReadUpdateRecord(...) (PoweredSubsystem base @004b0efc)
// SLOT MAP CORRECTED (task #51): the Read/TakeDamage attributions were
// SWAPPED. By symmetry with the Entity hierarchy (Mech slot 6 =
// ReadUpdateRecord @004a1232, slot 7 = WriteUpdateRecord @004a0c2c, both
// verified) and by body semantics (@004b964c reads record+0x10/+0x14 into
// the weapon alarm after chaining @0041bd34 = Simulation::ReadUpdateRecord;
// @004b0efc dispatches on *param==4 = Damage::damageType):
// slot 6 ReadUpdateRecord(...) @004b964c (base @0041bd34 = Simulation::ReadUpdateRecord)
// slot 7 WriteUpdateRecord(...) @004b9690 (base @0041c500 = Subsystem::WriteUpdateRecord)
// slot 9 TakeDamage(Damage&) @004b96d4 (chains @004b0efc = PoweredSubsystem::TakeDamage)
// slot 10 ResetToInitialState() (PoweredSubsystem base @004b0e6c)
// slot 13 PrintState() (PoweredSubsystem base @004b1224)
//
// NOTE the ENGINE-OVERRIDE signature rule: the virtuals are declared on
// Simulation with Simulation::UpdateRecord* (SIMULATE.h:127-131). Declaring
// them here with THIS class's shadowing `UpdateRecord` typedef silently
// fails to override (the engine base runs instead and none of the weapon
// fields ever serialize) -- base-typed params, cast inside.
//
public:
void
TakeDamage(Damage &damage); // slot 6, @004b964c
TakeDamage(Damage &damage); // slot 9, @004b96d4
void
ResetToInitialState(); // slot 10, @004b96ec
void
PrintState(); // slot 13, @004b9d00
protected:
void
WriteUpdateRecord( // slot 7, @004b9690
UpdateRecord *message,
Simulation__UpdateRecord *message,
int update_model
);
void
ReadUpdateRecord(UpdateRecord *message); // slot 9, @004b96d4
ReadUpdateRecord(Simulation__UpdateRecord *message); // slot 6, @004b964c
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
+6 -4
View File
@@ -314,13 +314,15 @@ Logical
}
//
// @004bbae0 -- slot 9. ProjectileWeapon adds nothing of its own; defers to the
// MechWeapon implementation.
// @004bbae0 -- slot 9 (TASK #51 RENAME: slot 9 is TakeDamage, not
// ReadUpdateRecord -- see the corrected slot map in mechweap.hpp).
// ProjectileWeapon adds nothing of its own; defers to the MechWeapon
// implementation (@004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage).
//
void
ProjectileWeapon::ReadUpdateRecord(UpdateRecord *message)
ProjectileWeapon::TakeDamage(Damage &damage)
{
MechWeapon::ReadUpdateRecord(message); // FUN_004b96d4
MechWeapon::TakeDamage(damage); // FUN_004b96d4
}
//
+9 -4
View File
@@ -172,11 +172,13 @@ class NotationFile;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0051242c)
//
// Slot indices verified by decoding the raw vtable bytes (section_dump):
// slot 6 TakeDamage INHERITED MechWeapon @004b964c
// Slot indices verified by decoding the raw vtable bytes (section_dump).
// SLOT MAP CORRECTED (task #51): 6=ReadUpdateRecord, 9=TakeDamage (the
// old attributions were swapped -- see mechweap.hpp for the evidence):
// slot 6 ReadUpdateRecord INHERITED MechWeapon @004b964c
// slot 7 WriteUpdateRecord INHERITED MechWeapon @004b9690
// slot 8 HandleMessage OVERRIDE @004bcabc
// slot 9 ReadUpdateRecord OVERRIDE @004bbae0 (thin -> MechWeapon)
// slot 9 TakeDamage OVERRIDE @004bbae0 (thin -> MechWeapon)
// slot 10 ResetToInitialState OVERRIDE @004bbaf8
// slot 12 GetStatusFlags OVERRIDE @004bbf88 (body not recovered)
// slot 13 PrintState OVERRIDE @004bc6c8
@@ -194,8 +196,11 @@ class NotationFile;
PrintState(); // slot 13, @004bc6c8
protected:
// TASK #51 RENAME: slot 9 is TakeDamage (the old ReadUpdateRecord
// attribution was swapped; the record read is INHERITED from
// MechWeapon slot 6 @004b964c -- see mechweap.hpp for the evidence).
void
ReadUpdateRecord(UpdateRecord *message); // slot 9, @004bbae0
TakeDamage(Damage &damage); // slot 9, @004bbae0
// slot 16, @004bbc20 -- MechWeapon-family per-frame hook (overrides
// MechWeapon @004b0b5c). Body not recovered. TODO: confirm role.
@@ -0,0 +1,467 @@
=== Mech::WriteUpdateRecord @0x4a0c2c (vtable slot 7) ===
=== args: (this=ebp+8, record=ebp+0xC, type=ebp+0x10); jump table @0x4a0c55 on type 0..8 ===
=== table: 0:0x4a0c79 1:0x4a11cb 2:0x4a0e5b 3:0x4a0d84 4:0x4a0e9e 5:0x4a0fab 6:0x4a104d 7:0x4a10ff 8:0x4a117f ===
===== prologue + dispatch @0x4a0c2c..0x4a0c55 =====
0x4a0c2c: push ebp
0x4a0c2d: mov ebp, esp
0x4a0c2f: add esp, -0x68
0x4a0c32: push ebx
0x4a0c33: push esi
0x4a0c34: push edi
0x4a0c35: mov edi, dword ptr [ebp + 0x10]
0x4a0c38: mov ebx, dword ptr [ebp + 8]
0x4a0c3b: mov eax, dword ptr [ebx + 0x128]
0x4a0c41: mov esi, dword ptr [eax]
0x4a0c43: mov eax, edi
0x4a0c45: cmp eax, 8
0x4a0c48: ja 0x4a11cb
0x4a0c4e: jmp dword ptr [eax*4 + 0x4a0c55]
===== case 0 (full pose snapshot) @0x4a0c79..0x4a0d84 =====
0x4a0c79: lea edx, [ebx + 0x138]
0x4a0c7f: push edx
0x4a0c80: lea ecx, [ebp - 0x44]
0x4a0c83: push ecx
0x4a0c84: call 0x408f44
0x4a0c89: add esp, 8
0x4a0c8c: lea eax, [ebx + 0x2d4]
0x4a0c92: push eax
0x4a0c93: lea edx, [ebp - 0x50]
0x4a0c96: push edx
0x4a0c97: call 0x408440
0x4a0c9c: add esp, 8
0x4a0c9f: push edi
0x4a0ca0: mov ecx, dword ptr [ebp + 0xc]
0x4a0ca3: push ecx
0x4a0ca4: push ebx
0x4a0ca5: call 0x4225a4
0x4a0caa: mov edi, dword ptr [ebp + 0xc]
0x4a0cad: add esp, 0xc
0x4a0cb0: lea eax, [ebp - 0x44]
0x4a0cb3: lea edx, [ebx + 0x138]
0x4a0cb9: mov word ptr [edi + 4], 0
0x4a0cbf: mov dword ptr [ebx + 0x77c], 1
0x4a0cc9: push eax
0x4a0cca: push edx
0x4a0ccb: call 0x409a00
0x4a0cd0: add esp, 8
0x4a0cd3: lea ecx, [ebx + 0x2d4]
0x4a0cd9: mov dword ptr [ebp - 4], ecx
0x4a0cdc: lea eax, [ebp - 0x50]
0x4a0cdf: push eax
0x4a0ce0: mov edx, dword ptr [ebp - 4]
0x4a0ce3: push edx
0x4a0ce4: call 0x408440
0x4a0ce9: add esp, 8
0x4a0cec: mov ecx, dword ptr [ebx + 0x3b0]
0x4a0cf2: cmp ecx, 2
0x4a0cf5: je 0x4a0d02
0x4a0cf7: mov eax, dword ptr [ebx + 0x3b0]
0x4a0cfd: cmp eax, 3
0x4a0d00: jne 0x4a0d16
0x4a0d02: push 0x4e0fd4 ; [0x4e0fd4]=0f
0x4a0d07: lea edx, [ebx + 0x298]
0x4a0d0d: push edx
0x4a0d0e: call 0x40a7f4
0x4a0d13: add esp, 8
0x4a0d16: mov ecx, dword ptr [ebx + 0x40]
0x4a0d19: mov eax, dword ptr [ebx + 0x3c]
0x4a0d1c: cmp ecx, eax
0x4a0d1e: je 0x4a0d64
0x4a0d20: mov edx, dword ptr [ebx + 0x3c]
0x4a0d23: cmp edx, 2
0x4a0d26: je 0x4a0d30
0x4a0d28: mov ecx, dword ptr [ebx + 0x40]
0x4a0d2b: cmp ecx, 2
0x4a0d2e: jne 0x4a0d64
0x4a0d30: lea eax, [ebx + 0x12c]
0x4a0d36: push eax
0x4a0d37: lea edx, [ebx + 0x260]
0x4a0d3d: push edx
0x4a0d3e: call 0x40a938
0x4a0d43: add esp, 8
0x4a0d46: lea ecx, [ebx + 0x2c8]
0x4a0d4c: push ecx
0x4a0d4d: lea eax, [ebx + 0x298]
0x4a0d53: push eax
0x4a0d54: call 0x40a7f4
0x4a0d59: add esp, 8
0x4a0d5c: xor edx, edx
0x4a0d5e: mov dword ptr [ebx + 0x77c], edx
0x4a0d64: mov ecx, dword ptr [esi + 0x128]
0x4a0d6a: mov dword ptr [edi + 0x74], ecx
0x4a0d6d: mov eax, dword ptr [esi + 0x128]
0x4a0d73: mov dword ptr [ebx + 0x6b4], eax
0x4a0d79: mov dword ptr [edi], 0x78
0x4a0d7f: jmp 0x4a11d9
===== case 3 (score/heat) @0x4a0d84..0x4a0e5b =====
0x4a0d84: mov edx, dword ptr [ebp + 0xc]
0x4a0d87: mov dword ptr [ebp - 8], edx
0x4a0d8a: push edi
0x4a0d8b: mov ecx, dword ptr [ebp - 8]
0x4a0d8e: push ecx
0x4a0d8f: push ebx
0x4a0d90: call 0x41bd60
0x4a0d95: add esp, 0xc
0x4a0d98: mov eax, dword ptr [ebp - 8]
0x4a0d9b: mov dword ptr [eax], 0x20
0x4a0da1: mov edx, dword ptr [ebp - 8]
0x4a0da4: mov word ptr [edx + 4], 0
0x4a0daa: mov ecx, dword ptr [ebp - 8]
0x4a0dad: mov eax, dword ptr [ebx + 0x654]
0x4a0db3: mov dword ptr [ecx + 0x10], eax
0x4a0db6: mov edx, dword ptr [ebp - 8]
0x4a0db9: mov ecx, dword ptr [ebx + 0x3b0]
0x4a0dbf: mov dword ptr [edx + 0x14], ecx
0x4a0dc2: mov eax, dword ptr [ebp - 8]
0x4a0dc5: mov edx, dword ptr [ebx + 0x4d8]
0x4a0dcb: mov dword ptr [eax + 0x18], edx
0x4a0dce: mov ecx, dword ptr [ebp - 8]
0x4a0dd1: mov eax, dword ptr [ecx + 0x10]
0x4a0dd4: mov dword ptr [ebx + 0x658], eax
0x4a0dda: mov edx, dword ptr [ebp - 8]
0x4a0ddd: mov ecx, dword ptr [edx + 0x14]
0x4a0de0: test ecx, ecx
0x4a0de2: jne 0x4a0e0d
0x4a0de4: push 1
0x4a0de6: lea eax, [ebx + 0x65c]
0x4a0dec: push eax
0x4a0ded: call 0x4283b8
0x4a0df2: add esp, 8
0x4a0df5: mov edx, dword ptr [ebp - 8]
0x4a0df8: mov ecx, dword ptr [edx + 0x14]
0x4a0dfb: push ecx
0x4a0dfc: lea eax, [ebx + 0x714]
0x4a0e02: push eax
0x4a0e03: call 0x41bbd8
0x4a0e08: add esp, 8
0x4a0e0b: jmp 0x4a0e3e
0x4a0e0d: mov edx, dword ptr [ebp - 8]
0x4a0e10: mov ecx, dword ptr [edx + 0x14]
0x4a0e13: dec ecx
0x4a0e14: jne 0x4a0e2e
0x4a0e16: mov eax, dword ptr [ebp - 8]
0x4a0e19: mov edx, dword ptr [eax + 0x14]
0x4a0e1c: push edx
0x4a0e1d: lea ecx, [ebx + 0x714]
0x4a0e23: push ecx
0x4a0e24: call 0x41bbd8
0x4a0e29: add esp, 8
0x4a0e2c: jmp 0x4a0e3e
0x4a0e2e: mov eax, dword ptr [ebp - 8]
0x4a0e31: mov edx, dword ptr [eax + 0x14]
0x4a0e34: push edx
0x4a0e35: push ebx
0x4a0e36: call 0x4a800c
0x4a0e3b: add esp, 8
0x4a0e3e: mov ecx, dword ptr [ebp - 8]
0x4a0e41: mov eax, dword ptr [esi + 0x128]
0x4a0e47: mov dword ptr [ecx + 0x1c], eax
0x4a0e4a: mov edx, dword ptr [esi + 0x128]
0x4a0e50: mov dword ptr [ebx + 0x6b4], edx
0x4a0e56: jmp 0x4a11d9
===== case 2 (alarm-only) @0x4a0e5b..0x4a0e9e =====
0x4a0e5b: mov ecx, dword ptr [ebp + 0xc]
0x4a0e5e: mov dword ptr [ebp - 0xc], ecx
0x4a0e61: push edi
0x4a0e62: mov eax, dword ptr [ebp - 0xc]
0x4a0e65: push eax
0x4a0e66: push ebx
0x4a0e67: call 0x41bd60
0x4a0e6c: add esp, 0xc
0x4a0e6f: mov edx, dword ptr [ebp - 0xc]
0x4a0e72: mov dword ptr [edx], 0x14
0x4a0e78: mov ecx, dword ptr [ebp - 0xc]
0x4a0e7b: mov word ptr [ecx + 4], 0
0x4a0e81: mov eax, dword ptr [ebp - 0xc]
0x4a0e84: mov edx, dword ptr [esi + 0x128]
0x4a0e8a: mov dword ptr [eax + 0x10], edx
0x4a0e8d: mov ecx, dword ptr [esi + 0x128]
0x4a0e93: mov dword ptr [ebx + 0x6b4], ecx
0x4a0e99: jmp 0x4a11d9
===== case 4 (pose + time re-sync) @0x4a0e9e..0x4a0fab =====
0x4a0e9e: mov eax, dword ptr [ebp + 0xc]
0x4a0ea1: mov dword ptr [ebp - 0x10], eax
0x4a0ea4: push edi
0x4a0ea5: mov edx, dword ptr [ebp - 0x10]
0x4a0ea8: push edx
0x4a0ea9: push ebx
0x4a0eaa: call 0x41bd60
0x4a0eaf: mov ecx, dword ptr [ebp - 0x10]
0x4a0eb2: add esp, 0xc
0x4a0eb5: lea edx, [ebx + 0x10c]
0x4a0ebb: mov dword ptr [ecx], 0x2c
0x4a0ec1: mov eax, dword ptr [ebp - 0x10]
0x4a0ec4: mov word ptr [eax + 4], 0
0x4a0eca: push edx
0x4a0ecb: call 0x40a138
0x4a0ed0: pop ecx
0x4a0ed1: lea ecx, [ebx + 0x10c]
0x4a0ed7: push ecx
0x4a0ed8: mov eax, dword ptr [ebp - 0x10]
0x4a0edb: add eax, 0x10
0x4a0ede: push eax
0x4a0edf: call 0x408f44
0x4a0ee4: add esp, 8
0x4a0ee7: lea eax, [ebx + 0x1d0]
0x4a0eed: mov edi, dword ptr [ebp - 0x10]
0x4a0ef0: add edi, 0x1c
0x4a0ef3: push eax
0x4a0ef4: push edi
0x4a0ef5: call 0x408440
0x4a0efa: add esp, 8
0x4a0efd: call 0x414b60
0x4a0f02: mov dword ptr [ebp - 0x14], eax
0x4a0f05: lea eax, [ebx + 0x778]
0x4a0f0b: mov edx, dword ptr [ebp - 0x14]
0x4a0f0e: mov ecx, dword ptr [edx]
0x4a0f10: mov dword ptr [eax], ecx
0x4a0f12: call 0x414b60
0x4a0f17: mov dword ptr [ebp - 0x18], eax
0x4a0f1a: lea eax, [ebx + 0x2e0]
0x4a0f20: mov edx, dword ptr [ebp - 0x18]
0x4a0f23: mov ecx, dword ptr [edx]
0x4a0f25: mov dword ptr [eax], ecx
0x4a0f27: lea eax, [ebx + 0x14]
0x4a0f2a: mov edx, dword ptr [ebx + 0x2e0]
0x4a0f30: mov ecx, dword ptr [eax]
0x4a0f32: sub edx, ecx
0x4a0f34: mov dword ptr [ebp - 0x68], edx
0x4a0f37: fild dword ptr [ebp - 0x68]
0x4a0f3a: fdiv dword ptr [0x52140c]
0x4a0f40: fstp dword ptr [ebp - 0x1c]
0x4a0f43: fld dword ptr [ebp - 0x1c]
0x4a0f46: fcomp dword ptr [0x4a1228]
0x4a0f4c: fnstsw ax
0x4a0f4e: sahf
0x4a0f4f: jae 0x4a0f62
0x4a0f51: mov edx, dword ptr [ebx + 0x2e0]
0x4a0f57: mov ecx, dword ptr [ebx + 0x14]
0x4a0f5a: sub edx, ecx
0x4a0f5c: add dword ptr [ebx + 0x2e0], edx
0x4a0f62: lea eax, [ebx + 0x10c]
0x4a0f68: push eax
0x4a0f69: lea edx, [ebx + 0x138]
0x4a0f6f: push edx
0x4a0f70: call 0x409968
0x4a0f75: add esp, 8
0x4a0f78: lea eax, [ebx + 0x1d0]
0x4a0f7e: lea edi, [ebx + 0x2d4]
0x4a0f84: push eax
0x4a0f85: push edi
0x4a0f86: call 0x408440
0x4a0f8b: add esp, 8
0x4a0f8e: mov eax, dword ptr [ebp - 0x10]
0x4a0f91: mov edx, dword ptr [esi + 0x128]
0x4a0f97: mov dword ptr [eax + 0x28], edx
0x4a0f9a: mov ecx, dword ptr [esi + 0x128]
0x4a0fa0: mov dword ptr [ebx + 0x6b4], ecx
0x4a0fa6: jmp 0x4a11d9
===== case 5 (subsystem alarm A) @0x4a0fab..0x4a104d =====
0x4a0fab: mov eax, dword ptr [ebp + 0xc]
0x4a0fae: mov dword ptr [ebp - 0x20], eax
0x4a0fb1: push edi
0x4a0fb2: mov edx, dword ptr [ebp - 0x20]
0x4a0fb5: push edx
0x4a0fb6: push ebx
0x4a0fb7: call 0x41bd60
0x4a0fbc: mov ecx, dword ptr [ebp - 0x20]
0x4a0fbf: add esp, 0xc
0x4a0fc2: mov dword ptr [ecx], 0x2c
0x4a0fc8: mov eax, dword ptr [ebp - 0x20]
0x4a0fcb: mov word ptr [eax + 4], 0
0x4a0fd1: mov edx, dword ptr [ebp - 0x20]
0x4a0fd4: mov ecx, dword ptr [ebx + 0x464]
0x4a0fda: mov dword ptr [edx + 0x10], ecx
0x4a0fdd: lea ecx, [ebx + 0x4a8]
0x4a0fe3: mov eax, dword ptr [ebp - 0x20]
0x4a0fe6: mov edx, dword ptr [ebx + 0x4a4]
0x4a0fec: mov dword ptr [eax + 0x14], edx
0x4a0fef: mov dword ptr [ebp - 0x24], ecx
0x4a0ff2: mov edi, dword ptr [ebp - 0x20]
0x4a0ff5: mov eax, dword ptr [ebp - 0x24]
0x4a0ff8: push eax
0x4a0ff9: add edi, 0x18
0x4a0ffc: push edi
0x4a0ffd: call 0x408440
0x4a1002: add esp, 8
0x4a1005: mov edx, dword ptr [ebp - 0x20]
0x4a1008: mov ecx, dword ptr [ebx + 0x4b4]
0x4a100e: mov dword ptr [edx + 0x24], ecx
0x4a1011: push 0x20
0x4a1013: push ebx
0x4a1014: call 0x4a800c
0x4a1019: add esp, 8
0x4a101c: push 0x4e0fd4 ; [0x4e0fd4]=0f
0x4a1021: lea eax, [ebx + 0x298]
0x4a1027: push eax
0x4a1028: call 0x40a7f4
0x4a102d: add esp, 8
0x4a1030: mov edx, dword ptr [ebp - 0x20]
0x4a1033: mov ecx, dword ptr [esi + 0x128]
0x4a1039: mov dword ptr [edx + 0x28], ecx
0x4a103c: mov eax, dword ptr [esi + 0x128]
0x4a1042: mov dword ptr [ebx + 0x6b4], eax
0x4a1048: jmp 0x4a11d9
===== case 6 (subsystem alarm B/death) @0x4a104d..0x4a10ff =====
0x4a104d: mov edx, dword ptr [ebp + 0xc]
0x4a1050: mov dword ptr [ebp - 0x28], edx
0x4a1053: push edi
0x4a1054: mov ecx, dword ptr [ebp - 0x28]
0x4a1057: push ecx
0x4a1058: push ebx
0x4a1059: call 0x41bd60
0x4a105e: mov eax, dword ptr [ebp - 0x28]
0x4a1061: add esp, 0xc
0x4a1064: mov dword ptr [eax], 0x2c
0x4a106a: mov edx, dword ptr [ebp - 0x28]
0x4a106d: mov word ptr [edx + 4], 0
0x4a1073: mov ecx, dword ptr [ebp - 0x28]
0x4a1076: mov eax, dword ptr [ebx + 0x464]
0x4a107c: mov dword ptr [ecx + 0x10], eax
0x4a107f: lea eax, [ebx + 0x4a8]
0x4a1085: mov edx, dword ptr [ebp - 0x28]
0x4a1088: mov ecx, dword ptr [ebx + 0x4a4]
0x4a108e: mov dword ptr [edx + 0x14], ecx
0x4a1091: mov dword ptr [ebp - 0x2c], eax
0x4a1094: mov edi, dword ptr [ebp - 0x28]
0x4a1097: mov eax, dword ptr [ebp - 0x2c]
0x4a109a: push eax
0x4a109b: add edi, 0x18
0x4a109e: push edi
0x4a109f: call 0x408440
0x4a10a4: mov edx, dword ptr [ebp - 0x28]
0x4a10a7: add esp, 8
0x4a10aa: mov ecx, dword ptr [ebx + 0x4b4]
0x4a10b0: lea eax, [ebx + 0x714]
0x4a10b6: mov dword ptr [edx + 0x24], ecx
0x4a10b9: push 0
0x4a10bb: push eax
0x4a10bc: call 0x41bbd8
0x4a10c1: add esp, 8
0x4a10c4: mov dword ptr [ebx + 0x658], 1
0x4a10ce: push 0x4e0fd4 ; [0x4e0fd4]=0f
0x4a10d3: lea edx, [ebx + 0x298]
0x4a10d9: push edx
0x4a10da: call 0x40a7f4
0x4a10df: add esp, 8
0x4a10e2: mov ecx, dword ptr [ebp - 0x28]
0x4a10e5: mov eax, dword ptr [esi + 0x128]
0x4a10eb: mov dword ptr [ecx + 0x28], eax
0x4a10ee: mov edx, dword ptr [esi + 0x128]
0x4a10f4: mov dword ptr [ebx + 0x6b4], edx
0x4a10fa: jmp 0x4a11d9
===== case 7 (subsystem alarm C) @0x4a10ff..0x4a117f =====
0x4a10ff: mov ecx, dword ptr [ebp + 0xc]
0x4a1102: mov dword ptr [ebp - 0x30], ecx
0x4a1105: push edi
0x4a1106: mov eax, dword ptr [ebp - 0x30]
0x4a1109: push eax
0x4a110a: push ebx
0x4a110b: call 0x41bd60
0x4a1110: mov edx, dword ptr [ebp - 0x30]
0x4a1113: add esp, 0xc
0x4a1116: mov dword ptr [edx], 0x2c
0x4a111c: mov ecx, dword ptr [ebp - 0x30]
0x4a111f: mov word ptr [ecx + 4], 0
0x4a1125: mov eax, dword ptr [ebp - 0x30]
0x4a1128: mov edx, dword ptr [ebx + 0x464]
0x4a112e: mov dword ptr [eax + 0x10], edx
0x4a1131: lea edx, [ebx + 0x4a8]
0x4a1137: mov ecx, dword ptr [ebp - 0x30]
0x4a113a: mov eax, dword ptr [ebx + 0x4a4]
0x4a1140: mov dword ptr [ecx + 0x14], eax
0x4a1143: mov dword ptr [ebp - 0x34], edx
0x4a1146: mov edi, dword ptr [ebp - 0x30]
0x4a1149: mov eax, dword ptr [ebp - 0x34]
0x4a114c: push eax
0x4a114d: add edi, 0x18
0x4a1150: push edi
0x4a1151: call 0x408440
0x4a1156: add esp, 8
0x4a1159: mov edx, dword ptr [ebp - 0x30]
0x4a115c: mov ecx, dword ptr [ebx + 0x4b4]
0x4a1162: mov dword ptr [edx + 0x24], ecx
0x4a1165: mov eax, dword ptr [ebp - 0x30]
0x4a1168: mov edx, dword ptr [esi + 0x128]
0x4a116e: mov dword ptr [eax + 0x28], edx
0x4a1171: mov ecx, dword ptr [esi + 0x128]
0x4a1177: mov dword ptr [ebx + 0x6b4], ecx
0x4a117d: jmp 0x4a11d9
===== case 8 (single scalar) @0x4a117f..0x4a11cb =====
0x4a117f: mov eax, dword ptr [ebp + 0xc]
0x4a1182: mov dword ptr [ebp - 0x38], eax
0x4a1185: push edi
0x4a1186: mov edx, dword ptr [ebp - 0x38]
0x4a1189: push edx
0x4a118a: push ebx
0x4a118b: call 0x41bd60
0x4a1190: add esp, 0xc
0x4a1193: mov ecx, dword ptr [ebp - 0x38]
0x4a1196: mov dword ptr [ecx], 0x18
0x4a119c: mov eax, dword ptr [ebp - 0x38]
0x4a119f: mov word ptr [eax + 4], 0
0x4a11a5: mov edx, dword ptr [ebp - 0x38]
0x4a11a8: mov ecx, dword ptr [ebx + 0x3f4]
0x4a11ae: mov dword ptr [edx + 0x10], ecx
0x4a11b1: mov eax, dword ptr [ebp - 0x38]
0x4a11b4: mov edx, dword ptr [esi + 0x128]
0x4a11ba: mov dword ptr [eax + 0x14], edx
0x4a11bd: mov ecx, dword ptr [esi + 0x128]
0x4a11c3: mov dword ptr [ebx + 0x6b4], ecx
0x4a11c9: jmp 0x4a11d9
===== case 1 + default + epilogue @0x4a11cb..0x4a1232 =====
0x4a11cb: push edi
0x4a11cc: mov eax, dword ptr [ebp + 0xc]
0x4a11cf: push eax
0x4a11d0: push ebx
0x4a11d1: call 0x4225a4
0x4a11d6: add esp, 0xc
0x4a11d9: mov eax, dword ptr [ebx + 0x3c]
0x4a11dc: mov edx, dword ptr [ebx + 0x40]
0x4a11df: cmp eax, edx
0x4a11e1: je 0x4a121e
0x4a11e3: cmp eax, 2
0x4a11e6: jne 0x4a121e
0x4a11e8: mov esi, ebx
0x4a11ea: push esi
0x4a11eb: lea eax, [ebp - 0x64]
0x4a11ee: push eax
0x4a11ef: call 0x436668
0x4a11f4: add esp, 8
0x4a11f7: lea edx, [ebp - 0x64]
0x4a11fa: push edx
0x4a11fb: push esi
0x4a11fc: mov ecx, dword ptr [0x4efc94] ; [0x4efc94]=0f
0x4a1202: mov eax, dword ptr [ecx + 0x38]
0x4a1205: push eax
0x4a1206: call 0x4364e4
0x4a120b: add esp, 0xc
0x4a120e: mov eax, dword ptr [ebx + 0x40]
0x4a1211: push eax
0x4a1212: add ebx, 0x2c
0x4a1215: push ebx
0x4a1216: call 0x41bbd8
0x4a121b: add esp, 8
0x4a121e: pop edi
0x4a121f: pop esi
0x4a1220: pop ebx
0x4a1221: mov esp, ebp
0x4a1223: pop ebp
0x4a1224: ret
0x4a1225: add byte ptr [eax], al
0x4a1227: add byte ptr [eax], al
0x4a1229: add byte ptr [eax], ah
0x4a122b: inc ecx
0x4a122c: push ebp
0x4a122d: mov ebp, esp
0x4a122f: add esp, -0x34