From c9f0c2a7f1632b32a74c603f1b51491448480ecc Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 11:38:49 -0500 Subject: [PATCH] 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 --- game/reconstructed/btstubs.cpp | 2 +- game/reconstructed/mech.cpp | 494 +++++++++++++++++++++++++------- game/reconstructed/mech.hpp | 195 +++++++++++-- game/reconstructed/mech2.cpp | 37 +-- game/reconstructed/mech4.cpp | 82 ++++-- game/reconstructed/mechmppr.cpp | 16 ++ 6 files changed, 641 insertions(+), 185 deletions(-) diff --git a/game/reconstructed/btstubs.cpp b/game/reconstructed/btstubs.cpp index 23f79f1..b9ead94 100644 --- a/game/reconstructed/btstubs.cpp +++ b/game/reconstructed/btstubs.cpp @@ -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 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 3a759bd..5a80b82 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -388,7 +388,6 @@ template inline void CloseNotation(A&&...) {} template inline void ParseVector(A&&...) {} template inline int ParseJointResource(A&&...) { return 0; } template inline int CreateModelResourceBase(A&&...){ return 0; } -template inline int IsNetworkCopy(A&&...) { return 0; } template inline void StreamDamageZone(A&&...) {} template 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; - - switch (record->recordType) // *(u16*)(msg+6) + 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 snapshot + case 0: // full pose (Mover record + speedDemand tail) { - 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) + Mech__PoseUpdateRecord *record = (Mech__PoseUpdateRecord *)message; + + // 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 { - 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); - } - else if (record->field5 == 1) - { - statusAlarm.SetLevel(record->field5); - } - else - { - SetStatusState(record->field5); // FUN_004a800c [mech2] - } - Wword(0x1ad) = record->field7; - break; - - case 4: // pose + server-time 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__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) { - Wword(0xb8) += (Wword(0xb8) - Wword(5)); // extrapolate + legAnimation.Reset(1); // FUN_004283b8(0x65c, 1) + bodyStateAlarm.SetLevel(record->legState); // @0x714 } - Wword(0x1ad) = record->fieldA; + else if (record->legState == 1) + { + bodyStateAlarm.SetLevel(record->legState); + } + else + { + SetBodyAnimation(record->legState); // FUN_004a800c + } + bodyTargetSpeed = record->speedDemand; // rec+0x1c } 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 4: // orientation + angular-velocity re-sync + { + Mech__ResyncUpdateRecord *record = (Mech__ResyncUpdateRecord *)message; + Simulation::ReadUpdateRecord(message); + creationTime = Now(); // @0x778 -- the dead-reckon ref time + { + EulerAngles e(Radian(record->eulerX), Radian(record->eulerY), + Radian(record->eulerZ)); + updateOrigin.angularPosition = e; // FUN_00409a00 (euler->quat) + } + 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 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 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 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 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 8: // single scalar - JointedMover::ReadUpdateRecord(message); - Wword(0xfd) = record->field4; - Wword(0x1ad) = record->field5; + 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; - default: - JointedMover::ReadUpdateRecord(message); // FUN_0042249c + 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: // 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) diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 08efb9e..b721ed6 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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< 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; diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 292d3eb..734c6b1 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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; diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 887ca7b..e5e0af0 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -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); +}