From b26e8205e3a5c3348bb94778ed35b1aa1c3c143b Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 21:59:51 -0500 Subject: [PATCH] Controls: the .CTL positional-id off-by-one -- CONFIRMED live and FIXED (step 2a) The streamed L4 mapping resource carries the binary's positional attribute ids (stick=3, throttle=4) but MechControlsMapper chained from Subsystem::NextAttributeID == 2 -- every streamed record resolved ONE MEMBER LATE (attr 4 -> pedalsPosition, verified via the new permanent BT_CTRLMAP_LOG diagnostic in CreateStreamedMappings). A latent real-pod bug: the serial RIO throttle would drive the pedals member; the dev keyboard bridge masked it. Fix per the mechweap/mech attrPad idiom: ids pinned to the binary numbering, id-2 gap padded, static_assert-locked. Torso + weapon chains verified already aligned. Regression: BT_FORCE_THROTTLE headless walk clean (speedDemand=61.501 through authentic InterpretControls, gait cycles, no faults). Co-Authored-By: Claude Fable 5 --- content/LAST.EGG | 24 ++++---------------- context/glass-cockpit.md | 13 ++++++++--- context/pod-hardware.md | 6 +++++ docs/GLASS_COCKPIT.md | 39 +++++++++++++++++++++++++++++++++ engine/MUNGA_L4/L4CTRL.cpp | 23 +++++++++++++++++++ game/reconstructed/mechmppr.cpp | 19 ++++++++++++++++ game/reconstructed/mechmppr.hpp | 13 ++++++++++- 7 files changed, 113 insertions(+), 24 deletions(-) diff --git a/content/LAST.EGG b/content/LAST.EGG index e0961a1..61255bd 100644 --- a/content/LAST.EGG +++ b/content/LAST.EGG @@ -1,8 +1,8 @@ [mission] adventure=BattleTech -map=cavern +map=grass scenario=freeforall -time=night +time=day weather=clear temperature=27 length=600 @@ -156,9 +156,8 @@ x=128 y=32 width=8 [pilots] -pilot=127.0.0.1:1502 -pilot=127.0.0.1:1602 -[127.0.0.1:1502] +pilot=200.0.0.96 +[200.0.0.96] hostType=0 advancedDamage=1 loadzones=1 @@ -172,20 +171,6 @@ dropzone=one vehicle=bhk1 vehicleValue=1000 color=White -[127.0.0.1:1602] -hostType=0 -advancedDamage=1 -loadzones=1 -name=Boreas -bitmapindex=2 -experience=expert -badge=VGL -patch=Yellow -role=Role::Default -dropzone=one -vehicle=madcat -vehicleValue=1000 -color=Crimson [largebitmap] bitmap=BitMap::Large::Aeolus [BitMap::Large::Aeolus] @@ -242,4 +227,3 @@ width=4 model=dfltrole [Role::NoReturn] model=noretun - diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 2c08e83..953cd81 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -5,7 +5,7 @@ status: living source_sections: "plan 2026-07-17; CMakeLists.txt gates; BT412 prior art (reference only)" related_topics: [pod-hardware, build-and-run, multiplayer, gauges-hud] key_terms: [PadRIO, miniconsole, glass-cockpit] -open_questions: [".CTL positional-attribute off-by-one — audit pending (step 2a)"] +open_questions: [] --- # Glass Cockpit — the desktop developer layer @@ -39,8 +39,15 @@ copied from them. ## Status -- **Step 1 (gates + scaffolding): IN PROGRESS 2026-07-17.** CMake options + defines land with - this file; gated `target_sources` blocks are added as each TU lands. +- **Step 1 (gates + scaffolding): DONE 2026-07-17.** CMake options + defines; 3-config + configure matrix + negative test + defaults build verified. +- **Step 2a (.CTL id audit): DONE 2026-07-17 [T2].** The streamed `.CTL` positional ids landed + every `MechControlsMapper` record ONE MEMBER LATE (our chain starts at 2, the binary's at 3) + — a latent REAL-POD bug (RIO throttle would drive pedalsPosition), masked on dev boxes by the + keyboard bridge. Fixed ungated: ids pinned to binary numbering + id-2 pad + static_assert + locks (`mechmppr.hpp/.cpp`); torso/weapon chains were already aligned. Permanent env-gated + diagnostic: `BT_CTRLMAP_LOG=1` dumps every streamed record's resolved member offset + (`L4CTRL.cpp CreateStreamedMappings`). Full evidence: `docs/GLASS_COCKPIT.md` §2a. - Step 2 (PadRIO + displays): pending. Secondary displays reuse the EXISTING dev-gauge modes (dock-bottom `BT_DEV_GAUGES=1`, combined separate window `BT_DEV_GAUGES_WINDOW=1`, overlay inset `BT_DEV_GAUGES_DOCK=1` — see [[gauges-hud]] + `docs/GAUGE_COMPOSITE.md`); NO new MFD diff --git a/context/pod-hardware.md b/context/pod-hardware.md index 59fe3fc..1f79048 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -43,6 +43,12 @@ The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity q 0x11d-1 = 0x11c, name "ThrottlePosition" @0050f28f). Reverse = record [2]: `Button Throttle1 (0x3F, on the throttle handle) → attr 6 ReverseThrust@0x124`. Turn = pedals value-bound in the ctor (manager+0x44/+0x64 → mapper+0x1b4/+0x1b8). + **Step-2a fix (2026-07-17) [T2]:** these ids are POSITIONAL (`Find(id)` = `index[id-1]`) and + our `MechControlsMapper` chain started one low (`Subsystem::NextAttributeID`==2 vs the + binary's 3) — every streamed record landed one member late (a real-pod landmine: the RIO + throttle scalar drove `pedalsPosition`). Ids are now PINNED to the binary numbering with an + id-2 pad + `static_assert` locks (`mechmppr.hpp/.cpp`); audit tool `BT_CTRLMAP_LOG=1`. + Details: `docs/GLASS_COCKPIT.md` §2a; see [[glass-cockpit]]. 4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10 computes `speedDemand@0x128 = maxSpeed(mech+0x34c) × throttlePosition(0x11c) × scale(mech+0x5c0)` diff --git a/docs/GLASS_COCKPIT.md b/docs/GLASS_COCKPIT.md index bf17ba9..2644bb8 100644 --- a/docs/GLASS_COCKPIT.md +++ b/docs/GLASS_COCKPIT.md @@ -19,3 +19,42 @@ first); this file carries the step-by-step detail, evidence, and verification tr (purity build); `-DBT_GLASS=ON` and ON/ON configures print correct gate state; the BT_STEAM-without-BT_GLASS negative test FATAL_ERRORs. Full 3-config BUILD matrix becomes meaningful from step 2 (first gated code) and is re-run there. + +## 2026-07-17 — Step 2a: the .CTL positional-attribute off-by-one — CONFIRMED and FIXED (ungated) + +**The bug [T2, live-verified]:** the streamed `.CTL` "L4" mapping resource carries the 1995 +binary's positional attribute ids (stick=3, throttle=4, ...), resolved at install time by +`Simulation::GetAttributePointer(id)` → `AttributeIndexSet::Find(id)` = `attributeIndex[id-1]` +(positional). `MechControlsMapper` chained its enum on `Subsystem::NextAttributeID == 2`, but +the binary's chain reaches 3 (one base attribute we don't publish) — so EVERY streamed mapper +record landed one member late. + +**Evidence (new `BT_CTRLMAP_LOG=1` diagnostic in `LBE4ControlsManager::CreateStreamedMappings`, +`engine/MUNGA_L4/L4CTRL.cpp` — permanent, env-gated, default off).** Before the fix +(`build-pod` Debug, `-egg DEV.EGG`, dev platform): +``` +rec 0 subsys 0 group 1(Joy) elem 0 attrID 3 -> +0xe4 (= throttlePosition: WRONG, stick intended) +rec 1 subsys 0 group 0(Scalar) elem 0 attrID 4 -> +0xe8 (= pedalsPosition: WRONG, throttle intended) +rec 2 subsys 0 group 3(Button) elem 63 attrID 6 -> +0xf0 (= speedDemand: WRONG, reverse intended) +rec 4-6 attrID 10/11/12 -> lookRight/lookBehind/lookDown (one late; LookLeft/Right/Behind intended) +``` +Spacing attr3→attr4 was 4 bytes (Scalar) where stick→throttle must be 8 (ControlsJoystick). +The weapons chain (attrID 19 → +0x31c TriggerState) was already correct — mechweap was +pad-aligned in task #16 (gotcha #11). Torso (subsys 17) also aligned (MechSubsystem publishes +binary id 2). ONLY the mapper chain (subsys 0) was off. On the dev box the keyboard INPUT +BRIDGE (writes the mapper members directly, after the engine push) masked it; on a REAL POD the +serial RIO throttle scalar would have driven `pedalsPosition` and the stick x/y would have +driven throttle+pedals — the Phase-8 landmine prior art predicted. + +**The fix (ungated, pod-correctness — user-approved 2026-07-17):** `mechmppr.hpp` pins the enum +to the binary numbering (`MechControlsMapperPadFirstAttributeID = 2`, stick=3 ... PilotArray= +0x16); `mechmppr.cpp` pads id 2 with a valid never-bound entry (the mech.cpp attrPad idiom) and +locks the base + pinned ids with `static_assert`s. `L4MechControlsMapper::TargetRangeExponent` +(chained) moves 0x16→0x17, matching the binary's own arithmetic; it is bound by name (audio +zoom-blip), so no consumer shifts. + +**After the fix:** `attrID 3 -> +0xdc` (stick), `attrID 4 -> +0xe4` (throttle, 8-byte spacing +✓), `attrID 6 -> +0xec` (reverse), `10/11/12 -> +0xfc/+0x100/+0x104` (the look trio); torso + +weapons unchanged. Regression: headless forced-walk run (`BT_FORCE_THROTTLE=1`) drives normally +— `[mppr] thr=1 -> speedDemand=61.501`, gait cycles, position advances, 31/31 subsystems +simulated, no faults. diff --git a/engine/MUNGA_L4/L4CTRL.cpp b/engine/MUNGA_L4/L4CTRL.cpp index f93b0a0..a788adc 100644 --- a/engine/MUNGA_L4/L4CTRL.cpp +++ b/engine/MUNGA_L4/L4CTRL.cpp @@ -2124,6 +2124,29 @@ void if (mapping->mappingType == ControlsMapping::DirectMapping) { attribute = subsystem->GetAttributePointer(mapping->attributeID); + // + // BT_CTRLMAP_LOG=1: dump each streamed .CTL record and the member + // OFFSET its attributeID resolved to on the live subsystem. The + // stream carries the 1995 binary's positional attribute ids, so a + // resolved offset that differs from the binary's member offset for + // that id means the reconstruction's id chain is misaligned (the + // gotcha-#11 class). Audit trail: docs/GLASS_COCKPIT.md step 2a. + // + { + static const int s_ctrlmapLog = + (getenv("BT_CTRLMAP_LOG") != 0 && *getenv("BT_CTRLMAP_LOG") != '0'); + if (s_ctrlmapLog) + { + DEBUG_STREAM << "[ctrlmap] rec " << i + << " subsys " << mapping->subsystemID + << " group " << (int)mapping->controlsGroup + << " elem " << element + << " attrID " << (int)mapping->attributeID + << " -> +0x" << std::hex + << (attribute ? (int)((char*)attribute - (char*)subsystem) : -1) + << std::dec << "\n" << std::flush; + } + } switch (mapping->controlsGroup) { case ScalarGroup: diff --git a/game/reconstructed/mechmppr.cpp b/game/reconstructed/mechmppr.cpp index 959958a..4c1f7c8 100644 --- a/game/reconstructed/mechmppr.cpp +++ b/game/reconstructed/mechmppr.cpp @@ -249,9 +249,28 @@ MechControlsMapper::MessageHandlerSet& // Attribute table @0050efd0. (Recorded offsets carry the engine scalar tag, // e.g. 0x115 -> stickPosition @0x114.) // +// Step-2a fix (2026-07-17): ids are pinned to the binary numbering (stick=3) +// and the id-2 chain gap is padded -- see the enum note in mechmppr.hpp. +// Locked so a future engine-base attribute publish shows up here instead of +// silently double-assigning ids: +static_assert((int)Subsystem::NextAttributeID + == (int)MechControlsMapper::MechControlsMapperPadFirstAttributeID, + "MechControlsMapper pad base != Subsystem::NextAttributeID -- re-count the pad"); +static_assert((int)MechControlsMapper::StickPositionAttributeID == 3 + && (int)MechControlsMapper::PilotArrayAttributeID == 0x16, + "MechControlsMapper ids drifted off the binary .CTL numbering"); + const MechControlsMapper::IndexEntry MechControlsMapper::AttributePointers[]= { + // + // The PAD fills the chain-vs-binary id gap (id 2) with a valid, named, + // never-bound entry (unique name, harmless target) so Find()'s strcmp + // walk never reads an uninitialized slot. + // + { (int)MechControlsMapper::MechControlsMapperPadFirstAttributeID, + "MechControlsMapperPad02", + (Simulation::AttributePointer)&MechControlsMapper::throttlePosition }, ATTRIBUTE_ENTRY(MechControlsMapper, StickPosition, stickPosition), // 0x114 ATTRIBUTE_ENTRY(MechControlsMapper, ThrottlePosition, throttlePosition), // 0x11C ATTRIBUTE_ENTRY(MechControlsMapper, PedalsPosition, pedalsPosition), // 0x120 diff --git a/game/reconstructed/mechmppr.hpp b/game/reconstructed/mechmppr.hpp index 5ba8f0b..f0bb261 100644 --- a/game/reconstructed/mechmppr.hpp +++ b/game/reconstructed/mechmppr.hpp @@ -132,7 +132,18 @@ class Pilot; // public: enum { - StickPositionAttributeID = Subsystem::NextAttributeID, // 3 + // Ids are PINNED to the binary table's numbering (stick=3 ...): + // the streamed .CTL mappings carry these ids POSITIONALLY, but our + // parent chain ends at Subsystem::NextAttributeID == 2 -- the + // binary's reaches 3 (one base attr we don't publish). Chained + // (un-pinned) ids landed every streamed record ONE MEMBER LATE -- + // live-verified 2026-07-17 via BT_CTRLMAP_LOG: attr 4 (binary + // ThrottlePosition) resolved to pedalsPosition; on a real pod the + // RIO throttle would drive the pedals. The id-2 gap is padded in + // the table (the mech.cpp attrPad idiom, gotcha #11) and locked by + // static_asserts in mechmppr.cpp. See docs/GLASS_COCKPIT.md §2a. + MechControlsMapperPadFirstAttributeID = 2, // == Subsystem::NextAttributeID (locked) + StickPositionAttributeID, // 3 ThrottlePositionAttributeID, // 4 PedalsPositionAttributeID, // 5 ReverseThrustAttributeID, // 6