Mouse buttons are bindable; mouse LOOK scoped (not built)

BUTTONS (shipped).  Win32 hands mouse buttons out as virtual keys, and the
binding path already stores VKs and polls GetAsyncKeyState -- so this needed no
verb, no parser change and no new machinery, just names:

    key MOUSE4 button 0x40        # side buttons
    key MOUSE5 button 0x46
    key MOUSEMIDDLE button 0x41

MOUSELEFT/MOUSERIGHT are named too, with a warning in the file header and the
docs: they are how the player presses cockpit buttons (left = press, right =
latch), and the poll is focus-guarded but not click-aware, so binding either
ALSO fires on every panel click.  The side and middle buttons are the free
ones.  Axes work as well (`key MOUSE4 axis Throttle slew + 0.7`).

One guard: the RGB lamp mirror skips mouse VKs when building its map -- a
keyboard lamp array has no mouse buttons to paint (BTPadBindingIsMouseKey).

Verified live: a bindings.txt carrying two mouse rows loads 76 keys (74 + 2),
and a real injected XBUTTON1 press produced `[emitter] FIRED #1`.  Clean A/B --
the earlier run whose SendInput failed the struct-size check logged no fire at
all.  Shipped commented-out examples in the default profile so the capability
is discoverable.

LOOK (scoped only).  docs/MOUSELOOK_PLAN.md.  Mouse MOVEMENT is a real feature,
not a table entry, and the hard part is not reading the device:

 - The cursor is already spoken for.  The glass cockpit's premise is that all
   72 buttons are mouse targets; mouse-look wants a captured, hidden, recentred
   cursor.  Both cannot be true, so this is a MODE question first -- four
   models compared, recommending hold-to-look (costs nothing when unbound,
   cannot strand a player, behaves the same in surround and exploded layouts).
 - The channels are POSITIONAL (JoystickX/Y are -1..1 deflections the mapper
   reads per control mode); a mouse gives deltas.  Accumulate-into-a-virtual-
   stick reuses every existing rule; a rate model would need new mapper
   semantics.
 - Control mode changes what it MEANS: BASIC steers with the stick, MID/ADV
   twist the torso -- so mouse-look steers in one and aims in the other.

