BT410 Phase 5.3.1: Mech::Simulate motion core -- mech integrates motion per-frame

Installs Mech::Simulate as the mech's per-frame Performance (was DoNothingOnce).
Reconstructs the load-bearing spine of the 1995 Simulate (mech4.cpp @004ab430):
integrate the body velocity into localOrigin (linearPosition.AddScaled(..,
worldLinearVelocity, dt)) and commit the Origin to the world transform with the
engine idiom (localToWorld = localOrigin, per ENTITY.cpp:988 / MOVER.cpp:850).

Uses the NAMED engine base Mover fields (localOrigin/localToWorld/
worldLinearVelocity) -- BT411's MechBaseLayoutCheck proved the WinTesla decomp's
raw this+0x100/0x260 offsets actually stomp those base fields; the clean 1995
build addresses them by name.

- MECH.HPP: Mech Performance typedef + SetPerformance + Simulate() decl.
- MECH.CPP: SetPerformance(&Mech::Simulate) at ctor tail; Simulate body.

Verified headlessly (BT_MECH_LOG "[sim] mech pos=" 1Hz): BT_DRIVE="5,0,10"
advances the mech +5.0/s x, +10.0/s z (y fixed) = exactly the injected world
velocity; no BT_DRIVE -> worldLinearVelocity is 0, mech holds spawn pose. Zero
Fail/Exception either way. BT_DRIVE is a retained dev hook for headless motion.

