Render bridge (live_bridge.py, vrview_gl.py): - Hat glances render (left/right frame the canopy, hat-down = clean rear). Root bug was a stale _ckpt['fix'] key -> KeyError every glance frame -> render aborted (screen froze on hold, snapped back on release). The glance itself is the authentic eye-DCS action-0x1f reflush fp_cam already applies. - Torso twist turret-true (root-axis yaw, zero parallax); lasers follow torso. - Rear glance drops the canopy shell for a clean view (original-hardware behavior); mission-fade shroud 9fd hidden. - Wireframe debug mode (VRVIEW_WIREFRAME / 'w' key), scene-pass scoped. - Renderer output = 832x512, the dPL3 board's native framebuffer res. DOSBox-X fork: namedpipe serial backend (serialnamedpipe.cpp/.h) for vRIO/vPLASMA, replacing com0com; overlapped non-blocking I/O; typed frames (0x00 data / 0x01 DTR+RTS). Tracked copies + apply steps in vpx-device. Docs: COCKPIT-CAGE-NOTES (full glance/twist/rear forensics), XP-PORT-PLAN (back-burnered), RIO-NOTES (namedpipe + keypad), pipe/egg conf variants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
405 lines
22 KiB
Markdown
405 lines
22 KiB
Markdown
# RIO cockpit controls — passthrough tuning (Phase 5+)
|
||
|
||
The RIO (Remote Input/Output) is the cockpit control board on **COM1** (the
|
||
plasma display is COM2, handled later). DOSBox-X talks to a real RIO through
|
||
`serial1=directserial realport:COM1` (`game_rio.conf`). On this host the RIO is
|
||
a **Prolific USB-to-Serial adapter enumerated as COM1**.
|
||
|
||
## Named-pipe transport for vRIO/vPLASMA (2026-07-12, replaces com0com)
|
||
|
||
The fork now has a `namedpipe` serial backend so the VIRTUAL peripherals need
|
||
no com0com pair (no kernel driver, no signing pain, sub-ms latency):
|
||
|
||
```
|
||
serial1=namedpipe pipe:vrio rxpollus:100 rxburst:16 # vRIO
|
||
serial2=namedpipe pipe:vplasma # plasma readout
|
||
```
|
||
|
||
DOSBox is the pipe **client** (500ms background retry; unconnected pipe =
|
||
unplugged cable: modem-in lines low, TX discarded). vRIO/vPLASMA are the
|
||
**servers** on `\\.\pipe\vrio` / `\\.\pipe\vplasma`. One duplex byte-mode
|
||
pipe, typed frames both directions: `0x00 <len:u8> <bytes>` = serial data,
|
||
`0x01 <lines:u8>` = sender's own DTR(bit0)/RTS(bit1), receiver applies the
|
||
null-modem cross (their DTR -> our DSR+CD, their RTS -> our CTS); each side
|
||
sends one `0x01` on connect (vRIO sends 0x03 = board present); unknown type =
|
||
drop the connection. Contract pinned with the vRIO session 2026-07-12; source
|
||
of truth = the `serialnamedpipe.h` header comment (tracked copy in
|
||
`emulator/vpx-device/`, apply steps in its README). Smoke-tested end-to-end
|
||
(connect, line handshake, DTR/RTS edges on DOS COM open, data both ways,
|
||
clean disconnect). Real pods keep `directserial realport:COM1` — the real
|
||
board is untouched by this. vRIO side pending: NamedPipeLink + framer (other
|
||
session builds it; DTR edge feeds the existing HostHandshake event).
|
||
|
||
## The analog-poll latency problem (solved)
|
||
|
||
The initial RIO *check* request tolerates latency and passed easily. But the
|
||
runtime control loop ([L4CTRL.CPP:1145](../CODE/RP/MUNGA_L4/L4CTRL.CPP)) sends
|
||
an **analog request** every cycle and, if the reply doesn't return inside its
|
||
window, logs `LBE4ControlsManager::Execute, lost RIO analog request` and
|
||
re-requests. The board itself refuses/drops comms if the ACK is late by more
|
||
than a few milliseconds — a hard real-time deadline.
|
||
|
||
Empirical result (2026-07-03): a **slower** CPU made the RIO fail **sooner**.
|
||
So the dominant latency was how fast the game processes the RIO packet and
|
||
emits the ACK, not the serial wiring. The fix:
|
||
|
||
```
|
||
[cpu]
|
||
core=dynamic ; recompiler — many x faster than the 'normal' interpreter
|
||
cputype=pentium
|
||
cycles=max ; full host speed
|
||
```
|
||
|
||
With `core=dynamic + cycles=max` the RIO **stays in sync** (the user confirmed
|
||
"the rio behaved"). Notes:
|
||
- `cycles=fixed 20000` / `150000` and `core=normal` were all too slow — the RIO
|
||
dropped comms, faster at lower speeds.
|
||
- The DOSBox-X serial path is already low-latency on **transmit**
|
||
(`directserial.cpp` `transmitByte` calls `SERIAL_sendchar` immediately).
|
||
- Prolific PL2303 has no adjustable `LatencyTimer` registry value (unlike
|
||
FTDI); an FTDI adapter set to 1 ms latency would be the lowest-latency host
|
||
option if ever needed.
|
||
|
||
## Receive-latency fork options (2026-07-03)
|
||
|
||
Even with the fast CPU, intermittent timeouts remained and the game would
|
||
drop into its **15-second analog retry fallback** — the source shows
|
||
`limit = 15.0; // 0.2` in L4CTRL.CPP, so the shipped binary re-requests very
|
||
slowly once replies stop arriving ("the polling is really slow"). Two
|
||
emulator receive latencies were fixed with new `directserial` options:
|
||
|
||
- **`rxpollus:<us>`** (50–1000, stock 1000): host-port receive poll tick.
|
||
Stock DOSBox-X discovers inbound bytes on a 1 ms tick; 100 µs discovers
|
||
them ~10× sooner. First validated result: the sim advanced and the camera
|
||
moved in the render window.
|
||
- **`rxburst:<n>`** (1–64): stock DOSBox re-serializes each received byte at
|
||
emulated wire speed (~1 ms/byte at 9600) even though the bytes already
|
||
paid their wire time on the physical cable and sit in the host buffer — a
|
||
15-byte analog reply gained ~14 ms of artificial latency, single-handedly
|
||
blowing the RIO's few-ms ACK window. `rxburst:16` delivers buffered bytes
|
||
16× faster.
|
||
|
||
`game_rio.conf` uses `realport:COM1 rxpollus:100 rxburst:16`.
|
||
|
||
## The crash-to-desktop: PCSPAK's DISABLE_AND_DIE (patched)
|
||
|
||
The recurring hard fault (`Exception 0E`, write to `0xFFFFFFFF`-ish, e.g. at
|
||
`BTL4OPT CODE+0x7D1D1` with `EAX=3`) is **deliberate**: the RIO serial packet
|
||
driver (`CODE/RP/MUNGA_L4/PCSPAK.ASM`) was shipped built with
|
||
`DIE_ON_ERROR equ 1`, which compiles a `DISABLE_AND_DIE <code>` debugging
|
||
macro at 12 error sites — it retracts the UART IRQ, EOIs the PIC, and then
|
||
"crashes loudly" by writing the error code to address `0xFFFFFFFF`.
|
||
|
||
Our crash is error **3** (`PCSPAK.ASM:1630`): a byte ≥ 0x80 found in the TX
|
||
ring body (the protocol reserves high-bit bytes for commands). It gets hit
|
||
via the ACK/NAK-interrupt → restart path — exactly what physical RIO resets
|
||
and timeout storms exercise. On clean pod serial timing this never fired; on
|
||
a USB-serial rig with resets it does.
|
||
|
||
The source's release configuration (`DIE_ON_ERROR equ 0`) makes the macro
|
||
empty and the code **recovers** (the next instruction masks the byte with
|
||
`and al,7Fh` and continues). We reproduce that intended behavior by patching
|
||
all 12 die sequences (`50 52 BA FF FF FF FF B8 xx 00 00 00 89 02` → NOPs) in
|
||
the working image's `BTL4OPT.EXE`. Original preserved as `BTL4OPT.EXE.orig`.
|
||
Error-code map (from PCSPAK.ASM): 0/1/2 rx framing states, 3 tx body >0x7F,
|
||
4/5 tx state.
|
||
|
||
**Patch v2 (2026-07-03): the first patch was incomplete and wedged the
|
||
driver.** The macro's *prologue* also executes before the crash write: it
|
||
retracts UART `MCR` bits `IRQ+RTS` (`RETRACT_MCR`) and EOIs the master PIC —
|
||
23 bytes (`66 8B 15 <addr> 66 83 C2 04 EC 24 F5 EE 66 83 EA 04 B0 20 E6 20`)
|
||
that a release build would not compile at all. With only the crash write
|
||
NOPed, the first PCSPAK protocol error silently killed the UART IRQ and
|
||
dropped RTS: the game went deaf/mute while the RIO kept retransmitting into
|
||
the void (captured on the wire tap at t≈112s — board streaming
|
||
`89 00 09 FF FF FF FF FE` forever, game emitting one byte per 15s retry).
|
||
Fixed by NOPing the full 37-byte macro expansion at all 12 sites; the state
|
||
before this extension is preserved as `BTL4OPT.EXE.nop14`. After patch v2 the
|
||
driver recovers from every error, exactly like `DIE_ON_ERROR equ 0`.
|
||
|
||
## Serial wire tap (`RIO_TAP`, 2026-07-03)
|
||
|
||
The fork's `directserial.cpp` logs every serial byte when the host env
|
||
`RIO_TAP=<path>` is set: `<host-us relative> <emu-ms> T|R <hex>` plus `#`
|
||
lines for port config, RTS/DTR, and break changes. This is the instrument
|
||
that proved all of the above. Findings from tapped runs (9600 8N1):
|
||
|
||
- Analog request = `82 02 FF FF FF FF FE`; ACK = `FC`; packets are
|
||
`<cmd≥0x80> … FE`-framed. The shipped 15s retry shows up as exactly
|
||
15.02s between request storms.
|
||
- **Without `rxburst`**: reply bytes reach the game at ~1ms/byte (wire-speed
|
||
re-serialization), so the first long analog stream makes the game's ACK
|
||
~14ms late → board drops comms permanently (captured at t≈45.6s). With
|
||
`rxburst:16` the same phase streams for minutes. The earlier "rxburst
|
||
corrupts the boot handshake" belief was wrong — that corruption was the
|
||
unpatched DISABLE_AND_DIE error-3 crash. Both confs now use
|
||
`rxpollus:100 rxburst:16`.
|
||
- **Focus loss caused the self-recovering dropouts.** A 4-minute run while
|
||
the user multitasked showed ~25 self-recovering board-quiet windows
|
||
(1.5–15s); an identical hands-off run showed **zero** gaps after boot
|
||
(195s clean). DOSBox-X's default `[sdl] priority = higher,normal` demotes
|
||
the process to NORMAL class when unfocused. Both gauge confs now set
|
||
`priority=highest,highest` (HIGH_PRIORITY_CLASS; user saw one dropout at
|
||
`higher,higher`). (The ~71–86ms "turnaround tail" in the tap analysis was
|
||
benign — bursts that need no ACK.)
|
||
|
||
## Button-press livelock (2026-07-03, open)
|
||
|
||
At `highest,highest` the link ran clean for 120s, then the user pressed a
|
||
RIO button and the link died permanently (until board reset). Tap decode of
|
||
the packet protocol (PCSPAK.ASM equates: `FC`=ACK, `FD`=NAK, `FE`=RESTART,
|
||
`FF`=IDLE, cmd bytes 0x80-0xFB, packet = cmd + body + 7-bit checksum):
|
||
|
||
- Steady state = game `82 02` analog request → board `87 <12 analog> 07`
|
||
reply → game `FC` — at ~10Hz, clean for minutes.
|
||
- At t=123.65s the board truncated its in-flight analog reply exactly when
|
||
the button was pressed, then went ~3s silent, then began retransmitting
|
||
the button event `88 03 0B` every ~10ms forever. The game ACKs (`FC`)
|
||
every copy AND storms its own `82 02` retransmits (RESTART-paced:
|
||
packet + `FF`×4 idle + `FE` restart, back-to-back). Neither side ever
|
||
accepts: **livelock**.
|
||
- Mechanism (PCSPAK.ASM `txBodyState`/`txWaitState`, and the board firmware
|
||
is the same protocol): an ACK is honored only during the ~4ms
|
||
post-checksum idle window (`TXMAXIDLE=4` idle chars); an ACK arriving
|
||
during the peer's transmit phase = `TX_EARLY_ERR` → restart+retransmit.
|
||
With both sides' timing driven by each other's bytes, the phases lock and
|
||
the ACKs land in the wrong window forever. Retry budget `TXMAXRESET=3`/
|
||
`TXMAXERROR=3` per packet, but fresh packets keep the storm alive.
|
||
- Why the pod never saw this: ISA-UART ACK latency is sub-ms, so the ACK
|
||
always lands at the START of the 4ms window. Our Prolific USB-serial adds
|
||
1-10ms each way (invisible to the tap, which stamps host-side I/O), so a
|
||
collision can push the exchange into the locked phase.
|
||
|
||
Tried, in order:
|
||
- Prolific FIFO disabled in Device Manager: **no effect** — button mash
|
||
still livelocked the link at t≈110s.
|
||
- **TXMAXIDLE binary patch (v3, 2026-07-03): FIXED the livelock.** The two
|
||
`mov txIdleCount[esi],TXMAXIDLE` reloads (`C6 46 0B 04`, txIdleCount =
|
||
struct offset 0x0B) at exe offsets 0x7dff0/0x7e093 changed `04`→`20`
|
||
(4 → 32 idle chars ≈ 33ms). This widens the game's ACK-accept window 8x
|
||
and calms its retransmit storm so its own ACKs to the board transmit
|
||
promptly. Backup: `BTL4OPT.EXE.pre_idle`. Result of a 5-minute
|
||
button-mash run: three board-quiet gaps (1.9s/9.3s/14.6s), **all
|
||
self-recovered**; clean steady state otherwise. Previously one button
|
||
press = permanent livelock until manual board reset.
|
||
|
||
**Recovery-time patch (v4, 2026-07-04).** Collision recovery rode L4CTRL's
|
||
analog re-request fallback: `limit = 15.0; // 0.2` (L4CTRL.CPP:1132 — the
|
||
dev value was 0.2s; 15s looks like a forgotten debug slowdown). In the
|
||
binary this is two `mov dword [ebp-24h], 41700000h` (15.0f) immediates —
|
||
one per branch of the `RunningMission` if — at file offsets
|
||
0x763fa/0x76403, anchored by the `fld/fcomp` of delta_t and the
|
||
"lost RIO analog request" string push at 0x76441 directly after. Both
|
||
immediates changed to `3F000000h` (0.5f). Backup: `BTL4OPT.EXE.pre_limit`.
|
||
**Validated 2026-07-04**: 5-minute two-handed button-mash run — forced
|
||
dropouts now last 1.2s/2.9s (vs 9.3s/14.6s under the 15s limit), max
|
||
turnaround 199ms (vs 11.8s), and most collisions no longer register as
|
||
>1s gaps at all. User: "after one more button is pressed and the .5
|
||
seconds elapses it picks right back up."
|
||
|
||
BTL4OPT.EXE patch lineage: `.orig` (pristine) → `.nop14` (v1: crash writes
|
||
NOPed) → `.pre_idle` (v2: full 37-byte DISABLE_AND_DIE NOPs) →
|
||
`.pre_limit` (v3: TXMAXIDLE 4→32) → current (v4: retry limit 15s→0.5s).
|
||
|
||
## Board firmware patch plan (user wants to pursue)
|
||
|
||
Goal: fix the livelock's other half at the root — the board rejects ACKs
|
||
arriving outside its ~4ms post-checksum window (its firmware mirrors the
|
||
PCSPAK state machine, including the early-ACK=error behavior). The
|
||
game-side patches make failures self-healing; a board patch would make
|
||
collisions harmless entirely.
|
||
|
||
1. **Identify**: open the RIO board, photograph it, note the MCU part
|
||
number and the ROM chip. Era suggests an 8051-family MCU (80C31/32 with
|
||
external 27C256/27C512 EPROM) or 68HC11/Z80-class part. The observed
|
||
wire behavior (9600 8N1, ~10ms retransmit cadence = 3-byte packet +
|
||
~4 idle chars + restart) confirms a TXMAXIDLE≈4-equivalent constant in
|
||
firmware.
|
||
2. **Dump**: any TL866-class programmer reads the EPROM to a .bin. If the
|
||
code is in MCU-internal ROM instead, dumping gets harder (part-specific
|
||
tricks) — check the board first.
|
||
3. **Disassemble** (Claude's job): locate the protocol state machine by
|
||
searching for 0xFC/0xFD/0xFE/0xFF handling, the idle-counter reload
|
||
value 4, and checksum `AND 7Fh` operations — we know the protocol
|
||
byte-for-byte from the game side, so this is pattern matching.
|
||
4. **Patch** (preference order): (a) accept ACK in any TX state — delete
|
||
the early-ACK=error path; (b) widen the idle window 4→32 like the
|
||
game-side v3 patch; (c) raise the retry budget.
|
||
5. **Burn to a NEW EPROM**, socket it, label and store the original chip
|
||
untouched (preservation first).
|
||
6. **Validate** with the RIO_TAP button-mash protocol; compare dropout
|
||
counts against the 2026-07-03/04 baseline captures in the session
|
||
scratchpad (riotap_*.txt).
|
||
|
||
No firmware source or image exists in the archive (searched sda4 + CODE
|
||
for *.HEX/*.A51/*.S19/*.ROM and for the protocol constant names — only
|
||
PCSPAK.ASM, the game side, matches).
|
||
|
||
**UPDATE 2026-07-04: steps 1-3 DONE — ROOT CAUSE FOUND.** Full writeup:
|
||
`emulator/rio-firmware/RIOv4_2-ANALYSIS.md` (disassembly via
|
||
`disasm_6811.py` -> `RIOv4_2.disasm.asm`). The wedge = an orphaned
|
||
"reply-in-progress" latch `$2521`: it gates every analog request (`$D758
|
||
TST $2521; BNE drop`), is set when a reply is generated (`$D84C`), but the
|
||
retry-exhausted give-up path (`$D9DD JMP $DA2F`) tears down reply state
|
||
WITHOUT clearing it (and the success teardown `$DA00` clears it only
|
||
conditionally on `$2522`). Leaked -> all analog requests dropped -> mute;
|
||
RX/event path stays alive; only a game-start host reset command (`$C686`
|
||
clears `$2521`) revives it = the observed button-resync ritual. Proposed
|
||
fix (untested, no spare chip): clear `$2521` on every teardown (redirect
|
||
`$D9DD` to a free-space stub at `$DFF0` clearing `$2521/$2522`; make
|
||
`$DA00`'s clear unconditional). Byte patches + validation plan in the doc.
|
||
Original first-look (MCU=Toshiba TMP68HC11, EPROM=AM27C512, code
|
||
$C000-$FFFF, reset $C000, SCI vector $D630) in
|
||
`emulator/rio-firmware/README.md` -
|
||
reset vector $C000, **SCI serial interrupt vector → $D630** (the protocol
|
||
state machine's entry point for step 3's disassembly). Also new evidence
|
||
for the wedge shape: a button press revives a mute board (event path
|
||
alive, reply path dead), so step 4 should look for a reply-path-only dead
|
||
state reachable from the SCI handler.
|
||
|
||
## Crash-on-advance fixed: arena terrain shadows
|
||
|
||
With the RIO in sync the sim advances and the game crashed dereferencing the
|
||
**mech terrain shadow** renderable: `*_TSHD.BGF` live only in
|
||
`VIDEO/GEO/ARENA/` + `…/POLAR/`, which the object search path misses, so
|
||
`thr_tshd.bgf` failed to load and left a null renderable that the moving sim
|
||
eventually touched. Fix: the 11 `*_TSHD.BGF` files are copied from `ARENA/`
|
||
into `VIDEO/GEO/` in the working image (reversible, one file each). After
|
||
the fix the game runs sustained (500+ frames, no crash).
|
||
|
||
## Next wall: mission content (a game-data issue, not protocol/RIO)
|
||
|
||
With the RIO feeding real input, the simulation advances — and then the game
|
||
faults:
|
||
|
||
```
|
||
32loader runtime error: Unhandled exception
|
||
Exception 0E at 00FF:0040223B
|
||
Module 'BTL4OPT.EXE' section 'CODE' offset 0000123B
|
||
[..] 8B 4D 0C (mov ecx,[ebp+0C]) 8B 11 (mov edx,[ecx]) ECX=2
|
||
The instruction referenced illegal address 00000002
|
||
```
|
||
|
||
A near-null pointer (`2`) dereference: a function got `2` where an object
|
||
pointer was expected. This is downstream of the failed content load logged
|
||
just before it:
|
||
|
||
```
|
||
Entity -1:63 class:42 couldn't figure out how to MakeEntityRenderables
|
||
L4VIDEO.cpp couldn't load object thr_tshd.bgf
|
||
```
|
||
|
||
`thr_tshd.bgf` exists in `VIDEO/GEO/ARENA/` and `VIDEO/GEO/POLAR/`, but not the
|
||
top `VIDEO/GEO/`. The game builds its object search path from **`objectpath`
|
||
entries in the mission notation file** ([L4VIDEO.CPP:1852](../CODE/RP/MUNGA_L4/L4VIDEO.CPP)),
|
||
i.e. from the `.egg`. The shipped `test.egg` is only mission parameters and
|
||
does not set up the arena object path, so arena-specific objects aren't found;
|
||
the entity gets a broken renderable and the sim eventually dereferences it.
|
||
|
||
**To progress past this needs a real mission/arena `.egg`** (or an object path
|
||
manually pointed at the selected arena's `VIDEO/GEO/<ARENA>` subdir). That is
|
||
content/mission configuration, separate from the VPX protocol, the renderer,
|
||
and the RIO — all of which now work.
|
||
|
||
## Board init handshake — what a virtual RIO must implement (2026-07-06)
|
||
|
||
The user's **vRIO** (virtual RIO on COM1) passed analog polling but the game
|
||
still printed its init complaint. Root cause decoded from
|
||
[L4RIO.CPP:901-975](../CODE/RP/MUNGA_L4/L4RIO.CPP) +
|
||
[L4RIO.HPP:138-152](../CODE/RP/MUNGA_L4/L4RIO.HPP) and verified byte-for-byte
|
||
against a real-board tap (riotap_arena.txt, fw v4.2):
|
||
|
||
Command map (packet = cmd + body + 7-bit checksum `sum&0x7F`; FC=ACK FD=NAK
|
||
FE=RESTART FF=IDLE): game→board `80` CheckRequest, `81` VersionRequest,
|
||
`82` AnalogRequest, `83` ResetRequest, `84` LampRequest; board→game
|
||
`85` CheckReply(status,unit; status: 0=BoardOk 1=BoardMissing 2=BoardBad
|
||
3=LampBad 4/5/6=Restart/Abandon/FullBuffer counts), `86` VersionReply(maj,min),
|
||
`87` AnalogReply(10 data bytes), `88`/`89` Button press/release,
|
||
`8A`/`8B` Key press/release, `8C` TestModeChange(1=enter 0=exit).
|
||
|
||
Init sequence (game side bombs with "RIO never came back from check request /
|
||
test mode / version request!" if any step times out at 5s):
|
||
|
||
1. game pulses DTR (assert 0.1s, drop, wait 1s) = hardware board reset
|
||
2. game `80 00` → board `FC`
|
||
3. **board `8C 01 0D` (TestModeChange ENTER)** ← the packet vRIO was missing;
|
||
game waits ≤5s for it, sets TestModeActive
|
||
4. board self-test (~0.6s on the real board), streaming `85 <status> <unit>
|
||
<ck>` per subsystem (these are swallowed during test mode, not host events;
|
||
the bench board reports status=1 BoardMissing for units 08,10,11,18,19,1A,30)
|
||
5. **board `8C 00 0C` (TestModeChange EXIT)** — game waits ≤5s
|
||
6. game `81 01` → board `FC`, then **`86 04 02 0C` (VersionReply v4.2)**;
|
||
game loops until MajorRevision != 0xFF
|
||
7. normal ops: `82 02` analog polls / button+key events. NOTE: the game sends
|
||
requests ONLY when `operational && !TestModeActive` — a board that enters
|
||
test mode and never exits mutes the game permanently.
|
||
|
||
Every board packet is retransmitted until the game ACKs `FC` (within the
|
||
TXMAXIDLE window; ~33ms with the patched EXE). Byte pacing at the 9600-baud
|
||
wire rate (~1ms/byte) matters: vRIO blasting bytes back-to-back caused NAK/
|
||
restart churn during init until the user added pacing.
|
||
|
||
## RIO button unit map (L4CTRL.HPP enum; wire unit byte == buttonGroup index)
|
||
|
||
Press = `88 <unit> <ck>`, release = `89 <unit> <ck>` (ck = sum&0x7F). The game
|
||
uses the unit byte DIRECTLY as its button index (L4CTRL.CPP RIO::ButtonPressedEvent).
|
||
|
||
| unit | button |
|
||
|-----------|--------------------------------------------------------|
|
||
| 0x00-0x07 | AuxLowerRight 8..1 (panel) |
|
||
| 0x08-0x0F | AuxLowerLeft 8..1 (panel) |
|
||
| 0x10-0x15 | Secondary 1..6 |
|
||
| 0x18-0x1D | Secondary 7..12 |
|
||
| 0x20-0x27 | AuxUpperCenter 8..1 |
|
||
| 0x28-0x2F | AuxUpperLeft 8..1 |
|
||
| 0x30-0x37 | AuxUpperRight 8..1 |
|
||
| 0x39-0x3B | IcomHeadPluggedIn / IcomSensor / IcomMikePluggedIn |
|
||
| 0x3C | Door |
|
||
| 0x3D | Panic |
|
||
| 0x3F | Throttle1 |
|
||
| 0x40 | **JoystickTrigger** (= FIRE) |
|
||
| 0x41/0x42 | JoystickHatDown / HatUp |
|
||
| 0x43/0x44 | **JoystickHatRight / HatLeft** (= TORSO TWIST R/L) |
|
||
| 0x45 | JoystickPinky (= torso/look DOWN per TORSO.CTL) |
|
||
| 0x46 | JoystickThumbLow (= TORSO CENTER) |
|
||
| 0x47 | JoystickThumbHigh (= torso/look UP) |
|
||
|
||
CORRECTION (user, operated the real pods): in the PRODUCTION cockpit torso
|
||
twist is AXIS-driven; TORSO.CTL's button mappings (TorsoLeft=HatLeft etc.,
|
||
limits per TORSO.SUB: +-80deg horiz @20deg/s, +10/-30 vert @40deg/s) were the
|
||
DEV-rig fallback for setups without the full cockpit. On the real pod the HAT
|
||
gives momentary GLANCE views (left/right/rear). Keypad keys are a SEPARATE
|
||
event type (8A KeyPressed: unit,key). Verified live 2026-07-06: vRIO hat
|
||
glances DO work in-game (units 0x41-0x44 arriving), so the 88/89 button path
|
||
is validated end-to-end -- weapons-not-firing is NOT a unit-code problem;
|
||
suspect weapon-group arming/mission state (see MECHWEAP.CTL) instead.
|
||
|
||
## RIO keypad usage in the games (2026-07-11)
|
||
|
||
Keypad = the separate `8A`/`8B` key press/release events, body = (unit, key):
|
||
unit selects the keyboard -- 0 = KeyboardPilot (cockpit internal keypad),
|
||
1 = KeyboardExternal (operator keypad), 2 = KeyboardPC -- and the engine
|
||
converts key to ASCII (0-9 -> '0'-'9', >=10 -> 'A'...) before feeding
|
||
`keyboardGroup[unit]` plus a typed-string matcher
|
||
([L4CTRL.CPP:2329](../CODE/RP/MUNGA_L4/L4CTRL.CPP)).
|
||
|
||
- **RP: the keypad is dead plumbing.** Nothing subscribes to KeyboardPilot or
|
||
KeyboardExternal; the only string registration in the shipped source --
|
||
`stringManager.Add("A90", KeyboardPilot, ...)`, type A-9-0 to begin
|
||
calibration -- is COMMENTED OUT ([L4CTRL.CPP:850](../CODE/RP/MUNGA_L4/L4CTRL.CPP)).
|
||
- **BT: pilot keypad works ONLY in mission review.** Ground truth from the
|
||
shipped BTL4OPT binary (C:\VWE\BT411 reconstruction, btl4mppr.cpp):
|
||
`MechRIOMapper` subscribes KeyboardPilot -> Mech `KeypressMessageID` under
|
||
mode mask `0x200000`, and that mode bit is toggled by the Mech's
|
||
mission-review flag -- so keypad presses reach the game only while
|
||
reviewing (review navigation; the Mech-side key handler is not yet
|
||
recovered, so per-key meanings are still open). In normal cockpit missions
|
||
BOTH games ignore the keypad entirely.
|
||
- **KeyboardExternal (operator keypad) has no subscriber in either game.**
|
||
- Related panel-button findings from the same dig: BT's eight AuxUpperRight
|
||
buttons (`0x30-0x37`) are the **"hotbox" pilot-target-select group**, each
|
||
with a linked lamp, and the Panic button's (`0x3D`) lamp doubles as the
|
||
review-mode/config lamp under `0x200000`.
|
||
- **vRIO implication:** keypad emulation is not needed for normal missions;
|
||
it only matters if/when mission-review pods are emulated.
|