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:
co-authored by
Claude Fable 5
parent
3c34ae6de6
commit
c9f0c2a7f1
@@ -120,7 +120,7 @@ int Mech::IsAirborne()
|
||||
// that lands. Faithful to the binary regardless.
|
||||
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
|
||||
|
||||
+376
-102
@@ -388,7 +388,6 @@ template<class...A> inline void CloseNotation(A&&...) {}
|
||||
template<class...A> inline void ParseVector(A&&...) {}
|
||||
template<class...A> inline int ParseJointResource(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 StreamDamageLookup(A&&...) {}
|
||||
|
||||
@@ -870,13 +869,16 @@ Mech::Mech(
|
||||
creationTime = Now(); // FUN_00414b60 -> Wword(0x1de)
|
||||
Wword(0xd2) = 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)
|
||||
Wword(0x4b) = bodyRotation; // FUN_0040a938(this+0x4b,this+0x40)
|
||||
projectedOrigin = localOrigin; // FUN_0040a938(this+0x98,this+0x40)
|
||||
updateOrigin = localOrigin; // FUN_0040a938(this+0x4b,this+0x40)
|
||||
Wword(0x163) = Wword(0x40); // FUN_00408440(this+0x163,this+0x40)
|
||||
turretBase.SetIdentity(); // FUN_0040a7f4(this+0x71, IdentityQuat)
|
||||
torsoAimCurrent.SetIdentity(); // this+0xa6
|
||||
@@ -888,7 +890,8 @@ Mech::Mech(
|
||||
Wword(0x166) = ZeroVector;
|
||||
Wword(0x169) = 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;
|
||||
mechName.Copy(&DAT_004e0f8c); // FUN_00408e90(this+0xd8,"")
|
||||
Wword(0x160) = Wword(0x15f) = 0;
|
||||
@@ -897,7 +900,8 @@ Mech::Mech(
|
||||
Wword(0x20b).SetIdentity();
|
||||
Wword(0x1e7) = Wword(0x1dd) = Wword(0x209) = 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
|
||||
Wword(0x1e0) = Wword(0x119);
|
||||
|
||||
@@ -1643,161 +1647,431 @@ Logical
|
||||
//###########################################################################
|
||||
// ReadUpdateRecord -- @004a1232 (vtable slot 6)
|
||||
//
|
||||
// Applies a streamed update packet. The packet's 16-bit type field (+6)
|
||||
// selects how much of the Mech's replicated state is overwritten. After the
|
||||
// switch, a torso/leg state transition re-syncs the stability alarm.
|
||||
// Applies one Mech-level replication record. The switch key is the
|
||||
// inherited Simulation::UpdateRecord recordID (u16 @+6) == the updateModel
|
||||
// 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
|
||||
// the vtable as a member fn); param shapes below are reconstructed.
|
||||
// Base chains: case 0 / default -> the JointedMover-level pair
|
||||
// (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
|
||||
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
|
||||
{
|
||||
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
|
||||
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
|
||||
projectedVelocity.angularMotion = ZeroVector;
|
||||
}
|
||||
// torso/leg sync when the move state flips to/from "2"
|
||||
if (Wword(0x10) != Wword(0xf) && (Wword(0xf) == 2 || Wword(0x10) == 2))
|
||||
// Enter/leave simulation state 2 (disabled/limbo): re-base the
|
||||
// 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));
|
||||
torsoAimCurrent.Mul(Wword(0x4b));
|
||||
torsoAimCurrent = torsoAimTarget;
|
||||
Wword(0x1df) = 0;
|
||||
if ((instanceFlags & 0x40) == 0)
|
||||
localOrigin = updateOrigin; // FUN_0040a938(0x100 <- 0x12c)
|
||||
projectedOrigin = updateOrigin; // FUN_0040a938(0x260 <- 0x12c)
|
||||
projectedVelocity = updateVelocity; // FUN_0040a7f4(0x298 <- 0x2c8)
|
||||
poseSyncLatch = 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;
|
||||
|
||||
case 2: // alarm-only
|
||||
JointedMover::ReadUpdateRecord(message); // FUN_0041bd34
|
||||
Wword(0x1ad) = record->field4;
|
||||
case 2: // commanded-speed update
|
||||
{
|
||||
Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message); // FUN_0041bd34
|
||||
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // score / heat state
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
Wword(0x196) = record->field4;
|
||||
stabilityAlarm.SetLevel(record->field6);
|
||||
if (record->field5 == 0)
|
||||
case 3: // leg/body state + stability
|
||||
{
|
||||
controllableSubsystems.Reset(); // FUN_004283b8
|
||||
statusAlarm.SetLevel(record->field5);
|
||||
Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
|
||||
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
|
||||
{
|
||||
SetStatusState(record->field5); // FUN_004a800c [mech2]
|
||||
SetBodyAnimation(record->legState); // FUN_004a800c
|
||||
}
|
||||
bodyTargetSpeed = record->speedDemand; // rec+0x1c
|
||||
}
|
||||
Wword(0x1ad) = record->field7;
|
||||
break;
|
||||
|
||||
case 4: // pose + server-time re-sync
|
||||
case 4: // orientation + angular-velocity re-sync
|
||||
{
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
creationTime = Now(); // Wword(0x1de)
|
||||
worldPosition = record->position; // this+0x4e <- msg+4
|
||||
netOrientation = record->orientation; // this+0xb5 <- msg+7
|
||||
Wword(0xb8) = Now();
|
||||
Scalar age = (Scalar)(Wword(0xb8) - Wword(5)) / FrameTimeScale;
|
||||
if (age < ReSyncSpeedThreshold) // 10.0f
|
||||
Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
creationTime = Now(); // @0x778 -- the dead-reckon ref time
|
||||
{
|
||||
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;
|
||||
|
||||
case 5: // subsystem alarm (variant A)
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->field4); // this+0x114
|
||||
Wword(0x129) = record->field5;
|
||||
Wword(0x12a) = record->field6;
|
||||
Wword(0x12d) = record->field9;
|
||||
SetStatusState(0x20); // FUN_004a800c [mech2]
|
||||
torsoAimCurrent.SetIdentity();
|
||||
Wword(0x1ad) = record->fieldA;
|
||||
case 5: // knockdown
|
||||
{
|
||||
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
|
||||
throttleState = record->throttleState; // @0x4a4 <- rec+0x14
|
||||
fallDirection = record->fallDirection; // @0x4a8 <- rec+0x18
|
||||
fallScalar = record->fallScalar; // @0x4b4 <- rec+0x24
|
||||
SetBodyAnimation(0x20); // FUN_004a800c -- the knockdown clip
|
||||
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
|
||||
projectedVelocity.angularMotion = ZeroVector;
|
||||
bodyTargetSpeed = record->speedDemand; // rec+0x28
|
||||
}
|
||||
break;
|
||||
|
||||
case 6: // subsystem alarm (variant B -- death)
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->field4);
|
||||
Wword(0x129) = record->field5;
|
||||
Wword(0x12a) = record->field6;
|
||||
Wword(0x12d) = record->field9;
|
||||
statusAlarm.SetLevel(0);
|
||||
Wword(0x195) = Wword(0x196) = 1;
|
||||
torsoAimCurrent.SetIdentity();
|
||||
Wword(0x1ad) = record->fieldA;
|
||||
case 6: // death
|
||||
{
|
||||
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->heatLevel);
|
||||
throttleState = record->throttleState;
|
||||
fallDirection = record->fallDirection;
|
||||
fallScalar = record->fallScalar;
|
||||
bodyStateAlarm.SetLevel(0); // @0x714
|
||||
legResetLatch = 1; // @0x654 (reader sets BOTH latches;
|
||||
bodyResetLatch = 1; // @0x658 the writer only 0x658)
|
||||
projectedVelocity.linearMotion = ZeroVector;
|
||||
projectedVelocity.angularMotion = ZeroVector;
|
||||
bodyTargetSpeed = record->speedDemand;
|
||||
}
|
||||
break;
|
||||
|
||||
case 7: // subsystem alarm (variant C)
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->field4);
|
||||
Wword(0x129) = record->field5;
|
||||
Wword(0x12a) = record->field6;
|
||||
Wword(0x12d) = record->field9;
|
||||
Wword(0x1ad) = record->fieldA;
|
||||
case 7: // impact (fields only, no side effects)
|
||||
{
|
||||
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
heatAlarm.SetLevel(record->heatLevel);
|
||||
throttleState = record->throttleState;
|
||||
fallDirection = record->fallDirection;
|
||||
fallScalar = record->fallScalar;
|
||||
bodyTargetSpeed = record->speedDemand;
|
||||
}
|
||||
break;
|
||||
|
||||
case 8: // single scalar
|
||||
JointedMover::ReadUpdateRecord(message);
|
||||
Wword(0xfd) = record->field4;
|
||||
Wword(0x1ad) = record->field5;
|
||||
case 8: // airborne/reverse cycle-rate selector
|
||||
{
|
||||
Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
|
||||
Simulation::ReadUpdateRecord(message);
|
||||
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
|
||||
bodyTargetSpeed = record->speedDemand; // rec+0x14
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
JointedMover::ReadUpdateRecord(message); // FUN_0042249c
|
||||
default: // type 1 (damage zones) + anything unknown -> the base chain
|
||||
Mover::ReadUpdateRecord(message); // FUN_0042249c
|
||||
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
|
||||
BroadcastToTeam(msg); // FUN_004364e4
|
||||
stabilityAlarm.SetLevel(Wword(0x10)); // FUN_0041bbd8(this+0xb,...)
|
||||
SetSimulationState(GetSimulationState()); // FUN_0041bbd8(this+0x2c, cur)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//###########################################################################
|
||||
// SetInstanceFlags -- @004a4c54
|
||||
// WriteUpdateRecord -- @004a0c2c (vtable slot 7)
|
||||
//
|
||||
// OR a flag word into the entity instance flags (this+0x18); copies (network
|
||||
// replicants) are stripped of all but the low/high reserved bits.
|
||||
// Serializes one Mech-level record; the jump-table type == the updateModel
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
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)
|
||||
}
|
||||
*(Word *)((char *)this + 0x18) |= flags;
|
||||
}
|
||||
|
||||
|
||||
// (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.)
|
||||
|
||||
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
// Resource authoring (offline tools)
|
||||
|
||||
+164
-31
@@ -152,26 +152,82 @@ struct ShotDescriptor
|
||||
//######################### Mech::UpdateRecord ##########################
|
||||
//###########################################################################
|
||||
//
|
||||
// The network/replay update packet applied by Mech::ReadUpdateRecord
|
||||
// @004a1232. The switch is keyed on a 16-bit record-type field at +6 of the
|
||||
// message (cases 0,2..8). Field layout is best-effort -- only the offsets
|
||||
// touched by the decomp are named.
|
||||
// The Mech-level replication records (task #1, 2026-07-11). The record TYPE
|
||||
// is the inherited Simulation::UpdateRecord `recordID` (u16 @+6) == the
|
||||
// updateModel BIT INDEX that requested it (Simulation::WriteSimulationUpdate,
|
||||
// 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:
|
||||
public JointedMover::UpdateRecord
|
||||
// EVERY record's trailing Scalar is the controls-mapper's speedDemand
|
||||
// (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)
|
||||
// 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
|
||||
Scalar speedDemand; // +0x74
|
||||
};
|
||||
|
||||
// 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 ##########################
|
||||
//###########################################################################
|
||||
@@ -308,9 +364,13 @@ struct ShotDescriptor
|
||||
//
|
||||
public:
|
||||
typedef Mech__ModelResource ModelResource;
|
||||
typedef Mech__UpdateRecord UpdateRecord;
|
||||
typedef Mech__MakeMessage MakeMessage;
|
||||
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*
|
||||
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:
|
||||
void
|
||||
ReadUpdateRecord( // @004a1232 (vtable slot 6)
|
||||
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
|
||||
SetInstanceFlags(Word flags); // @004a4c54
|
||||
WriteUpdateRecord( // @004a0c2c (vtable slot 7)
|
||||
Simulation::UpdateRecord *message,
|
||||
int record_type
|
||||
);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Subsystem roster (host pattern -- cf. VTV / CLASSMAP shared base)
|
||||
@@ -448,8 +543,14 @@ struct ShotDescriptor
|
||||
protected:
|
||||
Matrix34 orientation; // @0xd0 this[0x34] (FUN_0040ab44)
|
||||
Quaternion bodyRotation; // @0x100 this[0x40]
|
||||
BTVal torsoRotation; // @0x130 this[0x4b] (proxy: .Mul)
|
||||
Vector3D worldPosition; // @0x138 this[0x4e]
|
||||
// (task #1 corrections: the old `BTVal torsoRotation @0x130 [0x4b]`
|
||||
// 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)
|
||||
Quaternion headPitch; // @0x1cc this[0x73] (look)
|
||||
Quaternion torsoTwist; // @0x1dc this[0x77]
|
||||
@@ -467,6 +568,19 @@ struct ShotDescriptor
|
||||
public:
|
||||
Joint *shadowJointNode; // binary @0x42c this[0x10b]
|
||||
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;
|
||||
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
|
||||
// after the layout-locked fields so nothing shifts.
|
||||
@@ -636,8 +750,24 @@ protected:
|
||||
void *controlSource; // @0x128 controls subsystem handle
|
||||
|
||||
// --- locomotion / gait animation state (mech2/mech3 maps) ------------
|
||||
Word actionRequestFlags; // @0x18 pending action bits
|
||||
int movementMode; // @0x40 gait/death selector
|
||||
// movementMode UNIFIED with the engine (task #1, 2026-07-11): binary
|
||||
// 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 motionEventArmed; // @0x5a4
|
||||
int motionEventPending;
|
||||
@@ -697,7 +827,10 @@ protected:
|
||||
// --- gait helpers (mech2) -------------------------------------------
|
||||
void SetLegAnimation(int state); // @004a7fc4
|
||||
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):
|
||||
// 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
|
||||
|
||||
@@ -242,18 +242,11 @@ void
|
||||
}
|
||||
|
||||
//
|
||||
// @004a4c54 -- OR action-request bits into actionRequestFlags. A disabled
|
||||
// mech may only request a restricted subset (mask 0xFE03).
|
||||
//
|
||||
void
|
||||
Mech::RequestActionFlags(Word bits)
|
||||
{
|
||||
if (IsDisabled()) // FUN_0049fb54(this)
|
||||
{
|
||||
bits &= 0xFE03;
|
||||
}
|
||||
actionRequestFlags |= bits; // this+0x18
|
||||
}
|
||||
// (@004a4c54 lives inline in mech.hpp as Mech::ForceUpdate -- the "action
|
||||
// request bits" ARE the updateModel record-request mask; the old
|
||||
// RequestActionFlags name wrote a dead side-member the update emitter never
|
||||
// read. The call sites below keep their binary-exact masks: 8 = 1<<3 the
|
||||
// leg-state/stability record.)
|
||||
|
||||
|
||||
//###########################################################################
|
||||
@@ -285,7 +278,7 @@ Scalar
|
||||
{
|
||||
// airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe
|
||||
// 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;
|
||||
|
||||
const Scalar fcr = m->forwardCycleRate; // 0x344
|
||||
@@ -425,7 +418,7 @@ Scalar
|
||||
Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj)
|
||||
{
|
||||
// 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;
|
||||
|
||||
// The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0]
|
||||
@@ -549,7 +542,7 @@ Scalar
|
||||
//
|
||||
if (!deathAnimationLatched)
|
||||
{
|
||||
switch (movementMode) // this+0x40
|
||||
switch (MovementMode()) // this+0x40 = simulationState
|
||||
{
|
||||
case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break;
|
||||
case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break;
|
||||
@@ -642,14 +635,14 @@ Scalar
|
||||
if (standSpeed < commandedSpeed)
|
||||
{
|
||||
legStateAlarm.SetLevel(0);
|
||||
RequestActionFlags(8); // FUN_004a4c54(this,8)
|
||||
ForceUpdate(8); // FUN_004a4c54(this,8): type-3 record
|
||||
break;
|
||||
}
|
||||
distance = 0.0f;
|
||||
if (commandedSpeed < ZeroSpeed)
|
||||
{
|
||||
legStateAlarm.SetLevel(0);
|
||||
RequestActionFlags(8);
|
||||
ForceUpdate(8); // type-3 record
|
||||
break;
|
||||
}
|
||||
goto advance_normally;
|
||||
@@ -816,7 +809,7 @@ Scalar
|
||||
|
||||
if (!deathAnimationLatched) // this+0x650
|
||||
{
|
||||
switch (movementMode) // this+0x40
|
||||
switch (MovementMode()) // this+0x40 = simulationState
|
||||
{
|
||||
case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
|
||||
case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
|
||||
@@ -969,7 +962,7 @@ Scalar
|
||||
if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
|
||||
|| (unsigned)(state - 0x12) < 2)
|
||||
{
|
||||
if (movementMode == 3) // run jump
|
||||
if (MovementMode() == 3) // run jump
|
||||
{
|
||||
if (bodyTargetSpeed > jumpRunSpeedMax) bodyTargetSpeed = jumpRunSpeedMax; // 0x53c
|
||||
}
|
||||
@@ -1071,7 +1064,7 @@ Scalar
|
||||
case 0x18: case 0x19: // FallForward / FallBackward (jump)
|
||||
{
|
||||
Scalar ratio;
|
||||
if (movementMode == 3) // run jump: caps 0x53c / 0x544
|
||||
if (MovementMode() == 3) // run jump: caps 0x53c / 0x544
|
||||
{
|
||||
if (bodyTargetSpeed <= bodyCycleSpeed)
|
||||
{
|
||||
@@ -1144,7 +1137,7 @@ Scalar
|
||||
{
|
||||
ReconMotionSource *motionSource = *(ReconMotionSource **)(this->controlSource); // **(this+0x128)
|
||||
Scalar distance = 0.0f;
|
||||
int mode = movementMode; // this+0x40
|
||||
int mode = MovementMode(); // this+0x40 = simulationState
|
||||
int state = legAnimationState; // this+0x3b0
|
||||
|
||||
//
|
||||
@@ -1196,7 +1189,7 @@ Scalar
|
||||
if (standSpeed < motionSource->commandedSpeed)
|
||||
{
|
||||
legStateAlarm.SetLevel(0);
|
||||
RequestActionFlags(8);
|
||||
ForceUpdate(8); // type-3 record
|
||||
break;
|
||||
}
|
||||
goto advance_normally;
|
||||
|
||||
@@ -318,7 +318,7 @@ Logical
|
||||
|
||||
// Advance the body gait (airborne flavour while jumping).
|
||||
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
|
||||
else
|
||||
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
|
||||
// Score dispatched into the respawn window's severed playerVehicle -> the
|
||||
// engine's Check(playerVehicle) abort (task #52 cdb catch).
|
||||
if (movementMode == 2 || movementMode == 9)
|
||||
if (MovementMode() == 2 || MovementMode() == 9)
|
||||
return True;
|
||||
return graphicAlarm.GetLevel() >= 9 ? True : False;
|
||||
}
|
||||
@@ -1039,9 +1039,14 @@ void
|
||||
ReconQuatIdentity(&angularAccum, &kIdentityQuat);
|
||||
ReconQuatIdentity(&aimRate, &kIdentityQuat);
|
||||
|
||||
// --- CLEAR THE DEATH LATCH: alive gait mode + no going-down alarm, so
|
||||
// IsMechDestroyed() is false and the drive/render treat it as alive ---
|
||||
movementMode = 1; // ground/alive (death settled it to 9)
|
||||
// --- CLEAR THE DEATH LATCH (binary FUN_0049fb74: FUN_0041bbd8(+0x2c, 0)
|
||||
// + the motion/replication scalars zeroed) ---
|
||||
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)
|
||||
|
||||
// --- HEAL every damage zone: full structure, intact skin, no burning ---
|
||||
@@ -1074,15 +1079,14 @@ void
|
||||
BTRebuildMechModel((Entity *)this);
|
||||
}
|
||||
|
||||
// --- broadcast the reset state to replicants (FUN_004a4c54(this, 0x1f)) ---
|
||||
ForceUpdate();
|
||||
// Also re-broadcast the (now-healed) damage zones so a REPLICANT observer sees
|
||||
// the zone levels FALL from destroyed back to intact -- the falling edge that
|
||||
// MechDeathHandler::Tick uses to un-wreck + warp the peer on the other screen
|
||||
// (the master's Reset above un-wrecks + warps LOCALLY; without this the observer
|
||||
// has no signal that the peer respawned and its wreck sits forever). Same
|
||||
// channel the damage path uses (mechdmg.cpp:1016).
|
||||
ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
// --- broadcast the reset state to replicants: the binary Reset tail is
|
||||
// ForceUpdate(0x1f) -- record types 0-4 in one shot (pose + damage
|
||||
// zones + speed + leg-state + orientation resync), replicant-gated on
|
||||
// (flags+0x28 & 0xc) != 4. Bit 1 re-broadcasts the healed damage
|
||||
// zones: the falling edge MechDeathHandler::Tick uses to un-wreck +
|
||||
// warp the peer on the observer's screen. [T1 @0x49fdfc]
|
||||
if (GetInstance() != ReplicantInstance)
|
||||
ForceUpdate(0x1f);
|
||||
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[respawn] Mech::Reset " << GetEntityID()
|
||||
@@ -1099,7 +1103,7 @@ void
|
||||
{
|
||||
if (!IsMechDestroyed()) // alive
|
||||
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
|
||||
// the death/rubble smoke plume (psfx 1 = DDTHSMK, "the mech death/rubble
|
||||
@@ -1180,7 +1184,16 @@ void
|
||||
if (s != 0)
|
||||
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"))
|
||||
DEBUG_STREAM << "[death] mech " << GetEntityID()
|
||||
<< " destroyed -> subsystem shutdown + frozen wreck (IsDisabled="
|
||||
@@ -1983,7 +1996,7 @@ void
|
||||
if (!(forwardCycleRate >= 25.0f && forwardCycleRate < 10000.0f))
|
||||
forwardCycleRate = 25.0f;
|
||||
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
|
||||
// Animation case 12/13); LoadLocomotionClips does not set it -> it reads
|
||||
// 0xCDCDCDCD (-4.3e8) and the clamp clobbers bodyCycleSpeed -> the run cycle
|
||||
@@ -2529,12 +2542,37 @@ void
|
||||
localOrigin.angularPosition);
|
||||
if (
|
||||
error.LengthSquared() > 0.04f
|
||||
|| Abs(angular_deviation.w) < 0.997f
|
||||
|| 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"))
|
||||
{
|
||||
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
|
||||
// clip IN PHASE. BodyClipFinished case 32 recovers it (mech2.cpp:308).
|
||||
SetBodyAnimation(0x20);
|
||||
RequestActionFlags(1); // FUN_004a4c54(this, 1)
|
||||
RequestActionFlags(0x20); // FUN_004a4c54(this, 0x20)
|
||||
ForceUpdate(1); // FUN_004a4c54(this, 1): pose record
|
||||
ForceUpdate(0x20); // FUN_004a4c54(this, 0x20): the
|
||||
// type-5 KNOCKDOWN record -- the
|
||||
// peer's replicant staggers too
|
||||
if (GroundLog())
|
||||
{
|
||||
static int s_binds = 0;
|
||||
|
||||
@@ -1233,3 +1233,19 @@ void *BTResolveRosterPilot(int slot)
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user