Deferred (locomotion wave): gait-cycle self-propulsion feeding worldLinearVelocity
(IntegrateMotion->AdvanceBodyAnimation, steered by mapper throttle/turn), heading
integration, terrain-height drop, cockpit telemetry filters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-21 13:45:24 -05:00
co-authored by Claude Fable 5
parent 6d827cd9c8
commit 3364b65ae8
3 changed files with 137 additions and 0 deletions
+86
View File
@@ -310,6 +310,14 @@ Mech::Mech(
<< " weaponCount=" << weaponCount << endl << flush;
}
//
// Install the per-frame body Performance. Until now the mech ran the base
// DoNothingOnce; from here the engine dispatches Mech::Simulate every frame
// (Mover -> Entity -> Simulation::PerformAndWatch) once the mission is
// RunningMission.
//
SetPerformance(&Mech::Simulate);
Check_Fpu();
}
@@ -335,3 +343,81 @@ void
}
subsystemArray[0] = subsystem;
}
//
//#############################################################################
// Simulate -- the mech's per-frame body Performance (the Mover locomotion tick).
//
// FUNCTIONAL MOTION CORE (Phase 5.3, increment 1). A Mech is a Mover; its
// per-frame job is to advance its Origin and commit it to the world transform.
// This reconstructs the load-bearing spine of the 1995 Mech::Simulate
// (mech4.cpp @004ab430): integrate the body velocity into localOrigin, then
// rebuild localToWorld with the engine's own idiom (ENTITY.CPP:988 /
// MOVER.CPP:850, `localToWorld = localOrigin`).
//
// Still layered on top of this (next increments):
// * the gait-cycle self-propulsion that FEEDS worldLinearVelocity -- the
// authentic model drives forward speed from the walk/run animation cycle
// (IntegrateMotion -> AdvanceBodyAnimation -> cycleDistance) steered by the
// control-mapper demands (throttle/turn), not a raw velocity;
// * heading integration (rotate localOrigin.angularPosition by the turn rate);
// * the terrain-height drop (BoundingBoxTreeNode::FindBoundingBoxUnder) that
// rests the feet on the ground;
// * the cockpit telemetry FilteredScalars (head/aim/leg/torso angular rates).
//
// With no locomotion layer yet, worldLinearVelocity is zero for a freshly
// spawned mech, so it holds its pose -- identical to the prior DoNothing, but
// now on the real Simulate path. DEV hook BT_DRIVE="vx,vy,vz" injects a
// constant world velocity so the integrate + transform path is verifiable
// headlessly (no RIO/controls needed). See MECH.NOTES.md.
//#############################################################################
//
void
Mech::Simulate(Scalar time_slice)
{
Check(this);
Vector3D velocity = worldLinearVelocity;
{
const char *drive = getenv("BT_DRIVE");
if (drive != NULL)
{
float dx = 0.0f, dy = 0.0f, dz = 0.0f;
if (sscanf(drive, "%f,%f,%f", &dx, &dy, &dz) == 3)
{
velocity.x = dx;
velocity.y = dy;
velocity.z = dz;
}
}
}
//
// Integrate position (pos = pos + velocity*dt) and commit the Origin to the
// world transform.
//
localOrigin.linearPosition.AddScaled(
localOrigin.linearPosition,
velocity,
time_slice
);
localToWorld = localOrigin;
if (getenv("BT_MECH_LOG"))
{
static Scalar reportAccum = 0.0f;
reportAccum += time_slice;
if (reportAccum >= 1.0f)
{
reportAccum = 0.0f;
DEBUG_STREAM << "[sim] mech pos=("
<< localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.y << ","
<< localOrigin.linearPosition.z << ")"
<< endl << flush;
}
}
Check_Fpu();
}
+17
View File
@@ -187,6 +187,23 @@
void
SetMappingSubsystem(Subsystem *mapper);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Per-frame simulation (the mech's body Performance; installed by the ctor
// and dispatched each frame from Simulation::PerformAndWatch).
//
public:
typedef void
(Mech::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
Simulate(Scalar time_slice);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Resource creation
//
+34
View File
@@ -150,3 +150,37 @@ VehicleDeadMessageHandler(non-death), the reconstructed tree now boots with
(`LBE4ControlsManager::Execute` spinning). See BTPLAYER.NOTES.md. The remaining
gap to "playable" is behavioral (mech2/3/4 per-frame sim + renderer + real
control input), not structural — the boot path is complete.
## PHASE 5.3 INCREMENT 1: Mech::Simulate MOTION CORE (2026-07-21)
`Mech::Simulate(Scalar)` is reconstructed and installed as the mech's per-frame
Performance (`SetPerformance(&Mech::Simulate)` at the end of the ctor). Until now
the mech ran the base `DoNothingOnce`; it now runs real body code every frame
(dispatched via Mover→Entity→`Simulation::PerformAndWatch` once RunningMission).
This increment is the **load-bearing spine** of the 1995 Simulate
(mech4.cpp @004ab430): integrate the body velocity into `localOrigin`
(`linearPosition.AddScaled(linearPosition, worldLinearVelocity, dt)`) and commit
the Origin to the world transform with the engine's own idiom
(`localToWorld = localOrigin`, per ENTITY.CPP:988 / MOVER.CPP:850). BT411's RE
was decisive here: the mech's motion operates on the ENGINE BASE Mover fields
(`localOrigin`/`projectedOrigin`/`previousOrigin`/`projectedVelocity`/
`localToWorld`) — the raw `this+0x100/0x260/0x26c` offsets in the WinTesla decomp
actually *stomp* those base fields (their MechBaseLayoutCheck static_asserts).
So the clean reconstruction uses the NAMED base members directly.
**Verified headlessly** (BT_MECH_LOG `[sim] mech pos=` 1 Hz): with
`BT_DRIVE="5,0,10"` the mech advances +5.0/s in x and +10.0/s in z (y fixed),
exactly the injected world velocity; with no BT_DRIVE `worldLinearVelocity` is
zero and the mech holds its spawn pose — no regression, zero Fail. `BT_DRIVE`
is a retained DEV hook (world-velocity injection) for headless motion tests.
**Deferred to the next increments (the locomotion wave):**
1. gait-cycle self-propulsion that FEEDS `worldLinearVelocity` — the authentic
model derives forward speed from the walk/run animation cycle
(`IntegrateMotion``AdvanceBodyAnimation`→cycleDistance) steered by the
control-mapper throttle/turn demands, NOT a raw velocity;
2. heading integration (rotate `localOrigin.angularPosition` by the turn rate);
3. terrain-height drop (`BoundingBoxTreeNode::FindBoundingBoxUnder`) resting the
feet on the ground;
4. cockpit telemetry `FilteredScalar`s (head/aim/leg/torso angular rates).