diff --git a/RP_L4/RPL4MPPR.cpp b/RP_L4/RPL4MPPR.cpp index e8d001f..4e93159 100644 --- a/RP_L4/RPL4MPPR.cpp +++ b/RP_L4/RPL4MPPR.cpp @@ -725,6 +725,13 @@ void mode_manager->AddModeMask(previousPresetModeMask); } //----------------------------------- + // Move the lamps with the mappings. + // Doing it here rather than in the + // switch handler keeps the keyboard + // presets (1-6) in step as well. + //----------------------------------- + NotifyOfPresetChange(previousPresetNumber, preset_number); + //----------------------------------- // Save the new preset number //----------------------------------- previousPresetNumber = preset_number; @@ -733,6 +740,19 @@ void Check_Fpu(); } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +void + L4VTVControlsMapper::NotifyOfPresetChange( + int /*old_preset*/, + int /*new_preset*/ + ) +{ + Check(this); + // The base mapper has no preset lamps to move. + Check_Fpu(); +} + //############################################################################# //########################### ThrustmasterMapper ############################## //############################################################################# @@ -1400,45 +1420,13 @@ void if (message->dataContents > 0) { //----------------------------------- - // Choose a new preset + // Choose a new preset. PresetEnable + // ignores a repeat of the lit switch + // and moves the lamps itself. //----------------------------------- - int - current_preset_number = (message->dataContents - 1) - - LBE4ControlsManager::ButtonSecondary7; - - if (previousPresetNumber != current_preset_number) - { - //----------------------------------- - // Set the old preset lamp to 'dim' - //----------------------------------- - if (previousPresetNumber >= 0) - { - Verify(previousPresetNumber < presetCount); - - if (modeLamp[previousPresetNumber] != NULL) - { - Check(modeLamp[previousPresetNumber]); - modeLamp[previousPresetNumber]->SetState(L4Lamp::LampStateDim); - } - } - //----------------------------------- - // Set the new preset lamp to 'on' - //----------------------------------- - if (current_preset_number >= 0) - { - Verify(current_preset_number < presetCount); - - if (modeLamp[current_preset_number] != NULL) - { - Check(modeLamp[current_preset_number]); - modeLamp[current_preset_number]->SetState(L4Lamp::LampStateOn); - } - } - //----------------------------------- - // Change presets - //----------------------------------- - PresetEnable(current_preset_number); - } + PresetEnable( + (message->dataContents - 1) - LBE4ControlsManager::ButtonSecondary7 + ); } Check_Fpu(); } @@ -1655,6 +1643,44 @@ void Check_Fpu(); } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// The six amber switches down the map's right flank. Called by PresetEnable, +// so the lamps follow the mappings no matter what asked for the change. +// +void + VTVRIOMapper::NotifyOfPresetChange(int old_preset, int new_preset) +{ + Check(this); + + //---------------------------------- + // Set the old preset lamp to 'dim' + //---------------------------------- + if (old_preset >= 0) + { + Verify(old_preset < presetCount); + + if (presetLamp[old_preset] != NULL) + { + Check(presetLamp[old_preset]); + presetLamp[old_preset]->SetState(L4Lamp::LampStateDim); + } + } + //---------------------------------- + // Set the new preset lamp to 'on' + //---------------------------------- + if (new_preset >= 0) + { + Verify(new_preset < presetCount); + + if (presetLamp[new_preset] != NULL) + { + Check(presetLamp[new_preset]); + presetLamp[new_preset]->SetState(L4Lamp::LampStateOn); + } + } + Check_Fpu(); +} + //############################################################################# // Construction and Destruction Support // @@ -1680,6 +1706,20 @@ VTVRIOMapper::VTVRIOMapper( leftPedal = 0.0f; rightPedal = 0.0f; + //------------------------------------------------ + // There are no lamps until the mapping blocks + // below make them - and under NOMODES they never + // do, so the notify methods must see NULLs. + //------------------------------------------------ + { + int + i; + + for(i=0; iSetState( + Check(presetLamp[i]); + presetLamp[i]->SetState( (i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim ); } diff --git a/RP_L4/RPL4MPPR.h b/RP_L4/RPL4MPPR.h index 3055dac..6fa286f 100644 --- a/RP_L4/RPL4MPPR.h +++ b/RP_L4/RPL4MPPR.h @@ -104,6 +104,12 @@ ModeMask // void PresetEnable(int preset_number); + +// Announced by PresetEnable for EVERY preset change, whichever way it was +// triggered - map-flank switch or keyboard. Platforms carrying preset +// lamps move them here; the base mapper has none. +virtual void + NotifyOfPresetChange(int old_preset, int new_preset); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Protected data // @@ -250,6 +256,9 @@ public: void NotifyOfConfigurationModeChange(Logical new_state); + void + NotifyOfPresetChange(int old_preset, int new_preset); + void SetPerformance(Performance performance) { diff --git a/docs/CONTROL-PRESETS.md b/docs/CONTROL-PRESETS.md new file mode 100644 index 0000000..35ba094 --- /dev/null +++ b/docs/CONTROL-PRESETS.md @@ -0,0 +1,251 @@ +# Red Planet — the six control presets on the map screen + +The six amber buttons down the right flank of the map display, labelled +PRESET 1 … PRESET 6 on the glass itself. They are not a display option and +they do not touch the map: **each one is a complete factory layout for the +four mappable joystick buttons**, authored per vehicle and shipped in +`RPL4.RES`. Pressing one swaps the whole stick over, live, mid-race. + +``` + map glass, right flank (vRIO 0x18..0x1D, amber) + ┌──────────────┐ + │ PRESET 1 ● │ Secondary7 press + │ PRESET 2 ○ │ Secondary8 │ + │ PRESET 3 ○ │ Secondary9 ▼ + │ PRESET 4 ○ │ Secondary10 VTVRIOMapper::SelectPresetMessageHandler + │ PRESET 5 ○ │ Secondary11 │ (keyboard 1-6 joins here) + │ PRESET 6 ○ │ Secondary12 ▼ + └──────────────┘ L4VTVControlsMapper::PresetEnable(n) + ├─ remove old ModePresetN from the + │ mode manager, add the new one + │ │ + │ ▼ + │ every control mapping whose modeMask + │ carries that bit goes live; the other + │ five presets' mappings go dead + │ + └─ NotifyOfPresetChange(old, new) + ▼ + lamp old→dim, new→on +``` + +## 1. What the player sees + +The map is the pod's portrait-mounted secondary monitor. Its gauge group +paints the button legends itself — `tags` in +`assets/RP411/GAUGE/L4GAUGE.CFG:1611`, six 16×102 strips at x=464: + +| legend | file | cell | +|---|---|---| +| PRESET 1 … PRESET 6 | `butpres1.pcc` … `butpres6.pcc` | one per legend cell down the right edge | +| ZOOM + / ZOOM − / reticle / HORN | `butzmin`, `butzmout`, `butcross`, `buthorn` | left edge, top four cells | + +The legend grid is not `height/6`: the first cell starts 13 rows down and +the six cells are 102 tall on a 105 pitch (13 + 6×102 + 5×3 = 640). vRIO's +`MFDSplitView::LayoutButtons` scales exactly that grid so each on-screen +button lands against its own painted label +(`MUNGA_L4/L4MFDVIEW.cpp:683`, constants at `:103`). + +Addresses run **down** each column from the anchor — the map view is +created with `SideColumns, 0x10, 0x18` (`MUNGA_L4/L4VB16.cpp:4450`), so the +left column is Secondary1…6 top-to-bottom and the right column is +Secondary7…12, i.e. **PRESET 1 is the top button, PRESET 6 the bottom**. +That ordering is confirmed by the left column, where the code binds +Secondary1→zoom in, 2→zoom out, 3→reticle, 4→horn +(`RP_L4/RPL4MPPR.cpp:1857`) and the artwork paints ZOOM+/ZOOM−/cross/HORN +in exactly that order, leaving the bottom two cells blank — the pod wires +only 6 of each column's 8 addresses; 0x16/0x17 and 0x1E/0x1F are Tesla +relays (`MUNGA_L4/L4CTRL.cpp:1901`). + +Preset 1 is lit at construction, the other five dim +(`RP_L4/RPL4MPPR.cpp:1962`). These six lamps are driven explicitly by +`NotifyOfPresetChange`; unlike most panel lamps they are not linked to +their button's mapping state (`SetAutomaticOperation(False)` in +`CreateControlledLamp`, `RP_L4/RPL4MPPR.cpp:47`). + +## 2. The mechanism — a preset is a mode-mask bit + +The engine gates every control mapping behind a `ModeMask`. RP adds six +bits purely for this (`RP_L4/RPL4MODE.h:30`, `nextModeBit` = 0 so the +values are literal): + +| mode | bit | value | +|---|---|---| +| ModeNonConfig / ConfigReady / ConfigOn | 0–2 | 0x001 / 0x002 / 0x004 | +| **ModePreset1 … ModePreset6** | **3–8** | **0x008 … 0x100** | +| ModeBasic / ModeStandard / ModeIntercom | 9–11 | 0x200 / 0x400 / 0x800 | + +`PresetEnable` (`RP_L4/RPL4MPPR.cpp:685`) does one thing: removes the +outgoing preset's bit from the application mode manager and adds the +incoming one. `ControlsUpdateManager::Update` then only dispatches +instances whose `modeMask` intersects the live mask +(`MUNGA/CONTROLS.h:599` shows the same test in `GetMapState`), so five +sixths of the authored stick mappings are simply invisible at any moment. + +Consequences that fall out of that design: + +- Presets cost nothing to switch — no rebuild, no allocation, one mask word. +- They are exactly six because there are six switches: `presetCount = 6 // + limited to number of switches!` (`RP_L4/RPL4MPPR.h:119`). +- Nothing *but* control mappings is preset-scoped. No gauge, bitmap or + strip in `L4GAUGE.CFG` references `ModePreset*` — the mode names exist + in the lookup table (`RP_L4/RPL4MODE.cpp:23`) only so the resource + compiler can resolve them in the control-mapping source. +- Entering configuration state drops all six (`ModeAllNonConfig`) and + restores the previously selected one on exit + (`RP_L4/RPL4MPPR.cpp:483`). + +Keyboard `1`–`6` calls `PresetEnable` directly +(`RP_L4/RPL4MPPR.cpp:1048`), the same entry point the flank switches use. +Both paths therefore move the lamps, because `PresetEnable` announces the +change itself through the virtual `NotifyOfPresetChange` (`:733`) rather +than leaving it to the switch handler — see §7. + +## 3. What is actually in a preset + +Only the four mappable stick buttons are ever preset-scoped — +`FirstMappableButton`…`LastMappableButton` minus the hat +(`MUNGA_L4/L4CTRL.h:497`): + +| element | button | +|---|---| +| 0x40 | trigger | +| 0x45 | pinky | +| 0x46 | thumb low | +| 0x47 | thumb high (by the hat) | + +Everything else — throttle, pedals, stick axes, hat, the whole aux panel — +is bound `ModeAlwaysActive` or `ModeNonConfig` and is identical in all six +presets. The functions that can land on a stick button are the ones that +also have a panel button and a "configure" partner: booster fire, chute, +weapon trigger, LIFT CUT, SIDESLIP, HORN, and intercom PTT +(`RP/VTVMPPR.cpp:144`, `RP/BOOSTER.cpp:50`, `RP/CHUTE.cpp:48`, +`RP/WEAPSYS.cpp:103`). + +## 4. The shipped tables + +The mappings are streamed from the vehicle resource at mission start — +`LBE4ControlsManager::CreateStreamedMappings` +(`MUNGA_L4/L4CTRL.cpp:2153`), fed from the entity's ControlMappings list +resource by name: `"L4"` for the pod RIO, `"Thrustmaster"` for the stick +(`RP_L4/RPL4APP.cpp:761`). Each record is a `ControlsMapping` +(`MUNGA/CONTROLS.h:783`) carrying its own mode mask, so a single button can +appear six times with six different targets. + +**All 26 VTVs in `assets/RP411/RPL4.RES` ship a full six-preset table, for +both the RIO and the Thrustmaster.** Decoded, the *lepton* (two boosters, +one chute, unarmed) reads: + +| | trigger | pinky | thumb low | thumb high | +|---|---|---|---|---| +| PRESET 1 | booster 1 | **PTT** | chute | booster 2 | +| PRESET 2 | booster 1 | **LIFT CUT** | chute | booster 2 | +| PRESET 3 | booster 1 | PTT | **LIFT CUT** | booster 2 | +| PRESET 4 | booster 1 | chute | LIFT CUT | booster 2 | +| PRESET 5 | booster 1 | LIFT CUT | **HORN** | booster 2 | +| PRESET 6 | **LIFT CUT** | chute | booster 2 | booster 1 | + +An armed vehicle substitutes the weapon trigger for the primary booster. +The *puck* (one booster, laser): + +| | trigger | pinky | thumb low | thumb high | +|---|---|---|---|---| +| PRESET 1 | laser | — | HORN | booster | +| PRESET 2 | laser | LIFT CUT | HORN | booster | +| PRESET 3 | laser | — | LIFT CUT | booster | +| PRESET 4 | laser | HORN | LIFT CUT | booster | +| PRESET 5 | laser | LIFT CUT | HORN | booster | +| PRESET 6 | **LIFT CUT** | laser | — | booster | + +The authored intent is consistent across every vehicle in the file: + +1. **Presets 1–5 keep the primary weapon (or lead booster) on the + trigger** and shuffle the *secondary* duties — where LIFT CUT lives, + whether the stick carries PTT or HORN, and whether the chute/third + system stays on the stick at all. +2. **Preset 2 always puts LIFT CUT on the pinky** and **preset 6 always + puts LIFT CUT on the trigger** — verified true for all 26 tables. + Preset 6 is the outlier layout: everything shifts up a finger and the + trigger becomes a handling control rather than a fire control. +3. Vehicles carrying fewer systems leave cells empty, and some presets then + collapse into duplicates — on the puck, presets 2 and 5 are identical. +4. Heavily armed vehicles spend the freed cells on second and third + weapons instead of HORN (bttlbrg, gator, roadblk). + +## 5. Editing the active preset in flight + +The presets are also the storage for in-mission rebinding. With the CFG +button on the upper-right MFD (`AuxUpperRight1/2`, registered under +`ModeStandard` — `RP_L4/RPL4MPPR.cpp:1826`), the pod enters configuration +state: the aux strips swap their legends from tool art to SET art +(`strptol1/2.pcc` → `strpset1/2.pcc`, `L4GAUGE.CFG:715`), and every +mappable button gets a temporary mapping to the chosen system's "choose" +message (`RP_L4/RPL4MPPR.cpp:604`). Pressing a panel button arms a +function; pressing a stick button toggles it via `AddOrErase`, which +writes against **`previousPresetModeMask`** — the currently selected +preset, and only that one (`RP_L4/RPL4MPPR.cpp:1538`). + +The `configMap` gauge on that display (`RPL4GAUG.cpp:3199`, placed at +`L4GAUGE.CFG:749`) shows the state of all four stick buttons against the +armed function, one icon each, using the same preset mask: +`cfgNone` / `cfgOther` / `cfgMe` / `cfgBoth` for +`unmapped` / `mappedByOthers` / `mappedByMe` / both +(`MUNGA/CONTROLS.h:315`). + +Configuration is unavailable in Basic control mode — the CFG button and the +`configMap` gauge are both `ModeStandard`, which is set by Standard, +Veteran and Master but cleared by Basic (`RP_L4/RPL4MPPR.cpp:374`). + +**Nothing is persisted.** Edits live in the `ControlsMappingGroup` chains +for the mission only; no code writes mappings back to disk, and the next +mission re-streams the authored table from `RPL4.RES`. (Porting a real +bindings file is still open — see the roadmap's Workstream A.) + +## 6. The preset number goes out on the wire + +`PresetEnable` sets `mustMatch = preset_number` unconditionally, before the +early-out, tagged `HACK - for backward watcher compatibility` +(`RP_L4/RPL4MPPR.cpp:700`). `mustMatch` is a replicated attribute of the +mapper (`RP/VTVMPPR.cpp:384`), so a watcher/spectator station sees which +preset a pilot is on. It is the only preset state that leaves the machine. + +## 7. Defects found while reading + +**Fixed (2026-08-06):** both lamp defects below. + +1. **The preset loop wrote past `modeLamp`.** `modeLampCount` is 4, but the + preset pass stored six lamps into `modeLamp[i]`, i = 0…5. The members + are declared `configLamp[2], modeLamp[4], presetLamp[6]` in that order + (`RP_L4/RPL4MPPR.h:312`), so indices 4 and 5 landed in + `presetLamp[0..1]` and the whole thing was self-consistent by memory + layout — the preset press handler read the same out-of-range slots. + Two real consequences: an out-of-bounds write, and it **destroyed the + four control-mode lamps** created immediately above (`:1921`), so + `NotifyOfControlModeChange` drove PRESET 1–4's lamps on the map flank + and the Basic/Standard/Veteran/Master lamps on the upper-right MFD were + never lit at all. `presetLamp[]` was meanwhile never populated or read. + The preset pass now fills `presetLamp[]` (`:1960`), which is what the + array was always for. +2. **Keyboard preset switching desynchronized the lamps.** The lamp work + lived in `SelectPresetMessageHandler`, so only the flank switches moved + the lamps and keyboard `1`–`6` left the wrong one lit. It now lives in + the virtual `NotifyOfPresetChange`, announced by `PresetEnable` itself + (`:733`) — one place, every path. `VTVRIOMapper` overrides it to move + the six flank lamps (`:1651`); the base mapper and the Thrustmaster + mapper have no preset lamps and inherit the no-op (`:746`). + + Verified by reading the commanded RIO lamp states out of the running + game (`PadRIO::lampState`, PadRIO + `TEST.EGG`, at rest in Basic mode). + Before and after are byte-identical except for lamp 0x33: + + | lamps | before | after | + |---|---|---| + | 0x18 PRESET 1 / 0x19–0x1D PRESET 2–6 | 3c / 14 | 3c / 14 | + | 0x30–0x32 MASTER/VETERAN/STANDARD | 14 | 14 | + | **0x33 BASIC** | **14 (dim — never lit)** | **3c (lit)** | + +Still open, by design rather than by accident: `PresetEnable` early-outs on +`preset_number == previousPresetNumber`, so re-pressing the lit switch is a +no-op. Correct for switching, but it also means a preset can never be used +as a "reset to authored bindings" after the player has edited it in +configuration mode.