MP: the Mech-level update records @0x4a0c2c/@0x4a1232 -- all 9 types byte-exact (task #1)

- Writer transcribed from the recovered disasm; reader rewritten from
  part_012.c with every Wword absorber promoted to named engine/port members
  (updateOrigin/updateVelocity/projectedOrigin/projectedVelocity/nextUpdate/
  lastUpdate + bodyTargetSpeed/latches/alarms).  Wire sizes verified live:
  0x14/0x20/0x2c/0x78.
- movementMode UNIFIED with Simulation::simulationState (binary mech+0x40 =
  StateIndicator@0x2c currentState) -- death/limbo/airborne now replicate in
  every record header.  Three mislabels of the same binary fns retired:
  SetInstanceFlags + RequestActionFlags -> Mech::ForceUpdate (updateModel |=
  mask, 0xfe03 disabled filter); IsNetworkCopy -> IsDisabled.
- Senders wired byte-exact: gait transitions Force(8), knockdown Force(1|0x20),
  death Force(1|0x40), Reset Force(0x1f) + binary zero-set, perf-loop
  deadbands (speed type 2, orientation type 4, heat type 7).
- 2-node verified: types 2/3/4 flow while driving; kill -> type-6 record
  (simState=9) -> the OBSERVER's replicant runs the wreck sink loop with no
  double death transition; respawn 0x1f burst snaps + un-wrecks the peer;
  walking replicant un-regressed (run 12, cycle tracking); solo clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 11:38:49 -05:00
co-authored by Claude Fable 5
parent 3c34ae6de6
commit c9f0c2a7f1
6 changed files with 641 additions and 185 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ int Mech::IsAirborne()
// that lands. Faithful to the binary regardless. // that lands. Faithful to the binary regardless.
Logical Mech::IsDisabled() Logical Mech::IsDisabled()
{ {
return (movementMode == 2 || movementMode == 9) ? True : False; return (MovementMode() == 2 || MovementMode() == 9) ? True : False;
} }
// mech+0x404 = the radar display scale. Both the radar map's currentScale and // mech+0x404 = the radar display scale. Both the radar map's currentScale and
+376 -102
View File
@@ -388,7 +388,6 @@ template<class...A> inline void CloseNotation(A&&...) {}
template<class...A> inline void ParseVector(A&&...) {} template<class...A> inline void ParseVector(A&&...) {}
template<class...A> inline int ParseJointResource(A&&...) { return 0; } template<class...A> inline int ParseJointResource(A&&...) { return 0; }
template<class...A> inline int CreateModelResourceBase(A&&...){ return 0; } template<class...A> inline int CreateModelResourceBase(A&&...){ return 0; }
template<class...A> inline int IsNetworkCopy(A&&...) { return 0; }
template<class...A> inline void StreamDamageZone(A&&...) {} template<class...A> inline void StreamDamageZone(A&&...) {}
template<class...A> inline void StreamDamageLookup(A&&...) {} template<class...A> inline void StreamDamageLookup(A&&...) {}
@@ -870,13 +869,16 @@ Mech::Mech(
creationTime = Now(); // FUN_00414b60 -> Wword(0x1de) creationTime = Now(); // FUN_00414b60 -> Wword(0x1de)
Wword(0xd2) = 0; Wword(0xd2) = 0;
Wword(0x1ae) = 0; Wword(0x1ae) = 0;
Wword(0x1ad) = 0; bodyTargetSpeed = 0.0f; // @0x6b4 (was absorbed Wword(0x1ad))
// //
// Seed the pose: identity body rotation, identity aim quaternions. // Seed the pose + the dead-reckon baselines (task #1 corrections: the
// binary's this[0x98]/this[0x4b] Origin copies are projectedOrigin@0x260
// and updateOrigin@0x12c seeded from localOrigin@0x100 -- the ENGINE
// inherited members, not mech-local "torso" fields).
// //
torsoRotation = bodyRotation; // FUN_0040a938(this+0x98,this+0x40) projectedOrigin = localOrigin; // FUN_0040a938(this+0x98,this+0x40)
Wword(0x4b) = bodyRotation; // FUN_0040a938(this+0x4b,this+0x40) updateOrigin = localOrigin; // FUN_0040a938(this+0x4b,this+0x40)
Wword(0x163) = Wword(0x40); // FUN_00408440(this+0x163,this+0x40) Wword(0x163) = Wword(0x40); // FUN_00408440(this+0x163,this+0x40)
turretBase.SetIdentity(); // FUN_0040a7f4(this+0x71, IdentityQuat) turretBase.SetIdentity(); // FUN_0040a7f4(this+0x71, IdentityQuat)
torsoAimCurrent.SetIdentity(); // this+0xa6 torsoAimCurrent.SetIdentity(); // this+0xa6
@@ -888,7 +890,8 @@ Mech::Mech(
Wword(0x166) = ZeroVector; Wword(0x166) = ZeroVector;
Wword(0x169) = 0; Wword(0x169) = 0;
Wword(0xe6) = 0; Wword(0xe6) = 0;
Wword(0x1df) = 0; poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
Wword(0xfe) = Wword(0xfc) = Wword(0x105) = 0; Wword(0xfe) = Wword(0xfc) = Wword(0x105) = 0;
mechName.Copy(&DAT_004e0f8c); // FUN_00408e90(this+0xd8,"") mechName.Copy(&DAT_004e0f8c); // FUN_00408e90(this+0xd8,"")
Wword(0x160) = Wword(0x15f) = 0; Wword(0x160) = Wword(0x15f) = 0;
@@ -897,7 +900,8 @@ Mech::Mech(
Wword(0x20b).SetIdentity(); Wword(0x20b).SetIdentity();
Wword(0x1e7) = Wword(0x1dd) = Wword(0x209) = 0; Wword(0x1e7) = Wword(0x1dd) = Wword(0x209) = 0;
Wword(0xd5) = Wword(0xd6) = Wword(0xd7) = 0; Wword(0xd5) = Wword(0xd6) = Wword(0xd7) = 0;
Wword(0x12d) = 0; fallScalar = 0.0f; // @0x4b4 (was absorbed Wword(0x12d))
fallDirection = Vector3D(0.0f, 0.0f, 1.0f); // @0x4a8 fall/impact record payload
throttleState = 2; // Wword(0x129) = 2 throttleState = 2; // Wword(0x129) = 2
Wword(0x1e0) = Wword(0x119); Wword(0x1e0) = Wword(0x119);
@@ -1643,159 +1647,429 @@ Logical
//########################################################################### //###########################################################################
// ReadUpdateRecord -- @004a1232 (vtable slot 6) // ReadUpdateRecord -- @004a1232 (vtable slot 6)
// //
// Applies a streamed update packet. The packet's 16-bit type field (+6) // Applies one Mech-level replication record. The switch key is the
// selects how much of the Mech's replicated state is overwritten. After the // inherited Simulation::UpdateRecord recordID (u16 @+6) == the updateModel
// switch, a torso/leg state transition re-syncs the stability alarm. // bit that requested the record on the master. Byte-exact against
// part_012.c:9576-9692, with every binary offset mapped to the NAMED
// engine/port member (task #1, 2026-07-11; the old Wword-absorber body is
// gone). Cross-checked field-for-field against the writer @004a0c2c. [T1]
// //
// NOTE: the decomp recovers the body via the EBP frame (it is invoked through // Base chains: case 0 / default -> the JointedMover-level pair
// the vtable as a member fn); param shapes below are reconstructed. // (FUN_0042249c = Mover::ReadUpdateRecord); cases 2..8 -> ONLY the
// Simulation base (FUN_0041bd34: lastUpdate + SetSimulationState) -- the
// old reconstruction chained JointedMover everywhere, which misparsed the
// short records.
//
// simulationState rides EVERY record header (+0xC) and the base reader
// applies it -- so the replicant's MovementMode() (death 9 / limbo 2 /
// airborne 3,4) tracks the master automatically; the case-0 / tail edge
// tests below then see the old->current transition.
// //
void void
Mech::ReadUpdateRecord(Simulation::UpdateRecord *message) Mech::ReadUpdateRecord(Simulation::UpdateRecord *message)
{ {
Mech__UpdateRecord *record = (Mech__UpdateRecord *)message; static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
if (s_mrecLog && message->recordID != 0)
DEBUG_STREAM << "[mrec-rx] type=" << (int)message->recordID
<< " ent=" << GetEntityID() << " len=" << (int)message->recordLength
<< " simState=" << (int)message->simulationState << "\n" << std::flush;
switch (message->recordID) // u16 @+6
{
case 0: // full pose (Mover record + speedDemand tail)
{
Mech__PoseUpdateRecord *record = (Mech__PoseUpdateRecord *)message;
switch (record->recordType) // *(u16*)(msg+6) // The mech OWNS its replicated-orientation channel (the type-4
// resync record): save updateOrigin's rotation (quat->euler
// roundtrip, exactly the binary's FUN_00408f44/FUN_00409a00) and
// the update angular velocity around the base reader, which would
// otherwise overwrite both from the pose record.
EulerAngles savedRot;
savedRot = updateOrigin.angularPosition; // FUN_00408f44 (quat->euler)
Vector3D savedAngV = updateVelocity.angularMotion; // FUN_00408440
Mover::ReadUpdateRecord(message); // FUN_0042249c
updateOrigin.angularPosition = savedRot; // FUN_00409a00 (euler->quat)
updateVelocity.angularMotion = savedAngV; // FUN_00408440
poseSyncLatch = 1; // @0x77c -- arm the dead-reckon re-base
if (bodyAnimationState == 2 || bodyAnimationState == 3) // @0x728
{ {
case 0: // full pose snapshot projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
{ projectedVelocity.angularMotion = ZeroVector;
Vector3D savedPos = worldPosition; // Wword(0x4e)
EulerAngles savedRot = netOrientation; // Wword(0xb5)
JointedMover::ReadUpdateRecord(message); // FUN_0042249c
worldPosition = savedPos;
netOrientation = savedRot;
Wword(0x1df) = 1;
if (throttleState == 2 || throttleState == 3) // Wword(0x1ca)
{
torsoAimCurrent.SetIdentity(); // this+0xa6
} }
// torso/leg sync when the move state flips to/from "2" // Enter/leave simulation state 2 (disabled/limbo): re-base the
if (Wword(0x10) != Wword(0xf) && (Wword(0xf) == 2 || Wword(0x10) == 2)) // local + projected origins onto the freshly replicated
// updateOrigin and drop the re-base latch.
if (GetSimulationState() != GetOldSimulationState()
&& (GetOldSimulationState() == 2 || GetSimulationState() == 2))
{ {
torsoRotation.Mul(Wword(0x4b)); localOrigin = updateOrigin; // FUN_0040a938(0x100 <- 0x12c)
torsoAimCurrent.Mul(Wword(0x4b)); projectedOrigin = updateOrigin; // FUN_0040a938(0x260 <- 0x12c)
torsoAimCurrent = torsoAimTarget; projectedVelocity = updateVelocity; // FUN_0040a7f4(0x298 <- 0x2c8)
Wword(0x1df) = 0; poseSyncLatch = 0;
if ((instanceFlags & 0x40) == 0) if ((instanceFlags & 0x4000) == 0) // byte(this+0x29)&0x40 [T4 bit name]
{ {
ResetPose(); // (**(this+0x34))(this) ResetPose(); // vcall vtable+0x34 (slot 13) [T4]
} }
} }
Wword(0x1ad) = record->sequence; // puVar1[0x1d] bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x74
} }
break; break;
case 2: // alarm-only case 2: // commanded-speed update
JointedMover::ReadUpdateRecord(message); // FUN_0041bd34 {
Wword(0x1ad) = record->field4; Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
Simulation::ReadUpdateRecord(message); // FUN_0041bd34
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
}
break; break;
case 3: // score / heat state case 3: // leg/body state + stability
JointedMover::ReadUpdateRecord(message);
Wword(0x196) = record->field4;
stabilityAlarm.SetLevel(record->field6);
if (record->field5 == 0)
{ {
controllableSubsystems.Reset(); // FUN_004283b8 Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
statusAlarm.SetLevel(record->field5); Simulation::ReadUpdateRecord(message);
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
stabilityAlarm.SetLevel(record->stability); // @0x4c4 <- rec+0x18
if (record->legState == 0)
{
legAnimation.Reset(1); // FUN_004283b8(0x65c, 1)
bodyStateAlarm.SetLevel(record->legState); // @0x714
} }
else if (record->field5 == 1) else if (record->legState == 1)
{ {
statusAlarm.SetLevel(record->field5); bodyStateAlarm.SetLevel(record->legState);
} }
else else
{ {
SetStatusState(record->field5); // FUN_004a800c [mech2] SetBodyAnimation(record->legState); // FUN_004a800c
}
bodyTargetSpeed = record->speedDemand; // rec+0x1c
} }
Wword(0x1ad) = record->field7;
break; break;
case 4: // pose + server-time re-sync case 4: // orientation + angular-velocity re-sync
{ {
JointedMover::ReadUpdateRecord(message); Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
creationTime = Now(); // Wword(0x1de) Simulation::ReadUpdateRecord(message);
worldPosition = record->position; // this+0x4e <- msg+4 creationTime = Now(); // @0x778 -- the dead-reckon ref time
netOrientation = record->orientation; // this+0xb5 <- msg+7
Wword(0xb8) = Now();
Scalar age = (Scalar)(Wword(0xb8) - Wword(5)) / FrameTimeScale;
if (age < ReSyncSpeedThreshold) // 10.0f
{ {
Wword(0xb8) += (Wword(0xb8) - Wword(5)); // extrapolate EulerAngles e(Radian(record->eulerX), Radian(record->eulerY),
Radian(record->eulerZ));
updateOrigin.angularPosition = e; // FUN_00409a00 (euler->quat)
} }
Wword(0x1ad) = record->fieldA; updateVelocity.angularMotion = record->angularVelocity; // @0x2d4 <- rec+0x1c
nextUpdate = Now(); // @0x2e0
{
// extrapolate the next-update horizon unless the record is stale
Scalar age = (Scalar)(nextUpdate.ticks - lastUpdate.ticks) / FrameTimeScale;
if (age < ReSyncSpeedThreshold) // 10.0f (_DAT_004a1670)
{
nextUpdate.ticks += (nextUpdate.ticks - lastUpdate.ticks);
}
}
bodyTargetSpeed = record->speedDemand; // rec+0x28
} }
break; break;
case 5: // subsystem alarm (variant A) case 5: // knockdown
JointedMover::ReadUpdateRecord(message); {
heatAlarm.SetLevel(record->field4); // this+0x114 Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Wword(0x129) = record->field5; Simulation::ReadUpdateRecord(message);
Wword(0x12a) = record->field6; heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
Wword(0x12d) = record->field9; throttleState = record->throttleState; // @0x4a4 <- rec+0x14
SetStatusState(0x20); // FUN_004a800c [mech2] fallDirection = record->fallDirection; // @0x4a8 <- rec+0x18
torsoAimCurrent.SetIdentity(); fallScalar = record->fallScalar; // @0x4b4 <- rec+0x24
Wword(0x1ad) = record->fieldA; SetBodyAnimation(0x20); // FUN_004a800c -- the knockdown clip
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
projectedVelocity.angularMotion = ZeroVector;
bodyTargetSpeed = record->speedDemand; // rec+0x28
}
break; break;
case 6: // subsystem alarm (variant B -- death) case 6: // death
JointedMover::ReadUpdateRecord(message); {
heatAlarm.SetLevel(record->field4); Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Wword(0x129) = record->field5; Simulation::ReadUpdateRecord(message);
Wword(0x12a) = record->field6; heatAlarm.SetLevel(record->heatLevel);
Wword(0x12d) = record->field9; throttleState = record->throttleState;
statusAlarm.SetLevel(0); fallDirection = record->fallDirection;
Wword(0x195) = Wword(0x196) = 1; fallScalar = record->fallScalar;
torsoAimCurrent.SetIdentity(); bodyStateAlarm.SetLevel(0); // @0x714
Wword(0x1ad) = record->fieldA; legResetLatch = 1; // @0x654 (reader sets BOTH latches;
bodyResetLatch = 1; // @0x658 the writer only 0x658)
projectedVelocity.linearMotion = ZeroVector;
projectedVelocity.angularMotion = ZeroVector;
bodyTargetSpeed = record->speedDemand;
}
break; break;
case 7: // subsystem alarm (variant C) case 7: // impact (fields only, no side effects)
JointedMover::ReadUpdateRecord(message); {
heatAlarm.SetLevel(record->field4); Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Wword(0x129) = record->field5; Simulation::ReadUpdateRecord(message);
Wword(0x12a) = record->field6; heatAlarm.SetLevel(record->heatLevel);
Wword(0x12d) = record->field9; throttleState = record->throttleState;
Wword(0x1ad) = record->fieldA; fallDirection = record->fallDirection;
fallScalar = record->fallScalar;
bodyTargetSpeed = record->speedDemand;
}
break; break;
case 8: // single scalar case 8: // airborne/reverse cycle-rate selector
JointedMover::ReadUpdateRecord(message); {
Wword(0xfd) = record->field4; Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
Wword(0x1ad) = record->field5; Simulation::ReadUpdateRecord(message);
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
bodyTargetSpeed = record->speedDemand; // rec+0x14
}
break; break;
default: default: // type 1 (damage zones) + anything unknown -> the base chain
JointedMover::ReadUpdateRecord(message); // FUN_0042249c Mover::ReadUpdateRecord(message); // FUN_0042249c
break; break;
} }
// //
// Re-fire the stability alarm when the leg state transitions into "2". // Tail (shared with the writer @004a11d9): when the mech just LEFT
// simulation state 2 (re-activated from limbo), the binary rebuilt this
// entity's floating name bitmap (FUN_00436668 NameBitmapEntry +
// FUN_004364e4 RendererManager::ReplaceNameBitmap). That per-entry API
// belonged to the Division IG renderer and was removed in the WinTesla
// port -- the bulk SortAndReloadNameBitmaps refresh subsumes it (port
// precedent: btl4pb.cpp:560). The state edge is then explicitly
// acknowledged (SetState(current) collapses oldState = currentState).
// //
if (Wword(0xf) != Wword(0x10) && Wword(0xf) == 2) if (GetOldSimulationState() != GetSimulationState()
&& GetOldSimulationState() == 2)
{ {
StabilityMessage msg; // FUN_00436668 SetSimulationState(GetSimulationState()); // FUN_0041bbd8(this+0x2c, cur)
BroadcastToTeam(msg); // FUN_004364e4
stabilityAlarm.SetLevel(Wword(0x10)); // FUN_0041bbd8(this+0xb,...)
} }
} }
//########################################################################### //###########################################################################
// SetInstanceFlags -- @004a4c54 // WriteUpdateRecord -- @004a0c2c (vtable slot 7)
// //
// OR a flag word into the entity instance flags (this+0x18); copies (network // Serializes one Mech-level record; the jump-table type == the updateModel
// replicants) are stripped of all but the low/high reserved bits. // bit that requested it (see Mech::ForceUpdate and the senders in
// mech2/mech4). Transcribed instruction-for-instruction from the recovered
// disasm (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt), offsets
// mapped to named members (task #1, 2026-07-11). [T1]
//
// Conventions the engine requires: recordLength must equal each type's
// exact size (the receiver frames the stream on it, ENTITY.cpp:390) and
// subsystemID = 0 routes the record to the entity itself. The trailing
// speedDemand stamp mirrors into bodyTargetSpeed@0x6b4 -- the master-side
// "last replicated" baseline the perf-loop deadbands diff against.
// //
void void
Mech::SetInstanceFlags(Word flags) Mech::WriteUpdateRecord(Simulation::UpdateRecord *message, int record_type)
{ {
if (IsNetworkCopy()) // FUN_0049fb54 // The binary stamps *(subsystemArray[0] + 0x128) = the controls mapper's
// speedDemand into every record tail. Our live mapper handle is
// controlsMapper (the [0x10d] cache); replicants have no pilot input.
// (Bridge fn: this TU carries a local recon stub under the mapper's name,
// so the bridge is type-erased; mechmppr.cpp casts to the real class.)
extern Scalar BTMapperSpeedDemandRaw(void *mapper);
Scalar speedDemand = BTMapperSpeedDemandRaw((void *)controlsMapper);
static const int s_mrecLog = getenv("BT_REPL_LOG") ? 1 : 0;
if (s_mrecLog && record_type != 0)
DEBUG_STREAM << "[mrec-tx] type=" << record_type
<< " ent=" << GetEntityID() << " state=" << (int)GetSimulationState()
<< "\n" << std::flush;
switch (record_type)
{ {
flags &= 0xfe03; case 0: // full pose @4a0c79
{
Mech__PoseUpdateRecord *record = (Mech__PoseUpdateRecord *)message;
EulerAngles savedRot;
savedRot = updateOrigin.angularPosition; // FUN_00408f44 (quat->euler)
Vector3D savedAngV = updateVelocity.angularMotion; // FUN_00408440
Mover::WriteUpdateRecord(message, record_type); // FUN_004225a4
record->subsystemID = 0; // word @+4
poseSyncLatch = 1; // @0x77c = 1
updateOrigin.angularPosition = savedRot; // FUN_00409a00 restore
updateVelocity.angularMotion = savedAngV; // FUN_00408440 restore
if (legAnimationState == 2 || legAnimationState == 3) // @0x3b0
{
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zero)
projectedVelocity.angularMotion = ZeroVector;
} }
*(Word *)((char *)this + 0x18) |= flags; if (GetOldSimulationState() != GetSimulationState()
&& (GetOldSimulationState() == 2 || GetSimulationState() == 2))
{
projectedOrigin = updateOrigin; // FUN_0040a938(0x260 <- 0x12c)
projectedVelocity = updateVelocity; // FUN_0040a7f4(0x298 <- 0x2c8)
poseSyncLatch = 0;
} }
record->speedDemand = speedDemand; // rec+0x74
bodyTargetSpeed = speedDemand; // @0x6b4
record->recordLength = 0x78;
}
break;
case 3: // leg/body state + stability @4a0d84
{
Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type); // FUN_0041bd60
record->recordLength = 0x20;
record->subsystemID = 0;
record->legResetLatch = legResetLatch; // rec+0x10 <- @0x654
record->legState = legAnimationState; // rec+0x14 <- @0x3b0
record->stability = (int)stabilityAlarm.GetLevel(); // rec+0x18 <- @0x4d8
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
// the writer runs the SAME state dispatch on itself:
if (record->legState == 0)
{
legAnimation.Reset(1); // FUN_004283b8(0x65c, 1)
bodyStateAlarm.SetLevel(record->legState);
}
else if (record->legState == 1)
{
bodyStateAlarm.SetLevel(record->legState);
}
else
{
SetBodyAnimation(record->legState); // FUN_004a800c
}
record->speedDemand = speedDemand; // rec+0x1c
bodyTargetSpeed = speedDemand;
}
break;
case 2: // commanded speed @4a0e5b
{
Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x14;
record->subsystemID = 0;
record->speedDemand = speedDemand; // rec+0x10
bodyTargetSpeed = speedDemand;
}
break;
case 4: // orientation + angular-velocity re-sync @4a0e9e
{
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
localOrigin.angularPosition.Normalize(); // FUN_0040a138(0x10c)
{
EulerAngles e;
e = localOrigin.angularPosition; // FUN_00408f44 (quat->euler)
record->eulerX = e.pitch; // rec+0x10
record->eulerY = e.yaw;
record->eulerZ = e.roll;
}
record->angularVelocity = localVelocity.angularMotion; // rec+0x1c <- @0x1d0
creationTime = Now(); // @0x778
nextUpdate = Now(); // @0x2e0
{
Scalar age = (Scalar)(nextUpdate.ticks - lastUpdate.ticks) / FrameTimeScale;
if (age < ReSyncSpeedThreshold) // 10.0f
{
nextUpdate.ticks += (nextUpdate.ticks - lastUpdate.ticks);
}
}
updateOrigin.angularPosition = localOrigin.angularPosition; // FUN_00409968(0x138 <- 0x10c)
updateVelocity.angularMotion = localVelocity.angularMotion; // FUN_00408440(0x2d4 <- 0x1d0)
record->speedDemand = speedDemand; // rec+0x28
bodyTargetSpeed = speedDemand;
}
break;
case 5: // knockdown @4a0fab
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel(); // rec+0x10 <- @0x464
record->throttleState = throttleState; // rec+0x14 <- @0x4a4
record->fallDirection = fallDirection; // rec+0x18 <- @0x4a8
record->fallScalar = fallScalar; // rec+0x24 <- @0x4b4
SetBodyAnimation(0x20); // FUN_004a800c(this, 0x20)
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zero)
projectedVelocity.angularMotion = ZeroVector;
record->speedDemand = speedDemand; // rec+0x28
bodyTargetSpeed = speedDemand;
}
break;
case 6: // death @4a104d
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel();
record->throttleState = throttleState;
record->fallDirection = fallDirection;
record->fallScalar = fallScalar;
bodyStateAlarm.SetLevel(0); // @0x714
bodyResetLatch = 1; // @0x658 (writer sets ONLY this one)
projectedVelocity.linearMotion = ZeroVector;
projectedVelocity.angularMotion = ZeroVector;
record->speedDemand = speedDemand;
bodyTargetSpeed = speedDemand;
}
break;
case 7: // impact @4a10ff (fields only)
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x2c;
record->subsystemID = 0;
record->heatLevel = (int)heatAlarm.GetLevel();
record->throttleState = throttleState;
record->fallDirection = fallDirection;
record->fallScalar = fallScalar;
record->speedDemand = speedDemand;
bodyTargetSpeed = speedDemand;
}
break;
case 8: // airborne/reverse selector @4a117f
{
Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
Simulation::WriteUpdateRecord(message, record_type);
record->recordLength = 0x18;
record->subsystemID = 0;
record->airborne = airborneSelect; // rec+0x10 <- @0x3f4
record->speedDemand = speedDemand; // rec+0x14
bodyTargetSpeed = speedDemand;
}
break;
default: // type 1 (damage zones) + unknown @4a11cb
Mover::WriteUpdateRecord(message, record_type); // FUN_004225a4
break;
}
// Tail @4a11d9 -- identical to the reader's (see there for the name-
// bitmap note): acknowledge the leave-state-2 edge.
if (GetOldSimulationState() != GetSimulationState()
&& GetOldSimulationState() == 2)
{
SetSimulationState(GetSimulationState()); // FUN_0041bbd8(this+0x2c, cur)
}
}
// (The @004a4c54 body lives inline in mech.hpp as Mech::ForceUpdate -- the
// old "SetInstanceFlags" reading here was wrong twice over: +0x18 is
// Simulation::updateModel, not the entity instance flags, and FUN_0049fb54
// is IsDisabled (simulationState 2||9), not an is-network-copy test.)
//########################################################################### //###########################################################################
+164 -31
View File
@@ -152,26 +152,82 @@ struct ShotDescriptor
//######################### Mech::UpdateRecord ########################## //######################### Mech::UpdateRecord ##########################
//########################################################################### //###########################################################################
// //
// The network/replay update packet applied by Mech::ReadUpdateRecord // The Mech-level replication records (task #1, 2026-07-11). The record TYPE
// @004a1232. The switch is keyed on a 16-bit record-type field at +6 of the // is the inherited Simulation::UpdateRecord `recordID` (u16 @+6) == the
// message (cases 0,2..8). Field layout is best-effort -- only the offsets // updateModel BIT INDEX that requested it (Simulation::WriteSimulationUpdate,
// touched by the decomp are named. // SIMULATE.cpp:302-328). Writer @004a0c2c (slot 7, 9-case jump table) /
// reader @004a1232 (slot 6). Field layouts are byte-exact from the writer
// disasm (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) cross-mapped
// against the reader pseudocode (part_012.c:9576-9692). [T1]
// //
struct Mech__UpdateRecord: // EVERY record's trailing Scalar is the controls-mapper's speedDemand
public JointedMover::UpdateRecord // (subsystemArray[0]+0x128), stored into mech+0x6b4 `bodyTargetSpeed` on BOTH
// sides -- the authentic replicant-gait feed (it is NOT a sequence number).
//
// type 0 (len 0x78): the full Mover pose record + the speedDemand tail.
struct Mech__PoseUpdateRecord:
public Mover::UpdateRecord // 0x74
{ {
// case 0 (full pose) : position (+0x10), orientation (+0x74) Scalar speedDemand; // +0x74
// case 2/3 (alarm/state) : alarm levels, score/heat state
// case 4 (snap+time) : pose + server timestamp re-sync
// case 5/6/7 (subsystem) : per-subsystem alarm + flags
// case 8 (single scalar) : misc field [0xfd]
int recordType; // *(u16*)(msg+6) -- switch selector
int sequence; // trailing replication sequence word
int field4, field5, field6, field7, field9, fieldA; // per-case payload words
Vector3D position; // case 0/4 world position
EulerAngles orientation; // case 0/4 net orientation
}; };
// type 2 (len 0x14): speedDemand alone (sent when the commanded speed
// drifts from the last-replicated bodyTargetSpeed).
struct Mech__SpeedUpdateRecord:
public Simulation::UpdateRecord // 0x10
{
Scalar speedDemand; // +0x10
};
// type 3 (len 0x20): leg/body state + stability.
struct Mech__StateUpdateRecord:
public Simulation::UpdateRecord
{
int legResetLatch; // +0x10 <- 0x654 ; -> 0x658
int legState; // +0x14 <- legStateAlarm.currentLevel (0x3b0)
int stability; // +0x18 <- stabilityAlarm.currentLevel (0x4d8)
Scalar speedDemand; // +0x1c
};
// type 4 (len 0x2c): orientation + angular-velocity re-sync (pose delta).
struct Mech__ResyncUpdateRecord:
public Simulation::UpdateRecord
{
Scalar eulerX, eulerY, eulerZ; // +0x10 localOrigin.angularPosition as euler
Vector3D angularVelocity; // +0x1c live localVelocity.angularMotion (0x1d0)
Scalar speedDemand; // +0x28
};
// types 5 (knockdown) / 6 (death) / 7 (impact), all len 0x2c: heat +
// throttle + the ground-contact basis. Same payload, different side
// effects on each end.
struct Mech__FallUpdateRecord:
public Simulation::UpdateRecord
{
int heatLevel; // +0x10 <- heatAlarm.currentLevel (0x464)
int throttleState; // +0x14 <- 0x4a4
Vector3D fallDirection; // +0x18 <- 0x4a8 (ctor (0,0,1); name [T4])
Scalar fallScalar; // +0x24 <- 0x4b4 (name [T4])
Scalar speedDemand; // +0x28
};
// type 8 (len 0x18): the airborne/reverse cycle-rate selector flag.
struct Mech__AirborneUpdateRecord:
public Simulation::UpdateRecord
{
int airborne; // +0x10 <- 0x3f4
Scalar speedDemand; // +0x14
};
static_assert(sizeof(Simulation::UpdateRecord) == 0x10, "Simulation record header 0x10");
static_assert(sizeof(Mover::UpdateRecord) == 0x74, "Mover pose record 0x74");
static_assert(sizeof(Mech__PoseUpdateRecord) == 0x78, "type 0 record 0x78");
static_assert(sizeof(Mech__SpeedUpdateRecord) == 0x14, "type 2 record 0x14");
static_assert(sizeof(Mech__StateUpdateRecord) == 0x20, "type 3 record 0x20");
static_assert(sizeof(Mech__ResyncUpdateRecord) == 0x2c, "type 4 record 0x2c");
static_assert(sizeof(Mech__FallUpdateRecord) == 0x2c, "type 5/6/7 record 0x2c");
static_assert(sizeof(Mech__AirborneUpdateRecord) == 0x18, "type 8 record 0x18");
//########################################################################### //###########################################################################
//########################## Mech::MakeMessage ########################## //########################## Mech::MakeMessage ##########################
//########################################################################### //###########################################################################
@@ -308,9 +364,13 @@ struct ShotDescriptor
// //
public: public:
typedef Mech__ModelResource ModelResource; typedef Mech__ModelResource ModelResource;
typedef Mech__UpdateRecord UpdateRecord;
typedef Mech__MakeMessage MakeMessage; typedef Mech__MakeMessage MakeMessage;
typedef Mech__DamageZone DamageZone; typedef Mech__DamageZone DamageZone;
// (No UpdateRecord typedef: the Mech records are per-type structs --
// Mech__PoseUpdateRecord etc. above -- and a class-local UpdateRecord
// typedef is the silent-non-override trap: an override declared with
// the shadowing typedef does NOT override the engine virtual. See
// mechweap.hpp:174 / [[reconstruction-gotchas]].)
static Mech* static Mech*
Make(MakeMessage *creation_message); // @004a2d48 Make(MakeMessage *creation_message); // @004a2d48
@@ -363,22 +423,57 @@ struct ShotDescriptor
); );
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation / replication // Simulation / replication (task #1: the Mech-level update records)
// //
// The record type == the updateModel BIT INDEX (Simulation::
// WriteSimulationUpdate walks the mask, SIMULATE.cpp:302-328). Bits 0/1
// are the engine's (Mover pose / Entity damage-zone); 2..8 are Mech's.
// Request one with ForceUpdate(1<<bit); the emitter consumes-and-zeroes
// the mask, so each request produces exactly one record. [T1 senders
// mapped from the binary: 27 call sites, perf loop + events]
//
public:
enum {
MechSpeedUpdateModelBit = 2, // commanded-speed change
MechStateUpdateModelBit = 3, // leg/body state + stability
MechResyncUpdateModelBit = 4, // orientation/angular-vel delta
MechKnockdownUpdateModelBit= 5, // knockdown (SetBodyAnimation 0x20)
MechDeathUpdateModelBit = 6, // death
MechImpactUpdateModelBit = 7, // heat/impact state changed
MechAirborneUpdateModelBit = 8 // airborne/reverse cycle selector
};
//
// @004a4c54 -- Mech::ForceUpdate: OR record-request bits into the
// engine's Simulation::updateModel. A disabled/destroyed mech
// (simulationState 2 or 9) is masked to pose + damage-zone + high
// bits (mask &= 0xfe03): the event records 2..8 stop flowing from a
// wreck. SHADOWS the non-virtual Simulation::ForceUpdate. [T1]
// (Two earlier mislabels of the same binary fn are retired:
// "SetInstanceFlags(Word)" -- +0x18 is updateModel, NOT the entity
// instance flags -- and "RequestActionFlags(Word)" writing a
// side-member the emitter never read.)
//
void
ForceUpdate(Word mask = Simulation::DefaultUpdateModelFlag)
{
if (IsDisabled()) // FUN_0049fb54 (simState 2||9)
{
mask &= 0xfe03;
}
updateModel |= mask;
}
protected: protected:
void void
ReadUpdateRecord( // @004a1232 (vtable slot 6) ReadUpdateRecord( // @004a1232 (vtable slot 6)
Simulation::UpdateRecord *message Simulation::UpdateRecord *message
); );
// WriteUpdateRecord / per-frame Simulate live in the uncaptured
// mech.cpp gap (slot 7 @004a0c2c) and mech2.cpp. [mech2]
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Instance-flag helper
//
protected:
void void
SetInstanceFlags(Word flags); // @004a4c54 WriteUpdateRecord( // @004a0c2c (vtable slot 7)
Simulation::UpdateRecord *message,
int record_type
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem roster (host pattern -- cf. VTV / CLASSMAP shared base) // Subsystem roster (host pattern -- cf. VTV / CLASSMAP shared base)
@@ -448,8 +543,14 @@ struct ShotDescriptor
protected: protected:
Matrix34 orientation; // @0xd0 this[0x34] (FUN_0040ab44) Matrix34 orientation; // @0xd0 this[0x34] (FUN_0040ab44)
Quaternion bodyRotation; // @0x100 this[0x40] Quaternion bodyRotation; // @0x100 this[0x40]
BTVal torsoRotation; // @0x130 this[0x4b] (proxy: .Mul) // (task #1 corrections: the old `BTVal torsoRotation @0x130 [0x4b]`
Vector3D worldPosition; // @0x138 this[0x4e] // and `Vector3D worldPosition @0x138 [0x4e]` members were MISREADS of
// the ENGINE base fields -- word 0x4b = byte 0x12c = Entity::
// updateOrigin, and 0x138 = updateOrigin.angularPosition (Quaternion).
// The update-record reader/writer now uses the inherited engine
// members directly. The 24 bytes stay as PADDING so every following
// member keeps its layout-locked OUR-offset (mech4 static_asserts).)
int retiredUpdateRecordPad[6];
BTVal turretBase; // @0x1c4 this[0x71] (proxy: .SetIdentity) BTVal turretBase; // @0x1c4 this[0x71] (proxy: .SetIdentity)
Quaternion headPitch; // @0x1cc this[0x73] (look) Quaternion headPitch; // @0x1cc this[0x73] (look)
Quaternion torsoTwist; // @0x1dc this[0x77] Quaternion torsoTwist; // @0x1dc this[0x77]
@@ -467,6 +568,19 @@ struct ShotDescriptor
public: public:
Joint *shadowJointNode; // binary @0x42c this[0x10b] Joint *shadowJointNode; // binary @0x42c this[0x10b]
void UpdateShadowJoint(const Vector3D &ground_normal); void UpdateShadowJoint(const Vector3D &ground_normal);
// Update-record state (task #1; by-name access, declared after the locked
// fields so nothing shifts):
int poseSyncLatch; // binary @0x77c -- armed by every type-0 record
// on both sides; consumed by the dead-reckoner
// (FUN_004ab1c8: replicant motionEventVector =
// updateOrigin - localOrigin; master
// projectedOrigin re-base); cleared on the
// simState-2 edge
Vector3D fallDirection; // binary @0x4a8 -- knockdown/death/impact record
// payload; ctor (0,0,1) [T4 name]
Scalar fallScalar; // binary @0x4b4 -- same record family; ctor 0 [T4 name]
int heatLevelSnapshot; // binary @0x780 -- heatAlarm level at frame
// entry; the type-7 impact-record deadband
// AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode; // AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode;
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared // binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
// after the layout-locked fields so nothing shifts. // after the layout-locked fields so nothing shifts.
@@ -636,8 +750,24 @@ protected:
void *controlSource; // @0x128 controls subsystem handle void *controlSource; // @0x128 controls subsystem handle
// --- locomotion / gait animation state (mech2/mech3 maps) ------------ // --- locomotion / gait animation state (mech2/mech3 maps) ------------
Word actionRequestFlags; // @0x18 pending action bits // movementMode UNIFIED with the engine (task #1, 2026-07-11): binary
int movementMode; // @0x40 gait/death selector // mech+0x40 is Simulation::simulationState.currentState (StateIndicator
// @0x2c: +0x10 oldState, +0x14 currentState) -- the "gait/death
// selector" and the replicated simulation state are ONE field. Every
// update record carries it (header +0xC); Simulation::ReadUpdateRecord
// applies it on the replicant, so death (9) / limbo (2) / airborne
// (3/4) now replicate for free. The old parallel `int movementMode`
// member is GONE -- reads/writes go through the engine StateIndicator
// (SetState collapses old=current then fires only on change, exactly
// the binary's FUN_0041bbd8). [T1: writers in the binary are
// SetLevel(this+0x2c, n) calls -- FUN_0049f674 sets 2, death paths 9]
// (The old `Word actionRequestFlags @0x18` member is also gone: +0x18
// is Simulation::updateModel; see ForceUpdate above.)
public:
int MovementMode() { return (int)GetSimulationState(); }
void SetMovementMode(int m) { SetSimulationState((unsigned)m); }
// (the members below were and stay PUBLIC -- the recon TUs
// (btplayer/dmgtable/mechmppr/emitter) reach them directly)
int movementFlags; int movementFlags;
int motionEventArmed; // @0x5a4 int motionEventArmed; // @0x5a4
int motionEventPending; int motionEventPending;
@@ -697,7 +827,10 @@ protected:
// --- gait helpers (mech2) ------------------------------------------- // --- gait helpers (mech2) -------------------------------------------
void SetLegAnimation(int state); // @004a7fc4 void SetLegAnimation(int state); // @004a7fc4
void SetBodyAnimation(int state); // @004a800c void SetBodyAnimation(int state); // @004a800c
void RequestActionFlags(Word bits); // @004a4c54 // (RequestActionFlags(Word) @004a4c54 retired: it was Mech::ForceUpdate
// -- the "action request bits" ARE the updateModel record requests.
// The gait/knockdown call sites now call ForceUpdate directly and
// their masks match the binary's senders byte-for-byte.)
// P3 STEP-7 body finished-callback (the real PTR_LAB_0050d6fc == FUN_004a6d8c): // P3 STEP-7 body finished-callback (the real PTR_LAB_0050d6fc == FUN_004a6d8c):
// called by SequenceController::Advance at end-of-clip -- picks the next gait state // called by SequenceController::Advance at end-of-clip -- picks the next gait state
// from commanded speed vs the loaded caps, re-arms via SetBodyAnimation, recursively // from commanded speed vs the loaded caps, re-arms via SetBodyAnimation, recursively
+15 -22
View File
@@ -242,18 +242,11 @@ void
} }
// //
// @004a4c54 -- OR action-request bits into actionRequestFlags. A disabled // (@004a4c54 lives inline in mech.hpp as Mech::ForceUpdate -- the "action
// mech may only request a restricted subset (mask 0xFE03). // request bits" ARE the updateModel record-request mask; the old
// // RequestActionFlags name wrote a dead side-member the update emitter never
void // read. The call sites below keep their binary-exact masks: 8 = 1<<3 the
Mech::RequestActionFlags(Word bits) // leg-state/stability record.)
{
if (IsDisabled()) // FUN_0049fb54(this)
{
bits &= 0xFE03;
}
actionRequestFlags |= bits; // this+0x18
}
//########################################################################### //###########################################################################
@@ -285,7 +278,7 @@ Scalar
{ {
// airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe
// no-op while grounded -- the test mech never jumps). // no-op while grounded -- the test mech never jumps).
if ((m->movementMode == 3 || m->movementMode == 4) && m->jumpCapable) if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable)
return 0.0f; return 0.0f;
const Scalar fcr = m->forwardCycleRate; // 0x344 const Scalar fcr = m->forwardCycleRate; // 0x344
@@ -425,7 +418,7 @@ Scalar
Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj) Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj)
{ {
// airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a7970 (deferred). // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a7970 (deferred).
if ((m->movementMode == 3 || m->movementMode == 4) && m->jumpCapable) if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable)
return 0.0f; return 0.0f;
// The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0] // The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0]
@@ -549,7 +542,7 @@ Scalar
// //
if (!deathAnimationLatched) if (!deathAnimationLatched)
{ {
switch (movementMode) // this+0x40 switch (MovementMode()) // this+0x40 = simulationState
{ {
case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break; case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break; case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break;
@@ -642,14 +635,14 @@ Scalar
if (standSpeed < commandedSpeed) if (standSpeed < commandedSpeed)
{ {
legStateAlarm.SetLevel(0); legStateAlarm.SetLevel(0);
RequestActionFlags(8); // FUN_004a4c54(this,8) ForceUpdate(8); // FUN_004a4c54(this,8): type-3 record
break; break;
} }
distance = 0.0f; distance = 0.0f;
if (commandedSpeed < ZeroSpeed) if (commandedSpeed < ZeroSpeed)
{ {
legStateAlarm.SetLevel(0); legStateAlarm.SetLevel(0);
RequestActionFlags(8); ForceUpdate(8); // type-3 record
break; break;
} }
goto advance_normally; goto advance_normally;
@@ -816,7 +809,7 @@ Scalar
if (!deathAnimationLatched) // this+0x650 if (!deathAnimationLatched) // this+0x650
{ {
switch (movementMode) // this+0x40 switch (MovementMode()) // this+0x40 = simulationState
{ {
case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break; case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break; case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
@@ -969,7 +962,7 @@ Scalar
if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
|| (unsigned)(state - 0x12) < 2) || (unsigned)(state - 0x12) < 2)
{ {
if (movementMode == 3) // run jump if (MovementMode() == 3) // run jump
{ {
if (bodyTargetSpeed > jumpRunSpeedMax) bodyTargetSpeed = jumpRunSpeedMax; // 0x53c if (bodyTargetSpeed > jumpRunSpeedMax) bodyTargetSpeed = jumpRunSpeedMax; // 0x53c
} }
@@ -1071,7 +1064,7 @@ Scalar
case 0x18: case 0x19: // FallForward / FallBackward (jump) case 0x18: case 0x19: // FallForward / FallBackward (jump)
{ {
Scalar ratio; Scalar ratio;
if (movementMode == 3) // run jump: caps 0x53c / 0x544 if (MovementMode() == 3) // run jump: caps 0x53c / 0x544
{ {
if (bodyTargetSpeed <= bodyCycleSpeed) if (bodyTargetSpeed <= bodyCycleSpeed)
{ {
@@ -1144,7 +1137,7 @@ Scalar
{ {
ReconMotionSource *motionSource = *(ReconMotionSource **)(this->controlSource); // **(this+0x128) ReconMotionSource *motionSource = *(ReconMotionSource **)(this->controlSource); // **(this+0x128)
Scalar distance = 0.0f; Scalar distance = 0.0f;
int mode = movementMode; // this+0x40 int mode = MovementMode(); // this+0x40 = simulationState
int state = legAnimationState; // this+0x3b0 int state = legAnimationState; // this+0x3b0
// //
@@ -1196,7 +1189,7 @@ Scalar
if (standSpeed < motionSource->commandedSpeed) if (standSpeed < motionSource->commandedSpeed)
{ {
legStateAlarm.SetLevel(0); legStateAlarm.SetLevel(0);
RequestActionFlags(8); ForceUpdate(8); // type-3 record
break; break;
} }
goto advance_normally; goto advance_normally;
+61 -21
View File
@@ -318,7 +318,7 @@ Logical
// Advance the body gait (airborne flavour while jumping). // Advance the body gait (airborne flavour while jumping).
Scalar cycleDistance; Scalar cycleDistance;
if ((movementMode == 3 || movementMode == 4) && jumpCapable) // 0x40, 0x580 if ((MovementMode() == 3 || MovementMode() == 4) && jumpCapable) // 0x40, 0x580
cycleDistance = AdvanceBodyAnimationAirborne(time_slice, loop); // FUN_004a5bf8 cycleDistance = AdvanceBodyAnimationAirborne(time_slice, loop); // FUN_004a5bf8
else else
cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678 cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678
@@ -991,7 +991,7 @@ Logical
// vital hit run the WHOLE death transition again: double kill score, and a // vital hit run the WHOLE death transition again: double kill score, and a
// Score dispatched into the respawn window's severed playerVehicle -> the // Score dispatched into the respawn window's severed playerVehicle -> the
// engine's Check(playerVehicle) abort (task #52 cdb catch). // engine's Check(playerVehicle) abort (task #52 cdb catch).
if (movementMode == 2 || movementMode == 9) if (MovementMode() == 2 || MovementMode() == 9)
return True; return True;
return graphicAlarm.GetLevel() >= 9 ? True : False; return graphicAlarm.GetLevel() >= 9 ? True : False;
} }
@@ -1039,9 +1039,14 @@ void
ReconQuatIdentity(&angularAccum, &kIdentityQuat); ReconQuatIdentity(&angularAccum, &kIdentityQuat);
ReconQuatIdentity(&aimRate, &kIdentityQuat); ReconQuatIdentity(&aimRate, &kIdentityQuat);
// --- CLEAR THE DEATH LATCH: alive gait mode + no going-down alarm, so // --- CLEAR THE DEATH LATCH (binary FUN_0049fb74: FUN_0041bbd8(+0x2c, 0)
// IsMechDestroyed() is false and the drive/render treat it as alive --- // + the motion/replication scalars zeroed) ---
movementMode = 1; // ground/alive (death settled it to 9) SetMovementMode(0); // simulationState 0 = init/reset; the
// drive re-selects 1 next frame [T1]
legCycleSpeed = 0.0f; // @0x348 (decomp Reset zeroes these)
bodyCycleSpeed = 0.0f; // @0x6b8
bodyTargetSpeed = 0.0f; // @0x6b4
poseSyncLatch = 0; // @0x77c
graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger) graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger)
// --- HEAL every damage zone: full structure, intact skin, no burning --- // --- HEAL every damage zone: full structure, intact skin, no burning ---
@@ -1074,15 +1079,14 @@ void
BTRebuildMechModel((Entity *)this); BTRebuildMechModel((Entity *)this);
} }
// --- broadcast the reset state to replicants (FUN_004a4c54(this, 0x1f)) --- // --- broadcast the reset state to replicants: the binary Reset tail is
ForceUpdate(); // ForceUpdate(0x1f) -- record types 0-4 in one shot (pose + damage
// Also re-broadcast the (now-healed) damage zones so a REPLICANT observer sees // zones + speed + leg-state + orientation resync), replicant-gated on
// the zone levels FALL from destroyed back to intact -- the falling edge that // (flags+0x28 & 0xc) != 4. Bit 1 re-broadcasts the healed damage
// MechDeathHandler::Tick uses to un-wreck + warp the peer on the other screen // zones: the falling edge MechDeathHandler::Tick uses to un-wreck +
// (the master's Reset above un-wrecks + warps LOCALLY; without this the observer // warp the peer on the observer's screen. [T1 @0x49fdfc]
// has no signal that the peer respawned and its wreck sits forever). Same if (GetInstance() != ReplicantInstance)
// channel the damage path uses (mechdmg.cpp:1016). ForceUpdate(0x1f);
ForceUpdate(Entity::DamageZoneUpdateModelFlag);
if (getenv("BT_DEATH_LOG")) if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[respawn] Mech::Reset " << GetEntityID() DEBUG_STREAM << "[respawn] Mech::Reset " << GetEntityID()
@@ -1099,7 +1103,7 @@ void
{ {
if (!IsMechDestroyed()) // alive if (!IsMechDestroyed()) // alive
return; return;
if (movementMode == 9) // already settled (disabled/frozen) if (MovementMode() == 9) // already settled (disabled/frozen)
{ {
// WRECK SMOKE (port addition, [T3]): keep the dead hulk smoking -- re-fire // WRECK SMOKE (port addition, [T3]): keep the dead hulk smoking -- re-fire
// the death/rubble smoke plume (psfx 1 = DDTHSMK, "the mech death/rubble // the death/rubble smoke plume (psfx 1 = DDTHSMK, "the mech death/rubble
@@ -1180,7 +1184,16 @@ void
if (s != 0) if (s != 0)
s->DeathShutdown(1); s->DeathShutdown(1);
} }
movementMode = 9; // disabled: IsDisabled() true, locomotion frozen // Request the DEATH record BEFORE entering the disabled state (the
// ForceUpdate filter masks types 2..8 once IsDisabled) -- the binary
// death sender is Force(1) + Force(0x40) (@0x4aab2f/@0x4aab3a). The
// record emits on the next update pump with simulationState 9 in its
// header; that flips the replicant's MovementMode, whose own
// UpdateDeathState then runs the wreck sink/burial on the OBSERVER's
// screen (this function runs for every mech, replicants included).
ForceUpdate(Simulation::DefaultUpdateModelFlag
| (1 << MechDeathUpdateModelBit));
SetMovementMode(9); // disabled: IsDisabled() true, locomotion frozen
if (getenv("BT_DEATH_LOG")) if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] mech " << GetEntityID() DEBUG_STREAM << "[death] mech " << GetEntityID()
<< " destroyed -> subsystem shutdown + frozen wreck (IsDisabled=" << " destroyed -> subsystem shutdown + frozen wreck (IsDisabled="
@@ -1983,7 +1996,7 @@ void
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f)) if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
forwardCycleRate = 25.0f; forwardCycleRate = 25.0f;
if (!IsMechDestroyed()) // a dead mech keeps its death movementMode if (!IsMechDestroyed()) // a dead mech keeps its death movementMode
movementMode = 1; // ground, non-death, non-airborne SetMovementMode(1); // ground, non-death, non-airborne
// reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody // reverseSpeedMax2@0x7a0 is the run-cycle bodyCycleSpeed CLAMP (AdvanceBody
// Animation case 12/13); LoadLocomotionClips does not set it -> it reads // Animation case 12/13); LoadLocomotionClips does not set it -> it reads
// 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle // 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle
@@ -2529,12 +2542,37 @@ void
localOrigin.angularPosition); localOrigin.angularPosition);
if ( if (
error.LengthSquared() > 0.04f error.LengthSquared() > 0.04f
|| Abs(angular_deviation.w) < 0.997f
|| lastPerformance - lastUpdate > 2.0f || lastPerformance - lastUpdate > 2.0f
) )
{ {
ForceUpdate(); ForceUpdate(); // type 0: position deadband + heartbeat
} }
// ORIENTATION rides ONLY the type-4 resync record now (the
// authentic case-0 writer/reader save-restore strips it from the
// pose record) -- request it on angular deviation or the turn
// never reaches the replicant. Binary deadbands @0x4aac2b/
// @0x4aac6c diff the live vs last-replicated angular channels
// against model constants (+0x76c/+0x770, not yet streamed);
// the quat-deviation test stands in. [T3 threshold only]
if (Abs(angular_deviation.w) < 0.997f)
{
ForceUpdate(1 << MechResyncUpdateModelBit); // type 4
}
// Commanded-speed deadband (binary @0x4aac88): the mapper's live
// speedDemand vs the last-replicated bodyTargetSpeed -> the tiny
// type-2 record. This is the AUTHENTIC replicant-gait feed.
if (controlsMapper != 0
&& controlsMapper->speedDemand != bodyTargetSpeed)
{
ForceUpdate(1 << MechSpeedUpdateModelBit); // type 2
}
// Heat/impact-state change (binary @0x4aab75: heatAlarm level vs
// the frame-entry snapshot @0x780) -> the type-7 record.
if ((int)heatAlarm.GetLevel() != heatLevelSnapshot)
{
ForceUpdate(1 << MechImpactUpdateModelBit); // type 7
}
heatLevelSnapshot = (int)heatAlarm.GetLevel(); // @0x780 snapshot
if (getenv("BT_REPL_LOG")) if (getenv("BT_REPL_LOG"))
{ {
static float s_emitLog = 0.0f; s_emitLog += dt; static float s_emitLog = 0.0f; s_emitLog += dt;
@@ -3366,8 +3404,10 @@ void
// Stagger the BODY channel too so both freeze + recover from the SAME bmp // Stagger the BODY channel too so both freeze + recover from the SAME bmp
// clip IN PHASE. BodyClipFinished case 32 recovers it (mech2.cpp:308). // clip IN PHASE. BodyClipFinished case 32 recovers it (mech2.cpp:308).
SetBodyAnimation(0x20); SetBodyAnimation(0x20);
RequestActionFlags(1); // FUN_004a4c54(this, 1) ForceUpdate(1); // FUN_004a4c54(this, 1): pose record
RequestActionFlags(0x20); // FUN_004a4c54(this, 0x20) ForceUpdate(0x20); // FUN_004a4c54(this, 0x20): the
// type-5 KNOCKDOWN record -- the
// peer's replicant staggers too
if (GroundLog()) if (GroundLog())
{ {
static int s_binds = 0; static int s_binds = 0;
+16
View File
@@ -1233,3 +1233,19 @@ void *BTResolveRosterPilot(int slot)
} }
return (void *)mapper->GetPilot(slot); // FUN_004b0898 return (void *)mapper->GetPilot(slot); // FUN_004b0898
} }
//###########################################################################
// BTMapperSpeedDemand -- complete-type bridge (task #1)
//
// Mech::WriteUpdateRecord stamps the controls mapper's live speedDemand
// (binary: *(subsystemArray[0] + 0x128)) into every record tail. mech.cpp
// sees only the forward declaration of MechControlsMapper, so the read
// lives here in the complete-type TU (the house databinding pattern).
//
Scalar BTMapperSpeedDemandRaw(void *mapper)
{
// void* signature: mech.cpp's TU carries a local recon stub type under the
// same name, so a typed signature mangles differently there (class/struct
// + type identity). The cast happens HERE, in the complete-type TU.
return (mapper != 0) ? ((MechControlsMapper *)mapper)->speedDemand : Scalar(0.0f);
}