//===========================================================================// // File: mechmppr.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: Mech controls mapper -- maps pilot control inputs and view // // selection onto the Mech's motion / torso / eyepoint demands // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // --/--/95 ?? Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra // pseudo-C for the mechmppr cluster (@004afbe0-@004b08c0); class / member / // method names follow the Red Planet sibling RP\VTVMPPR.cpp (VTVControlsMapper) // and the strings recovered from the .rdata class string pool. Each non- // trivial method cites the originating @ADDR. // // Read-only globals / constants resolved from section_dump.txt: // _DAT_004b0274 = 0x00000000 = 0.0f // _DAT_004b0278 = 0x3f800000 = 1.0f // _DAT_004b027c = 0x38d1b717 = 1.0e-4f (JM_CLOSE_ENOUGH) // 0x40490fdb = PI (3.14159274f) (eyepoint yaw, "look behind") // DAT_004e0f8c = EulerAngles::Identity (all zero) // // Helper-function name mapping (engine internals referenced by the decomp): // FUN_004ac530 Subsystem base constructor (8-arg) // FUN_004ac868 Subsystem destructor // FUN_0040385c Verify()/Fail(msg,file,line) // FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_00417ab4 SharedData::Resolve() // FUN_00408644 Vector3D::Subtract(result, a, b) // FUN_00408e90 EulerAngles::operator=(dst, src) // FUN_0049fb54 Entity::IsDestroyed() -> Logical // FUN_004022b0 Allocate(bytes) FUN_004022e8 Free(ptr) // FUN_004022d0 operator delete FUN_004024d8 Resource::Release // FUN_00403ad0 Group::FindGroup(root, name) // FUN_00421414 CollectionIterator::CollectionIterator(it, collection) // FUN_00421452 CollectionIterator::~CollectionIterator(it) // FUN_004afacf ChildIterator::ChildIterator(it, list) (eyepoint list) // FUN_004afb0d ChildIterator::~ChildIterator(it) // FUN_004dcd00 fabsf() // FUN_004bff74 VoiceAssist::Toggle() (lives in another module) // DAT_004efc94 the global Application object // #include #pragma hdrstop #if !defined(MECHMPPR_HPP) # include #endif #if !defined(APP_HPP) # include #endif #if !defined(NTTMGR_HPP) # include // EntityManager / EntityGroup / FindGroup #endif #if !defined(MECH_HPP) # include #endif #if !defined(PILOT_HPP) # include #endif #include // real Torso: analog aim axes @0x1F0/0x1F4, horizontalEnabled @0x250 #include // real HUD: freeAimSlew @0x28C #define JM_CLOSE_ENOUGH 1.0e-4f // _DAT_004b027c // // Reconstruction helpers -- engine internals referenced by the decomp that // have no direct analog in the surviving WinTesla MUNGA/MUNGA_L4 headers. // (CROSS-FAMILY / engine gap -- see report.) Declared here so the recovered // bodies compile; the linker will bind them to the real engine symbols. // // FUN_004bff74 toggles the voice-assist flag on the pilot subsystem. // FUN_0049fb54 Entity::IsDestroyed() reached through an entity handle. // extern void ToggleVoiceAssist(int voice_assist_subsystem); extern Logical Is_Destroyed(int entity_handle); // // Mech::GetHorizontalFiringReach -- the reachable horizontal firing half-arc // (radians) the mech's torso can bring its guns to bear off dead-ahead. Weapons // carry no arc field (the .SUB resources have none); the TORSO mount is what lets // a mech point its guns to the side, so the authentic per-mech weapon traverse IS // the torso's horizontal twist range (0 for a fixed torso like the Blackhawk). // Defined here (not mech.cpp) because the cast to the full Torso type needs its // header, which mech.cpp deliberately does not include (subsystem-stub collision). // Scalar Mech::GetHorizontalFiringReach() { Torso *torso = (Torso *)GetTorsoSubsystem(); // @0x438 cache (real Torso or 0) return (torso != 0) ? torso->GetHorizontalReach() : 0.0f; } // // ChildIterator -- walks the eyepoint / camera child list hung off the torso // articulation block (engine FUN_004afacf/FUN_004afb0d). The real type is a // scene-graph DCS child iterator; the recovered InterpretControls() touches // the list head as a raw address and the entries as raw int* records, so the // iterator is reconstructed here as a minimal intrusive-list walk. BEST-EFFORT. // namespace { struct ChildIterator { int *node; explicit ChildIterator(int list_head) : node(*(int **)list_head) {} int *Next() { int *current = node; if (current != 0) { node = *(int **)current; // next-link assumed at offset 0 } return current; } }; } // // Owner (Mech) sub-object offsets touched by this mapper. The exact Mech // layout is not recoverable from the pseudo-C; these accessors document the // observed byte offsets and are flagged best-effort. A human should fold them // back onto the real Mech / TorsoArticulation / Cockpit accessors. // // mech + 0x438 -> torso articulation block // +0x220/+0x224 current torso yaw / pitch demand // +0x228/+0x22c neutral (centered) yaw / pitch // +0x230/+0x234 torso yaw / pitch travel limits // +0x1f0 torso free-aim demand // +0x274 torso auto-center flag // +0x1f4 (500) torso pitch demand // +0x250 free-aim-enabled flag // +0x34c maximum yaw rate (also reverse scale) // +0x534 minimum (high-speed) yaw rate // +0x5c0 forward throttle scale // +0x360 eyepoint rotation (EulerAngles) // +0x378 eyepoint slave flag // +0x410 eyepoint slave amount // +0x564/+0x568/+0x56c/+0x570 look L/R/down/behind eyepoint pitches // +0x7bc eyepoint / camera child list // mech + 0x5b4 -> cockpit block (+0x28c torso free-aim demand, +0x2a0 flag) // // // Pilot record offsets (entries of pilotArray): // pilot + 0x100 world position (Vector3D) // pilot + 0x1e0 pilot id (network ordinal) // pilot + 0x1fc linked entity handle // pilot + 0x284 current target handle (written on the LOCAL pilot) // // Player drive input, owned by the launcher (btbuild/btl4main.cpp). Consumed by // the dev-box key bridge at the top of MechControlsMapper::InterpretControls. struct BTDriveInput { float throttle; float turn; int forced; int fire; int fireForced; float forcedThrottle; int keyFwd; int keyBack; int keyLeft; int keyRight; int allStop; }; extern BTDriveInput gBTDrive; //########################################################################### //########################################################################### // MechControlsMapper //########################################################################### //########################################################################### //############################################################################# // Shared Data Support // MechControlsMapper::SharedData MechControlsMapper::DefaultData( MechControlsMapper::GetClassDerivations(), MechControlsMapper::GetMessageHandlers(), MechControlsMapper::GetAttributeIndex(), MechControlsMapper::StateCount ); Derivation* MechControlsMapper::GetClassDerivations() // @0050ee10 { static Derivation classDerivations( Subsystem::GetClassDerivations(), "MechControlsMapper" // @0050f173 ); return &classDerivations; } //############################################################################# // Messaging Support // // Message table @0050ee40. IDs 3..0x13 share the single ConfigureMappable // handler; the last three drive the dedicated cycle / toggle handlers. The // engine MESSAGE_ENTRY macro forces handler-name == message-name, so the rows // that fan into the shared handler are written out explicitly. // #define MAPPABLE_ENTRY(message) \ { \ MechControlsMapper::message##MessageID, \ #message, \ (Receiver::Handler) \ &MechControlsMapper::ConfigureMappableMessageHandler \ } const MechControlsMapper::HandlerEntry MechControlsMapper::MessageHandlerEntries[]= { MAPPABLE_ENTRY(Aux1Quad), MAPPABLE_ENTRY(Aux1Eng1), MAPPABLE_ENTRY(Aux1Eng2), MAPPABLE_ENTRY(Aux1Eng3), MAPPABLE_ENTRY(Aux1Eng4), MAPPABLE_ENTRY(Aux2Quad), MAPPABLE_ENTRY(Aux2Eng1), MAPPABLE_ENTRY(Aux2Eng2), MAPPABLE_ENTRY(Aux2Eng3), MAPPABLE_ENTRY(Aux2Eng4), MAPPABLE_ENTRY(Aux3Quad), MAPPABLE_ENTRY(Aux3Eng1), MAPPABLE_ENTRY(Aux3Eng2), MAPPABLE_ENTRY(Aux3Eng3), MAPPABLE_ENTRY(Aux3Eng4), MAPPABLE_ENTRY(ZoomIn), MAPPABLE_ENTRY(ZoomOut), MESSAGE_ENTRY(MechControlsMapper, CycleControlMode), MESSAGE_ENTRY(MechControlsMapper, CycleDisplayMode), MESSAGE_ENTRY(MechControlsMapper, ToggleVoiceAssist) }; #undef MAPPABLE_ENTRY MechControlsMapper::MessageHandlerSet& MechControlsMapper::GetMessageHandlers() { static MessageHandlerSet messageHandlers( ELEMENTS(MechControlsMapper::MessageHandlerEntries), MechControlsMapper::MessageHandlerEntries, Subsystem::GetMessageHandlers() ); return messageHandlers; } //############################################################################# // Attribute Support // // Attribute table @0050efd0. (Recorded offsets carry the engine scalar tag, // e.g. 0x115 -> stickPosition @0x114.) // const MechControlsMapper::IndexEntry MechControlsMapper::AttributePointers[]= { ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114 ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120 ATTRIBUTE_ENTRY(MechControlsMapper, ReverseThrust, reverseThrust), // 0x124 ATTRIBUTE_ENTRY(MechControlsMapper, SpeedDemand, speedDemand), // 0x128 ATTRIBUTE_ENTRY(MechControlsMapper, TurnDemand, turnDemand), // 0x12C ATTRIBUTE_ENTRY(MechControlsMapper, LookForward, lookForward), // 0x130 ATTRIBUTE_ENTRY(MechControlsMapper, LookLeft, lookLeft), // 0x134 ATTRIBUTE_ENTRY(MechControlsMapper, LookRight, lookRight), // 0x138 ATTRIBUTE_ENTRY(MechControlsMapper, LookBehind, lookBehind), // 0x13C ATTRIBUTE_ENTRY(MechControlsMapper, LookDown, lookDown), // 0x140 ATTRIBUTE_ENTRY(MechControlsMapper, TorsoUp, torsoUp), // 0x144 ATTRIBUTE_ENTRY(MechControlsMapper, TorsoDown, torsoDown), // 0x148 ATTRIBUTE_ENTRY(MechControlsMapper, TorsoLeft, torsoLeft), // 0x14C ATTRIBUTE_ENTRY(MechControlsMapper, TorsoRight, torsoRight), // 0x150 ATTRIBUTE_ENTRY(MechControlsMapper, TorsoCenter, torsoCenter), // 0x154 ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode), // 0x190 ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode), // 0x194 ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage), // 0x158 ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray) // 0x15C }; MechControlsMapper::AttributeIndexSet& MechControlsMapper::GetAttributeIndex() { static AttributeIndexSet attributeIndex( ELEMENTS(MechControlsMapper::AttributePointers), MechControlsMapper::AttributePointers, Subsystem::GetAttributeIndex() ); return attributeIndex; } //############################################################################# // Construction and Destruction // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b02f0 // MechControlsMapper::MechControlsMapper( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): Subsystem( // FUN_004ac530(...,0,0) owner, subsystem_ID, subsystem_resource, shared_data ) { Check(owner); // // This mapper is always the active "Performance" of its Mech; the engine // installs the default InterpretControls member-pointer (vtable @0050f120) // into the simulation slot (this[7..9]). // SetPerformance(&MechControlsMapper::InterpretControls); // // Clear all published control inputs / demands. // stickPosition.x = 0.0f; stickPosition.y = 0.0f; throttlePosition = 0.0f; pedalsPosition = 0.0f; speedDemand = 0.0f; turnDemand = 0.0f; reverseThrust = 0; lookForward = 0; lookLeft = 0; lookRight = 0; lookDown = 0; lookBehind = 0; lookState = LookNone; torsoUp = 0; torsoDown = 0; torsoLeft = 0; torsoRight = 0; torsoCenter = 0; controlMode = BasicMode; displayMode = 0; pilotArrayPage = 0; if (getenv("BT_MODE_LOG")) DEBUG_STREAM << "[mode-ctor] mapper=" << (void*)this << " controlMode=" << (int)controlMode << " &controlMode=" << (void*)&controlMode << "\n" << std::flush; // // Recenter the torso articulation: current yaw/pitch <- neutral yaw/pitch. // // TODO(bring-up): the shipped code reads a torso-source pointer at the binary // byte offset owner+0x438 (Mech::sinkSourceSubsystem) and copies that object's // +0x228/+0x22c into +0x220/+0x224. Our RECONSTRUCTED Mech has a different // object layout (sizeof != the binary's 0x854), so this raw offset yields a // garbage pointer and faults. Skipped: the Mech ctor already identity-inits // torsoAimCurrent/torsoAimTarget, so the torso starts centered. Re-enable // via a named Mech accessor once the cross-module field layout is mapped. { int torso = *(int *)((int)owner + 0x438); // torso articulation block if (torso != 0 && torso != (int)0xcdcdcdcd) { // (intentionally inert until the layout is mapped -- see note above) } (void)torso; } pilotArrayBuilt = False; pilotIDs = 0; for (int pi = 0; pi < PilotArraySlots; ++pi) pilotArray[pi] = 0; Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b044c -- frees the pilot id table, then chains to ~Subsystem. // MechControlsMapper::~MechControlsMapper() { Check(this); if (pilotIDs != 0) { delete [] pilotIDs; // FUN_004022e8 (Free of the id table) } Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b08c0 // Logical MechControlsMapper::TestInstance() const { return IsDerivedFrom(*GetClassDerivations()); // FUN_0041a1a4(**this[3], 0x50ee10) } //############################################################################# // Message Handlers // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afbc4 -- shared handler for the auxiliary-equipment and zoom buttons. // task #6 CORRECTION (disasm-verified): the base body is a pure FAIL TRAP -- // push 0x7a / "Unhandled button mapping!" @0x50f24a / MECHMPPR.CPP @0x50f265 / // call Fail -- i.e. "not overridden". The real aux/zoom behavior lives in the // derived L4 layer's own handlers. The old guessed body called // AddOrErase(value, NULL) -- with the RIO mapper live that would TOGGLE a // NULL-destination direct mapping into the fire groups. // void MechControlsMapper::ConfigureMappableMessageHandler( ReceiverDataMessageOf * /*message*/ ) { DEBUG_STREAM << "[FAIL] Unhandled button mapping (base ConfigureMappableMessageHandler)" << std::endl; Fail("Unhandled button mapping!\n"); // MECHMPPR.CPP:0x7a } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afbe0 -- cycle BasicMode -> StandardMode -> VeteranMode -> BasicMode. // Each mode reconfigures the torso articulation: basic mode recenters and // auto-centers the torso, the assisted modes free the torso to its limits. // void MechControlsMapper::CycleControlModeMessageHandler( ReceiverDataMessageOf *message ) { Check(this); Check(message); if (message->dataContents > 0) { CycleControlModeNow(); } } // // The mode-cycle body, shared by the console-button message handler above and // the desktop 'M' key (mech4 key poll -> gBTModeCycle). // void MechControlsMapper::CycleControlModeNow() { { controlMode = (ControlMode)(controlMode + 1); if (controlMode > VeteranMode) { controlMode = BasicMode; } NotifyOfControlModeChange(controlMode); // vtable+0x48 // TYPED torso reconfiguration (2026-07-13): the raw block this // replaces wrote the BINARY's offsets (torso+0x1f0/0x274/0x220...) // straight onto OUR compiled Torso -- the databinding trap: garbage // writes into whatever members live there in this build. The // observable semantics via named members: Basic clears the analog // axes and recenters (the sim's centerCommand -> Recenter); the // assisted modes just free the torso (the sim clamps to the authored // limits on its own). Mech *mech = GetMech(); Torso *torso = (mech != 0) ? (Torso *)mech->GetTorsoSubsystem() : 0; if (torso != 0) { if (controlMode == BasicMode) { torso->SetAnalogTwistAxis(0.0f); torso->SetAnalogElevationAxis(0.0f); torso->CommandRecenter(); } // Standard/Veteran: nothing to force -- the sim's limits govern. } DEBUG_STREAM << "[mode] control mode -> " << (int)controlMode << " (0=Basic 1=Standard 2=Veteran)" << std::endl; } Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afcac -- cycle the HUD display mode (0 -> 1 -> 2 -> 0). // void MechControlsMapper::CycleDisplayModeMessageHandler( ReceiverDataMessageOf *message ) { Check(this); Check(message); if (message->dataContents > 0) { displayMode = displayMode + 1; if (displayMode > 2) { displayMode = 0; } NotifyOfDisplayModeChange(displayMode); // vtable+0x4c } Check_Fpu(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afce8 -- toggle voice assist on the Mech's pilot subsystem. // void MechControlsMapper::ToggleVoiceAssistMessageHandler( ReceiverDataMessageOf *message ) { Check(this); Check(message); if (message->dataContents > 0) { // FUN_004bff74(mech->subsystem(0x190)) -- toggles the voice-assist flag. ToggleVoiceAssist(*(int *)((int)GetMech() + 0x190)); } Check_Fpu(); } //############################################################################# // Model Support -- InterpretControls // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Owner accessor. The mapper is always owned by a Mech; the decomp used the // stored owner pointer directly (this[?]). Routed here through the engine's // Subsystem::GetEntity(). // Mech* MechControlsMapper::GetMech() { Check(this); return (Mech *)GetEntity(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004afd10 -- the active master-Mech performance. Reads the raw stick, // throttle, pedals and look buttons and writes the locomotion demands // (speedDemand / turnDemand), the torso aim and the eyepoint pose. The // per-control-mode steering/torso math is faithful to the decomp; the Mech // sub-object offsets are documented at the top of this file. BEST-EFFORT on // the precise Mech-field semantics. // void MechControlsMapper::InterpretControls(Scalar time_slice) { Check(this); // // OFFSET RECONCILIATION (the revival fix): the earlier draft read the owner // at RAW binary offsets, including TYPED-POINTER arithmetic on Mech* // ("*(Scalar *)(mech + 0x34c)" == mech + 0x34c*sizeof(Mech) -- a wild pointer, // the AV that kept this tick bypassed). Every owner access now goes through // the reconciled members: mech+0x34c -> reverseStrideLength (naming caveat: // LoadLocomotionClips measures it from the rr* clips -- it is the TOP/run // cycle speed the throttle scales), mech+0x534 -> walkStrideLength, // mech+0x5c0 -> forwardThrottleScale, mech+0x438 -> sinkSourceSubsystem (the // re-based binary-exact Torso), mech+0x5b4 -> hudSubsystem (the HUD -- raw // factory part_012.c:10164; MechTech's 0x104 alloc cannot hold +0x28c). // Torso/HUD writes go through their real members (analog axes @0x1F0/0x1F4, // horizontalEnabled @0x250, freeAimSlew @0x28C). Null-guards are bring-up // safety for mechs without those subsystems (the binary trusts the data). // Mech *mech = GetMech(); Torso *torso = (Torso *)mech->GetTorsoSubsystem(); // @0x438 (raw iVar5) HUD *cockpit= (HUD *)mech->GetHudSubsystem(); // @0x5b4 (raw iVar3) // // BRING-UP INPUT BRIDGE (dev box; env BT_KEY_BRIDGE=0 to disable on pods). // The engine controls push refreshes the registered input attributes from // DEVICE elements every frame BEFORE this Performance runs; on a keyboard // dev box the RIO scalar throttle channel doesn't exist and its element // reads a constant (observed: throttlePosition forced back to 1.0 each // frame -> the mech could never stop). So key state is written HERE -- // after the push, immediately before interpretation -- making the keyboard // authoritative on the dev box. Interpretation below stays 100% authentic. // { static const int s_keyBridge = (getenv("BT_KEY_BRIDGE") == 0 || *getenv("BT_KEY_BRIDGE") != '0'); if (s_keyBridge && mech != 0 && application != 0 && (Entity *)mech == application->GetViewpointEntity()) { float key_throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle; float key_turn = gBTDrive.forced ? 0.0f : gBTDrive.turn; // Headless harness (forced mode only): BT_FORCE_TURN holds a steering // demand; BT_FORCE_SECONDS releases the forced throttle after n // sim-seconds while KEEPING the turn (the turn-in-place repro). if (gBTDrive.forced) { static float s_hClock = 0.0f, s_hLimit = -1.0f, s_hTurn = -999.0f; if (s_hLimit < 0.0f) { const char *fs = getenv("BT_FORCE_SECONDS"); s_hLimit = fs ? (float)atof(fs) : 0.0f; } if (s_hTurn < -900.0f) { const char *ft = getenv("BT_FORCE_TURN"); s_hTurn = ft ? (float)atof(ft) : 0.0f; } key_turn = s_hTurn; // BT_GOTO beeline (DEBUG harness): the turn demand is computed in // mech4.cpp's drive block (where position/heading are in scope) // and published through gBTGotoTurn/gBTGotoActive. { extern int gBTGotoActive; extern float gBTGotoTurn; extern float gBTGotoThrottle; if (gBTGotoActive) { key_turn = gBTGotoTurn; key_throttle = key_throttle * gBTGotoThrottle; } } // BT_FORCE_OSC=: square-wave the forced throttle between the // commanded value and near-idle every sim-seconds -- the // headless FEATHERING repro (run->walk-downshift->run churn, the // live-play gait-desync trigger pattern, task #49). { static float s_oscP = -1.0f, s_oscClock = 0.0f; if (s_oscP < 0.0f) { const char *fo = getenv("BT_FORCE_OSC"); s_oscP = fo ? (float)atof(fo) : 0.0f; } if (s_oscP > 0.0f) { s_oscClock += time_slice; if (fmodf(s_oscClock, 2.0f * s_oscP) >= s_oscP) key_throttle = 0.08f; // near-idle half-cycle } } // BT_FORCE_SWEEP=: triangle-sweep the forced throttle 0.2..0.9 // continuously -- mimics a keyboard key-hold sweeping the lever every // frame (a type-2 speed record per frame), the keyboard-skip repro. { static float s_swpP = -1.0f, s_swpClock = 0.0f; if (s_swpP < 0.0f) { const char *fw = getenv("BT_FORCE_SWEEP"); s_swpP = fw ? (float)atof(fw) : 0.0f; } if (s_swpP > 0.0f) { s_swpClock += time_slice; float ph = fmodf(s_swpClock, 2.0f * s_swpP) / s_swpP; // 0..2 float tri = (ph < 1.0f) ? ph : (2.0f - ph); // 0..1..0 key_throttle = 0.2f + 0.7f * tri; // BT_FORCE_STEP=1: quantize the swept throttle to the RIO // Ranger 0.05 grid (the authentic ADC/deadband step) -- the // stepped-vs-continuous A/B for the speed-change glitch. static int s_swpStep = -1; if (s_swpStep < 0) s_swpStep = getenv("BT_FORCE_STEP") ? 1 : 0; if (s_swpStep) key_throttle = floorf(key_throttle / 0.05f + 0.5f) * 0.05f; } } // BT_FORCE_FLIP=: invert the forced throttle after t sim-seconds // (the mid-stride direction-change repro). static float s_hFlip = -1.0f; if (s_hFlip < 0.0f) { const char *ff = getenv("BT_FORCE_FLIP"); s_hFlip = ff ? (float)atof(ff) : 0.0f; } if (s_hLimit > 0.0f || s_hFlip > 0.0f) { s_hClock += time_slice; if (s_hFlip > 0.0f && s_hClock > s_hFlip) { key_throttle = -key_throttle; } else if (s_hLimit > 0.0f && s_hClock > s_hLimit) { key_throttle = 0.0f; } } } throttlePosition = (key_throttle >= 0.0f) ? key_throttle : -key_throttle; reverseThrust = (key_throttle < 0.0f) ? 1 : 0; // CONTROL-MODE AXIS ROUTING (2026-07-13, the pod mapping): in // BASIC the stick steers the legs (turn) and the torso auto- // centers; in STANDARD/VETERAN the stick is the TORSO (free aim / // twist) and the PEDALS steer -- so the keyboard's A/D feed the // pedals and Q/E ('the stick') feed the twist. 'M' cycles modes // (the same body the pod console button message drives, so the // CONTROL MODE gauge tracks). { extern int gBTModeCycle; extern float gBTTwistAxis; extern int gBTTorsoRecenter; if (gBTModeCycle) { gBTModeCycle = 0; CycleControlModeNow(); } if (controlMode == BasicMode) { stickPosition.x = key_turn; pedalsPosition = 0.0f; } else { stickPosition.x = gBTTwistAxis; // the torso axis pedalsPosition = key_turn; // A/D = the pedals // 'X' recenter pulse (2026-07-13): one-frame centerCommand // -> the sim arms recenterActive and Recenter (@004b6918) // slews the torso home; any Q/E input cancels it sim-side. // centerCommand is a pod BUTTON state, so the writer clears // it while unpressed (Basic's own path re-asserts every // frame; this branch owns it in Standard/Veteran). { Torso *rcTorso = (Torso *)mech->GetTorsoSubsystem(); if (rcTorso != 0) { if (gBTTorsoRecenter) { gBTTorsoRecenter = 0; rcTorso->CommandRecenter(); } else { rcTorso->ClearRecenterCommand(); } } } } } stickPosition.y = 0.0f; } } // //------------------------------------------------------------------ // Throttle -> forward speed demand (reverse thrust inverts & rescales) //------------------------------------------------------------------ // // CANARY HEAL: something stomps mech->forwardThrottleScale at runtime // nondeterministically (observed 0.14 and -1.0 across runs; ctor sets 1.0 // guarded) -- a wild raw-offset write, hunt via ba w4 on mech+0x5c0 (on the // ledger). Until the writer is caught, restore a sane value and log. // (band tightened: the stomp value 0.14 -- observed repeatedly, ~8 degrees // in radians, likely an angle write landing on the wrong member -- passed // the old 0.01..100 sanity band and the user's forward speed silently // capped at 8.6 u/s while reverse ran full. The ctor guard currently // always yields exactly 1.0, so heal anything else.) if (mech->forwardThrottleScale != 1.0f) { DEBUG_STREAM << "[mppr] forwardThrottleScale STOMPED to " << mech->forwardThrottleScale << " -- healed to 1.0" << "\n" << std::flush; mech->forwardThrottleScale = 1.0f; } if (reverseThrust < 1) { speedDemand = mech->reverseStrideLength * throttlePosition * mech->forwardThrottleScale; } else { speedDemand = -mech->reverseStrideLength * throttlePosition; } { static int s_cTrace = -1; if (s_cTrace < 0) { const char *e = getenv("BT_MPPR_TRACE"); s_cTrace = (e && *e != '0') ? 1 : 0; } if (s_cTrace) { static float s_cAcc = 0.0f; s_cAcc += time_slice; if (s_cAcc >= 0.5f) { s_cAcc = 0.0f; DEBUG_STREAM << "[mppr-c] thr=" << throttlePosition << " rev=" << reverseThrust << " topSpd=" << mech->reverseStrideLength << " fScale=" << mech->forwardThrottleScale << " -> dmd=" << speedDemand << "\n" << std::flush; } } } // //------------------------------------------------------------------ // Square the stick for a soft response; cube the pedals. Preserve sign. //------------------------------------------------------------------ // Scalar stick_x = stickPosition.x * stickPosition.x; Scalar stick_y = stickPosition.y * stickPosition.y; Scalar pedal_3 = pedalsPosition * pedalsPosition * pedalsPosition; if (stickPosition.x < 0.0f) stick_x = -stick_x; // _DAT_004b0274 == 0.0f if (stickPosition.y < 0.0f) stick_y = -stick_y; turnDemand = 0.0f; if (controlMode == BasicMode) { // // Basic: stick yaw drives the turn directly; stick pitch drives the // torso pitch. The achievable turn rate is clamped down with speed. // turnDemand = stick_x; if (torso) { torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 (500) torso->SetAnalogTwistAxis(0.0f); // raw torso+0x1f0 } if (cockpit) { cockpit->SetFreeAimSlew(0.0f); // raw cockpit+0x28c } Scalar max_turn = (mech->reverseStrideLength - mech->walkStrideLength) * (1.0f - Abs(turnDemand)) // _DAT_004b0278 == 1.0f + mech->walkStrideLength; if (fabsf(turnDemand) > JM_CLOSE_ENOUGH) // _DAT_004b027c { Clamp(speedDemand, -max_turn, max_turn); } } else if (controlMode == StandardMode) { // // Standard: torso free-aim follows the stick yaw; pedals provide the // turn. Turn rate clamped as in basic mode. // if (torso == 0 || !torso->GetHorizontalEnabled()) // raw torso+0x250 == 0 { if (cockpit) cockpit->SetFreeAimSlew(stick_x); // raw cockpit+0x28c } else { torso->SetAnalogTwistAxis(stick_x); // raw torso+0x1f0 } if (torso) torso->SetAnalogElevationAxis(stick_y); // raw torso+0x1f4 turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3; Scalar max_turn = (mech->reverseStrideLength - mech->walkStrideLength) * (1.0f - Abs(turnDemand)) + mech->walkStrideLength; if (fabsf(turnDemand) > JM_CLOSE_ENOUGH) { Clamp(speedDemand, -max_turn, max_turn); } } else if (controlMode == VeteranMode) { // // Veteran: as standard, but no speed-dependent turn clamp. // if (torso == 0 || !torso->GetHorizontalEnabled()) { if (cockpit) cockpit->SetFreeAimSlew(stick_x); } else { torso->SetAnalogTwistAxis(stick_x); } if (torso) torso->SetAnalogElevationAxis(stick_y); turnDemand = (pedal_3 == 0.0f) ? 0.0f : pedal_3; } // //------------------------------------------------------------------ // Look / eyepoint selection. Choose a look direction from the buttons; // when it changes, re-aim the eyepoint and re-slave the camera children. //------------------------------------------------------------------ // previousLookState = lookState; if (lookLeft > 0) lookState = LookLeftState; else if (lookRight > 0) lookState = LookRightState; else if (lookBehind > 0) lookState = LookBehindState; else if (lookDown > 0) lookState = LookDownState; else lookState = LookNone; if (lookState != previousLookState) { // TODO(look-eyepoint): the commit block below writes the eyepoint pose at // binary offsets whose Mech-member mapping is CONFLICTED (mech+0x360 is // claimed by mechName, +0x410 by stateFlags, +0x7bc by weaponRoster -- // and the old draft even routed them through the TORSO pointer). Arbitrate // those labels against the raw before enabling; the port has no eyepoint // consumer yet. Look-state SELECTION above stays live (this-only). DEBUG_STREAM << "[mppr] look state -> " << lookState << " (eyepoint commit deferred)\n" << std::flush; } else if (0) // DEFERRED body (kept for the reconstruction record) { const int torso = 0; // shadow: the raw-offset text below predates // the reconciliation (see TODO above) EulerAngles eyepoint; ChildIterator cameras(torso + 0x7bc); // FUN_004afacf switch (lookState) { case LookNone: eyepoint = EulerAngles::Identity; // DAT_004e0f8c *(LWord *)(torso + 0x378) = 1; // re-slave eyepoint for (int *camera; (camera = cameras.Next()) != 0; ) { *(LWord *)(camera + 0x3e0) = (*(int *)(camera + 0x334) == 0); } break; case LookLeftState: eyepoint = EulerAngles::Identity; eyepoint.pitch = *(Scalar *)(torso + 0x564); *(LWord *)(torso + 0x378) = 0; for (int *camera; (camera = cameras.Next()) != 0; ) { *(LWord *)(camera + 0x3e0) = 0; } break; case LookRightState: eyepoint = EulerAngles::Identity; eyepoint.pitch = *(Scalar *)(torso + 0x568); *(LWord *)(torso + 0x378) = 0; for (int *camera; (camera = cameras.Next()) != 0; ) { *(LWord *)(camera + 0x3e0) = 0; } break; case LookBehindState: eyepoint = EulerAngles::Identity; eyepoint.yaw = PI; // 0x40490fdb eyepoint.pitch = *(Scalar *)(torso + 0x570); *(LWord *)(torso + 0x378) = *(LWord *)(torso + 0x410); for (int *camera; (camera = cameras.Next()) != 0; ) { *(LWord *)(camera + 0x3e0) = *(int *)(camera + 0x334); } break; case LookDownState: eyepoint = EulerAngles::Identity; eyepoint.pitch = *(Scalar *)(torso + 0x56c); *(LWord *)(torso + 0x378) = 0; for (int *camera; (camera = cameras.Next()) != 0; ) { *(LWord *)(camera + 0x3e0) = 0; } break; } *(EulerAngles *)(torso + 0x360) = eyepoint; // commit eyepoint rotation } Check_Fpu(); } //############################################################################# // Pilot-array (other-player roster) management // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b0600 -- build the pilot roster once, lazily, from the application's // "Players" group (skipping spectators flagged 0x40). // void MechControlsMapper::BuildPilotArray() { if (pilotArrayBuilt) { return; } pilotArrayBuilt = True; pilotCount = 0; EntityGroup *players = application->GetEntityManager()->FindGroup("Players"); // @0050f44b if (players != 0) { ChainIteratorOf pilots(players->groupMembers); // FUN_00421414 for (Node *entry; (entry = pilots.ReadAndNext()) != 0; ) { if ((*(byte *)((int)entry + 0x29) & 0x40) == 0) // not a spectator { ++pilotCount; } } } // Capacity clamp: the binary reserves 10 slots (local + up to 9 remotes; // an 8-pod game fits). pilotCount beyond that would overrun the block in // the binary too -- clamp so the `<= pilotCount` zero-loop (which zeroes // the local slot 0 PLUS pilotCount remotes, faithful) stays in bounds. if (pilotCount > PilotArraySlots - 1) pilotCount = PilotArraySlots - 1; pilotIDs = new int[pilotCount]; // FUN_004022b0 for (int i = 0; i <= pilotCount; ++i) { pilotArray[i] = 0; } FillPilotArray(); // FUN_004b06cc ChooseDefaultPilot(); // FUN_004b07f0 } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b06cc -- slot 0 holds the local pilot; the remaining slots are filled // from the "Players" group (paged by pilotArrayPage). // void MechControlsMapper::FillPilotArray() { for (int i = 0; i < pilotCount; ++i) { pilotIDs[i] = -1; } // // The local "station" (the local network player record) lives at // application+0x6c in the binary; its pilot roster entry at +0x190. // ENGINE DRIFT FIX (the AV that kept the mapper tick bypassed): the WinTesla // Application exposes the local player through GetMissionPlayer() (APP.h:169) // -- the same reconciliation SendFakeButtonEvent uses (this file's sibling in // btl4mppr.cpp). The old draft read the 1995 byte offsets through the 2007 // object -> wild "pilot" pointer -> access violation. // Player *local_pilot = (application != 0) ? application->GetMissionPlayer() : 0; if (local_pilot == 0) { return; } pilotArray[0] = (Pilot *)local_pilot; pilotIDs[0] = local_pilot->GetEntityID(); // raw read pilot+0x1e0 (the entity id) EntityGroup *players = application->GetEntityManager()->FindGroup("Players"); if (players != 0) { ChainIteratorOf pilots(players->groupMembers); // skip earlier pages int skip = pilotArrayPage * pilotCount; while (skip > 0 && pilots.ReadAndNext() != 0) { --skip; } int slot = 1; for (Node *entry; (entry = pilots.ReadAndNext()) != 0; ) { // TODO(mp-roster): the binary skips spectators via a flag byte at // entry+0x29 (bit 0x40) -- its WinTesla home is unmapped; solo has no // spectators, so the check is omitted until the flag is reconciled. if (entry == (Node *)local_pilot) // already slot 0 { continue; } pilotArray[slot] = (Pilot *)entry; pilotIDs[slot] = ((Entity *)entry)->GetEntityID(); // raw entry+0x1e0 if (++slot >= pilotCount) { break; } } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b07f0 -- pick the initial target for the local pilot: the next pilot id // after the local one (or the first non-self pilot). // void MechControlsMapper::ChooseDefaultPilot() { int target = 0; if (pilotCount != 1) { int index = 0; if (pilotCount == pilotIDs[0]) { for (int i = 1; i < pilotCount; ++i) { if (pilotIDs[i] != 1) { index = i; break; } } } else { for (int i = 1; i < pilotCount; ++i) { if (pilotIDs[i] == pilotIDs[0] + 1) { index = i; break; } } } Pilot *chosen = pilotArray[index]; if (chosen != 0) { target = *(int *)((int)chosen + 0x1fc); } *(int *)((int)pilotArray[0] + 0x284) = target; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b049c -- set the local pilot's current target to the pilot on the given // roster page. // void MechControlsMapper::UpdateCurrentPilot(int page) { if (pilotArrayBuilt && pilotArray[0] != 0) { int target = 0; Pilot *chosen = pilotArray[page]; if (chosen != 0) { target = *(int *)((int)chosen + 0x1fc); } *(int *)((int)pilotArray[0] + 0x284) = target; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b04d8 -- choose the nearest living pilot (other than self) as the local // pilot's current target. // void MechControlsMapper::ChooseNearestPilot(int self_id) { if (!pilotArrayBuilt || pilotArray[0] == 0) { return; } int target = 0; Scalar nearest_distance = 0.0f; int nearest_index = 0; Logical none_yet = True; for (int i = 1; i < pilotCount; ++i) { int entity = *(int *)((int)pilotArray[i] + 0x1fc); Vector3D delta; delta.Subtract( // FUN_00408644 *(Vector3D *)((int)pilotArray[i] + 0x100), *(Vector3D *)((int)pilotArray[0] + 0x100) ); Scalar distance = delta.x * delta.x + delta.y * delta.y + delta.z * delta.z; if (none_yet) { if (!Is_Destroyed(entity) && entity != self_id) { none_yet = False; nearest_distance = distance; nearest_index = i; } } else if (!Is_Destroyed(entity) && entity != self_id && distance < nearest_distance) { nearest_distance = distance; nearest_index = i; } } if (!none_yet) { Pilot *chosen = pilotArray[nearest_index]; if (chosen != 0) { target = *(int *)((int)chosen + 0x1fc); } *(int *)((int)pilotArray[0] + 0x284) = target; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // @004b0898 -- bounds-checked roster accessor. // Pilot * MechControlsMapper::GetPilot(int index) { if (index >= pilotCount || index < 0) { return 0; } return pilotArray[index]; } //############################################################################# // Configuration / event-mapping interface (secondary vtable @0050f498) // // These are "not overridden" defaults: a platform-specific derived mapper is // expected to supply real implementations. The base class traps the call. // // // @004b029c // void MechControlsMapper::ExitConfiguration(int /*held_element*/) { DEBUG_STREAM << "[FAIL] ExitConfiguration not overridden (base mapper in slot 0)" << std::endl; Fail("ExitConfiguration not overridden!\n"); // @004b029c MECHMPPR.CPP:0x245 } // // @004b0280 (vtable +0x38; "EnterConfiguration not overridden!" @0050f370) // void MechControlsMapper::EnterConfiguration( int /*held_element*/, ControlsButton * /*destination*/, Receiver * /*receiver*/, Receiver::MessageID /*choose_message_id*/, Receiver::MessageID /*configure_message_id*/, Receiver::MessageID /*active_message_id*/ ) { DEBUG_STREAM << "[FAIL] EnterConfiguration not overridden (base mapper in slot 0)" << std::endl; Fail("EnterConfiguration not overridden!\n"); } // // @004b02b8 (vtable +0x40; the EVENT AddOrErase overload -- the old // "CreateTemporaryEventMappings" name was RP drift, task #6) // void MechControlsMapper::AddOrErase( unsigned int /*button_ID*/, Receiver * /*receiver*/, Receiver::MessageID /*message_ID*/ ) { DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase event)" << std::endl; Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x24f } // // @004b02d4 (vtable +0x44; the DIRECT AddOrErase overload) // void MechControlsMapper::AddOrErase( unsigned int /*button_ID*/, ControlsButton * /*destination*/ ) { DEBUG_STREAM << "[FAIL] Unhandled mapping (base AddOrErase direct)" << std::endl; Fail("Unhandled mapping!\n"); // MECHMPPR.CPP:0x25a } // // @004b048c -- vtable+0x48; defaults to a no-op (override for HUD feedback). // void MechControlsMapper::NotifyOfControlModeChange(int /*new_mode*/) { } // // @004b0494 -- vtable+0x4c; defaults to a no-op. // void MechControlsMapper::NotifyOfDisplayModeChange(int /*new_mode*/) { } // // gauge wave P2: BTResolveRosterPilot -- the bridge PilotList (btl4gau3.cpp, the // Comm KILLS/DEATHS roster) uses to read the viewpoint mech's pilot roster. The // roster lives on the mech's ControlsMapper (subsystemArray[0]); this complete-Mech // TU resolves it, PilotList reads the returned pilot at raw BTPlayer offsets. // Faithful to the binary's Execute: **(App+0x6c mech +0x128)[slot] -> GetPilot. // void *BTResolveRosterPilot(int slot) { if (application == 0) { return 0; } Mech *mech = (Mech *)application->GetViewpointEntity(); // App+0x6c local mech if (mech == 0 || mech->GetSubsystemCount() < 1) { return 0; } MechControlsMapper *mapper = (MechControlsMapper *)mech->GetSubsystem(0); // subsystemArray[0] if (mapper == 0) { return 0; } 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). // //########################################################################### // BTBuildMapperDemandLatch -- slot-0 latch (task #7) // // The binary leaves roster slot 0 NULL except on the viewpoint mech // (MakeViewpointEntity installs the real device mapper via // SetMappingSubsystem @0049fe40). Our port drives NON-viewpoint masters // (the BT_GOTO/BT_AUTODRIVE harness) and animates replicant gait through // mapper demand fields, so every mech gets a default-data BASE mapper as a // demand LATCH at ctor end; SetMappingSubsystem deletes + replaces it on // the viewpoint mech exactly as the binary replaces slot 0. [T3 // accommodation -- relocates the retired 0xBD3 squatter's role to the slot // the binary actually reads.] Complete-type TU (the mech.cpp caller // carries a local stub under the mapper's name). // Subsystem *BTBuildMapperDemandLatch(Mech *mech) { static MechControlsMapper::SubsystemResource latch_resource; Str_Copy(latch_resource.subsystemName, "ControlsMapper", sizeof(latch_resource.subsystemName)); latch_resource.classID = (RegisteredClass::ClassID)0xf; // TrivialSubsystemClassID (btl4app idiom) latch_resource.subsystemModelSize = sizeof(latch_resource); return new MechControlsMapper(mech, 0, &latch_resource, MechControlsMapper::DefaultData); } 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); }