Proposed grammar (new row type, so old builds skip it with a warning),
where the code goes, ~250 lines, and a verification plan whose load-bearing
item is "panel clicks still work when a mouselook binding exists but is not
engaged".  Also flagged: the pod had NO mouse, so nothing here is
reconstruction -- every choice is design, judged on feel, and it must stay out
of the pod build's path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-26 09:06:58 -05:00
co-authored by Claude Opus 5
parent 5f88a4eeb2
commit 9d536294e4
6 changed files with 192 additions and 1 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ precise than anything you can infer.
| Term / acronym definitions | `reference/glossary.yaml` |
| The ORIGINAL 1995 player manual (controls, per-mech stats, coolant loops) | `reference/manual/Tesla40_BT_manual.pdf` (+ alignment audit in `context/pod-hardware.md` §Manual) |
| The complete verbatim detail (fallback) | `docs/PROGRESS_LOG.md` (the old 2236-line CLAUDE.md) |
| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md`, `docs/RESPAWN_REARM_PLAN.md`, `docs/KD_SCOREBOARD_PLAN.md`, `docs/DIST_LAYOUT_PLAN.md` |
| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md`, `docs/RESPAWN_REARM_PLAN.md`, `docs/KD_SCOREBOARD_PLAN.md`, `docs/DIST_LAYOUT_PLAN.md`, `docs/MOUSELOOK_PLAN.md` |
---
+1
View File
@@ -743,6 +743,7 @@
<tr><th>Buttons</th><td class="addr">0x00 0x47</td></tr>
<tr><th>No function</th><td class="addr">0x16 0x17 0x19 0x1E 0x1F 0x380x3E <span style="color:var(--ink-quiet);font-family:var(--sans)">— the pod never wired these</span></td></tr>
<tr><th>Key names</th><td class="mono">AZ · 09 · F1F12 · NUMPAD09 · NUMPADDOT · UP DOWN LEFT RIGHT · SPACE · LSHIFT LCTRL LALT · MINUS EQUALS COMMA PERIOD LBRACKET RBRACKET BACKTICK</td></tr>
<tr><th>Mouse</th><td class="mono">MOUSE4 · MOUSE5 · MOUSEMIDDLE <span style="color:var(--ink-quiet);font-family:var(--sans)">— buttons only; left/right also press cockpit buttons</span></td></tr>
<tr><th>Pad</th><td class="mono">A B X Y · DPAD_UP/DOWN/LEFT/RIGHT · LB RB · BACK START · LS RS</td></tr>
<tr><th>Pad axes</th><td class="mono">LX LY RX RY LT RT</td></tr>
</tbody>
+10
View File
@@ -82,6 +82,16 @@ control in turn — stick, twist, throttle, fire buttons — writes the bindings
the solo menu to try them. It only rewrites its own section of `bindings.txt`, so hand edits
elsewhere in the file survive.
**Mouse buttons** are bindable like any key — `MOUSE4`, `MOUSE5` (the side buttons),
`MOUSEMIDDLE`, and `MOUSELEFT`/`MOUSERIGHT`. The side and middle buttons are the free ones; left
and right are how you press cockpit buttons, so binding those fires on every panel click too.
Mouse *movement* is not bindable — see `docs/MOUSELOOK_PLAN.md`.
```
key MOUSE4 button 0x40 # side button = main trigger
key MOUSE5 button 0x46 # side button = middle thumb
```
**RGB keyboards** with Windows Dynamic Lighting mirror the panel: any key you have bound to a
lamp button glows in the panel's own colours and flashes in step with the on-screen buttons. Turn
it off with `BT_KEYLIGHT=0` in `environ.ini`.
+130
View File
@@ -0,0 +1,130 @@
# MOUSELOOK_PLAN — scope: mouse movement as a cockpit axis
**Status: SCOPED, NOT IMPLEMENTED.** Written 2026-07-26 at the author's request, alongside the
mouse-BUTTON support that did ship (4.11.565). Read §2 before agreeing to build it — the hard
part is not reading the mouse.
---
## 1. What shipped, and what this is instead
**Shipped:** mouse *buttons*, as key names (`MOUSE4`, `MOUSE5`, `MOUSEMIDDLE`, `MOUSELEFT`,
`MOUSERIGHT`). Zero new machinery — Win32 hands mouse buttons out as virtual keys and the binding
path already stores VKs and polls `GetAsyncKeyState`, so five table entries were the whole change.
**This document:** mouse *movement* driving `JoystickX`/`JoystickY` (torso twist and aim) — the
thing a modern player means by "mouse look". That is a real feature, not a table entry, for three
reasons that have nothing to do with reading the device.
## 2. The three real problems
### 2.1 The cursor is already spoken for ⚠ (the blocking one)
The glass cockpit's whole premise is that **every one of the 72 pod buttons is a mouse target**
left-click presses, right-click latches, and under the exploded layout there are seven separate
windows to click in. Mouse-look wants the opposite: a captured, hidden, recentred cursor that
never touches a button.
These cannot both be true at once, so the feature is really a **mode question**, and the answer
decides the whole design:
| Model | Feel | Cost |
|---|---|---|
| **A. Hold-to-look** (`key MOUSE5 mouselook` or a modifier) | Cursor stays free; press to steer, release to click buttons | No mode confusion, discoverable, but you cannot look and hold a trigger comfortably |
| **B. Toggle** (a key flips captured/free) | Full mouse-look while engaged | Needs a visible on-screen indicator or players get stuck "unable to click anything" |
| **C. Region** (look while the pointer is over the 3D view, click when over a display) | No mode at all | Fails in the exploded layout (displays are separate windows) and near the surround's corner MFDs, which overlap the view by design |
| **D. Always captured**, panel clicks via a "release" key | Cleanest for a shooter | Throws away the cockpit's defining interaction; **not recommended** |
**Recommendation: A, with B as an option.** A costs nothing when unbound and cannot strand a
player. It is also the only one that behaves identically in the surround and exploded layouts.
### 2.2 The pod's channels are POSITIONAL; a mouse is not
`JoystickX`/`JoystickY` are absolute stick deflections in 1..1 that `MechControlsMapper`
interprets per control mode. A mouse produces *deltas*. Bridging that is a design choice, not a
conversion:
- **Accumulate** (recommended): integrate deltas into the channel, clamp to ±1 — a "virtual
stick". Reuses every existing rule (composition, the #36 release edge, the gait detent).
- **Rate**: treat the delta as a velocity of torso angle. Feels better for aiming, but the mapper
has no rate input — it would mean a new channel semantic and a new path through the mapper.
Bigger and less authentic.
With Accumulate, the stick does not spring back (a mouse has no centre). Options, all cheap:
a `spring <rate>` trailing option that decays toward 0, and/or a recentre key (0x42 torso centre
is already bound to the Up arrow).
### 2.3 Control mode changes what it means
BASIC steers with the stick; MID/ADV twist the torso with it and steer with the pedals
([[experience-levels]], [[pod-hardware]]). So mouse-look **steers the mech in BASIC and aims the
torso in MID/ADV** — authentic, but it will read as a bug to anyone who hasn't been told. The
docs must say so, and the feature is most useful in MID/ADV.
## 3. Proposed shape (if we build it)
New row type — the sanctioned way to extend the grammar (old builds skip unknown rows with a
logged warning, so a player's file stays forward-compatible):
```
mouseaxis <X|Y> axis <channel> [invert] [sensitivity <n>] [spring <rate>] [deadzone <f>]
key <KEY> mouselook # hold-to-look (model A)
```
Example default (shipped commented out):
```
#mouseaxis X axis JoystickX sensitivity 1.0 spring 0
#mouseaxis Y axis JoystickY sensitivity 1.0 spring 0 invert
#key MOUSE5 mouselook
```
### Where the code goes
| Piece | Where | Notes |
|---|---|---|
| Raw delta capture | `L4PADRIO` (new), fed from the window proc | **`WM_INPUT` (RawInput)**, not `WM_MOUSEMOVE`: relative motion, no cursor clamping at screen edges, no pointer acceleration. Register `HID_USAGE_GENERIC_MOUSE` with `RIDEV_INPUTSINK` off (foreground only). |
| Capture/hide | same | `SetCapture` + `ShowCursor(FALSE)` + recentre per frame while engaged; release on focus loss |
| Bindings | `L4PADBINDINGS` | `mouseAxisBindings[]` + the `mouselook` action, parallel to `padAxisBindings` |
| Channel write | `PadRIO::Poll` | Beside the pad/joy axis writes, with the SAME per-binding previous-value array so it inherits the #36 release edge and the #24 disconnect rule |
| Docs | `bindings.txt` header, `docs/CONTROLS.md`, `docs/CONTROLS.html` | including the control-mode caveat |
Estimated size: ~250 lines plus docs. No engine or reconstruction changes — this is entirely in
the glass desktop layer, like PadRIO itself.
## 4. Authenticity note
**The pod had no mouse.** There is nothing to reconstruct here and no binary behaviour to be
faithful to — this is a desktop-convenience layer, exactly like PadRIO and the glass cockpit
itself. That is fine (the layer exists precisely so developers and players can fly without a
cabinet), but it means every decision above is a *design* decision and should be judged on feel,
not fidelity. It also means it must stay out of the pod build's path: gate it the way PadRIO is
gated, so `BT_PLATFORM=pod` never sees it.
## 5. Verification plan
1. `mouseaxis` rows parse; unknown-option rows warn and skip (grammar-compatibility rule).
2. Hold-to-look engages/releases; cursor visible again on release AND on focus loss (the #24
disconnect rule, mouse edition).
3. Channel values track motion, clamp at ±1, and release cleanly to 0 (the #36 latch bug is
exactly the hazard here).
4. **Panel clicks still work** while a mouselook binding exists but is not engaged — in BOTH the
surround and exploded layouts. This is the regression that matters.
5. Control-mode sweep: BASIC steers, MID/ADV twists — confirmed against `[mppr]` trace.
6. Pod + dev profiles: no mouse path constructed at all.
## 6. Open questions
1. Model A or B (§2.1)? Everything else follows from it.
2. Should mouse-look imply a crosshair/reticle change, so the player can see it is engaged?
3. Does the exploded layout need its own answer, given the seven windows each have their own
client area and the main window may not have focus?
4. Is this wanted for the tester builds at all, or only for the operator/dev seat? It is not
pod-authentic, and the cockpit's mouse-click identity is a feature people have praised.
## Key Relationships
- Extends: [[glass-cockpit]] (PadRIO, the desktop input layer) · Constrained by: [[pod-hardware]]
(the RIO channel model), [[experience-levels]] (control modes)
- Prior art in-tree: `L4JOY` (DirectInput layer + the wizard) is the closest analogue for adding
a whole new input device cleanly.
+42
View File
@@ -61,8 +61,42 @@ static const NamedValue keyNames[] =
{ "BACKTICK", VK_OEM_3 }, { "LBRACKET", VK_OEM_4 },
{ "BACKSLASH", VK_OEM_5 }, { "RBRACKET", VK_OEM_6 },
{ "QUOTE", VK_OEM_7 },
//
// MOUSE BUTTONS (2026-07-26). Win32 hands these out as virtual keys, and
// the whole binding path already stores VKs and reads them with
// GetAsyncKeyState -- so they need no verb, no parser change and no new
// machinery, just a name. `key MOUSE4 button 0x40` works exactly like any
// other key row, axes included.
//
// ⚠ MOUSELEFT/MOUSERIGHT are how the player presses cockpit buttons (left
// = press, right = latch). The poll is focus-guarded but not click-aware,
// so binding either ALSO fires on every panel click. They are named here
// for completeness; the middle and side buttons are the free ones.
//
{ "MOUSELEFT", VK_LBUTTON }, { "MOUSERIGHT", VK_RBUTTON },
{ "MOUSEMIDDLE", VK_MBUTTON },
{ "MOUSE4", VK_XBUTTON1 }, { "MOUSE5", VK_XBUTTON2 },
{ "MOUSEX1", VK_XBUTTON1 }, { "MOUSEX2", VK_XBUTTON2 },
};
//
// A mouse button rather than a keyboard key? (Used where a binding only
// makes sense for a real key -- the RGB lamp mirror, which paints keycaps.)
//
static int
IsMouseVirtualKey(int virtualKey)
{
return (virtualKey == VK_LBUTTON || virtualKey == VK_RBUTTON ||
virtualKey == VK_MBUTTON || virtualKey == VK_XBUTTON1 ||
virtualKey == VK_XBUTTON2) ? 1 : 0;
}
int
BTPadBindingIsMouseKey(int virtualKey)
{
return IsMouseVirtualKey(virtualKey);
}
static const NamedValue padButtonNames[] =
{
{ "A", XINPUT_GAMEPAD_A },
@@ -202,6 +236,10 @@ static const char *defaultProfileText =
"# key <KEYNAME> axis <channel> set <value> jump/detent\n"
"# pad <PADBTN> button <addr>\n"
"# padaxis <PADAXIS> axis <channel> [invert] [slew <rate>]\n"
"# <KEYNAME> may be a MOUSE BUTTON: MOUSEMIDDLE, MOUSE4, MOUSE5 (the two\n"
"# side buttons; MOUSEX1/MOUSEX2 are aliases). MOUSELEFT and MOUSERIGHT\n"
"# also work but are how you press cockpit buttons -- bind them and they\n"
"# fire on every panel click too. Mouse MOVEMENT is not bindable.\n"
"# Channels: Throttle JoystickX JoystickY LeftPedal RightPedal Turn\n"
"# (Turn = signed composite: + drives the right pedal, - the left --\n"
"# made for mapping a gamepad stick to turning)\n"
@@ -240,6 +278,10 @@ static const char *defaultProfileText =
"key RALT button 0x3F\n"
"\n"
"# --- the four mappable fire buttons ------------------------------------\n"
"# The mouse's side buttons are free if you want them on the trigger:\n"
"# key MOUSE4 button 0x40\n"
"# key MOUSE5 button 0x46\n"
"# key MOUSEMIDDLE button 0x41\n"
"key SPACE button 0x40 # main trigger\n"
"key NUMPAD0 button 0x40 # main trigger\n"
"key NUMPAD1 button 0x45 # pinky\n"
+8
View File
@@ -230,6 +230,14 @@ PadRIO::PadRIO():
{
continue;
}
// A keyboard lamp array has no mouse buttons to paint.
{
extern int BTPadBindingIsMouseKey(int virtualKey);
if (BTPadBindingIsMouseKey(binding.virtualKey))
{
continue;
}
}
Logical duplicate = False;
for (int j = 0; j < count; ++j)
{