Merge glass-cockpit-refit: cockpit scaling + one button geometry, the keyboard button board, RGB keylight, crouch, the cwd guard
Cyd's 9-commit branch, reviewed before merge (clean merge-tree, zero overlap with the console/relay work that landed after his fork point; his L4VB16 refit preserves the #48 plane-audit tripwires, and his lamp-decode fix corrects the #47 flash rendering). Four review findings are fixed in the follow-up commit: the stale README controls table, the unguarded backtick view-toggle, the environ.ini one-shot loophole, and volume-key documentation (the -/= -> PgUp/ PgDn move itself landed pre-merge so this branch's Comm-bank -/= bindings are collision-free). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ dist/
|
||||
|
||||
# glass-cockpit dev layer: per-developer / per-machine runtime files
|
||||
content/bindings.txt
|
||||
content/environ.ini
|
||||
content/frontend.egg
|
||||
content/steam_appid.txt
|
||||
content/fe_last.ini
|
||||
|
||||
@@ -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` |
|
||||
| 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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -265,9 +265,19 @@ target_sources(munga_engine PRIVATE
|
||||
"engine/MUNGA_L4/L4PADBINDINGS.cpp"
|
||||
"engine/MUNGA_L4/L4JOY.cpp"
|
||||
"engine/MUNGA_L4/L4PADPANEL.cpp"
|
||||
"engine/MUNGA_L4/L4RIOBANK.cpp"
|
||||
"engine/MUNGA_L4/L4GLASSWIN.cpp"
|
||||
"engine/MUNGA_L4/L4PLASMAWIN.cpp"
|
||||
"engine/MUNGA_L4/L4KEYLIGHT.cpp"
|
||||
)
|
||||
# RGB keyboard lamp mirror (Windows Dynamic Lighting). C++/WinRT needs C++17
|
||||
# AND conformance mode, while the rest of the project builds C++14 /permissive
|
||||
# -- so this ONE file gets its own flags. Its interface (l4keylight.h) is
|
||||
# scalars only, so nothing else has to change dialect. (RP412 additionally
|
||||
# forced default struct packing here because its engine is /Zp1; BT411 sets no
|
||||
# /Zp, so that part does not apply -- see the file header.)
|
||||
set_source_files_properties("engine/MUNGA_L4/L4KEYLIGHT.cpp" PROPERTIES
|
||||
COMPILE_OPTIONS "/std:c++17;/permissive-")
|
||||
target_include_directories(munga_engine BEFORE PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/engine/shim"
|
||||
"${DXSDK}/Include")
|
||||
|
||||
+96
-52
@@ -43,61 +43,105 @@
|
||||
# hotkey channel (no double dispatch); unbound keys keep their authentic
|
||||
# 1995 meaning (see pckey above).
|
||||
#
|
||||
# ---- WASD CLASSIC (the shipped default) ----------------------------------
|
||||
# ---- THE KEYBOARD IS THE BUTTON BOARD ------------------------------------
|
||||
# Mirrors contentindings.txt exactly, so the glass (PadRIO) and desktop
|
||||
# (btinput) boots feel identical -- the letter and number rows are the MFD
|
||||
# button banks laid out where they sit on the panel, and flight lives on the
|
||||
# numpad. G and B are the physical gap between the lower clusters and stay
|
||||
# unbound. A bound key is REMOVED from the typed 1995 hotkey channel, so
|
||||
# this board gives those up by design (see pckey above to rebind any back).
|
||||
|
||||
# Driving: W/S sweep the throttle lever (sticks, detent at zero); A/D are
|
||||
# the turn pedals (spring back); Q/E twist the torso; R/F aim the torso
|
||||
# up/down (elevation); X = all stop (recenters twist AND pitch).
|
||||
key W axis Throttle rate 0.7
|
||||
key S axis Throttle rate -0.7
|
||||
key Up axis Throttle rate 0.7
|
||||
key Down axis Throttle rate -0.7
|
||||
key A axis LeftPedal deflect 1
|
||||
key D axis RightPedal deflect 1
|
||||
key Left axis LeftPedal deflect 1
|
||||
key Right axis RightPedal deflect 1
|
||||
key Q axis JoystickX deflect -1
|
||||
key E axis JoystickX deflect 1
|
||||
key R axis JoystickY deflect 1
|
||||
key F axis JoystickY deflect -1
|
||||
key X action AllStop
|
||||
|
||||
# Fire: the pod joystick's four buttons.
|
||||
key D1 button 0x40 # Main trigger (lasers group)
|
||||
key Space button 0x40
|
||||
key D2 button 0x46 # Middle thumb (PPCs group)
|
||||
key D3 button 0x47 # Upper thumb (missiles group)
|
||||
key Ctrl button 0x47
|
||||
key D4 button 0x45 # Pinky (4th group)
|
||||
key Alt button 0x3F # throttle-head = REVERSE THRUST
|
||||
|
||||
# --- numpad aim cluster (torso twist/aim -- mirrors bindings.txt so the
|
||||
# --- glass (PadRIO) and desktop (btinput) boots feel identical) ---
|
||||
key NumPad8 axis JoystickY deflect 1 # torso aim up
|
||||
key NumPad2 axis JoystickY deflect -1 # torso aim down
|
||||
key NumPad4 axis JoystickX deflect -1 # torso twist left
|
||||
key NumPad6 axis JoystickX deflect 1 # torso twist right
|
||||
key NumPad7 axis LeftPedal deflect 1 # turn left (alt)
|
||||
key NumPad9 axis RightPedal deflect 1 # turn right (alt)
|
||||
# Flight: the numpad. 8/2/4/6 stick, 7/9 pedals, 5 all-stop, 0 trigger;
|
||||
# Shift/Ctrl walk the throttle lever and it sticks; Alt is reverse thrust.
|
||||
key NumPad8 axis JoystickY deflect 1
|
||||
key NumPad2 axis JoystickY deflect -1
|
||||
key NumPad4 axis JoystickX deflect -1
|
||||
key NumPad6 axis JoystickX deflect 1
|
||||
key NumPad7 axis LeftPedal deflect 1
|
||||
key NumPad9 axis RightPedal deflect 1
|
||||
key NumPad5 action AllStop
|
||||
key Shift axis Throttle rate 0.7 # throttle up (alt)
|
||||
key Shift axis Throttle rate 0.7
|
||||
key Ctrl axis Throttle rate -0.7
|
||||
key Alt button 0x3F
|
||||
|
||||
# Systems.
|
||||
key G action ConfigHold # hold + fire keys = regroup the weapon
|
||||
key C action Valve # coolant valve cycle
|
||||
key H action Flush # coolant flush (HOLD -- dumps reservoir coolant)
|
||||
key M action ModeCycle # Basic -> Standard -> Veteran controls
|
||||
key N action DisplayCycle # secondary schematic: Damage -> Critical -> Heat
|
||||
key J action Mfd1Cycle # lower-left MFD page: Quad -> Eng detail pages
|
||||
key K action Mfd2Cycle # upper-center MFD page (engineering)
|
||||
key L action Mfd3Cycle # lower-right MFD page
|
||||
key V action ViewToggle # cockpit <-> chase camera (dev)
|
||||
key B action LookBehind # HOLD = the pod's rear-view button
|
||||
key F5 action Generator1
|
||||
key F6 action Generator2
|
||||
key F7 action Generator3
|
||||
key F8 action Generator4
|
||||
key F9 action Reconnect
|
||||
# The four mappable fire buttons.
|
||||
key Space button 0x40
|
||||
key NumPad0 button 0x40
|
||||
key NumPad1 button 0x45
|
||||
key NumPad3 button 0x46
|
||||
key Decimal button 0x47
|
||||
|
||||
# The hat, on the arrows. 0x42 is TORSO CENTER, not a look (streamed
|
||||
# mapping: elem 66 -> subsys 17 Torso attr 14; the other three are the
|
||||
# mapper's LookBehind/Right/Left).
|
||||
key Up button 0x42
|
||||
key Down button 0x41
|
||||
key Left button 0x44
|
||||
key Right button 0x43
|
||||
|
||||
# UPPER MFD banks: number row on top, QWERTY beneath.
|
||||
# Heat / coolant (1-4, QWER = Condensers 1-6, flush, balance).
|
||||
key D1 button 0x2F
|
||||
key D2 button 0x2E
|
||||
key D3 button 0x2D
|
||||
key D4 button 0x2C
|
||||
key Q button 0x2B
|
||||
key W button 0x2A
|
||||
key E button 0x29
|
||||
key R button 0x28
|
||||
# Engineering / Mfd2.
|
||||
key D5 button 0x27
|
||||
key D6 button 0x26
|
||||
key D7 button 0x25
|
||||
key D8 button 0x24
|
||||
key T button 0x23
|
||||
key Y button 0x22
|
||||
key U button 0x21
|
||||
key I button 0x20
|
||||
# Comm / target hotbox.
|
||||
key D9 button 0x37
|
||||
key D0 button 0x36
|
||||
key OemMinus button 0x35
|
||||
key Oemplus button 0x34
|
||||
key O button 0x33
|
||||
key P button 0x32
|
||||
key OemOpenBrackets button 0x31
|
||||
key OemCloseBrackets button 0x30
|
||||
|
||||
# LOWER MFD banks: home row on top, the row below beneath.
|
||||
key A button 0x0F
|
||||
key S button 0x0E
|
||||
key D button 0x0D
|
||||
key F button 0x0C
|
||||
key Z button 0x0B
|
||||
key X button 0x0A
|
||||
key C button 0x09
|
||||
key V button 0x08
|
||||
key H button 0x07
|
||||
key J button 0x06
|
||||
key K button 0x05
|
||||
key L button 0x04
|
||||
key N button 0x03
|
||||
key M button 0x02
|
||||
key Oemcomma button 0x01
|
||||
key OemPeriod button 0x00
|
||||
|
||||
# The two columns flanking the map.
|
||||
key F1 button 0x10
|
||||
key F2 button 0x11
|
||||
key F3 button 0x12
|
||||
key F4 button 0x13
|
||||
key F5 button 0x14
|
||||
key F6 button 0x15
|
||||
key F7 button 0x18
|
||||
key F8 button 0x19
|
||||
key F9 button 0x1A
|
||||
key F10 button 0x1B
|
||||
key F11 button 0x1C
|
||||
key F12 button 0x1D
|
||||
|
||||
# View toggle keeps its own key (V is a board button now).
|
||||
key Oemtilde action ViewToggle
|
||||
|
||||
# ---- Xbox controller -----------------------------------------------------
|
||||
# Left stick steers (the pedals), right stick twists the torso and pushes
|
||||
|
||||
@@ -39,6 +39,26 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
|
||||
- **cwd MUST be `content\`** — the engine resolves `BTL4.RES`, `VIDEO\`, `BTDPL.INI`, eggs relative
|
||||
to cwd (the `loadTables` gotcha: `L4VIDEO.cpp:849` `fopen("VIDEO\\REPLACEMATS.tbl")` is relative +
|
||||
unchecked → fread on NULL if cwd is wrong). Logs to `btl4.log` in `content\` (or `BT_LOG=<file>`). [T2]
|
||||
**Since 2026-07-26 the exe enforces this itself** (`BTEnsureContentDirectory`, btl4main.cpp): if
|
||||
cwd has no `BTL4.RES` it probes from the exe's own directory (`..\..\content` for the shipped
|
||||
layout, plus flattened / in-content cases) and `SetCurrentDirectory`s there, before the log file
|
||||
opens. The boot line says so when it had to look. ⚠ **The landmine it defuses** (field report):
|
||||
a bare `btl4.exe` launch from `build\Release` found no resources ("Resource file btl4.res
|
||||
v1.0.0.0 is obsolete!"), wrote a stray `bindings.txt`/`environ.ini`/`btl4.log` NEXT TO THE EXE —
|
||||
so the player's real ones in `content\` looked like they were never created — and killed the
|
||||
mission generation the menu launched (the child inherits the parent's cwd). Invisible for years
|
||||
because every launcher `cd`s to `content\` first; it became reachable once glass became the
|
||||
desktop default and a zero-arg launch started opening the menu.
|
||||
- **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits
|
||||
next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412
|
||||
keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's
|
||||
two-folder split (`build\Release\btl4.exe` + `content\`) is not a design decision: `mkdist.py`
|
||||
zips TRACKED REPO PATHS verbatim, so the dist inherited the repo's developer layout. In the
|
||||
authentic single-folder shape cwd is right by construction and a double-click just works, which
|
||||
is why the other games never hit this. **A proposal to ship the authentic shape is written up
|
||||
in `docs/DIST_LAYOUT_PLAN.md` — PROPOSED, not implemented, awaiting a decision between the
|
||||
authors.** The cwd guard stays either way (it protects the developer tree, where the split is
|
||||
real and permanent).
|
||||
- Interactive: **WASD** drive; weapon groups (keyboard, task #43) **1/Space** = lasers, **2** =
|
||||
PPCs, **3/Ctrl** = missiles; **X** all-stop; **V** cockpit/chase view. Default egg = `DEV.EGG`
|
||||
(map=grass, time=day). Swap mech via the egg's `vehicle=` — **ALL 18 ModelList names
|
||||
|
||||
+12
-7
@@ -92,17 +92,22 @@ viewport), and `gWindowAspect` = the view rect's on-screen aspect (`BTWorldAspec
|
||||
sizing (`btl4main.cpp`), the world viewport, the panel/button draw, the aspect, and the mouse
|
||||
hit-test. Constants: `SCALE=0.5, OVL=44 (corner overlap), LAMP=16 (protruding lamp edge),
|
||||
REDCELL=64 (hidden hit depth), RAILW=26`; `canvas = view + (552, 548)`.
|
||||
- **Buttons** = the L4GLASSWIN geometry ×0.5 with the same address banks (Heat 0x2F, Mfd2 0x27,
|
||||
Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar rails 0x10-0x15/0x18-0x1D + bottom
|
||||
- **Buttons** = `L4RIOBANK` (2026-07-26: the ONE geometry, shared with the exploded windows — see
|
||||
[[glass-cockpit]] §ONE button-bank geometry) at half scale, same address banks (Heat 0x2F,
|
||||
Mfd2 0x27, Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar columns 0x10-0x15/0x18-0x1D + foot
|
||||
{0x16,0x17,0x1F,0x1E} yellow; flight 0x38-0x3F/0x40-0x47 blue, labeled). Full rect = hit target;
|
||||
the surface draws OVER it so only the lamp edge shows (the PaintGlass painter trick). Mouse:
|
||||
the surface draws OVER it so only the lamp strip shows (the PaintGlass painter trick). ⚠ Before
|
||||
that date the surround's MFD lamps sat ENTIRELY outside the glass on a 24px band — a 76×24
|
||||
target where the exploded window gave 156×138; both now reach half the glass. Mouse:
|
||||
main WndProc `WM_L/RBUTTON` → `BTCockpitMouseDown/Up` (client→bb map, glass press/release/right-
|
||||
latch contract) → `PadRIO::SetScreenButton` (`#ifdef BT_GLASS`; dim/no-op in pod builds). Lamp
|
||||
brightness = `BTLampBrightnessOf` (shared inline in l4vb16.h) over `PadRIO::GetLampState`.
|
||||
- **Env / precedence** (resolved once in btl4main → `gBTGaugeCockpit`): `BT_GLASS_PANELS=1`
|
||||
(Cyd's per-display windows) stands cockpit down > explicit `BT_COCKPIT=1` > `BT_DEV_GAUGES_WINDOW`
|
||||
(separate window) / `BT_DEV_GAUGES_DOCK` (legacy inset) opt-out > **cockpit default**.
|
||||
`BT_COCKPIT=0` forces the dock-bottom strip. `-res W H` = the WORLD VIEW size (canvas clamped to
|
||||
- **Env / precedence** (resolved ONCE in btl4main → `glassLayout` → `gBTGaugeCockpit`; the full
|
||||
table lives in [[glass-cockpit]] §Layout modes): `BT_GLASS_PANELS`≠0 (the per-display windows)
|
||||
stands cockpit down > `BT_DEV_GAUGES_WINDOW` (separate window) > `BT_DEV_GAUGES_DOCK` (legacy
|
||||
inset) > `BT_COCKPIT=0` (also the dock strip) > **cockpit surround default**. ⚠ Before
|
||||
2026-07-26 `BT_COCKPIT=0` actually landed on the per-display windows — the dock strip was
|
||||
unreachable under glass. `-res W H` = the WORLD VIEW size (canvas clamped to
|
||||
the work area). Green tint tunable via `BT_COCKPIT_TINT=RRGGBB` (default `0x27E8`). Labels are a
|
||||
lazy GDI-baked MANAGED atlas (survives device reset). Renders in ALL builds; only the PadRIO
|
||||
click/lamp seam is BT_GLASS-gated. Full detail: `docs/GAUGE_COMPOSITE.md`.
|
||||
|
||||
+258
-4
@@ -63,6 +63,258 @@ version skew (a stale steam exe shipped 2 days behind), tripled build time, and
|
||||
([[reconstruction-gotchas]]); everything links always, so this class of trap no longer varies
|
||||
by build flavor.
|
||||
|
||||
## Layout modes — ONE resolver (2026-07-26) [T2 four-mode boot-verified]
|
||||
|
||||
Where the five MFDs + radar go is a SEPARATE axis from the platform profile. Under a glass boot
|
||||
(`BT_DEV_GAUGES` set) `btl4main.cpp` resolves it **once**, right after the profile putenvs land,
|
||||
into `glassLayout`; the boot banner, the pad-panel decision and the window-sizing block all read
|
||||
that one answer. Precedence, highest first:
|
||||
|
||||
| Env | Layout | Buttons live in |
|
||||
|---|---|---|
|
||||
| `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window |
|
||||
| `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel |
|
||||
| `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel |
|
||||
| `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel |
|
||||
| (nothing) | **COCKPIT SURROUND — the glass default** | the main window (D3D-drawn lamps) |
|
||||
|
||||
The banner now names the live one: `[boot] platform profile: GLASS (PadRIO + plasma window)
|
||||
[secondary displays: cockpit surround]`.
|
||||
|
||||
⚠ **Two bugs this replaced** (both were live until 2026-07-26): the mode was decided in TWO places
|
||||
with duplicated precedence (the glass profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`; the
|
||||
sizing block re-derived cockpit-vs-dock), and the banner read NEITHER — it announced "per-display
|
||||
cockpit windows [BT_GLASS_PANELS]" for every glass boot, surround included. Worse, the profile
|
||||
block turned `BT_COCKPIT=0` into `BT_GLASS_PANELS=1`, so the docked strip it is documented to
|
||||
select was **unreachable under the glass profile**. `BT_GLASS_PANELS` is now explicit-only, and
|
||||
dock/window modes auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
|
||||
|
||||
## ONE button-bank geometry — L4RIOBANK (2026-07-26) [T2 click-verified both modes]
|
||||
|
||||
The RIO button field around a pod display is drawn TWICE — composited into the main window
|
||||
(surround, `L4VB16.cpp`) and one window per display (exploded, `L4GLASSWIN.cpp`) — and each
|
||||
renderer used to carry its own copy of the geometry. They drifted badly: the same MFD button was
|
||||
a 156×138 under-glass target in the exploded window and a **76×24 sliver entirely outside the
|
||||
glass** in the surround. `engine/MUNGA_L4/L4RIOBANK.cpp` is now that geometry, once; both
|
||||
renderers are consumers, and placement (WHERE each display goes) stays with each renderer because
|
||||
that genuinely differs.
|
||||
|
||||
**The under-glass rule** (ported from RP412 `L4MFDVIEW`, which took it from the pod): a button
|
||||
reaches **half the glass** in BEHIND the display and only a **lamp strip** clears the edge. The
|
||||
renderer paints buttons first, imagery over them — so the lamp reads as a slim bar, practically
|
||||
the whole display is the press target, and nothing is occluded (the picture wins every pixel it
|
||||
covers). The strip scales off the display's SHORT axis against the native 480 (an MFD is 640×480
|
||||
landscape, the map 480×640 portrait — both short-axis 480) with a 6px floor, so a half-scale
|
||||
surround lamp is still a bar and not a smudge. The MFD per-column nudge against the unevenly
|
||||
spaced DISPLAY/PROGRAM legends (`mfdColumnDX`) moved here too, so both renderers inherit the
|
||||
same alignment.
|
||||
|
||||
⚠ **The map's foot row is pushed FIRST**, deliberately: the side columns now reach half the map
|
||||
each, so they cover the foot band too, and the hit test takes the first match — foot-first is
|
||||
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
|
||||
first-hit reachability, not mere presence.
|
||||
|
||||
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
|
||||
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
|
||||
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
|
||||
button's centre. Both modes: **72/72 addresses placed, zero shadowed, zero duplicated, 72/72
|
||||
dispatched** (`[cockpit] CLICK` / `[glasswin] CLICK`), process survived 144 posted messages per
|
||||
mode. Pod + dev profiles boot and simulate un-regressed. Surround BT_SHOT confirms the legends
|
||||
are clear and the lamps read as edge strips.
|
||||
|
||||
## The cockpit SCALES — uniform-fit letterbox + `-fit` (2026-07-26) [T2 live]
|
||||
|
||||
The canvas is a fixed size and D3D9 stretched it into whatever the client area was, so a window
|
||||
dragged to a different shape **squashed the instruments** — the projection was aspect-corrected
|
||||
(task #20) but the panels, lamps and MFD glass were not. Now the canvas is fitted at ONE uniform
|
||||
scale, centred, leftover black: the cockpit scales both ways and never distorts, whatever the
|
||||
window.
|
||||
|
||||
- `BTCockpitFitRect(clientW, clientH, RECT*)` — integer math throughout, so the rect is stable
|
||||
frame to frame (a float scale wobbles a pixel and the bars shimmer during a drag).
|
||||
- D3D9 does the scaling at Present via a **destination rect**, which `D3DSWAPEFFECT_DISCARD`
|
||||
forbids — so the WINDOWED swap effect becomes **`D3DSWAPEFFECT_COPY`** when the surround is up
|
||||
and multisampling is off (COPY cannot multisample). `gBTCockpitLetterbox` records whether that
|
||||
happened; when 0 the old full-client stretch stands, so an MSAA run still works.
|
||||
- Both present sites take the rect (scene + the wait-screen overlay, which paints into the same
|
||||
backbuffer). `WM_SIZE` invalidates so the bars repaint black — a COPY present only writes its
|
||||
dest rect, so stale bar content would otherwise persist.
|
||||
- **The click transform had to follow.** `BTCockpitMouseDown` mapped client→canvas against the
|
||||
full client; under the letterbox that drifts the hit test off every button by the bar width.
|
||||
It now runs the identical fit (and a click on a bar hits nothing).
|
||||
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
|
||||
enters into it.
|
||||
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
|
||||
letterboxed inside. Verified on a 3440×1440 ultrawide.
|
||||
|
||||
⚠ **Ordering trap found live:** the letterbox flag was first set at device creation, but the
|
||||
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
|
||||
stretch formula) and applied it on frame 1, because nothing resizes the window again. The INTENT
|
||||
is now decided in `btl4main` alongside `gBTGaugeCockpit`; `L4VIDEO` only confirms or withdraws it.
|
||||
|
||||
⚠ **Capture note:** `PrintWindow` returns an all-black frame for the borderless `-fit` window
|
||||
(it works for the normal chrome'd one). Use a screen-region grab (`scratchpad/shot.ps1`) for
|
||||
`-fit`, and don't read a black PrintWindow there as a render failure — it isn't.
|
||||
|
||||
Verified: wide (1684×661) and tall (744×961) drags both letterbox with zero distortion; **72/72
|
||||
buttons still dispatch through the transform at 0.62× scale with a 389px bar**; exploded / dock /
|
||||
pod / dev all keep DISCARD and boot un-regressed.
|
||||
|
||||
## Player-tunable displays — scale, map placement, environ.ini (2026-07-26) [T2 live]
|
||||
|
||||
The pod bolted its instruments down at one size; a desktop panel has room to trade viewscreen for
|
||||
instrument, so the player scales them. All resolved ONCE (`BTCkResolvedSizes`, L4VB16.cpp) because
|
||||
the surround recomputes its layout every frame.
|
||||
|
||||
| Env | Effect |
|
||||
|---|---|
|
||||
| `BT_MFD_SCALE` | all five MFDs, % of pod size (25-200, default 100) |
|
||||
| `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` | one display, overriding the group |
|
||||
| `BT_RADAR_SCALE` | the portrait map |
|
||||
| `BT_RADAR_POS` | `CENTER` (default) / `LEFT` / `RIGHT` / `MIDLEFT` / `MIDRIGHT` (+ `BOTTOM*`, `*CENTER` spellings) |
|
||||
|
||||
**The bands derive from the resolved sizes** — that is why the sizes could not stay constants: the
|
||||
surround band a display hangs in has to grow with it or the canvas clips it. Each band is what its
|
||||
LARGEST occupant needs (a corner MFD only claims `size - kCkOVL`, since it overlaps the view), and
|
||||
the bottom band drops to just the lower MFDs when the map leaves that row for a side. Verified:
|
||||
100% reproduces the historical `L276 R276 T223 B336` exactly; 150% gives `L436 R436 T343 B448`;
|
||||
80% + MIDLEFT gives `L256 R212 T175 B148`.
|
||||
|
||||
⚠ **A corner map goes flush to the CANVAS edge**, not the view edge, and the lower MFD whose corner
|
||||
it took sits beside it. Measuring off the view edge (first cut) overlapped them by the band width —
|
||||
232px of map drawn over the Right Weapons MFD.
|
||||
|
||||
**Map legend grid — MEASURED, not inherited [T2].** The side columns must line up with the six
|
||||
cells the map paints beside them (⊕MAP / ⊖MAP / IR / crouch / searchlight / display-mode =
|
||||
0x10-0x15 — the imagery independently confirms our address map). `scratchpad/measurelegend.py` over
|
||||
a native 480×640 capture gives **top 3, cell 102, pitch 107** on the 640 span. RP412's map is
|
||||
`13 + 6×102 @ pitch 105` — same cell height, different top and pitch, so **its numbers do not
|
||||
transfer**; ours are ours. Our old even division (`displayH/6 + 1 = 107`) had the pitch right by
|
||||
luck and sat 3px high of the labels. Top and bottom are now scaled separately and subtracted, so
|
||||
rounding cannot drift the buttons out of step with the labels down the column.
|
||||
|
||||
**`environ.ini` (the settings file) [T2].** ⚠ It was read ~300 lines into `WinMain`, AFTER the
|
||||
platform-profile block had run its `getenv`s — so every setting the profile reads (`BT_PLATFORM`,
|
||||
`BT_COCKPIT`, `BT_GLASS_PANELS`, `BT_DEV_GAUGES`, `L4CONTROLS`…) was **silently ignored from the
|
||||
file** and only worked as a real env var. It also `putenv`'d comment lines verbatim, which would
|
||||
have turned a shipped commented-out option into a variable literally named `#BT_MFD_SCALE`. Now:
|
||||
loaded immediately after the first-breath boot line, comments and blanks skipped, and **the real
|
||||
environment WINS** so a launcher `.bat` overrides the file rather than fighting it. A documented
|
||||
default is written on first run when absent — the `bindings.txt` convention: untracked, so an
|
||||
extract-over-top upgrade never clobbers a player's settings, and every option ships commented out
|
||||
so a fresh install behaves exactly as before. Verified all three: first run writes it and applies
|
||||
0 settings; uncommented file settings drive the layout (130% group + 60% UC + MIDRIGHT); a real
|
||||
`BT_MFD_SCALE=90` overrides the file's 130 while the file's other settings still apply.
|
||||
|
||||
Verified: **72/72 placed, 0 shadowed, 72/72 dispatched** at baseline, at 150%/135%, and at
|
||||
75% + BOTTOMRIGHT; exploded / dock / pod / dev boot un-regressed.
|
||||
|
||||
## Dead-button backlog CLOSED — DuckRequest was the last (2026-07-26) [T2 live]
|
||||
|
||||
The 2026-07-20 audit's "8 buttons dispatch a streamed message with no reconstructed handler" is
|
||||
finished. ⚠ Only ONE of the eight was actually still missing when this pass started — generator
|
||||
on/off, ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed between
|
||||
07-20 and 07-25 while `pod-hardware.md` / `open-questions.md` still called them dead
|
||||
(`docs/INPUT_PATH_AUDIT.md` had already flagged the census as "stale in both directions").
|
||||
**Check the code, not the census.**
|
||||
|
||||
**`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 — the manual's CROUCH button).** The binary's
|
||||
entire body is two lines: press-only (`msg+0xc > 0`), then `duckState`(`mech+0x398`) = 1. It is a
|
||||
one-shot REQUEST flag, not a posture toggle — the handler never clears it and the only other
|
||||
writer in the whole binary is the mech reset (part_012.c:9439, the same reset that zeroes
|
||||
`incomingLock`). Registered in `Mech::MessageHandlerEntries`; `[duck]` log line on press.
|
||||
|
||||
**Its consumer is a DATABINDING, not code [T1] — which is why the flag looked pointless.**
|
||||
`duckState` has ZERO readers anywhere in the decomp, because it is published as attribute 0x37 and
|
||||
consumed through the gauge system: `content/GAUGE/L4GAUGE.CFG` runs a 3-frame `bduck.pcc`
|
||||
`oneOfSeveralPixInt` widget bound to `DuckState` on the map's legend column ("crouch mode: button
|
||||
4"). Verified live: pressing 0x13 turns that legend icon grey→orange. **Do not invent a crouch
|
||||
pose to "finish" this** — the flag plus the indicator IS the code side.
|
||||
|
||||
Bonus confirmation: those legend widgets sit at gauge offsets 537/430/322/215/108 — a **107
|
||||
pitch**, independently corroborating the map legend grid measured from pixels in §Player-tunable
|
||||
displays.
|
||||
|
||||
## THE KEYBOARD IS THE BUTTON BOARD — new default keymap (2026-07-26) [T2]
|
||||
|
||||
Ported from RP412's bindings design (user decision: make it the DEFAULT, not an alternate). The
|
||||
letter and number rows are the MFD button banks **laid out where they sit on the panel**, and
|
||||
flight moves to the NUMPAD so the board stays free:
|
||||
|
||||
| Keys | Bank |
|
||||
|---|---|
|
||||
| `1 2 3 4` / `Q W E R` | Heat / coolant 0x2F-0x2C / 0x2B-0x28 |
|
||||
| `5 6 7 8` / `T Y U I` | Engineering (Mfd2) 0x27-0x24 / 0x23-0x20 |
|
||||
| `9 0 - =` / `O P [ ]` | Comm / target hotbox 0x37-0x34 / 0x33-0x30 |
|
||||
| `A S D F` / `Z X C V` | Left Weapons (Mfd1) 0x0F-0x0C / 0x0B-0x08 |
|
||||
| `H J K L` / `N M , .` | Right Weapons (Mfd3) 0x07-0x04 / 0x03-0x00 |
|
||||
| `F1`-`F6` / `F7`-`F12` | the map's two columns 0x10-0x15 / 0x18-0x1D |
|
||||
| numpad | 8/2/4/6 stick, 7/9 pedals, 5 all-stop, 0/1/3/. the four fire buttons |
|
||||
| `Shift`/`Ctrl` · `Alt` · arrows · `` ` `` | throttle lever · reverse · look/torso-centre · view |
|
||||
|
||||
`G` and `B` stay unbound — the physical gap between the lower clusters. **Coverage: 61 of 72
|
||||
addresses on the keyboard, and the 11 absent ones are exactly those with no authored function**
|
||||
(0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 remain clickable.
|
||||
|
||||
It lands on BT's addresses better than it deserves to: `1-4`+`QWER` are the ENTIRE coolant system
|
||||
(Condensers 1-6, flush, balance), `F6`/`F7` the display/control-mode cycles, `F9`-`F12` Generators
|
||||
A-D, `F4` the crouch button.
|
||||
|
||||
⚠ **What it costs, deliberately.** A bound key is removed from the authentic 1995 typed-hotkey
|
||||
channel to stop double-dispatch, and this board binds nearly everything — so `5` (Quad page), `z`
|
||||
(Eng1), `t/y/u/i/o` (pilot select) and `+`/`-` (target zoom) are given up. Unbind a key to get its
|
||||
1995 meaning back.
|
||||
|
||||
⚠ **New rule: a bindings row WINS over a built-in convenience key** (`PadRIO::KeyHasBinding`).
|
||||
`V` and `J/K/L` are board buttons now, so the view-toggle and preset-cycle polls stand down for
|
||||
them and the view toggle lives on `` ` `` alone. Without this they would have fired BOTH.
|
||||
|
||||
`content/CONTROLS.MAP` was rewritten to mirror the board (90 bindings, 0 parse complaints) so the
|
||||
glass/pod/dev boots still feel identical — the 2026-07-21 settlement. An existing `bindings.txt`
|
||||
is never overwritten, so a player only meets this board by deleting theirs.
|
||||
|
||||
## Hat labels corrected — 0x42 is TORSO CENTER [T1, streamed mapping]
|
||||
|
||||
`docs/INPUT_PATH_AUDIT.md` flagged the 0x41-0x44 labels as wrong and it was right. Settled from
|
||||
`BT_CTRLMAP_LOG`: **0x42 → subsys 17 (Torso), attrID 14** — not a look at all; the other three go
|
||||
to the mapper's look trio (0x44 → attrID 10 LookLeft, 0x43 → 11 LookRight, 0x41 → 12 LookBehind).
|
||||
The shipped `.RES` has no "TORSO CENTER" string (its names are LookBehind/Down/Forward/Left/Right),
|
||||
so the audit's phrasing was loose but its substance correct. Swept the three UI tables
|
||||
(`L4GLASSWIN`, `L4PADPANEL`, `L4VB16`): 0x42 is now "Torso Ctr", 0x41/0x43/0x44 "Look Bk/R/L".
|
||||
|
||||
## RGB keyboard lamp mirror — L4KEYLIGHT (2026-07-26) [T2 live, claimed a real board]
|
||||
|
||||
Ported from RP412 (itself vRIO's `KeyboardLampMirror`). Keys bound to a lamp address in
|
||||
`bindings.txt` glow with the panel palette through **Windows Dynamic Lighting** — yellow for the
|
||||
map's side columns (0x10-0x1F), red for the rest — flashing in step with the on-screen buttons
|
||||
(its `LampLevel` copy matches the FIXED `BTLampBrightnessOf`). Per-key boards light each bound
|
||||
key; zone-lit boards mirror the strongest lamp board-wide. All WinRT runs on a private worker
|
||||
thread. Gate: `BT_KEYLIGHT=0` opts out; no Dynamic Lighting = one log line, then dormant.
|
||||
|
||||
⚠ **RP412's packing hazard does NOT apply here.** It compiles that file with forced default struct
|
||||
packing because *its* engine is `/Zp1`, which would break the WinRT ABI. BT411's `BT_OPTS` is
|
||||
`/permissive /W0 /wd4996 /EHsc /bigobj /MP` — **no `/Zp`** — so only the dialect flags are needed
|
||||
(`/std:c++17 /permissive-`, per-file in CMakeLists, since the project otherwise builds C++14
|
||||
/permissive). The scalars-only interface is kept anyway so the isolation survives if packing is
|
||||
ever added.
|
||||
|
||||
Wired in `PadRIO`: map from `bindings.keyBindings` (ActionButton binds only, first-binding-wins
|
||||
per key), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands the LEDs back to Windows).
|
||||
Verified: `[keylight] mirroring 25 bound key(s)` + claimed this machine's 24-zone keyboard;
|
||||
`BT_KEYLIGHT=0` and the pod profile produce zero keylight lines and run clean.
|
||||
|
||||
## Lamp flash decode was wrong — fixed (2026-07-26) [T1, from the L4RIO.h enum]
|
||||
|
||||
`BTLampBrightnessOf` (l4vb16.h) returned `max(state1, state2)` and blanked to 0 on the alternate
|
||||
phase. The engine's own enum [T0] says otherwise: bits 0-1 = flash mode, bits 2-3 = **state 1**
|
||||
brightness, bits 4-5 = **state 2** brightness (0 off / 1 dim / 3 bright) — solid shows state 1,
|
||||
flashing ALTERNATES the two at 500/250/125 ms. The old formula agrees only when one state is Off,
|
||||
which is why it survived: the Panic lamp is `flashFast+state1Off+state2Bright` and L4LAMP's other
|
||||
pulse is off/dim. But **`L4LAMP.cpp:252` commands `flashFast + state1Dim + state2Bright`** — a
|
||||
dim→bright pulse that rendered as a hard bright→off blink. RP412's `L4MFDVIEW::LampLevel` has the
|
||||
faithful formula; that is now ours. THREE copies of the decode existed (l4vb16.h, L4GLASSWIN,
|
||||
L4PADPANEL), all three with the bug; the two locals now forward to the one inline.
|
||||
|
||||
## The pre-mission WAITING SCREEN (2026-07-22) [T2]
|
||||
Every pre-run state (seat wait, roster full, WaitingForEgg, LoadingMission, WaitingForLaunch)
|
||||
paints an animated overlay -- Consolas green text + a 12-segment spinner phased on
|
||||
@@ -199,8 +451,9 @@ copied from them.
|
||||
dead panel: `MechRIOMapper`'s message ids were chained one enum too high (0x19-0x2a) — the
|
||||
binary RIO table @0051dd30 re-registers the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a); fixed
|
||||
ungated + static_assert-locked (`btl4mppr.hpp`) → all 24 MFD-bank + 2 zoom buttons live
|
||||
(52/72 panel buttons now work; 8 dead await handler reconstruction — Gitea backlog issue;
|
||||
12 have no streamed mapping authored, authentically inert). **Keyboard = the ~20 core
|
||||
(52/72 panel buttons worked at the time; the 8 dead ones are ALL wired as of 2026-07-26 —
|
||||
`DuckRequest` was the last, see §Dead-button backlog closed; 12 have no streamed mapping
|
||||
authored, authentically inert). **Keyboard = the ~20 core
|
||||
gameplay actions on the CONTROLS.MAP keys; the PANEL covers every pod address by click**
|
||||
(right-click = hold latch) — the merged default profile lives in `L4PADBINDINGS.cpp`
|
||||
(auto-writes `content\bindings.txt`; W/S/A/D/Q/E/R/F/X drive, 1-4/Space/Ctrl fire, M/N/H/C =
|
||||
@@ -243,8 +496,9 @@ copied from them.
|
||||
(`SVGA16::ExpandPlaneToBGRA`, no D3D) and `StretchDIBits`'d in, so the **whole D3D
|
||||
dev-composite path (dock / separate window / overlay) stands down** while these are up
|
||||
(`BTDrawGaugeInset`/`BTGaugeWindowRenderAndPresent` early-return; no `gBTGaugeDockBottom`
|
||||
strip). Runtime gate **`BT_GLASS_PANELS`** (default ON under `-platform glass`, `=0` falls
|
||||
back to the single pad panel + docked gauges) — read by `L4PADRIO` (which panel to create),
|
||||
strip). Runtime gate **`BT_GLASS_PANELS`** (⚠ was "default ON under `-platform glass`" — NO
|
||||
LONGER TRUE; the surround is the glass default and panels are explicit-only, see §Layout modes
|
||||
below) — read by `L4PADRIO` (which panel to create),
|
||||
`L4VB16` (suppress compositing), and `btl4main` (skip the world-window dock strip). The
|
||||
display↔bank map (spine of the feature): Heat 0x28-2F, Mfd2 0x20-27, Comm 0x30-37,
|
||||
Mfd1 0x08-0F, Mfd3 0x00-07, sec 0x10-1B ([[pod-hardware]] §Bank→MFD). Verified: build +
|
||||
|
||||
+23
-11
@@ -169,17 +169,29 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
exactly is the all-roster-hosts-connected launch wait, and can absent seats stay `Opening`
|
||||
without stalling? Go/no-go = the 2-node late-attach spike in the plan doc. Phase 1 (round
|
||||
carousel) is console-only and has no engine unknowns. [[multiplayer]]
|
||||
- **Glass panel dead-button backlog (input-coverage audit 2026-07-20) [T1 census].** 8 of the 72
|
||||
panel buttons dispatch a streamed message with NO reconstructed handler (silently ignored):
|
||||
0x13 → Mech `DuckRequest` @0049fa00 (CROUCH — `duckState` attr + SQUAT clips exist!), 0x28 →
|
||||
Mech `BalanceCoolant` @0049f728, 0x1A-0x1D → Generator A-D `ToggleGeneratorOnOff` @004b1ed0,
|
||||
0x12 → ThermalSight toggle (pvision unported), 0x14 → Searchlight `ToggleLamp` (body
|
||||
reconstructed, handler-set unwired — note the 1995 latent bug below: even wired, the light
|
||||
never turns on in the original). Plus page-gated Eng-page meanings: `ToggleCooling` @004ad6f8
|
||||
(msg 3), `ToggleSeekVoltage` @004ba478/@004b8a48 (msg 0xb energy / 9 Myomers), `EjectAmmo`
|
||||
@004bb9b8 (msg 0xb ammo weapons), and MechRIOMapper's own Keypress @004d2514 (id 0x19).
|
||||
Filed as the Gitea "glass panel: unreconstructed button handlers backlog" issue; tables in
|
||||
[[decomp-reference]] §2026-07-20; census in `docs/GLASS_COCKPIT.md`.
|
||||
- **Glass panel dead-button backlog (input-coverage audit 2026-07-20) — ✅ CLOSED 2026-07-26.**
|
||||
The 8 buttons that dispatched a streamed message with NO reconstructed handler are all wired
|
||||
now; this entry stays as the index of where each one landed:
|
||||
0x13 → Mech `DuckRequest` @0049fa00 (**CROUCH — wired 2026-07-26**, the last one; see below),
|
||||
0x28 → Mech `BalanceCoolant` @0049f728 (2026-07-21, issue #20),
|
||||
0x1A-0x1D → Generator A-D `ToggleGeneratorOnOff` @004b1ed0 (`powersub.cpp`),
|
||||
0x12 → ThermalSight / 0x14 → Searchlight `ToggleLamp` (both 2026-07-25, #61),
|
||||
`ToggleCooling` @004ad6f8 msg 3 (2026-07-20, `heatfamily_reslice.cpp`),
|
||||
`ToggleSeekVoltage` @004ba478 msg 0xb energy (`emitter.cpp`) / @004b8a48 msg 9 Myomers,
|
||||
`EjectAmmo` @004bb9b8 msg 0xb ammo (`projweap.cpp`).
|
||||
Still open from that census: **MechRIOMapper's own Keypress @004d2514 (id 0x19)**.
|
||||
Tables in [[decomp-reference]] §2026-07-20; census in `docs/GLASS_COCKPIT.md`.
|
||||
⚠ This entry was stale in BOTH directions for weeks (`docs/INPUT_PATH_AUDIT.md` flagged it) —
|
||||
most of the list had been reconstructed while the prose still called it dead. Check the code,
|
||||
not the census, before reconstructing anything here.
|
||||
- **`DuckState` has no CODE consumer, and that is authentic [T1].** `Mech::DuckRequest` sets
|
||||
`duckState`(attr 0x37, binary `mech+0x398`) to 1 and nothing else: the flag has exactly two
|
||||
writers in the whole binary (that handler and the mech reset) and ZERO readers. Its consumer is
|
||||
a DATABINDING — `content/GAUGE/L4GAUGE.CFG` drives a 3-frame `bduck.pcc` widget off `DuckState`
|
||||
on the map's legend column, verified live (the crouch icon lights grey→orange on a press). So
|
||||
the button is COMPLETE as a request flag + indicator. What is NOT known: whether the 1995 game
|
||||
ever consumed it for posture/collision (no SQUAT clip name survives in the decomp or in
|
||||
`content/`, only `DuckServo01.wav` in AUDIO1.RES). Do not invent a crouch pose to "finish" it.
|
||||
- **Searchlight-driven fog swap — STILL DEFERRED, but the "ORIGINAL 1995 LATENT BUG" premise is
|
||||
❌ RETRACTED (2026-07-25, #61).** The arcade swaps fog between `fog=` (lights on) and
|
||||
`nosearchlightfog=` (off) via `PullFogRenderable` watching the Searchlight's `lightState`.
|
||||
|
||||
@@ -52,10 +52,12 @@ handler chain, so the Eng-page "Coolant" button now toggles the shown weapon's c
|
||||
sub 3 = Reservoir, 4-9 = Condenser1-6, 10-13 = GeneratorA-D): **0x2C = the COOLANT FLUSH
|
||||
button** (Reservoir InjectCoolant, hold-to-flush — works), **0x2F/0x2E/0x2D/0x2B/0x2A/0x29 =
|
||||
the per-condenser VALVE buttons** (MoveValve, Cond1-6 — work), **0x1A-0x1D = Generator A-D
|
||||
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — DEAD until
|
||||
reconstructed). Newly decoded from the binary message tables: **0x13 → Mech `DuckRequest`
|
||||
(0x1a @0049fa00 — the manual's CROUCH button)**, **0x28 → Mech `BalanceCoolant` (0x16
|
||||
@0049f728)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
||||
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — ✅ **WIRED**,
|
||||
`powersub.cpp`). Newly decoded from the binary message tables: **0x13 → Mech `DuckRequest`
|
||||
(0x1a @0049fa00 — the manual's CROUCH button)** — ✅ **WIRED 2026-07-26**, the last handler in
|
||||
this census; press-only, sets `duckState` and the map legend's `bduck.pcc` widget lights (see
|
||||
[[open-questions]] for why it has no code consumer) —, **0x28 → Mech `BalanceCoolant` (0x16
|
||||
@0049f728)** ✅ **WIRED 2026-07-21 (#20)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
||||
Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)** — ✅ **BOTH WIRED
|
||||
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
|
||||
`lightState 0→1`, 0x12 → `thermalActive 0→1`). ⚠ The fn addresses were **swapped** in this file
|
||||
|
||||
+1062
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
# BattleTech 4.11 — controls reference
|
||||
|
||||
Two ways to fly: the **keyboard/pad** (the ~25 core actions) and the **cockpit panel**, where
|
||||
every one of the pod's 72 illuminated buttons is mouse-clickable exactly where it sat in the
|
||||
cabinet. Left-click presses; **right-click latches** a button held.
|
||||
|
||||
Rebind keys in `content\bindings.txt` (delete it to regenerate the defaults).
|
||||
Cockpit layout and display sizes live in `content\environ.ini` (likewise self-documenting).
|
||||
|
||||
---
|
||||
|
||||
## Keyboard — the keyboard IS the button board
|
||||
|
||||
The letter and number rows are the MFD button banks, laid out where they sit on the panel.
|
||||
Flight lives on the number pad so the board stays free.
|
||||
|
||||
### Flight — the number pad
|
||||
|
||||
| Key | Action | | Key | Action |
|
||||
|---|---|---|---|---|
|
||||
| NumPad 8 / 2 | torso aim up / down | | NumPad 7 / 9 | left / right pedal |
|
||||
| NumPad 4 / 6 | torso twist left / right | | NumPad 5 | all stop |
|
||||
| NumPad 0, Space | main trigger | | NumPad 1 / 3 / . | pinky / middle / upper |
|
||||
| Shift / Ctrl | throttle up / down (the lever sticks) | | Alt | reverse thrust |
|
||||
| ← ↓ → | look left / behind / right | | ↑ | torso centre |
|
||||
| ` | cockpit / chase camera | | − / = *(see below)* | — |
|
||||
|
||||
**You spawn in BASIC control mode.** In BASIC the stick steers and the pedals do nothing.
|
||||
Press **F7** once for MID, where the pedals steer and the stick twists the torso — the full
|
||||
pod experience. If "turning doesn't work", this is why.
|
||||
|
||||
### The panel — letter and number rows
|
||||
|
||||
```
|
||||
UPPER DISPLAYS Heat / coolant Engineering Comm / hotbox
|
||||
top row 1 2 3 4 | 5 6 7 8 | 9 0 - =
|
||||
bottom row Q W E R | T Y U I | O P [ ]
|
||||
0x2F..0x2C 0x27..0x24 0x37..0x34
|
||||
0x2B..0x28 0x23..0x20 0x33..0x30
|
||||
|
||||
LOWER DISPLAYS Left Weapons Right Weapons
|
||||
top row A S D F (G unbound) H J K L
|
||||
bottom row Z X C V (B unbound) N M , .
|
||||
0x0F..0x0C 0x07..0x04
|
||||
0x0B..0x08 0x03..0x00
|
||||
|
||||
COLUMNS beside the map
|
||||
left F1 zoom+ F2 zoom- F3 thermal F4 CROUCH F5 lamp F6 display
|
||||
right F7 control mode F8 - F9..F12 Generators A-D
|
||||
```
|
||||
|
||||
G and B are deliberately unbound: they are the physical gap between the lower clusters.
|
||||
|
||||
**1 2 3 Q W E are the coolant valves** and the detents run 1 - 5 - 50 (max) - **0 (CLOSED)** -
|
||||
back to 1. One press past max shuts that loop off and everything on it overheats; boosting one
|
||||
loop starves the others, since the supply is shared. **4** is the flush (hold it) and **R**
|
||||
re-balances every valve.
|
||||
|
||||
The weapon and engineering displays are **page-gated**: on the Quad page the outer buttons jump
|
||||
to a populated Eng page; on an Eng page they drive generator select A-D, bus mode, coolant
|
||||
toggle, trigger configure, and **eject** (tap to clear a jam, hold ~3 s to jettison a burning
|
||||
ammo bay before it detonates).
|
||||
|
||||
### What a bound key gives up
|
||||
|
||||
Binding a key removes it from the authentic 1995 typed-hotkey channel so it cannot
|
||||
double-dispatch. This board binds nearly everything, so those hotkeys are given up by design:
|
||||
`5` = MFD1 Quad page, `z` = MFD3 Eng1, `t/y/u/i/o` = pilot select, `+`/`-` = target zoom.
|
||||
Unbind a key in `bindings.txt` to get its 1995 meaning back. Same rule for the built-ins:
|
||||
**`** is the view toggle (V is a board button now), and J/K/L cycle the MFD preset pages only
|
||||
while unbound.
|
||||
|
||||
Addresses with no function at all — the pod never wired them — are `0x16` `0x17` `0x19`
|
||||
`0x1E` `0x1F` and `0x38`-`0x3E`.
|
||||
|
||||
## Controllers
|
||||
|
||||
An **Xbox controller** works out of the box.
|
||||
|
||||
**Flight sticks, HOTAS and pedals:** run `joyconfig.bat` once. A wizard asks you to move each
|
||||
control in turn — stick, twist, throttle, fire buttons — writes the bindings, and drops you into
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Cockpit layout
|
||||
|
||||
Set these in `content\environ.ini`:
|
||||
|
||||
- **`BT_MFD_SCALE`** (and `_UL`/`_UC`/`_UR`/`_LL`/`_LR`) — size of the five MFDs, as a percentage
|
||||
of their pod size. Turn them up if you want to actually read them while you fly.
|
||||
- **`BT_RADAR_SCALE`**, **`BT_RADAR_POS`** — size the map, and move it out of the middle of the
|
||||
road: `CENTER`, `LEFT`, `RIGHT`, `MIDLEFT`, `MIDRIGHT`.
|
||||
- **`BT_GLASS_PANELS=1`** — break every display out into its own desktop window instead of the
|
||||
composited cockpit. Good for reading one display at full size.
|
||||
|
||||
Run with **`-fit`** for a borderless window over the whole monitor. The cockpit scales to any
|
||||
window shape at one uniform scale, so nothing ever stretches — a window that isn't the cockpit's
|
||||
shape just gets black bars.
|
||||
@@ -0,0 +1,162 @@
|
||||
# DIST_LAYOUT_PLAN — proposal: ship the authentic single-folder layout
|
||||
|
||||
**Status: PROPOSED, NOT IMPLEMENTED.** Written 2026-07-26 for discussion between the authors.
|
||||
Nothing in this document has been built. The seatbelt described in §7 *has* shipped (4.11.560);
|
||||
this proposal is about removing the need for it.
|
||||
|
||||
---
|
||||
|
||||
## 1. What prompted this
|
||||
|
||||
Field report, 4.11.559:
|
||||
|
||||
```
|
||||
PS C:\Games\BT411_4.11.559\build\Release> .\btl4.exe -fit
|
||||
```
|
||||
|
||||
→ the mission console opens, then the game **crashes on Launch**, and the player's
|
||||
`environ.ini` / `bindings.txt` appear to have never been created.
|
||||
|
||||
Root cause: the engine resolves `BTL4.RES`, `VIDEO\`, `BTDPL.INI`, the eggs **and** both player
|
||||
config files relative to **cwd**. Launched from `build\Release`, it found no resources
|
||||
(`Resource file btl4.res v1.0.0.0 is obsolete!`), wrote stray copies of `bindings.txt` /
|
||||
`environ.ini` / `btl4.log` next to the exe, and killed the mission generation the menu launched
|
||||
(the child inherits the parent's cwd).
|
||||
|
||||
The observation that matters more than the bug: **RP411, RP412 and the 1995 pod cannot hit this,
|
||||
because they ship one folder.**
|
||||
|
||||
## 2. Evidence that one folder is the authentic shape
|
||||
|
||||
- **The 1995 pod.** `content\` in this repo *is* an original pod game directory:
|
||||
`BTL4OPT.EXE`, `BTL4.RES`, `SETENV.BAT`, `32RTM.EXE`, `DPMI32VM.OVL`, `VIDEO\`, `GAUGE\`,
|
||||
`AUDIO\` all sit together. The exe lived beside its resources. [T1]
|
||||
- **RP411 / RP412.** `pack-dist.ps1` copies `rpl4opt.exe`, `AUDIO/`, `GAUGE/`, `VIDEO/`,
|
||||
`RPL4.RES`, `RPDPL.INI`, the eggs, the DLLs, `environ.ini` and `bindings.txt` into ONE dist
|
||||
root. A dist listing confirms it. [T1]
|
||||
- **BT411.** `mkdist.py` zips **tracked repo paths verbatim** (`git ls-files content/` written as
|
||||
`<name>/content/...`, the exe as `<name>/build/Release/btl4.exe`). The player therefore receives
|
||||
the *developer tree*. [T1]
|
||||
|
||||
So the split is not a decision anyone made about how the game should be installed — it is an
|
||||
artifact of how the packer was written. In the authentic shape cwd is correct by construction and
|
||||
a double-click just works.
|
||||
|
||||
## 3. Proposed dist tree
|
||||
|
||||
```
|
||||
BT411_4.11.NNN/
|
||||
btl4.exe <- was build/Release/btl4.exe
|
||||
OpenAL32.dll d3dx9_43.dll msvcp140.dll vcruntime140.dll steam_api.dll
|
||||
BTL4.RES BTDPL.INI CONTROLS.MAP *.EGG ... <- was content/*
|
||||
AUDIO/ GAUGE/ VIDEO/ <- was content/*/
|
||||
play_solo.bat join.bat join_lan.bat joyconfig.bat play_steam.bat
|
||||
README.txt CONTROLS.txt
|
||||
(bindings.txt, environ.ini, volume.cfg, *.log appear here on first run)
|
||||
```
|
||||
|
||||
Identical in shape to the pod's own directory and to RP's dist.
|
||||
|
||||
## 4. What changes
|
||||
|
||||
| File | Change | Size |
|
||||
|---|---|---|
|
||||
| `tools/mkdist.py` | rewrite archive paths: `content/X` → `X`, `build/Release/btl4.exe` → `btl4.exe`, DLLs → root. The redist/PDB/steam logic is unaffected. | ~15 lines |
|
||||
| `players/*.bat` (5) | drop `cd content`; drop the `build\Release\` prefix; the `badpath` guard tests `BTL4.RES` instead of `content\OPERATOR.EGG` | ~4 lines each |
|
||||
| `players/README.txt` | "next to the content and build folders" → "in the game folder"; upgrade section (see §6) | ~10 lines |
|
||||
| `docs/CONTROLS.md` | `content\bindings.txt` → `bindings.txt` (3 mentions) | trivial |
|
||||
| game code | **none required.** `BTEnsureContentDirectory` already probes the exe's own directory, so a single-folder install satisfies it on the first candidate. Log strings that say `content\environ.ini` become slightly wrong — cosmetic, worth a sweep. | ~5 strings |
|
||||
|
||||
**The repo layout does NOT change.** `build/` is generated and `content/` stays where it is; only
|
||||
the packer's output shape moves. Developers keep running `run\run.cmd` exactly as now.
|
||||
|
||||
## 5. What it buys
|
||||
|
||||
- The reported crash class disappears at the source rather than being caught by a guard.
|
||||
- A double-clicked `btl4.exe` works — which is what a player tries first, and what the menu
|
||||
front end was built to support.
|
||||
- `bindings.txt` / `environ.ini` / `volume.cfg` / `btl4.log` land where the player can see them,
|
||||
next to the game, instead of one folder down.
|
||||
- One fewer way to run a **stale exe**: today `build\Release\btl4.exe` can survive an upgrade and
|
||||
be launched by an old shortcut. (That has already cost us one debugging session — see
|
||||
`docs/GLASS_COCKPIT.md` §2026-07-20 "STALE EXE, not code".)
|
||||
- We stop shipping a directory named `build\` to players, which invites exactly the "run the exe
|
||||
directly" instinct that caused this.
|
||||
|
||||
## 6. The hard part — upgrades ⚠
|
||||
|
||||
The README tells players to extract the new zip **over** the existing folder and overwrite, so
|
||||
their `bindings.txt` and `volume.cfg` survive. A layout change collides with that:
|
||||
|
||||
| Hazard | Effect |
|
||||
|---|---|
|
||||
| Player's `content\bindings.txt` stays in the now-unused `content\` | Their key/joystick setup is silently ignored — and still visible on disk, so it looks like the game lost it |
|
||||
| Old `build\Release\btl4.exe` remains | A shortcut or habit runs a **stale build** |
|
||||
| Old `content\` tree remains | ~130 MB duplicated, and two copies of every asset |
|
||||
|
||||
**But note the wrinkle worth checking first:** the zip root is *versioned*
|
||||
(`BT411_4.11.560/…`), so extracting into `C:\Games\` produces a **new sibling folder**, not an
|
||||
overlay — the reporter's own install is `C:\Games\BT411_4.11.559\`. The documented
|
||||
extract-over-top only works if the player extracts the *inner folder's contents* onto the old
|
||||
install, which is a fiddly manual step. **If in practice every version already lands in its own
|
||||
folder, then bindings.txt already does not carry over today**, and the migration cost of this
|
||||
change is close to zero. That should be confirmed before weighing the options below.
|
||||
|
||||
### Migration options
|
||||
|
||||
- **A. Versioned folder, no migration.** Rely on each version extracting fresh; README tells
|
||||
players to copy `bindings.txt` (+ `volume.cfg`, `environ.ini`) from the old folder's `content\`
|
||||
into the new folder's root. Simple, honest, one paragraph of README — but a manual step, and
|
||||
wrong if anyone really is overlaying.
|
||||
- **B. One-shot in-game migration.** On boot, if `<exe dir>\content\bindings.txt` exists and
|
||||
`<exe dir>\bindings.txt` does not, copy the player files up and log it loudly. ~30 lines,
|
||||
covers the overlay case invisibly. Does not solve the stale exe or the duplicated tree.
|
||||
- **C. B + a loud stale-install warning.** Additionally, if `<exe dir>\build\Release\btl4.exe`
|
||||
exists, log a warning and show it once in the front end ("an old copy of the game is still in
|
||||
this folder — delete build\ and content\"). Most protective, most code.
|
||||
- **D. Stable zip root** (`BT411/` instead of `BT411_<version>/`) so extract-over-top genuinely
|
||||
works, combined with B. Fixes the upgrade story properly but changes how testers file reports
|
||||
(the folder no longer names the version — though the title bar and log head still do).
|
||||
|
||||
**Recommendation: A if §6's wrinkle confirms versioned folders are the reality, else B+C.**
|
||||
Either way the change is small; the risk is entirely in the migration, not in the layout.
|
||||
|
||||
## 7. Relationship to the shipped seatbelt (4.11.560)
|
||||
|
||||
`BTEnsureContentDirectory` (btl4main.cpp) already makes both layouts work: if cwd has no
|
||||
`BTL4.RES` it probes `..\..\content`, `..\content`, `content`, and the exe dir itself. **It stays
|
||||
either way** — it costs nothing, it protects the developer tree (where the split is real and
|
||||
permanent), and it means this proposal can be adopted or rejected without any risk of regression.
|
||||
If the layout changes, the first probe candidate simply becomes the hit.
|
||||
|
||||
## 8. Verification plan (if adopted)
|
||||
|
||||
1. `mkdist.py` → extract clean → **double-click `btl4.exe`** (no bat, no cwd help): menu opens,
|
||||
Launch runs a mission, `bindings.txt`/`environ.ini`/`btl4.log` appear at the root.
|
||||
2. Each of the five bats from a clean extract.
|
||||
3. `btl4.exe -fit` bare, and from a shell sitting anywhere else on disk.
|
||||
4. 2-node LAN via `join_lan.bat` (path assumptions in the join path are the least exercised).
|
||||
5. Migration path per the chosen option: install 4.11.559 old-shape, customize `bindings.txt`,
|
||||
upgrade, confirm the setup survives or that the README's step recovers it.
|
||||
6. Pod profile boot (`SETENV.BAT` now sits at the root — confirm the cabinet contract in
|
||||
`context/glass-cockpit.md` §ONE unified build still reads correctly).
|
||||
|
||||
## 9. Open questions for the other author
|
||||
|
||||
1. Do we actually have players *overlaying* installs, or does everyone get a fresh folder per
|
||||
version? This decides §6 entirely.
|
||||
2. Do the `.bat` launchers survive at all in a single-folder world? They still earn their keep
|
||||
(launch forensics `#41`, the handoff wait, `BT_FE_SOLO`, `BT_LOG` naming) — but a player who
|
||||
can now just double-click the exe may never touch them.
|
||||
3. Should `environ.ini` / `bindings.txt` be **shipped** in the zip rather than written on first
|
||||
run? RP ships both. We deliberately do not, so extract-over-top cannot clobber a player's
|
||||
settings — but that reasoning weakens under option A/D.
|
||||
4. Does the real pod cabinet care? Its `SETENV.BAT` + `BT_PLATFORM=pod` contract is
|
||||
path-independent, but the pod's own install shape should be confirmed against Nick's notes
|
||||
before we call the single folder "authentic" for the cabinet as well as the 1995 build.
|
||||
|
||||
## Key Relationships
|
||||
|
||||
- Caused by: `context/build-and-run.md` §cwd guard (the landmine + why BT411 differs)
|
||||
- Touches: `tools/mkdist.py`, `players/*.bat`, `players/README.txt`, `docs/CONTROLS.md`
|
||||
- Prior art: `C:\VWE\RP412\pack-dist.ps1` (single-folder dist), `content\BTL4OPT.EXE` (the pod's own)
|
||||
@@ -843,9 +843,13 @@ below (view bottom clean). Buttons: the full rect is the click target, the surfa
|
||||
only the LAMP edge shows (PaintGlass painter trick). Address banks = L4GLASSWIN
|
||||
BuildMfd/BuildRadar/BuildFlight.
|
||||
|
||||
**Gating precedence** (resolved once → `gBTGaugeCockpit`): `BTGlassPanelsActive` (BT_GLASS_PANELS)
|
||||
> `BT_COCKPIT=1` > `BT_DEV_GAUGES_WINDOW` / `BT_DEV_GAUGES_DOCK` > **cockpit default**. `BT_COCKPIT=0`
|
||||
= dock-bottom. `DevGaugeDocked()` includes cockpit so the separate window stands down. Green tint
|
||||
**Gating precedence** (2026-07-26: resolved ONCE in btl4main → `glassLayout` → `gBTGaugeCockpit`):
|
||||
`BTGlassPanelsActive` (BT_GLASS_PANELS) > `BT_DEV_GAUGES_WINDOW` > `BT_DEV_GAUGES_DOCK` >
|
||||
`BT_COCKPIT=0` > **cockpit surround default**. `BT_COCKPIT=0` = dock-bottom — ⚠ which it did NOT
|
||||
do before that date: the glass profile block converted it to `BT_GLASS_PANELS=1`, so the dock
|
||||
strip was unreachable under glass. Dock/window modes now auto-raise `BT_PAD_PANEL` (those layouts
|
||||
carry no buttons of their own).
|
||||
`DevGaugeDocked()` includes cockpit so the separate window stands down. Green tint
|
||||
`BT_COCKPIT_TINT=RRGGBB` (default 0x27E8 ≈ rgb(33,255,66)); radar stays palette amber.
|
||||
|
||||
**Pod-build seam.** Panels + dim lamps render in ALL builds; only `PadRIO::SetScreenButton/
|
||||
|
||||
+296
-3
@@ -385,9 +385,10 @@ on the pod monitor. This reuses the plasma-window CPU→window pattern and sides
|
||||
D3D dev-composite bug family (additional-swap-chain lifetime, device-state save/restore,
|
||||
depth-stencil mismatch, layered strobing).
|
||||
|
||||
**Gate: `BT_GLASS_PANELS`** (runtime env; `BTGlassPanelsActive()`). Default ON under
|
||||
`-platform glass` (preset putenv in `btl4main.cpp`); `=0` restores the legacy single pad panel
|
||||
+ docked gauge strip. Wired at three seams:
|
||||
**Gate: `BT_GLASS_PANELS`** (runtime env; `BTGlassPanelsActive()`). ⚠ **Was** default-ON under
|
||||
`-platform glass` (preset putenv in `btl4main.cpp`) — as of 2026-07-26 the COCKPIT SURROUND is the
|
||||
glass default and this gate is **explicit-only**; the one resolver + the full precedence table are
|
||||
in `context/glass-cockpit.md` §Layout modes. Wired at three seams:
|
||||
- `L4PADRIO` ctor: `BTGlassPanelsActive()` → `BTGlassPanels_Create()` instead of
|
||||
`BTPadPanel_Create()`; dtor destroys both (safe no-ops).
|
||||
- `L4VB16`: `BTDrawGaugeInset` / `BTGaugeWindowRenderAndPresent` early-return when
|
||||
@@ -439,3 +440,295 @@ Verified after the fix (`BT_GLASS_LOG`): `[glass] 'Heat MFD' port=Heat mask=0x40
|
||||
enable=2 ow=640 oh=480 nonzero=22227/307200`, `[glass] 'Secondary / Radar' port=sec mask=0x3f
|
||||
pal=1 enable=3 ow=480 oh=640 nonzero=26206/307200` — surfaces resolve and carry live pixels.
|
||||
`BT_GLASS_LOG` left in as a permanent per-surface resolve/pixel diagnostic (throttled).
|
||||
|
||||
## 2026-07-26 — Layout modes get ONE resolver; button geometry gets ONE owner (L4RIOBANK)
|
||||
|
||||
The RP412 replication pass, step 1 of the agreed plan (port the geometry, keep our renderers).
|
||||
|
||||
### The mode resolver (btl4main.cpp)
|
||||
Where the secondary displays go was decided in TWO places with duplicated precedence -- the glass
|
||||
profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`, the window-sizing block re-derived
|
||||
cockpit-vs-dock -- and the boot banner read NEITHER, announcing "per-display cockpit windows
|
||||
[BT_GLASS_PANELS]" for every glass boot including the surround default. The split had also
|
||||
broken `BT_COCKPIT=0`: documented as the dock-bottom opt-out, the profile block converted it to
|
||||
`BT_GLASS_PANELS=1`, so **the docked strip was unreachable under the glass profile**.
|
||||
|
||||
Now: one `glassLayout` resolved after the profile putenvs land, consumed by the banner, the
|
||||
pad-panel decision and the sizing block. Precedence `BT_GLASS_PANELS` > `BT_DEV_GAUGES_WINDOW` >
|
||||
`BT_DEV_GAUGES_DOCK` > `BT_COCKPIT=0` > surround default; `BT_GLASS_PANELS` is explicit-only now
|
||||
(it was auto-set); dock/window auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
|
||||
Banner: `[boot] platform profile: GLASS (PadRIO + plasma window) [secondary displays: <mode>]`.
|
||||
Verified all four modes boot to the mode they name, plus pod/dev.
|
||||
|
||||
### L4RIOBANK -- the shared field
|
||||
`engine/MUNGA_L4/L4RIOBANK.cpp/.h` (new TU). Both renderers were carrying their own copy of the
|
||||
button geometry and had drifted: an MFD button was 156x138 reaching under the glass in the
|
||||
exploded window, and a **76x24 sliver entirely OUTSIDE the glass** in the surround. One module
|
||||
now owns it, both are consumers, placement stays per-renderer.
|
||||
|
||||
The under-glass rule, from RP412 `L4MFDVIEW` (which took it from the pod): reach half the glass
|
||||
in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery
|
||||
over. Strip scales off the display's SHORT axis vs the native 480 (MFD 640x480 landscape, map
|
||||
480x640 portrait) with a 6px floor. The MFD per-column nudge against the DISPLAY/PROGRAM legends
|
||||
moved here so both renderers inherit one alignment. The map's foot row is pushed FIRST because
|
||||
the side columns now cover that band and the hit test takes the first match.
|
||||
|
||||
Retired: `L4GLASSWIN`'s `PlaceCellAt`/`PlaceLine`/`PlaceRect` and its `RedCellH`/`EdgeProtrude`/
|
||||
`RailW`/`RailSlotAdd`/`RailGap`/`RedOffsetX`/`kRedColDX` constants; the surround's inline MFD/
|
||||
radar/flight button loops. `kCkRedH`/`kCkLamp` survive only as band reserves for the canvas math.
|
||||
|
||||
### Lamp flash decode [T1 -> fixed]
|
||||
`BTLampBrightnessOf` returned `max(state1,state2)` and blanked on the alternate phase. Per the
|
||||
engine's own `RIO::LampState` enum [T0] solid shows state 1 and flashing ALTERNATES state 1 and
|
||||
state 2. Agrees only when one state is Off -- true for Panic (`flashFast+state1Off+state2Bright`)
|
||||
which is why it survived -- but `L4LAMP.cpp:252` commands `flashFast+state1Dim+state2Bright`, a
|
||||
dim->bright pulse that rendered as bright->off. Three copies existed (l4vb16.h, L4GLASSWIN,
|
||||
L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline.
|
||||
|
||||
### Verification harness (new, reusable)
|
||||
- `BT_RIOBANK_LOG=1` -- one-shot dump of every bank's buttons + bounds (capped at 16 banks so the
|
||||
per-frame surround caller prints one pass).
|
||||
- `scratchpad/checkbank.py <log>` -- per-bank census + **first-hit reachability**: an address
|
||||
whose rect is entirely covered by earlier buttons in its bank is dead however big it looks, and
|
||||
the overlapping under-glass banks make that a live hazard. Reports duplicates and shadowing.
|
||||
- `scratchpad/clickbank.py <log> <title> --mode surround|exploded` -- posts a real
|
||||
WM_LBUTTONDOWN/UP at every button's centre; the game's own CLICK forensics prove dispatch.
|
||||
|
||||
Result both modes: **72/72 placed, 0 shadowed, 0 duplicated, 72/72 dispatched**, process survived
|
||||
144 posted messages each. Pod + dev profiles boot and simulate un-regressed. Surround `BT_SHOT`
|
||||
confirms the legends are clear and the lamps read as edge strips; exploded `PrintWindow` captures
|
||||
confirm surfaces still land under their banks.
|
||||
|
||||
## 2026-07-26 (later) — Phase 2: the cockpit scales (uniform-fit letterbox + -fit)
|
||||
|
||||
RP412 replication step 2. The canvas is a FIXED size and D3D9 stretched it into the client, so a
|
||||
window dragged to a different shape squashed the instruments -- the projection was aspect-corrected
|
||||
back in task #20, but the panels, lamps and MFD glass never were. Now: one uniform scale, centred,
|
||||
leftover black.
|
||||
|
||||
**Mechanism.** `BTCockpitFitRect()` (L4VB16.cpp) computes the centred uniform-scale rect in integer
|
||||
math -- a float scale wobbles by a pixel between frames and the bars shimmer during a drag. D3D9
|
||||
applies it as a Present **destination rect**, which `D3DSWAPEFFECT_DISCARD` forbids, so the
|
||||
WINDOWED swap effect becomes `D3DSWAPEFFECT_COPY` when the surround is up and multisampling is off
|
||||
(COPY cannot multisample). `gBTCockpitLetterbox` records the outcome; 0 = the old full-client
|
||||
stretch, which is what every non-cockpit mode and any MSAA run still gets.
|
||||
|
||||
Wired at five seams:
|
||||
- `L4VIDEO` device setup: swap effect + confirm/withdraw the flag.
|
||||
- Both Present sites: the scene present AND the wait-screen overlay present (it paints into the
|
||||
same backbuffer, so it has to land in the same rect).
|
||||
- `btl4main` WM_SIZE: `InvalidateRect` -- a COPY present only writes its dest rect, so stale bar
|
||||
content persists without an erase (the class brush is black).
|
||||
- `BTCockpitMouseDown`: client->canvas through the SAME fit. Mapping against the full client
|
||||
drifts the hit test off every button by the bar width; a click on a bar now hits nothing.
|
||||
- `BTWorldAspectOf`: under the letterbox the view's on-screen aspect is its OWN -- the client
|
||||
drops out of the formula entirely.
|
||||
|
||||
**`-fit` / `-windowed-fullscreen`** (btl4main): borderless `WS_POPUP` over the monitor rect, canvas
|
||||
letterboxed inside. Unknown tokens fall through `L4Application::ParseCommandLine`'s ladder to
|
||||
`return True`, so no arg-parser change was needed.
|
||||
|
||||
### Ordering trap (found live, fixed)
|
||||
The letterbox flag was originally set at device creation -- but **the first WM_SIZE beats the
|
||||
device**. A `-fit` boot on the 3440x1440 ultrawide logged `[resize] client 3440x1440 aspect=3.14`
|
||||
(the stretch formula), and since nothing resizes the window again, the per-frame
|
||||
`gWindowAspect != appliedAspect` check applied it on frame 1. The INTENT is now decided in
|
||||
btl4main alongside `gBTGaugeCockpit` (windowed cockpit + MULTISAMPLE unset/0); L4VIDEO only
|
||||
confirms it or withdraws it. Re-verified: the boot line reads `aspect=1.8`.
|
||||
|
||||
### Capture note (for whoever verifies this next)
|
||||
`PrintWindow` returns an all-black frame for the borderless `-fit` window, while working fine for
|
||||
the normal chrome'd window. Use a screen-region grab (`scratchpad/shot.ps1`) for `-fit`. The
|
||||
all-black PrintWindow is NOT a render failure -- the log showed 18 sim ticks and the screen grab
|
||||
showed the full cockpit.
|
||||
|
||||
### Verified
|
||||
- Wide drag 1684x661 and tall drag 744x961: canvas uniformly scaled and centred, bars on the long
|
||||
axis, ZERO distortion (captures `fit_wide.png` / `fit_tall.png`).
|
||||
- `-fit` on 3440x1440: borderless, centred, bars left/right, undistorted.
|
||||
- **72/72 buttons still dispatch after a resize** -- clickbank.py now maps canvas->client through
|
||||
its own copy of the fit (an independent check that the two transforms agree): at fit 906x661
|
||||
with a 389px left bar, all 72 `[cockpit] CLICK` lines land.
|
||||
- exploded / dock / pod / dev: DISCARD retained, boot + simulate un-regressed.
|
||||
|
||||
## 2026-07-26 (later still) — Phase 3: player-tunable displays + the settings file
|
||||
|
||||
RP412 replication step 3.
|
||||
|
||||
### Scaling (BTCkResolvedSizes, L4VB16.cpp)
|
||||
`BT_MFD_SCALE` (all five), `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` (per display, overriding the group),
|
||||
`BT_RADAR_SCALE` -- percentages of the pod size, 25-200, resolved ONCE (the surround recomputes its
|
||||
layout every frame, so reading the env per frame would be silly).
|
||||
|
||||
The load-bearing consequence: **the surround BANDS derive from the resolved sizes**. That is why
|
||||
the sizes could not stay compile-time constants -- the band a display hangs in has to grow with it
|
||||
or the canvas clips it. Each band is what its largest occupant needs; a corner MFD only claims
|
||||
`size - kCkOVL` because it overlaps the view; the bottom band drops to just the lower MFDs when the
|
||||
map leaves that row for a side. Verified 100% reproduces the historical `L276 R276 T223 B336`
|
||||
byte-for-byte, 150% gives `L436 R436 T343 B448`, 80%+MIDLEFT gives `L256 R212 T175 B148`.
|
||||
|
||||
The flight label blocks now hang off their OWN lower MFD's scaled height + lamp strip, instead of
|
||||
the constant -- a shrunken MFD used to leave the block floating in the gap and an enlarged one to
|
||||
overlap it.
|
||||
|
||||
### Map placement (BT_RADAR_POS)
|
||||
CENTER (default) / LEFT / RIGHT / MIDLEFT / MIDRIGHT, with the BOTTOM*/`*CENTER` spellings RP
|
||||
accepts. Bottom corner: the map goes flush to the canvas edge and the lower MFD whose corner it
|
||||
took slides beside it. Halfway up a side: it leaves the bottom row entirely and the bottom band
|
||||
shrinks accordingly.
|
||||
|
||||
⚠ **First cut was wrong and the capture caught it:** the corner map was placed relative to the VIEW
|
||||
edge, which overlapped the neighbouring MFD by the band width -- 232px of map drawn over the Right
|
||||
Weapons display. Corner placement is measured off the CANVAS edge.
|
||||
|
||||
### The map legend grid -- measured, not inherited
|
||||
The side columns have to line up with the six cells the map imagery paints beside them. Zooming a
|
||||
native Secondary/Radar capture shows those cells are exactly our address map: MAP+ 0x10, MAP- 0x11,
|
||||
IR 0x12 (ThermalSight lamp), crouch 0x13 (DuckRequest), searchlight 0x14, display-mode 0x15 -- the
|
||||
art independently corroborating [[pod-hardware]].
|
||||
|
||||
`scratchpad/measurelegend.py` scans the capture for the legend's vertical rule and for our lamp
|
||||
bars, and reports both. Ours: **top 3, cell 102, pitch 107** of the 640 span. RP412's map:
|
||||
`13 + 6x102 @ 105`. Same cell height, different top and pitch -- confirming the deferral call in
|
||||
Phase 1 was right, the numbers do NOT transfer. Our old even division (`displayH/6 + 1`) had the
|
||||
pitch right by luck and sat 3px high of the labels; the grid is now pinned to the measurement, with
|
||||
top and bottom scaled SEPARATELY and subtracted (RP's trick) so rounding cannot walk the buttons
|
||||
out of step with the labels down a scaled column.
|
||||
|
||||
### environ.ini
|
||||
Two defects, both fixed:
|
||||
1. **Read far too late.** It sat ~300 lines into WinMain, after the platform-profile block had
|
||||
already run its getenv()s -- so every setting the profile reads (BT_PLATFORM, BT_COCKPIT,
|
||||
BT_GLASS_PANELS, BT_DEV_GAUGES, L4CONTROLS...) was silently ignored FROM THE FILE and only ever
|
||||
worked as a real environment variable. Now loaded immediately after the first-breath boot line.
|
||||
2. **putenv()'d comments verbatim**, so a commented-out option would have become an environment
|
||||
variable literally named `#BT_MFD_SCALE`. Comments (`#`, `;`) and blanks are skipped, and lines
|
||||
with no `=` are counted and ignored.
|
||||
|
||||
Added: the real environment WINS over the file (a launcher .bat overrides rather than fights), and
|
||||
a fully documented default is written on first run when the file is absent -- the bindings.txt
|
||||
convention (untracked via .gitignore, so extract-over-top never clobbers a player's settings; every
|
||||
option ships commented out so a fresh install behaves exactly as before).
|
||||
|
||||
Verified live, all three paths: first run writes 3886 bytes and applies `0 setting(s)`; uncommenting
|
||||
`BT_MFD_SCALE=130` + `BT_MFD_SCALE_UC=60` + `BT_RADAR_POS=MIDRIGHT` in the FILE drives the layout
|
||||
(`displays UL 130% UC 60% ... radar on the right side, centred`); and `set BT_MFD_SCALE=90` beats
|
||||
the file's 130 while the file's other two settings still apply.
|
||||
|
||||
### Verified
|
||||
- 72/72 placed, 0 shadowed, 72/72 dispatched at baseline, at 150%/135%, and at 75% + BOTTOMRIGHT.
|
||||
- exploded / dock / pod / dev boot and simulate un-regressed.
|
||||
- Captures: p3_big2 (150%), p3_radleft2 (bottom-left + slid MFD), p3_radmidl (side-mounted map at
|
||||
80%), p3_fromini (settings taken from the file).
|
||||
|
||||
## 2026-07-26 (final) — Phases 4-6: the dead-button backlog, the lamp mirror, the shipped doc
|
||||
|
||||
### Phase 4 — Mech::DuckRequest, and a census that lied in both directions
|
||||
The 2026-07-20 audit listed 8 buttons with no reconstructed handler. **Only ONE was still
|
||||
missing.** Generator on/off (`powersub.cpp`), ToggleSeekVoltage (`emitter.cpp`), EjectAmmo
|
||||
(`projweap.cpp`), ToggleCooling (`heatfamily_reslice.cpp`) and BalanceCoolant (`mech.cpp`) had all
|
||||
landed between 07-20 and 07-25 while `pod-hardware.md` and `open-questions.md` still called them
|
||||
dead. `docs/INPUT_PATH_AUDIT.md:190` had already flagged this ("census stale in both directions")
|
||||
and was right. Rule for next time: **grep the reconstruction, not the prose.**
|
||||
|
||||
`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 -- the manual's CROUCH button). Binary body, in
|
||||
full:
|
||||
|
||||
if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }
|
||||
|
||||
Press-only; sets `duckState` (`mech+0x398`, attribute 0x37). A one-shot REQUEST flag -- the
|
||||
handler never clears it and the only other writer in the entire binary is the mech reset
|
||||
(part_012.c:9439, the same reset that zeroes `incomingLock`, which is how that region was already
|
||||
mapped). Registered in `Mech::MessageHandlerEntries`; `[duck]` log on press.
|
||||
|
||||
**Why the flag has no reader, and why that is correct.** `duckState` has ZERO readers anywhere in
|
||||
the decomp. It is published as an ATTRIBUTE, so its consumer is a databinding:
|
||||
`content/GAUGE/L4GAUGE.CFG` runs `oneOfSeveralPixInt(E, ModeAlwaysActive, bduck.pcc, 3, 1,
|
||||
DuckState)` -- "crouch mode: button 4" on the map's legend column. Verified live by capturing the
|
||||
Secondary/Radar window before and after a 0x13 press: the crouch icon goes grey -> orange. So the
|
||||
handler is COMPLETE; a crouch pose invented here would be a stand-in for data we have not found
|
||||
(no SQUAT clip name survives in the decomp or in content/, only DuckServo01.wav in AUDIO1.RES).
|
||||
|
||||
Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a **107 pitch**, independently
|
||||
corroborating the map legend grid measured from pixels in Phase 3.
|
||||
|
||||
Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).
|
||||
|
||||
### Phase 5 — L4KEYLIGHT (RGB keyboard lamp mirror)
|
||||
Ported from RP412 (itself a port of vRIO's KeyboardLampMirror). Keys bound to a lamp address in
|
||||
bindings.txt glow with the panel palette -- yellow for the map's side columns (0x10-0x1F), red for
|
||||
the rest -- flashing in step with the on-screen buttons (the LampLevel copy matches the FIXED
|
||||
BTLampBrightnessOf from Phase 1). Per-key boards light each bound key; zone-lit boards mirror the
|
||||
strongest lamp board-wide. All WinRT runs on a private worker thread (watcher, claim, 100 ms paint
|
||||
loop, repaint only on change).
|
||||
|
||||
**The packing hazard did NOT transfer.** RP412 compiles this file with forced default struct
|
||||
packing because its engine is `/Zp1`, which would break the WinRT ABI. BT411's `BT_OPTS` is
|
||||
`/permissive /W0 /wd4996 /EHsc /bigobj /MP` -- no `/Zp` -- so only the dialect flags are needed:
|
||||
`set_source_files_properties(... COMPILE_OPTIONS "/std:c++17;/permissive-")`, since the project
|
||||
otherwise builds C++14 /permissive. The scalars-only interface is kept regardless.
|
||||
|
||||
Wired in `PadRIO`: map built from `bindings.keyBindings` (ActionButton binds only, first binding
|
||||
per key wins, addresses < LampCount), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands
|
||||
the LEDs back to Windows). Gate `BT_KEYLIGHT=0`.
|
||||
|
||||
Verified live: `[keylight] mirroring 25 bound key(s)` + `+ LAPTOP-767CF84B (24 zones - board-wide
|
||||
mirror)` -- it claimed this machine's real keyboard. `BT_KEYLIGHT=0` -> zero keylight lines, game
|
||||
fine. Pod profile (no PadRIO) -> zero lines. Exploded -> works.
|
||||
|
||||
### Phase 6 — the shipped controls reference
|
||||
`docs/CONTROLS.md`: the keyboard table plus **the full 72-button pod map**, grouped by the display
|
||||
each bank surrounds, with the coolant-valve detent warning and the eject/jam procedure. mkdist
|
||||
flattens it to ASCII as `CONTROLS.txt` at the zip root (the README's own flattening idiom).
|
||||
`players/README.txt` gained pointers to it, to environ.ini, to -fit, and to the RGB mirror.
|
||||
Verified: `mkdist.py` writes a 5149-byte pure-ASCII CONTROLS.txt into the zip.
|
||||
|
||||
## 2026-07-26 (last) — the keyboard becomes the button board (RP412 bindings port)
|
||||
|
||||
User decision: port RP412's bindings design and make it the DEFAULT (not an opt-in alternate),
|
||||
plus a BT-flavoured interactive controls page.
|
||||
|
||||
**The layout.** Letter and number rows = the MFD banks laid out where they sit on the panel;
|
||||
flight to the numpad so the board stays free; F1-F12 = the map's two columns; G and B unbound as
|
||||
the physical gap between the lower clusters. Expressed in BT's OWN grammar (slew/deflect/set,
|
||||
the Turn channel) -- the bindings.txt format is a compatibility surface and was not touched.
|
||||
|
||||
Coverage audit: **61 of 72 addresses on the keyboard**, and the 11 absent ones are exactly the
|
||||
addresses with no authored function (0x16/0x17/0x1E/0x1F, 0x38-0x3E). 0x19 is bound (F8) purely
|
||||
for column completeness. All 72 stay clickable on the panel.
|
||||
|
||||
The scheme lands on BT's addresses unreasonably well: 1-4 + QWER are the whole coolant system
|
||||
(Condensers 1-6, flush, balance), F6/F7 the display and control-mode cycles, F9-F12 Generators
|
||||
A-D, F4 the crouch button reconstructed earlier today.
|
||||
|
||||
**The deliberate cost.** A bound key is removed from the 1995 typed-hotkey channel, and this board
|
||||
binds nearly everything -- so 5 (Quad), z (Eng1), t/y/u/i/o (pilot select) and +/- (zoom) go.
|
||||
Documented in the file header, the markdown and the page.
|
||||
|
||||
**New rule: a bindings row WINS over a built-in convenience key.** `PadRIO::KeyHasBinding` --
|
||||
V and J/K/L are board buttons now, so the view-toggle and preset-cycle polls stand down for them
|
||||
(they poll GetAsyncKeyState directly and would otherwise have fired BOTH). View toggle lives on
|
||||
BACKTICK alone.
|
||||
|
||||
**CONTROLS.MAP mirrored** so glass/pod/dev still feel identical (the 2026-07-21 settlement):
|
||||
90 bindings, 0 parse complaints under BT_INPUT_LOG.
|
||||
|
||||
### Hat labels: 0x42 is TORSO CENTER [T1]
|
||||
INPUT_PATH_AUDIT flagged 0x41-0x44 as mislabelled. Settled from the streamed mapping
|
||||
(BT_CTRLMAP_LOG): `elem 66 -> subsys 17 attrID 14` = the TORSO subsystem, while 0x44/0x43/0x41 go
|
||||
to the mapper's look trio (attrID 10/11/12 = LookLeft/LookRight/LookBehind). The .RES itself has
|
||||
no "TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so the audit's
|
||||
wording was loose but its substance right. Swept L4GLASSWIN / L4PADPANEL / L4VB16.
|
||||
|
||||
### The controls page
|
||||
`docs/CONTROLS.html` -- interactive keyboard (hover any key for its address and meaning, with the
|
||||
MFD clusters outlined), pad diagram, panel map, the under-glass press-target figure, the radar
|
||||
placement thumbnails, and the environ.ini table. Modelled on RP412's page, rewritten for BT's
|
||||
addresses and hazards (the coolant-valve detent warning, jams and ammo-bay fires, BASIC control
|
||||
mode). mkdist wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt.
|
||||
|
||||
### Verified
|
||||
- bindings.txt regenerates and loads: 74 keys, 0 parse errors; CONTROLS.MAP 90 bindings, 0 errors.
|
||||
- 72/72 panel addresses still dispatch on click.
|
||||
- surround / exploded / dock / pod / dev: boot + simulate, zero faults.
|
||||
- Dist carries CONTROLS.txt + a 46 KB CONTROLS.html with doctype and charset.
|
||||
|
||||
@@ -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.
|
||||
+80
-114
@@ -13,6 +13,7 @@
|
||||
#include "l4glasswin.h"
|
||||
#include "l4padrio.h"
|
||||
#include "l4vb16.h"
|
||||
#include "l4riobank.h" // the shared button-bank geometry (with the surround)
|
||||
#include "../munga/gaugrend.h"
|
||||
|
||||
#include <windows.h>
|
||||
@@ -41,23 +42,17 @@ int
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Layout constants (tunable -- Cyd flagged refinements to the exact button
|
||||
// sub-positions + the yellow left/right split).
|
||||
// Window constants. The BUTTON GEOMETRY (how far a bank reaches under the
|
||||
// glass, the lamp strip, the column pitch, the per-column nudge against the
|
||||
// imagery legends) moved to L4RIOBANK on 2026-07-26 so the cockpit surround
|
||||
// draws the identical field; what is left here is this window's own chrome.
|
||||
//###########################################################################
|
||||
|
||||
enum
|
||||
{
|
||||
Margin = 0, // no padding around the edges (surface fills the window)
|
||||
TitleH = 0, // no in-client title band (the OS caption labels the window)
|
||||
CellW = 58, // default button cell (flight-controls window)
|
||||
CellW = 58, // flight-controls cell (no glass behind it)
|
||||
CellH = 28,
|
||||
RedCellH = 128, // red MFD buttons -- tall, extend ~100px into the display
|
||||
EdgeProtrude = 10, // ...and this far OUT past the display edge
|
||||
Gap = 4,
|
||||
RailW = 40, // radar side-rail button thickness (rotated 90deg, fill height)
|
||||
RailSlotAdd = 1, // rail pitch = 640/6 + 1 = 107 -> 104px buttons + 3px gaps = 639 (fits 640)
|
||||
RailGap = 3, // vertical gap between rail buttons (own value; red/horizontals use Gap)
|
||||
RedOffsetX = 5, // nudge the red MFD buttons right to line up with the imagery
|
||||
MfdSurfW = 640, MfdSurfH = 480, // mono MFD surface at native 640x480
|
||||
RadarSurfW = 480, RadarSurfH = 640, // portrait secondary/radar CRT at native (rotated 640x480)
|
||||
RepaintTimerId = 1,
|
||||
@@ -67,11 +62,6 @@ enum
|
||||
|
||||
enum ColorClass { ClrRed, ClrYellow, ClrBlue };
|
||||
|
||||
// Per-column horizontal nudge (px) for the 4 red MFD buttons, left->right, to line
|
||||
// them up with the (unevenly-spaced) imagery legends. Tunable -- Cyd is dialing
|
||||
// these in against DISPLAY / PROGRAM / etc. Applied on top of RedOffsetX.
|
||||
static const int kRedColDX[4] = { -5, -4, 0, 0 };
|
||||
|
||||
struct GButton
|
||||
{
|
||||
int address;
|
||||
@@ -121,8 +111,8 @@ struct NamedButton { int address; const char *name; };
|
||||
static const NamedButton namedButtons[] =
|
||||
{
|
||||
{ 0x3D, "Panic" }, { 0x3F, "Throttle" },
|
||||
{ 0x40, "Main" }, { 0x41, "Hat Bk" }, { 0x42, "Hat Up" },
|
||||
{ 0x43, "Hat R" }, { 0x44, "Hat L" }, { 0x45, "Pinky" },
|
||||
{ 0x40, "Main" }, { 0x41, "Look Bk" }, { 0x42, "Torso Ctr" },
|
||||
{ 0x43, "Look R" }, { 0x44, "Look L" }, { 0x45, "Pinky" },
|
||||
{ 0x46, "Middle"}, { 0x47, "Upper" },
|
||||
};
|
||||
enum { NamedButtonCount = sizeof(namedButtons) / sizeof(namedButtons[0]) };
|
||||
@@ -137,71 +127,53 @@ static const char *
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Lamp decode (identical to L4PADPANEL::LampBrightnessOf -- RIOBase::LampState)
|
||||
// Lamp decode: the ONE copy lives in l4vb16.h (BTLampBrightnessOf). This TU
|
||||
// used to carry its own, as did L4PADPANEL -- three copies of the same
|
||||
// formula, all three carrying the same dim-to-bright flash bug (see the
|
||||
// header's 2026-07-26 note).
|
||||
//###########################################################################
|
||||
|
||||
static int
|
||||
LampBrightnessOf(int state, unsigned long tick)
|
||||
{
|
||||
int level1 = (state >> 2) & 0x3;
|
||||
int level2 = (state >> 4) & 0x3;
|
||||
int level = (level1 > level2) ? level1 : level2;
|
||||
if (level == 0)
|
||||
return 0;
|
||||
int flash = state & 0x3;
|
||||
if (flash != 0)
|
||||
{
|
||||
unsigned long half_period = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
if ((tick / half_period) & 1)
|
||||
return 0;
|
||||
}
|
||||
return level;
|
||||
return BTLampBrightnessOf(state, tick);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Layout builders
|
||||
//###########################################################################
|
||||
|
||||
static void
|
||||
PlaceCellAt(GWin &w, int address, int x, int y, ColorClass color)
|
||||
{
|
||||
GButton &b = w.buttons[w.buttonCount++];
|
||||
b.address = address;
|
||||
b.color = color;
|
||||
b.rect.left = x + 2;
|
||||
b.rect.top = y + 2;
|
||||
b.rect.right = x + CellW - 2;
|
||||
b.rect.bottom = y + CellH - 2;
|
||||
}
|
||||
// (The local PlaceCellAt / PlaceLine / PlaceRect placers retired 2026-07-26 --
|
||||
// the geometry they carried now comes from L4RIOBANK, shared with the cockpit
|
||||
// surround. AdoptBank below is the only thing that writes GButton rects.)
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// Copy a laid-out bank into this window at the given offset. A glass window
|
||||
// passes -bounds so the bank's bounding box lands at the client origin (the
|
||||
// bank reaches OUTSIDE the display, so its rects arrive with coordinates
|
||||
// left of / above it); the flight window, which has no glass, passes 0.
|
||||
//---------------------------------------------------------------------------
|
||||
static void
|
||||
PlaceLine(GWin &w, int startAddr, int count, int step,
|
||||
int x, int y, int horizontal, ColorClass color)
|
||||
AdoptBank(GWin &w, const BTRioBank &bank, ColorClass color, int dx, int dy)
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
for (int i = 0; i < bank.buttonCount &&
|
||||
w.buttonCount < (int)(sizeof(w.buttons) / sizeof(w.buttons[0])); ++i)
|
||||
{
|
||||
int cx = horizontal ? (x + i * CellW) : x;
|
||||
int cy = horizontal ? y : (y + i * CellH);
|
||||
PlaceCellAt(w, startAddr + i * step, cx, cy, color);
|
||||
const BTRioButton &src = bank.buttons[i];
|
||||
GButton &b = w.buttons[w.buttonCount++];
|
||||
b.address = src.address;
|
||||
b.color = (src.colorClass == 1) ? ClrYellow
|
||||
: (src.colorClass == 2) ? ClrBlue : color;
|
||||
b.rect.left = src.x + dx;
|
||||
b.rect.top = src.y + dy;
|
||||
b.rect.right = src.x + dx + src.w;
|
||||
b.rect.bottom = src.y + dy + src.h;
|
||||
}
|
||||
}
|
||||
|
||||
// Place a button with an explicit size (custom-shaped cells: wide red buttons,
|
||||
// tall rotated radar rails).
|
||||
static void
|
||||
PlaceRect(GWin &w, int address, int x, int y, int cw, int ch, ColorClass color)
|
||||
{
|
||||
GButton &b = w.buttons[w.buttonCount++];
|
||||
b.address = address;
|
||||
b.color = color;
|
||||
b.rect.left = x;
|
||||
b.rect.top = y;
|
||||
b.rect.right = x + cw;
|
||||
b.rect.bottom = y + ch;
|
||||
}
|
||||
|
||||
// An MFD display: 8 red buttons split 4 above / 4 below the surface (top row =
|
||||
// the high 4 addresses descending, matching the L4PADPANEL cluster order).
|
||||
// Geometry is L4RIOBANK's -- the under-glass rule, shared with the surround.
|
||||
static void
|
||||
BuildMfd(GWin &w, const char *title, const char *portP, const char *portA, int bankHi)
|
||||
{
|
||||
@@ -212,23 +184,19 @@ static void
|
||||
w.monoTint = MfdMonoTint;
|
||||
w.rotate = 0;
|
||||
|
||||
int surfX = Margin;
|
||||
int surfY = Margin + TitleH + EdgeProtrude; // room for the top buttons to poke out
|
||||
BTRioBankMetrics metrics;
|
||||
BTRioBankMetricsFor(MfdSurfW, MfdSurfH, &metrics);
|
||||
|
||||
// Buttons OVERLAY the display: 4 tile the full width, reaching RedCellH INTO the
|
||||
// display and EdgeProtrude OUT past the edge (top row + bottom row; hi addresses
|
||||
// descending -- L4PADPANEL order).
|
||||
int slotW = MfdSurfW / 4;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int x = surfX + i * slotW + RedOffsetX + kRedColDX[i];
|
||||
PlaceRect(w, bankHi - i, x, surfY - EdgeProtrude, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // top
|
||||
PlaceRect(w, bankHi - 4 - i, x, surfY + MfdSurfH - RedCellH, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // bottom
|
||||
}
|
||||
SetRect(&w.surfaceRect, surfX, surfY, surfX + MfdSurfW, surfY + MfdSurfH);
|
||||
BTRioBank bank;
|
||||
BTRioBankLayout(BTRioBankMfd, 0, 0, MfdSurfW, MfdSurfH,
|
||||
bankHi, 0, &metrics, 0, &bank);
|
||||
|
||||
w.clientW = Margin + MfdSurfW + Margin;
|
||||
w.clientH = surfY + MfdSurfH + EdgeProtrude + Margin;
|
||||
BTRioBankDump(title, &bank);
|
||||
int dx = -bank.boundsX, dy = -bank.boundsY;
|
||||
AdoptBank(w, bank, ClrRed, dx, dy);
|
||||
SetRect(&w.surfaceRect, dx, dy, dx + MfdSurfW, dy + MfdSurfH);
|
||||
w.clientW = bank.boundsW;
|
||||
w.clientH = bank.boundsH;
|
||||
}
|
||||
|
||||
// The secondary/radar display: 12 yellow Secondary buttons split left / right
|
||||
@@ -243,37 +211,23 @@ static void
|
||||
w.monoTint = -1; // palette
|
||||
w.rotate = gRadarRot;
|
||||
|
||||
int surfX = Margin + EdgeProtrude; // room for the left rail to poke out
|
||||
int surfY = Margin + TitleH;
|
||||
|
||||
// Rails: big clickable strips that reach RailW INTO the display (hidden under the
|
||||
// imagery) and poke EdgeProtrude OUT past the side edge (the lamp light).
|
||||
// Left 0x10..0x15, right 0x18 (top)..0x1D (bottom). (0x15 kept on the left rail
|
||||
// -- Cyd listed 0x10-0x14; move it if wanted.)
|
||||
int railCount = 6;
|
||||
int slotH = RadarSurfH / railCount + RailSlotAdd;
|
||||
int leftX = surfX - EdgeProtrude;
|
||||
int rightX = surfX + RadarSurfW - RailW;
|
||||
for (int i = 0; i < railCount; ++i)
|
||||
{
|
||||
int y = surfY + i * slotH;
|
||||
PlaceRect(w, 0x10 + i, leftX, y, EdgeProtrude + RailW, slotH - RailGap, ClrYellow);
|
||||
PlaceRect(w, 0x18 + i, rightX, y, RailW + EdgeProtrude, slotH - RailGap, ClrYellow);
|
||||
}
|
||||
SetRect(&w.surfaceRect, surfX, surfY, surfX + RadarSurfW, surfY + RadarSurfH);
|
||||
|
||||
// Bottom row: 16, 17, 1F, 1E (left to right), BETWEEN the rails -- reaches CellH
|
||||
// up into the display, pokes EdgeProtrude out the bottom.
|
||||
// Left column 0x10-0x15, right 0x18-0x1D, foot row 0x16/0x17/0x1F/0x1E.
|
||||
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
|
||||
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
|
||||
int botLeft = surfX + RailW;
|
||||
int botSpan = RadarSurfW - 2 * RailW;
|
||||
int botW = botSpan / 4;
|
||||
int botY = surfY + RadarSurfH - CellH;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
PlaceRect(w, bottomAddrs[i], botLeft + i * botW, botY, botW - Gap, CellH + EdgeProtrude, ClrYellow);
|
||||
|
||||
w.clientW = surfX + RadarSurfW + EdgeProtrude + Margin;
|
||||
w.clientH = surfY + RadarSurfH + EdgeProtrude + Margin;
|
||||
BTRioBankMetrics metrics;
|
||||
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
|
||||
|
||||
BTRioBank bank;
|
||||
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
|
||||
0x10, 0x18, &metrics, bottomAddrs, &bank);
|
||||
|
||||
BTRioBankDump("Secondary / Radar", &bank);
|
||||
int dx = -bank.boundsX, dy = -bank.boundsY;
|
||||
AdoptBank(w, bank, ClrYellow, dx, dy);
|
||||
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
|
||||
w.clientW = bank.boundsW;
|
||||
w.clientH = bank.boundsH;
|
||||
}
|
||||
|
||||
// The Flight Controls window: the no-display banks -- throttle/panic/door/icom
|
||||
@@ -286,15 +240,27 @@ static void
|
||||
w.portPrimary = NULL;
|
||||
w.showLabels = 1; // the only window that shows hex/name on its buttons
|
||||
|
||||
int topY = Margin + TitleH;
|
||||
int col1 = Margin;
|
||||
int col2 = Margin + CellW + Gap;
|
||||
// No glass to hide behind, so these cells carry their own size (see
|
||||
// l4riobank.h) -- two 1-wide columns, eight rows each. Cell minus the
|
||||
// 4px gap keeps the historical CellW/CellH PITCH and the 2px inset the
|
||||
// old PlaceCellAt drew, so the window is pixel-identical.
|
||||
BTRioBankMetrics metrics;
|
||||
metrics.strip = 0;
|
||||
metrics.gap = Gap;
|
||||
metrics.cellW = CellW - Gap;
|
||||
metrics.cellH = CellH - Gap;
|
||||
|
||||
PlaceLine(w, 0x38, 8, +1, col1, topY, 0, ClrBlue); // throttle / panic / door / icom
|
||||
PlaceLine(w, 0x40, 8, +1, col2, topY, 0, ClrBlue); // joystick + fire buttons
|
||||
BTRioBank bank;
|
||||
memset(&bank, 0, sizeof(bank));
|
||||
BTRioBankFlightGrid(2, 2, 0x38, 8, 1, 0, 0, &metrics, &bank); // throttle/panic/door/icom
|
||||
BTRioBankFlightGrid(CellW + Gap + 2, 2, 0x40, 8, 1, 0, 0, &metrics, &bank); // joystick + fire
|
||||
|
||||
w.clientW = col2 + CellW + Margin;
|
||||
w.clientH = topY + 8 * CellH + Margin;
|
||||
BTRioBankDump("Flight Controls", &bank);
|
||||
AdoptBank(w, bank, ClrBlue, 0, 0);
|
||||
// no translation here (the cells are already at their 2px inset); the
|
||||
// client is the historical two-column extent
|
||||
w.clientW = CellW + Gap + CellW;
|
||||
w.clientH = 8 * CellH;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
//===========================================================================//
|
||||
// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror.
|
||||
//
|
||||
// Ported from RP412's L4KEYLIGHT (itself a port of vRIO's
|
||||
// KeyboardLampMirror). Keys bound to a lamp address in bindings.txt glow
|
||||
// with the panel palette, so the keyboard reads as the pod's button field.
|
||||
//
|
||||
// COMPILED APART FROM THE REST: /std:c++17 + conformance mode, per-file in
|
||||
// CMakeLists (C++/WinRT needs both; the project builds C++14 /permissive).
|
||||
// NOTE: RP412 also had to force DEFAULT struct packing here because its
|
||||
// engine compiles /Zp1, which would break the WinRT ABI -- BT411 does NOT
|
||||
// set /Zp (checked 2026-07-26: BT_OPTS is /permissive /W0 /wd4996 /EHsc
|
||||
// /bigobj /MP), so that hazard does not apply. The scalars-only interface
|
||||
// in l4keylight.h is kept anyway: it costs nothing and keeps this TU
|
||||
// isolated if packing is ever added.
|
||||
//
|
||||
// Everything WinRT runs on a private worker thread: device watcher,
|
||||
// keyboard claiming, and a 100 ms paint loop that mirrors the lamp bytes
|
||||
// (the same flash formula as BTLampBrightnessOf, so the board and the
|
||||
// on-screen cockpit buttons blink in step).
|
||||
//===========================================================================//
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Devices.Enumeration.h>
|
||||
#include <winrt/Windows.Devices.Lights.h>
|
||||
#include <winrt/Windows.System.h>
|
||||
#include <winrt/Windows.UI.h>
|
||||
|
||||
#pragma comment(lib, "windowsapp.lib")
|
||||
|
||||
#include "l4keylight.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace winrt;
|
||||
using namespace winrt::Windows::Devices::Enumeration;
|
||||
using namespace winrt::Windows::Devices::Lights;
|
||||
using winrt::Windows::UI::Color;
|
||||
using winrt::Windows::System::VirtualKey;
|
||||
|
||||
struct KeyEntry
|
||||
{
|
||||
int virtualKey;
|
||||
int address;
|
||||
bool yellow;
|
||||
};
|
||||
|
||||
struct ClaimedArray
|
||||
{
|
||||
hstring id;
|
||||
LampArray array{ nullptr };
|
||||
bool perKey = false;
|
||||
bool baseCoated = false;
|
||||
};
|
||||
|
||||
std::mutex gLock;
|
||||
std::vector<KeyEntry> gMap;
|
||||
unsigned char gLamps[64] = {};
|
||||
void (*gLogger)(const char *) = nullptr;
|
||||
|
||||
std::atomic<bool> gRunning{ false };
|
||||
std::thread gWorker;
|
||||
|
||||
void Log(const char *line)
|
||||
{
|
||||
void (*logger)(const char *);
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
logger = gLogger;
|
||||
}
|
||||
if (logger != nullptr)
|
||||
{
|
||||
logger(line);
|
||||
}
|
||||
}
|
||||
|
||||
void Logf(const char *format, ...)
|
||||
{
|
||||
char line[256];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
_vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args);
|
||||
va_end(args);
|
||||
Log(line);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Lamp byte -> brightness level, animating the flash modes.
|
||||
// MUST match BTLampBrightnessOf (l4vb16.h) exactly, so the board and
|
||||
// the on-screen buttons blink together: solid shows state 1, flashing
|
||||
// ALTERNATES state 1 and state 2 (RIO::LampState, L4RIO.h). Copied
|
||||
// rather than included because this TU takes no engine headers.
|
||||
//---------------------------------------------------------------
|
||||
int LampLevel(int lamp_state)
|
||||
{
|
||||
int mode = lamp_state & 0x03;
|
||||
int level1 = (lamp_state >> 2) & 0x03;
|
||||
int level2 = (lamp_state >> 4) & 0x03;
|
||||
|
||||
if (mode == 0)
|
||||
{
|
||||
return level1;
|
||||
}
|
||||
|
||||
static const int half_period[4] = { 0, 500, 250, 125 };
|
||||
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The panel palette (vRIO's KeyboardLampMirror shades): red for
|
||||
// the banks, yellow for Secondary/Screen; the off shade keeps the
|
||||
// bound keys faintly visible so the board reads as a button field.
|
||||
//---------------------------------------------------------------
|
||||
Color Shade(int level, bool yellow)
|
||||
{
|
||||
Color color;
|
||||
color.A = 255;
|
||||
if (yellow)
|
||||
{
|
||||
if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; }
|
||||
else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; }
|
||||
else { color.R = 70; color.G = 60; color.B = 24; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; }
|
||||
else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; }
|
||||
else { color.R = 64; color.G = 40; color.B = 40; }
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
bool SameColor(const Color &a, const Color &b)
|
||||
{
|
||||
return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The worker: watcher + claim + paint loop
|
||||
//---------------------------------------------------------------
|
||||
void Worker()
|
||||
{
|
||||
try
|
||||
{
|
||||
init_apartment();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// apartment already set on this thread; carry on
|
||||
}
|
||||
|
||||
std::mutex claimedLock;
|
||||
std::vector<ClaimedArray> claimed;
|
||||
bool anySeen = false;
|
||||
|
||||
DeviceWatcher watcher{ nullptr };
|
||||
try
|
||||
{
|
||||
watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector());
|
||||
|
||||
watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info)
|
||||
{
|
||||
try
|
||||
{
|
||||
LampArray array = LampArray::FromIdAsync(info.Id()).get();
|
||||
if (array == nullptr ||
|
||||
array.LampArrayKind() != LampArrayKind::Keyboard)
|
||||
{
|
||||
return; // mice / strips / cases stay untouched
|
||||
}
|
||||
anySeen = true;
|
||||
ClaimedArray entry;
|
||||
entry.id = info.Id();
|
||||
entry.array = array;
|
||||
entry.perKey = array.SupportsVirtualKeys();
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
claimed.push_back(entry);
|
||||
}
|
||||
Logf(entry.perKey
|
||||
? "KeyLight: + %ls (%d LEDs, per-key)"
|
||||
: "KeyLight: + %ls (%d zones - board-wide mirror)",
|
||||
info.Name().c_str(), (int) array.LampCount());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Log("KeyLight: could not open a lamp array");
|
||||
}
|
||||
});
|
||||
watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update)
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
for (size_t i = 0; i < claimed.size(); ++i)
|
||||
{
|
||||
if (claimed[i].id == update.Id())
|
||||
{
|
||||
claimed.erase(claimed.begin() + i);
|
||||
Log("KeyLight: keyboard disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &)
|
||||
{
|
||||
// required for the watcher to progress
|
||||
});
|
||||
watcher.Start();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Log("KeyLight: Dynamic Lighting unavailable on this system");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Paint loop: 100 ms cadence, repaint only on change
|
||||
//
|
||||
std::vector<Color> lastColors;
|
||||
int waited = 0;
|
||||
while (gRunning.load())
|
||||
{
|
||||
Sleep(50);
|
||||
waited += 50;
|
||||
if (waited < 100)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
waited = 0;
|
||||
|
||||
std::vector<KeyEntry> map;
|
||||
unsigned char lamps[64];
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
map = gMap;
|
||||
memcpy(lamps, gLamps, sizeof(lamps));
|
||||
}
|
||||
|
||||
std::vector<Color> colors(map.size());
|
||||
std::vector<VirtualKey> keys(map.size());
|
||||
int bestLevel = 0;
|
||||
bool bestYellow = false;
|
||||
bool changed = (lastColors.size() != map.size());
|
||||
for (size_t i = 0; i < map.size(); ++i)
|
||||
{
|
||||
int address = map[i].address;
|
||||
int level = (address >= 0 && address < 64)
|
||||
? LampLevel(lamps[address]) : 0;
|
||||
if (level > bestLevel)
|
||||
{
|
||||
bestLevel = level;
|
||||
bestYellow = map[i].yellow;
|
||||
}
|
||||
colors[i] = Shade(level, map[i].yellow);
|
||||
keys[i] = (VirtualKey) map[i].virtualKey;
|
||||
if (!changed && !SameColor(colors[i], lastColors[i]))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
bool freshClaim = false;
|
||||
for (ClaimedArray &entry : claimed)
|
||||
{
|
||||
if (!entry.baseCoated)
|
||||
{
|
||||
freshClaim = true;
|
||||
}
|
||||
}
|
||||
if (!changed && !freshClaim)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Color aggregate = Shade(bestLevel, bestYellow);
|
||||
for (ClaimedArray &entry : claimed)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (entry.perKey)
|
||||
{
|
||||
if (!entry.baseCoated)
|
||||
{
|
||||
Color black;
|
||||
black.A = 255; black.R = 0; black.G = 0; black.B = 0;
|
||||
entry.array.SetColor(black);
|
||||
entry.baseCoated = true;
|
||||
}
|
||||
if (!map.empty())
|
||||
{
|
||||
entry.array.SetColorsForKeys(
|
||||
array_view<Color const>(colors.data(), colors.data() + colors.size()),
|
||||
array_view<VirtualKey const>(keys.data(), keys.data() + keys.size()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.baseCoated = true;
|
||||
entry.array.SetColor(aggregate);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// device wobble; the watcher handles removal
|
||||
}
|
||||
}
|
||||
lastColors = colors;
|
||||
}
|
||||
|
||||
//
|
||||
// Releasing the arrays hands the LEDs back to Windows
|
||||
//
|
||||
try
|
||||
{
|
||||
watcher.Stop();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(claimedLock);
|
||||
claimed.clear();
|
||||
}
|
||||
if (!anySeen)
|
||||
{
|
||||
Log("KeyLight: no Dynamic Lighting keyboard was found this session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
// The scalar interface (safe across the packing boundary)
|
||||
//########################################################################
|
||||
|
||||
void
|
||||
KeyLight_SetLogger(void (*logger)(const char *line))
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
gLogger = logger;
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_SetMap(
|
||||
const int *virtual_keys,
|
||||
const int *addresses,
|
||||
const unsigned char *yellow,
|
||||
int count
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
gMap.clear();
|
||||
gMap.reserve(count);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
KeyEntry entry;
|
||||
entry.virtualKey = virtual_keys[i];
|
||||
entry.address = addresses[i];
|
||||
entry.yellow = (yellow[i] != 0);
|
||||
gMap.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count)
|
||||
{
|
||||
if (count > 64)
|
||||
{
|
||||
count = 64;
|
||||
}
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
memcpy(gLamps, lamp_state, count);
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_Start()
|
||||
{
|
||||
if (gRunning.exchange(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
gWorker = std::thread(Worker);
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_Stop()
|
||||
{
|
||||
if (!gRunning.exchange(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (gWorker.joinable())
|
||||
{
|
||||
gWorker.join();
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
@@ -161,20 +195,35 @@ static int
|
||||
// the 12 secondary-panel buttons, 0x00-0x0F the lower aux banks, 0x20-0x37
|
||||
// the upper aux banks. Keypad unit 0 = the pilot's MFD keypad.
|
||||
//
|
||||
// KEYBOARD RECONCILIATION (2026-07-20, input-coverage audit): the keyboard
|
||||
// carries the ~20 core gameplay actions on the SAME keys the desktop
|
||||
// CONTROLS.MAP uses (W/S/A/D drive, Q/E twist, R/F elevation, X all-stop,
|
||||
// 1-4/Space/Ctrl fire, M/N mode/display cycle, H flush, C valve, B look
|
||||
// behind, G configure, V view); FULL pod-address coverage lives on the
|
||||
// on-screen panel (every one of the 72 buttons is one click away,
|
||||
// right-click = hold latch). Keys DISPLACED from the previous default
|
||||
// (each still reachable by editing bindings.txt or on the panel):
|
||||
// - number row 1-0/-/= -> secondary-panel 0x10-0x1D (1-4 now fire)
|
||||
// - QWERTY row Q..P -> pilot MFD keypad unit 0 (dropped)
|
||||
// - arrow keys -> hat looks 0x41-0x44 (arrows now drive)
|
||||
// - V/C/B -> fire 0x47/0x46/0x45 (now view/valve/behind)
|
||||
// - LCTRL -> throttle slew down (now fire 0x47)
|
||||
// The XInput pad section is unchanged.
|
||||
// THE KEYBOARD IS THE BUTTON BOARD (2026-07-26, ported from RP412).
|
||||
//
|
||||
// Every one of the pod's 72 buttons is reachable from the keyboard, laid out
|
||||
// WHERE IT SITS ON THE PANEL: the number row is the upper MFDs' top row and
|
||||
// QWERTY the row beneath it, running left to right across Heat / Engineering /
|
||||
// Comm; the home row and the row below it do the same for the two lower
|
||||
// weapon MFDs; F1-F12 are the two columns flanking the map. G and B stay
|
||||
// unbound on purpose -- they are the physical gap between the lower clusters.
|
||||
// Flight moves to the NUMPAD (which already carried it as an alternative), so
|
||||
// the letter field is free for the board.
|
||||
//
|
||||
// It lands on BT's own addresses better than it has any right to: 1-4 and
|
||||
// QWER are the Heat display's two rows, which is the entire coolant system
|
||||
// (Condensers 1-6, the flush and the balance button); F6/F7 are the display
|
||||
// and control-mode cycles; F9-F12 are Generators A-D.
|
||||
//
|
||||
// WHAT THIS COSTS (the deliberate trade). A bound key is REMOVED from the
|
||||
// authentic 1995 typed-hotkey channel to stop double-dispatch, and this board
|
||||
// binds nearly the whole field -- so the typed hotkeys go: 5 = MFD1 Quad page,
|
||||
// z = MFD3 Eng1, t/y/u/i/o = pilot select 3-7, +/- = target zoom. They remain
|
||||
// reachable on the panel and by editing this file. The built-in convenience
|
||||
// keys yield the same way: V and J/K/L are board buttons now, so PadRIO's
|
||||
// view-toggle and preset-cycle polls stand down for them (KeyHasBinding) and
|
||||
// the view toggle lives on BACKTICK alone.
|
||||
//
|
||||
// PREVIOUS DEFAULT (2026-07-20 - 2026-07-26) was the ~20-action CONTROLS.MAP
|
||||
// mirror: W/S/A/D drive, Q/E twist, R/F elevation, 1-4/Space/Ctrl fire,
|
||||
// M/N/H/C/G systems. An existing bindings.txt is never overwritten, so a
|
||||
// player only meets this board by deleting theirs.
|
||||
//###########################################################################
|
||||
|
||||
static const char *defaultProfileText =
|
||||
@@ -187,71 +236,134 @@ 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"
|
||||
"# Pad buttons: A B X Y LB RB BACK START LS RS DPAD_* Axes: LX LY RX RY LT RT\n"
|
||||
"#\n"
|
||||
"# LAYOUT: the keyboard hosts the core gameplay actions on the desktop\n"
|
||||
"# CONTROLS.MAP keys; the on-screen cockpit panel covers EVERY pod button\n"
|
||||
"# by click (right-click = press-and-hold latch). A key bound here is\n"
|
||||
"# REMOVED from the authentic 1995 typed-hotkey channel (no double\n"
|
||||
"# dispatch); unbound keys keep their 1995 meaning -- 5 = MFD1 Quad page,\n"
|
||||
"# z = MFD3 Eng1 page, t/y/u/i/o = pilot select 3-7, + / - = target zoom.\n"
|
||||
"# Hardcoded (not rebindable here): ` or V = view toggle, J/K/L = cycle\n"
|
||||
"# the Mfd1/Mfd2/Mfd3 preset page.\n"
|
||||
"# LAYOUT: THE KEYBOARD IS THE POD'S BUTTON BOARD. The letter and number\n"
|
||||
"# rows are the MFD button banks laid out where they sit on the panel, and\n"
|
||||
"# flight lives on the numpad so the board stays free. Every one of the 72\n"
|
||||
"# pod buttons is also one mouse click away on the cockpit itself\n"
|
||||
"# (right-click = press-and-hold latch).\n"
|
||||
"#\n"
|
||||
"# A key bound here is REMOVED from the authentic 1995 typed-hotkey channel\n"
|
||||
"# so it cannot double-dispatch -- and this board binds nearly everything,\n"
|
||||
"# so those hotkeys (5 = MFD1 Quad, z = MFD3 Eng1, t/y/u/i/o = pilot select,\n"
|
||||
"# +/- = target zoom) are given up by design. Unbind a key here to get its\n"
|
||||
"# 1995 meaning back. Same rule for the built-ins: BACKTICK = view toggle\n"
|
||||
"# (V is a board button now), and J/K/L cycle the Mfd1/2/3 preset pages ONLY\n"
|
||||
"# while unbound.\n"
|
||||
"\n"
|
||||
"# --- drive: W/S sweep the throttle lever (sticks; X = all-stop detent),\n"
|
||||
"# --- A/D spring the turn pedals, Q/E twist the torso, R/F aim it up/down\n"
|
||||
"key W axis Throttle slew + 0.7\n"
|
||||
"key S axis Throttle slew - 0.7\n"
|
||||
"key UP axis Throttle slew + 0.7\n"
|
||||
"key DOWN axis Throttle slew - 0.7\n"
|
||||
"key A axis LeftPedal deflect + 2.5\n"
|
||||
"key D axis RightPedal deflect + 2.5\n"
|
||||
"key LEFT axis LeftPedal deflect + 2.5\n"
|
||||
"key RIGHT axis RightPedal deflect + 2.5\n"
|
||||
"key Q axis JoystickX deflect - 2.5\n"
|
||||
"key E axis JoystickX deflect + 2.5\n"
|
||||
"key R axis JoystickY deflect + 2.5\n"
|
||||
"key F axis JoystickY deflect - 2.5\n"
|
||||
"key X axis Throttle set 0\n"
|
||||
"\n"
|
||||
"# --- numpad flight cluster (alternative stick/pedals/lever) ---\n"
|
||||
"# --- flight: the numpad ------------------------------------------------\n"
|
||||
"# 8/2/4/6 stick, 7/9 pedals, 5 all-stop detent, 0 main trigger.\n"
|
||||
"# SHIFT/CTRL walk the throttle lever and it STICKS where you leave it;\n"
|
||||
"# ALT is the throttle handle's reverse-thrust button.\n"
|
||||
"key NUMPAD8 axis JoystickY deflect + 2.5\n"
|
||||
"key NUMPAD2 axis JoystickY deflect - 2.5\n"
|
||||
"key NUMPAD4 axis JoystickX deflect - 2.5\n"
|
||||
"key NUMPAD6 axis JoystickX deflect + 2.5\n"
|
||||
"key NUMPAD7 axis LeftPedal deflect + 2.5\n"
|
||||
"key NUMPAD9 axis RightPedal deflect + 2.5\n"
|
||||
"key LSHIFT axis Throttle slew + 0.7\n"
|
||||
"key NUMPAD5 axis Throttle set 0\n"
|
||||
"\n"
|
||||
"# --- fire groups (the joystick's four mappable buttons) + reverse + look\n"
|
||||
"key 1 button 0x40\n"
|
||||
"key SPACE button 0x40\n"
|
||||
"key 2 button 0x46\n"
|
||||
"key 3 button 0x47\n"
|
||||
"key LCTRL button 0x47\n"
|
||||
"key RCTRL button 0x47\n"
|
||||
"key 4 button 0x45\n"
|
||||
"key LSHIFT axis Throttle slew + 0.7\n"
|
||||
"key RSHIFT axis Throttle slew + 0.7\n"
|
||||
"key LCTRL axis Throttle slew - 0.7\n"
|
||||
"key RCTRL axis Throttle slew - 0.7\n"
|
||||
"key LALT button 0x3F\n"
|
||||
"key B button 0x41\n"
|
||||
"key RALT button 0x3F\n"
|
||||
"\n"
|
||||
"# --- systems (pod console buttons) ---\n"
|
||||
"key M button 0x18 # cycle control mode (BAS/MID/ADV)\n"
|
||||
"key N button 0x15 # cycle secondary schematic (Damage/Critical/Heat)\n"
|
||||
"key H button 0x2C # HOLD = coolant flush (Reservoir InjectCoolant)\n"
|
||||
"key C button 0x2F # cycle Condenser1 coolant valve (MoveValve)\n"
|
||||
"key G button 0x0E # HOLD + fire key = regroup weapon 1 (Mfd1 Quad page)\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"
|
||||
"key NUMPAD3 button 0x46 # middle thumb\n"
|
||||
"key NUMPADDOT button 0x47 # upper thumb\n"
|
||||
"\n"
|
||||
"# --- Mfd2 bank on F5-F8 + F9 (page-gated: Quad page = select the Eng\n"
|
||||
"# --- pages, Eng page = generator A-D select / F9 gen-mode toggle) ---\n"
|
||||
"key F5 button 0x27\n"
|
||||
"key F6 button 0x26\n"
|
||||
"key F7 button 0x25\n"
|
||||
"key F8 button 0x24\n"
|
||||
"key F9 button 0x22\n"
|
||||
"# --- the hat: look around, on the arrows -------------------------------\n"
|
||||
"key UP button 0x42 # torso CENTER (the shipped .RES name)\n"
|
||||
"key DOWN button 0x41 # look behind\n"
|
||||
"key LEFT button 0x44 # look left\n"
|
||||
"key RIGHT button 0x43 # look right\n"
|
||||
"\n"
|
||||
"# --- UPPER MFD banks: number row on top, QWERTY beneath ----------------\n"
|
||||
"# Heat / coolant (0x28-0x2F) -- 1-4 and QWER ARE the coolant system:\n"
|
||||
"# 1/2/3 Condenser 1-3 valve, 4 coolant FLUSH (hold), Q/W/E Condenser\n"
|
||||
"# 4-6 valve, R balance coolant. The valve detents run 1-5-50-CLOSED.\n"
|
||||
"key 1 button 0x2F\n"
|
||||
"key 2 button 0x2E\n"
|
||||
"key 3 button 0x2D\n"
|
||||
"key 4 button 0x2C\n"
|
||||
"key Q button 0x2B\n"
|
||||
"key W button 0x2A\n"
|
||||
"key E button 0x29\n"
|
||||
"key R button 0x28\n"
|
||||
"# Engineering / Mfd2 (0x20-0x27) -- page-gated: Quad page picks an Eng\n"
|
||||
"# page, Eng page drives generator select A-D / bus mode / coolant.\n"
|
||||
"key 5 button 0x27\n"
|
||||
"key 6 button 0x26\n"
|
||||
"key 7 button 0x25\n"
|
||||
"key 8 button 0x24\n"
|
||||
"key T button 0x23\n"
|
||||
"key Y button 0x22\n"
|
||||
"key U button 0x21\n"
|
||||
"key I button 0x20\n"
|
||||
"# Comm / target hotbox (0x30-0x37) -- pilot select.\n"
|
||||
"key 9 button 0x37\n"
|
||||
"key 0 button 0x36\n"
|
||||
"key MINUS button 0x35\n"
|
||||
"key EQUALS button 0x34\n"
|
||||
"key O button 0x33\n"
|
||||
"key P button 0x32\n"
|
||||
"key LBRACKET button 0x31\n"
|
||||
"key RBRACKET button 0x30\n"
|
||||
"\n"
|
||||
"# --- LOWER MFD banks: home row on top, the row below beneath -----------\n"
|
||||
"# G and B are the PANEL GAP between the two lower clusters -- left\n"
|
||||
"# unbound on purpose, so they keep their 1995 typed meaning.\n"
|
||||
"# Left Weapons / Mfd1 (0x08-0x0F).\n"
|
||||
"key A button 0x0F\n"
|
||||
"key S button 0x0E\n"
|
||||
"key D button 0x0D\n"
|
||||
"key F button 0x0C\n"
|
||||
"key Z button 0x0B\n"
|
||||
"key X button 0x0A\n"
|
||||
"key C button 0x09\n"
|
||||
"key V button 0x08\n"
|
||||
"# Right Weapons / Mfd3 (0x00-0x07).\n"
|
||||
"key H button 0x07\n"
|
||||
"key J button 0x06\n"
|
||||
"key K button 0x05\n"
|
||||
"key L button 0x04\n"
|
||||
"key N button 0x03\n"
|
||||
"key M button 0x02\n"
|
||||
"key COMMA button 0x01\n"
|
||||
"key PERIOD button 0x00\n"
|
||||
"\n"
|
||||
"# --- the two columns flanking the map, top to bottom -------------------\n"
|
||||
"# Left (0x10-0x15): map zoom in/out, thermal IR, CROUCH, searchlight,\n"
|
||||
"# cycle secondary display. Right (0x18-0x1D): cycle control mode\n"
|
||||
"# (BAS/MID/ADV), unused, Generator A-D on/off.\n"
|
||||
"key F1 button 0x10\n"
|
||||
"key F2 button 0x11\n"
|
||||
"key F3 button 0x12\n"
|
||||
"key F4 button 0x13\n"
|
||||
"key F5 button 0x14\n"
|
||||
"key F6 button 0x15\n"
|
||||
"key F7 button 0x18\n"
|
||||
"key F8 button 0x19\n"
|
||||
"key F9 button 0x1A\n"
|
||||
"key F10 button 0x1B\n"
|
||||
"key F11 button 0x1C\n"
|
||||
"key F12 button 0x1D\n"
|
||||
"\n"
|
||||
"# --- XInput pad ---\n"
|
||||
"# POD-FAITHFUL (default): stick X = torso twist, triggers = pedals.\n"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "l4padpanel.h"
|
||||
#include "l4padrio.h"
|
||||
#include "l4vb16.h" // BTLampBrightnessOf -- the one lamp decode
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
@@ -95,10 +96,10 @@ static const NamedButton namedButtons[] =
|
||||
{ 0x3D, "Panic" },
|
||||
{ 0x3F, "Throttle" },
|
||||
{ 0x40, "Main" },
|
||||
{ 0x41, "Hat Back" },
|
||||
{ 0x42, "Hat Up" },
|
||||
{ 0x43, "Hat Right" },
|
||||
{ 0x44, "Hat Left" },
|
||||
{ 0x41, "Look Back" },
|
||||
{ 0x42, "Torso Ctr" },
|
||||
{ 0x43, "Look Right" },
|
||||
{ 0x44, "Look Left" },
|
||||
{ 0x45, "Pinky" },
|
||||
{ 0x46, "Middle" },
|
||||
{ 0x47, "Upper" },
|
||||
@@ -191,33 +192,18 @@ static int
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Lamp decode (RIOBase::LampState): flash = low 2 bits (solid/slow/med/
|
||||
// fast), filament brightness = max of the state1 (>>2) and state2 (>>4)
|
||||
// 2-bit fields (0 off / 1 dim / 3 bright). Flash blinks between the
|
||||
// commanded brightness and off, half-periods 500/250/125 ms (vRIO).
|
||||
// Lamp decode (RIOBase::LampState): the ONE copy lives in l4vb16.h
|
||||
// (BTLampBrightnessOf) -- flash mode in the low 2 bits picks a 500/250/125 ms
|
||||
// half-period and the two 2-bit brightness fields (state1 >>2, state2 >>4;
|
||||
// 0 off / 1 dim / 3 bright) ALTERNATE across it. This TU used to carry its
|
||||
// own max-then-blank copy, as did L4GLASSWIN; see the header's 2026-07-26
|
||||
// note for the dim-to-bright pulse all three got wrong.
|
||||
//###########################################################################
|
||||
|
||||
static int
|
||||
LampBrightnessOf(int state, unsigned long tick)
|
||||
{
|
||||
int level1 = (state >> 2) & 0x3;
|
||||
int level2 = (state >> 4) & 0x3;
|
||||
int level = (level1 > level2) ? level1 : level2;
|
||||
if (level == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
int flash = state & 0x3;
|
||||
if (flash != 0)
|
||||
{
|
||||
unsigned long half_period =
|
||||
(flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
if ((tick / half_period) & 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return level;
|
||||
return BTLampBrightnessOf(state, tick);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "l4glasswin.h"
|
||||
#include "l4ctrl.h"
|
||||
#include "l4joy.h"
|
||||
#include "l4keylight.h" // RGB keyboard lamp mirror (scalars-only interface)
|
||||
|
||||
#include <windows.h>
|
||||
#include <xinput.h>
|
||||
@@ -63,6 +64,35 @@ int
|
||||
// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers
|
||||
// to 0..1 past the stock threshold.
|
||||
//
|
||||
//
|
||||
// The lamp mirror's log sink. It cannot use DEBUG_STREAM itself: its TU
|
||||
// takes no engine headers (l4keylight.h is scalars only), so it hands us
|
||||
// finished lines instead.
|
||||
//
|
||||
//
|
||||
// Does the loaded profile bind this virtual key? The built-in convenience
|
||||
// keys (view toggle, preset cycles) consult this and stand down when the
|
||||
// player's bindings claim the key -- see the note at their poll sites.
|
||||
//
|
||||
Logical
|
||||
PadRIO::KeyHasBinding(int virtualKey) const
|
||||
{
|
||||
for (int i = 0; i < bindings.keyBindingCount; ++i)
|
||||
{
|
||||
if (bindings.keyBindings[i].virtualKey == virtualKey)
|
||||
{
|
||||
return True;
|
||||
}
|
||||
}
|
||||
return False;
|
||||
}
|
||||
|
||||
static void
|
||||
KeyLightLog(const char *line)
|
||||
{
|
||||
DEBUG_STREAM << "[keylight] " << (line ? line : "") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
static float
|
||||
NormalizeThumb(int value, int dead_zone)
|
||||
{
|
||||
@@ -170,6 +200,75 @@ PadRIO::PadRIO():
|
||||
flipStickAxes =
|
||||
(getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0');
|
||||
|
||||
//
|
||||
// RGB KEYBOARD LAMP MIRROR (Windows Dynamic Lighting, l4keylight.h):
|
||||
// every key bound to a lamp ADDRESS glows with the panel palette --
|
||||
// yellow for the Secondary/Screen columns (0x10-0x1F), red for the rest,
|
||||
// exactly like the on-screen buttons. Keypad binds have no lamp, so
|
||||
// they are skipped; a key bound twice takes its FIRST binding.
|
||||
// BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once
|
||||
// and stays dormant.
|
||||
//
|
||||
keyLightActive = False;
|
||||
{
|
||||
const char *gate = getenv("BT_KEYLIGHT");
|
||||
if (gate == NULL || atoi(gate) != 0)
|
||||
{
|
||||
static int lightKeys[192];
|
||||
static int lightAddresses[192];
|
||||
static unsigned char lightYellow[192];
|
||||
int count = 0;
|
||||
for (int k = 0; k < bindings.keyBindingCount && count < 192; ++k)
|
||||
{
|
||||
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
|
||||
if (binding.action.kind != PadBindingProfile::ActionButton)
|
||||
{
|
||||
continue; // axes/keypads carry no lamp
|
||||
}
|
||||
int address = binding.action.address;
|
||||
if (address < 0 || address >= LampCount)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (lightKeys[j] == binding.virtualKey)
|
||||
{
|
||||
duplicate = True; // first binding wins
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lightKeys[count] = binding.virtualKey;
|
||||
lightAddresses[count] = address;
|
||||
lightYellow[count] = (unsigned char)
|
||||
((address >= 0x10 && address <= 0x1F) ? 1 : 0);
|
||||
++count;
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
KeyLight_SetLogger(KeyLightLog);
|
||||
KeyLight_SetMap(lightKeys, lightAddresses, lightYellow, count);
|
||||
KeyLight_Start();
|
||||
keyLightActive = True;
|
||||
DEBUG_STREAM << "[keylight] mirroring " << count
|
||||
<< " bound key(s) onto RGB keyboards\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Never revision 0.0 -- some diagnostics print it; give the synthetic
|
||||
// board a recognizable version.
|
||||
@@ -199,6 +298,11 @@ PadRIO::PadRIO():
|
||||
|
||||
PadRIO::~PadRIO()
|
||||
{
|
||||
if (keyLightActive)
|
||||
{
|
||||
KeyLight_Stop(); // hands the LEDs back to Windows
|
||||
keyLightActive = False;
|
||||
}
|
||||
BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created
|
||||
BTPadPanel_Destroy();
|
||||
if (activeInstance == this)
|
||||
@@ -537,11 +641,17 @@ void
|
||||
// message-path-free) and consumed by the game's view-toggle block via
|
||||
// BTPadViewToggleEdge.
|
||||
//
|
||||
// ⚠ A bindings.txt ROW WINS over these built-in convenience keys
|
||||
// (2026-07-26). The default profile now hands the whole letter field to
|
||||
// the MFD banks -- V is a Left Weapons button, J/K/L are Right Weapons --
|
||||
// and polling them here as well would fire BOTH the button and the
|
||||
// built-in, which is the double-dispatch the typed-channel suppression
|
||||
// exists to prevent. Bind the key, and the built-in stands down.
|
||||
{
|
||||
static int s_backtickWas = 0;
|
||||
int backtick_held = focused &&
|
||||
(((GetAsyncKeyState(VK_OEM_3) & 0x8000) != 0) ||
|
||||
((GetAsyncKeyState('V') & 0x8000) != 0));
|
||||
(!KeyHasBinding('V') && (GetAsyncKeyState('V') & 0x8000) != 0));
|
||||
if (backtick_held && !s_backtickWas)
|
||||
{
|
||||
extern int gBTPadViewToggleEdges;
|
||||
@@ -560,7 +670,7 @@ void
|
||||
static const int s_presetKey[3] = { 'J', 'K', 'L' };
|
||||
for (int g = 0; g < 3; ++g)
|
||||
{
|
||||
int held = focused &&
|
||||
int held = focused && !KeyHasBinding(s_presetKey[g]) &&
|
||||
(GetAsyncKeyState(s_presetKey[g]) & 0x8000) != 0;
|
||||
if (held && !s_presetWas[g])
|
||||
{
|
||||
@@ -1038,6 +1148,18 @@ void
|
||||
if (lampNumber >= 0 && lampNumber < LampCount)
|
||||
{
|
||||
lampState[lampNumber] = state;
|
||||
if (keyLightActive)
|
||||
{
|
||||
// The mirror keeps its own copy (its paint loop runs on a private
|
||||
// thread and must not reach into the device); the first 64
|
||||
// addresses are the whole lamp field.
|
||||
unsigned char bytes[64];
|
||||
for (int i = 0; i < 64; ++i)
|
||||
{
|
||||
bytes[i] = (unsigned char) lampState[i];
|
||||
}
|
||||
KeyLight_UpdateLamps(bytes, 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,17 @@ protected:
|
||||
int
|
||||
lampState[LampCount];
|
||||
|
||||
// RGB keyboard lamp mirror running? (l4keylight.h; BT_KEYLIGHT=0 off,
|
||||
// also stays False when no bound key carries a lamp address)
|
||||
Logical
|
||||
keyLightActive;
|
||||
|
||||
// True when the loaded bindings claim this virtual key. The built-in
|
||||
// convenience keys (` / V view toggle, J/K/L preset cycles) yield to an
|
||||
// explicit row rather than double-dispatching with it.
|
||||
Logical
|
||||
KeyHasBinding(int virtualKey) const;
|
||||
|
||||
//
|
||||
// Typed-channel suppression tables (built from the loaded bindings +
|
||||
// the hardcoded view/preset keys; see SuppressKey).
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "mungal4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
//###########################################################################
|
||||
// L4RIOBANK -- the ONE button-bank geometry for a pod display. Design +
|
||||
// the under-glass rule: l4riobank.h. Consumed by the cockpit surround
|
||||
// (L4VB16.cpp, canvas space) and the exploded per-display windows
|
||||
// (L4GLASSWIN.cpp, client space); neither owns a copy any more.
|
||||
//###########################################################################
|
||||
|
||||
#include "l4riobank.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// The lamp strip at NATIVE glass size, scaled down with the display
|
||||
// but floored so a half-scale surround lamp is still a bar and not a
|
||||
// smudge. RP412 L4MFDVIEW works at native only and hardcodes 10.
|
||||
//-------------------------------------------------------------------
|
||||
const int nativeShortAxis = 480; // MFD 640x480 / map 480x640
|
||||
const int nativeStrip = 10;
|
||||
const int minimumStrip = 6;
|
||||
const int defaultGap = 4;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Per-column horizontal nudge for the 4 MFD buttons, left->right, so
|
||||
// they line up with the (unevenly spaced) DISPLAY / PROGRAM / ...
|
||||
// legends painted in the imagery. Measured against the native 640
|
||||
// glass in L4GLASSWIN (RedOffsetX + kRedColDX) and scaled here, so
|
||||
// both renderers inherit the same alignment. Tunable.
|
||||
//-------------------------------------------------------------------
|
||||
const int nativeMfdW = 640;
|
||||
const int mfdColumnDX[4] = { 0, 1, 5, 5 };
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// The map paints its own legend beside each side button (the six
|
||||
// cells: MAP+, MAP-, IR, crouch, searchlight, display-mode), and the
|
||||
// buttons have to line up with THAT grid, not with height/6.
|
||||
//
|
||||
// MEASURED off a native 480x640 capture (scratchpad/measurelegend.py
|
||||
// over an exploded Secondary/Radar window): the first cell border sits
|
||||
// 3 rows down and the six cells are 102 tall on a 107 pitch. RP412's
|
||||
// map measures 13 + 6x102 on a 105 pitch -- same cell height, different
|
||||
// top and pitch, so its numbers do NOT transfer; these are ours.
|
||||
//
|
||||
// Our previous even division (displayH/6 + 1 = 107) had the pitch right
|
||||
// by luck and sat 3px high of the labels.
|
||||
//-------------------------------------------------------------------
|
||||
const int mapLegendSpan = 640;
|
||||
const int mapLegendTop = 3;
|
||||
const int mapLegendCell = 102;
|
||||
const int mapLegendPitch = 107;
|
||||
|
||||
int Scaled(int nativeValue, int actual, int nativeExtent)
|
||||
{
|
||||
if (nativeExtent <= 0) return nativeValue;
|
||||
return (nativeValue * actual) / nativeExtent;
|
||||
}
|
||||
|
||||
void Push(
|
||||
BTRioBank *bank,
|
||||
int address, int x, int y, int w, int h,
|
||||
int colorClass, int inert, const char *label
|
||||
)
|
||||
{
|
||||
if (bank->buttonCount >= BTRioBankMaxButtons) return;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
BTRioButton *button = &bank->buttons[bank->buttonCount++];
|
||||
button->address = address;
|
||||
button->x = x;
|
||||
button->y = y;
|
||||
button->w = w;
|
||||
button->h = h;
|
||||
button->colorClass = colorClass;
|
||||
button->inert = inert;
|
||||
button->label = label;
|
||||
}
|
||||
|
||||
void GrowBounds(BTRioBank *bank, int x, int y, int w, int h)
|
||||
{
|
||||
if (bank->boundsW == 0 && bank->boundsH == 0)
|
||||
{
|
||||
bank->boundsX = x; bank->boundsY = y;
|
||||
bank->boundsW = w; bank->boundsH = h;
|
||||
return;
|
||||
}
|
||||
int left = (x < bank->boundsX) ? x : bank->boundsX;
|
||||
int top = (y < bank->boundsY) ? y : bank->boundsY;
|
||||
int right = bank->boundsX + bank->boundsW;
|
||||
int bottom = bank->boundsY + bank->boundsH;
|
||||
if (x + w > right) right = x + w;
|
||||
if (y + h > bottom) bottom = y + h;
|
||||
bank->boundsX = left;
|
||||
bank->boundsY = top;
|
||||
bank->boundsW = right - left;
|
||||
bank->boundsH = bottom - top;
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out)
|
||||
{
|
||||
if (out == 0) return;
|
||||
|
||||
int shortAxis = (displayW < displayH) ? displayW : displayH;
|
||||
|
||||
int strip = Scaled(nativeStrip, shortAxis, nativeShortAxis);
|
||||
if (strip < minimumStrip) strip = minimumStrip;
|
||||
|
||||
out->strip = strip;
|
||||
out->gap = defaultGap;
|
||||
|
||||
// The cells with no glass behind them (the flight bank, the map's foot
|
||||
// row) are drawn in full, so they get a plain readable size scaled off
|
||||
// the same axis rather than a derived depth.
|
||||
out->cellW = Scaled(58, shortAxis, nativeShortAxis);
|
||||
out->cellH = Scaled(28, shortAxis, nativeShortAxis);
|
||||
if (out->cellW < 12) out->cellW = 12;
|
||||
if (out->cellH < 12) out->cellH = 12;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankLayout(
|
||||
BTRioBankStyle style,
|
||||
int originX,
|
||||
int originY,
|
||||
int displayW,
|
||||
int displayH,
|
||||
int anchorA,
|
||||
int anchorB,
|
||||
const BTRioBankMetrics *metrics,
|
||||
const int *bottomAddrs,
|
||||
BTRioBank *out
|
||||
)
|
||||
{
|
||||
if (out == 0 || metrics == 0) return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
const int strip = metrics->strip;
|
||||
const int gap = metrics->gap;
|
||||
|
||||
// the glass itself is always inside the bounds
|
||||
GrowBounds(out, originX, originY, displayW, displayH);
|
||||
|
||||
if (style == BTRioBankMfd)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// 4 above the glass, 4 below. Addresses descend from the anchor
|
||||
// row-major (vRIO CockpitLayout::Mfd): top = anchorA-i, bottom =
|
||||
// anchorA-4-i. Each button reaches HALF THE GLASS in and clears
|
||||
// the edge by `strip`, so the two banks meet in the middle and
|
||||
// every pixel of the display is some button's press target.
|
||||
//---------------------------------------------------------------
|
||||
int depth = displayH / 2;
|
||||
if (depth < strip) depth = strip;
|
||||
|
||||
int slotW = displayW / 4;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int dx = Scaled(mfdColumnDX[i], displayW, nativeMfdW);
|
||||
int x = originX + i * slotW + dx;
|
||||
int w = slotW - gap;
|
||||
|
||||
int topY = originY - strip;
|
||||
int botY = originY + displayH - depth;
|
||||
Push(out, anchorA - i, x, topY, w, depth + strip, 0, 0, 0);
|
||||
Push(out, anchorA - 4 - i, x, botY, w, depth + strip, 0, 0, 0);
|
||||
GrowBounds(out, x, topY, w, depth + strip);
|
||||
GrowBounds(out, x, botY, w, depth + strip);
|
||||
}
|
||||
}
|
||||
else if (style == BTRioBankRadar)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// The portrait map: 6 buttons down each side (anchorA left,
|
||||
// anchorB right) plus an optional 4 along the foot. Same rule
|
||||
// turned on its side -- each column reaches half the map in and
|
||||
// leaves `strip` clearing the edge.
|
||||
//
|
||||
// The FOOT ROW IS PUSHED FIRST, deliberately: the columns now
|
||||
// reach half the width each, so they cover the foot band too,
|
||||
// and the hit test takes the first match. Foot-first keeps
|
||||
// those four addresses reachable.
|
||||
//---------------------------------------------------------------
|
||||
int depth = displayW / 2;
|
||||
if (depth < strip) depth = strip;
|
||||
|
||||
if (bottomAddrs != 0)
|
||||
{
|
||||
int bandH = metrics->cellH;
|
||||
int bandY = originY + displayH - bandH;
|
||||
int bandW = displayW / 4;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int x = originX + i * bandW;
|
||||
Push(out, bottomAddrs[i], x, bandY, bandW - gap,
|
||||
bandH + strip, 1, 0, 0);
|
||||
GrowBounds(out, x, bandY, bandW - gap, bandH + strip);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Six cells down the height, on the map's MEASURED legend grid
|
||||
// (see mapLegend* above). Top and bottom are scaled SEPARATELY
|
||||
// and subtracted rather than scaling a height directly: rounding
|
||||
// a height would let the buttons drift out of step with the
|
||||
// labels a display or two down the column.
|
||||
//---------------------------------------------------------------
|
||||
int leftX = originX - strip;
|
||||
int rightX = originX + displayW - depth;
|
||||
for (int i = 0; i < 6; ++i)
|
||||
{
|
||||
int cellTop = mapLegendTop + i * mapLegendPitch;
|
||||
int top = (cellTop * displayH) / mapLegendSpan;
|
||||
int bottom = ((cellTop + mapLegendCell) * displayH) / mapLegendSpan;
|
||||
int h = bottom - top;
|
||||
if (h < 1) h = 1;
|
||||
int y = originY + top;
|
||||
Push(out, anchorA + i, leftX, y, depth + strip, h, 1, 0, 0);
|
||||
Push(out, anchorB + i, rightX, y, depth + strip, h, 1, 0, 0);
|
||||
GrowBounds(out, leftX, y, depth + strip, h);
|
||||
GrowBounds(out, rightX, y, depth + strip, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankFlightGrid(
|
||||
int originX,
|
||||
int originY,
|
||||
int baseAddress,
|
||||
int count,
|
||||
int columns,
|
||||
const char *const *labels,
|
||||
const int *inert,
|
||||
const BTRioBankMetrics *metrics,
|
||||
BTRioBank *out
|
||||
)
|
||||
{
|
||||
if (out == 0 || metrics == 0 || columns <= 0) return;
|
||||
|
||||
const int cw = metrics->cellW;
|
||||
const int ch = metrics->cellH;
|
||||
const int gap = metrics->gap;
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
int column = i % columns;
|
||||
int row = i / columns;
|
||||
int x = originX + column * (cw + gap);
|
||||
int y = originY + row * (ch + gap);
|
||||
Push(out, baseAddress + i, x, y, cw, ch, 2,
|
||||
(inert != 0) ? inert[i] : 0,
|
||||
(labels != 0) ? labels[i] : 0);
|
||||
GrowBounds(out, x, y, cw, ch);
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankDump(const char *tag, const BTRioBank *bank)
|
||||
{
|
||||
static int enabled = -1;
|
||||
static int printed = 0;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char *e = getenv("BT_RIOBANK_LOG");
|
||||
enabled = (e != 0 && e[0] != '0') ? 1 : 0;
|
||||
}
|
||||
if (!enabled || bank == 0 || printed >= 16) return;
|
||||
++printed;
|
||||
|
||||
DEBUG_STREAM << "[riobank] " << (tag ? tag : "?")
|
||||
<< " bounds=(" << bank->boundsX << "," << bank->boundsY << ","
|
||||
<< bank->boundsW << "," << bank->boundsH << ") "
|
||||
<< bank->buttonCount << " buttons\n";
|
||||
for (int i = 0; i < bank->buttonCount; ++i)
|
||||
{
|
||||
const BTRioButton &b = bank->buttons[i];
|
||||
DEBUG_STREAM << "[riobank] addr=0x" << std::hex << b.address << std::dec
|
||||
<< " rect=(" << b.x << "," << b.y << "," << b.w << "," << b.h << ")"
|
||||
<< " class=" << b.colorClass << "\n";
|
||||
}
|
||||
DEBUG_STREAM << std::flush;
|
||||
}
|
||||
+350
-72
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4vb16.h"
|
||||
#include "l4riobank.h" // the shared button-bank geometry (with the exploded windows)
|
||||
#include "../munga/gaugrend.h"
|
||||
#include "L4VIDEO.h"
|
||||
#include "DXUtils.h"
|
||||
@@ -195,41 +196,229 @@ int gBTCockpitCanvasW = 0; // backbuffer canvas dims (set by btl4main; the
|
||||
int gBTCockpitCanvasH = 0; // aspect calc needs them after a client resize)
|
||||
|
||||
// Layout constants (backbuffer px). MFD = 640x480 native x 0.5; radar = 480x640
|
||||
// portrait x 0.5. The lamp EDGE (kCkLamp) protrudes past the surface; the FULL
|
||||
// button rect reaches kCkRedCell INTO the display (hit-only, covered by the
|
||||
// surface). User asked for larger/more-clickable buttons than the glass windows'
|
||||
// 10px edge -> kCkLamp 16.
|
||||
// portrait x 0.5.
|
||||
//
|
||||
// ⚠ 2026-07-26: the BUTTON rects no longer come from here -- L4RIOBANK owns
|
||||
// that geometry now and the exploded per-display windows draw the identical
|
||||
// field. What changed for the surround: its MFD lamps used to sit ENTIRELY
|
||||
// outside the glass on a kCkRedH=24 strip (a 76x24 click target), while the
|
||||
// same button in the exploded window reached 128px under the display. Both
|
||||
// now follow the pod's under-glass rule -- half the glass deep, a scaled lamp
|
||||
// strip clearing the edge. kCkTopBand still has to reserve room for the top
|
||||
// row's protruding strip, so the band constants stay.
|
||||
enum {
|
||||
kCkMFDW = 320, kCkMFDH = 240,
|
||||
kCkMFDW = 320, kCkMFDH = 240, // the POD sizes (100%) -- see BTCkSizes
|
||||
kCkRADW = 240, kCkRADH = 320,
|
||||
kCkOVL = 44, // (int)(0.14f * 320): corner overlap into the view
|
||||
kCkLamp = 16, // radar rail / bottom-cell protrusion
|
||||
kCkRedH = 24, // MFD red lamp height -- sits fully OUTSIDE the surface
|
||||
kCkRedGap = 3, // gap between the red lamp and the MFD screen edge
|
||||
kCkRailW = 26, // radar side-rail width
|
||||
kCkGap = 2,
|
||||
kCkSideBand = kCkMFDW - kCkOVL, // 276
|
||||
kCkTopBand = (kCkMFDH - kCkOVL) + kCkRedGap + kCkRedH, // 223 (fits the top red lamps)
|
||||
kCkBotBand = kCkRADH + kCkLamp // 336 (radar flush below view)
|
||||
kCkLamp = 16, // legacy band reserve (radar rail / bottom cell)
|
||||
kCkRedH = 24, // legacy band reserve above the top MFD row
|
||||
kCkRedGap = 3, // gap between that reserve and the MFD screen edge
|
||||
kCkGap = 2
|
||||
};
|
||||
|
||||
//===========================================================================//
|
||||
// PLAYER-SCALED DISPLAYS (2026-07-26).
|
||||
//
|
||||
// The pod bolted its instruments down at one size; a desktop panel has room to
|
||||
// trade viewscreen for instrument, so the player scales them -- BT_MFD_SCALE
|
||||
// sets all five MFDs, BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR override individually,
|
||||
// BT_RADAR_SCALE the map. Percentages of the pod size, 25-200; anything
|
||||
// unreadable or out of range falls back to the group setting, then to 100.
|
||||
//
|
||||
// Resolved ONCE (the surround recomputes its layout every frame and this reads
|
||||
// the environment), and the SURROUND BANDS derive from the result -- which is
|
||||
// the whole reason the sizes could not just be constants: the band a display
|
||||
// hangs in has to grow with it or the canvas clips it.
|
||||
//===========================================================================//
|
||||
struct BTCkSizes
|
||||
{
|
||||
int mfdW[5], mfdH[5]; // UL, UC, UR, LL, LR -- kCkMfdBank order
|
||||
int radW, radH;
|
||||
int sideBandL, sideBandR, topBand, botBand;
|
||||
};
|
||||
|
||||
static int CkScalePercent(const char *name, int fallback)
|
||||
{
|
||||
const char *text = getenv(name);
|
||||
if (text == NULL) return fallback;
|
||||
int percent = atoi(text);
|
||||
if (percent <= 0) return fallback;
|
||||
if (percent < 25) percent = 25;
|
||||
if (percent > 200) percent = 200;
|
||||
return percent;
|
||||
}
|
||||
|
||||
// Where the map sits. The pod had it dead centre under the viewscreen, which
|
||||
// on a wide panel is exactly where the road is -- BT_RADAR_POS moves it out of
|
||||
// the way. In a bottom corner it takes that corner and the lower MFD there
|
||||
// slides inboard beside it; halfway up a side it leaves the bottom row
|
||||
// entirely (and the bottom band shrinks to what the lower MFDs need).
|
||||
enum BTCkRadarPos { CkRadarBottomCenter = 0, CkRadarBottomLeft, CkRadarBottomRight,
|
||||
CkRadarMidLeft, CkRadarMidRight };
|
||||
|
||||
static int CkRadarPosition(void)
|
||||
{
|
||||
static int cached = -1;
|
||||
if (cached >= 0) return cached;
|
||||
|
||||
static const struct { const char *name; int pos; } names[] = {
|
||||
{ "CENTER", CkRadarBottomCenter }, { "CENTRE", CkRadarBottomCenter },
|
||||
{ "BOTTOM", CkRadarBottomCenter },
|
||||
{ "LEFT", CkRadarBottomLeft }, { "BOTTOMLEFT", CkRadarBottomLeft },
|
||||
{ "RIGHT", CkRadarBottomRight }, { "BOTTOMRIGHT", CkRadarBottomRight },
|
||||
{ "MIDLEFT", CkRadarMidLeft }, { "LEFTCENTER", CkRadarMidLeft },
|
||||
{ "LEFTCENTRE", CkRadarMidLeft },
|
||||
{ "MIDRIGHT", CkRadarMidRight }, { "RIGHTCENTER", CkRadarMidRight },
|
||||
{ "RIGHTCENTRE", CkRadarMidRight }
|
||||
};
|
||||
|
||||
cached = CkRadarBottomCenter;
|
||||
const char *text = getenv("BT_RADAR_POS");
|
||||
if (text != NULL)
|
||||
for (int i = 0; i < (int)(sizeof(names) / sizeof(names[0])); i++)
|
||||
if (_stricmp(text, names[i].name) == 0) { cached = names[i].pos; break; }
|
||||
|
||||
static const char *described[] = { "bottom centre", "bottom left", "bottom right",
|
||||
"left side, centred", "right side, centred" };
|
||||
DEBUG_STREAM << "[cockpit] radar on the " << described[cached] << "\n" << std::flush;
|
||||
return cached;
|
||||
}
|
||||
|
||||
static int CkRadarOnASide(int pos)
|
||||
{ return (pos == CkRadarMidLeft || pos == CkRadarMidRight) ? 1 : 0; }
|
||||
|
||||
static const BTCkSizes *BTCkResolvedSizes(void)
|
||||
{
|
||||
static BTCkSizes s;
|
||||
static int resolved = 0;
|
||||
if (resolved) return &s;
|
||||
resolved = 1;
|
||||
|
||||
int group = CkScalePercent("BT_MFD_SCALE", 100);
|
||||
static const char *perDisplay[5] = {
|
||||
"BT_MFD_SCALE_UL", "BT_MFD_SCALE_UC", "BT_MFD_SCALE_UR",
|
||||
"BT_MFD_SCALE_LL", "BT_MFD_SCALE_LR"
|
||||
};
|
||||
int percent[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
percent[i] = CkScalePercent(perDisplay[i], group);
|
||||
s.mfdW[i] = kCkMFDW * percent[i] / 100;
|
||||
s.mfdH[i] = kCkMFDH * percent[i] / 100;
|
||||
}
|
||||
int radarPercent = CkScalePercent("BT_RADAR_SCALE", 100);
|
||||
s.radW = kCkRADW * radarPercent / 100;
|
||||
s.radH = kCkRADH * radarPercent / 100;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// The bands: each is whatever the LARGEST display hanging in it needs.
|
||||
// A corner MFD overlaps the view by kCkOVL, so it only claims
|
||||
// (its size - the overlap) of the band.
|
||||
//-------------------------------------------------------------------
|
||||
int radarPos = CkRadarPosition();
|
||||
|
||||
int leftMax = (s.mfdW[0] > s.mfdW[3]) ? s.mfdW[0] : s.mfdW[3]; // UL, LL
|
||||
int rightMax = (s.mfdW[2] > s.mfdW[4]) ? s.mfdW[2] : s.mfdW[4]; // UR, LR
|
||||
s.sideBandL = leftMax - kCkOVL;
|
||||
s.sideBandR = rightMax - kCkOVL;
|
||||
|
||||
// top: the corner MFDs claim (h - overlap); UC drops 75% of its height in
|
||||
int topCorner = ((s.mfdH[0] > s.mfdH[2]) ? s.mfdH[0] : s.mfdH[2]) - kCkOVL;
|
||||
int topCentre = (int)(0.75f * s.mfdH[1]);
|
||||
s.topBand = (topCorner > topCentre) ? topCorner : topCentre;
|
||||
s.topBand += kCkRedGap + kCkRedH; // room for the top lamp strip
|
||||
|
||||
// bottom: the lower MFDs hang (h - overlap) below the view; the radar,
|
||||
// when it is down there, hangs its full height plus its lamp strip
|
||||
int botCorner = ((s.mfdH[3] > s.mfdH[4]) ? s.mfdH[3] : s.mfdH[4]) - kCkOVL;
|
||||
s.botBand = botCorner;
|
||||
if (!CkRadarOnASide(radarPos))
|
||||
{
|
||||
int radarBand = s.radH + kCkLamp;
|
||||
if (radarBand > s.botBand) s.botBand = radarBand;
|
||||
}
|
||||
|
||||
// a side-mounted radar has to fit BESIDE the view, in that side's band
|
||||
if (CkRadarOnASide(radarPos))
|
||||
{
|
||||
int *band = (radarPos == CkRadarMidLeft) ? &s.sideBandL : &s.sideBandR;
|
||||
if (s.radW + kCkLamp > *band) *band = s.radW + kCkLamp;
|
||||
}
|
||||
|
||||
if (s.sideBandL < 1) s.sideBandL = 1;
|
||||
if (s.sideBandR < 1) s.sideBandR = 1;
|
||||
|
||||
DEBUG_STREAM << "[cockpit] displays UL " << percent[0] << "% UC " << percent[1]
|
||||
<< "% UR " << percent[2] << "% LL " << percent[3] << "% LR " << percent[4]
|
||||
<< "% radar " << radarPercent << "% bands L" << s.sideBandL
|
||||
<< " R" << s.sideBandR << " T" << s.topBand << " B" << s.botBand
|
||||
<< "\n" << std::flush;
|
||||
return &s;
|
||||
}
|
||||
|
||||
// Canvas = view region + the surround bands.
|
||||
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH)
|
||||
{
|
||||
if (canvasW) *canvasW = viewW + 2 * kCkSideBand;
|
||||
if (canvasH) *canvasH = viewH + kCkTopBand + kCkBotBand;
|
||||
const BTCkSizes *s = BTCkResolvedSizes();
|
||||
if (canvasW) *canvasW = viewW + s->sideBandL + s->sideBandR;
|
||||
if (canvasH) *canvasH = viewH + s->topBand + s->botBand;
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
// LETTERBOX FIT -- one uniform scale, centred, leftover black. Contract +
|
||||
// why the swap effect changes: l4vb16.h.
|
||||
//===========================================================================//
|
||||
int gBTCockpitLetterbox = 0;
|
||||
|
||||
int BTCockpitFitRect(int clientW, int clientH, RECT *out)
|
||||
{
|
||||
int cw = gBTCockpitCanvasW, ch = gBTCockpitCanvasH;
|
||||
if (out == NULL || cw <= 0 || ch <= 0 || clientW <= 0 || clientH <= 0)
|
||||
return 0;
|
||||
|
||||
// The larger axis is the constraint; integer math throughout so the rect
|
||||
// is stable frame to frame (a float scale wobbles by a pixel and the bars
|
||||
// shimmer during a drag).
|
||||
int w = clientW;
|
||||
int h = (int)((__int64)clientW * ch / cw);
|
||||
if (h > clientH)
|
||||
{
|
||||
h = clientH;
|
||||
w = (int)((__int64)clientH * cw / ch);
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
|
||||
out->left = (clientW - w) / 2;
|
||||
out->top = (clientH - h) / 2;
|
||||
out->right = out->left + w;
|
||||
out->bottom = out->top + h;
|
||||
|
||||
return (w != clientW || h != clientH) ? 1 : 0;
|
||||
}
|
||||
|
||||
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage)
|
||||
{
|
||||
if (!gBTCockpitLetterbox || !gBTGaugeCockpit || hwnd == NULL || storage == NULL)
|
||||
return NULL;
|
||||
RECT client;
|
||||
if (!GetClientRect((HWND)hwnd, &client))
|
||||
return NULL;
|
||||
if (!BTCockpitFitRect(client.right, client.bottom, storage))
|
||||
return NULL; // already an exact fit -- present full client
|
||||
return storage;
|
||||
}
|
||||
|
||||
// The five MFD banks: {cockpit surface slot, high address}. Top row = bankHi-i
|
||||
// (descending), bottom row = bankHi-4-i (L4GLASSWIN BuildMfd order + the
|
||||
// BTGlassPanels_Create address assignment).
|
||||
static const struct { int slot, bankHi; } kCkMfdBank[5] =
|
||||
static const struct { int slot, bankHi; const char *name; } kCkMfdBank[5] =
|
||||
{
|
||||
{ 0, 0x2F }, // Heat (UL)
|
||||
{ 1, 0x27 }, // Mfd2 (UC)
|
||||
{ 5, 0x37 }, // Comm (UR)
|
||||
{ 2, 0x0F }, // Mfd1 (LL)
|
||||
{ 3, 0x07 }, // Mfd3 (LR)
|
||||
{ 0, 0x2F, "surround/Heat MFD" }, // UL
|
||||
{ 1, 0x27, "surround/Engineering" }, // UC
|
||||
{ 5, 0x37, "surround/Comm MFD" }, // UR
|
||||
{ 2, 0x0F, "surround/Left Weapons" }, // LL
|
||||
{ 3, 0x07, "surround/Right Weapons" }, // LR
|
||||
};
|
||||
|
||||
static void CkPushBtn(BTCockpitLayout *L, int addr, int x, int y, int w, int h,
|
||||
@@ -247,66 +436,114 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
|
||||
memset(L, 0, sizeof(*L));
|
||||
L->canvasW = canvasW;
|
||||
L->canvasH = canvasH;
|
||||
int viewW = canvasW - 2 * kCkSideBand;
|
||||
int viewH = canvasH - kCkTopBand - kCkBotBand;
|
||||
const BTCkSizes *S = BTCkResolvedSizes();
|
||||
int viewW = canvasW - S->sideBandL - S->sideBandR;
|
||||
int viewH = canvasH - S->topBand - S->botBand;
|
||||
if (viewW < 64) viewW = 64;
|
||||
if (viewH < 64) viewH = 64;
|
||||
int vx = kCkSideBand, vy = kCkTopBand;
|
||||
int vx = S->sideBandL, vy = S->topBand;
|
||||
L->viewX = vx; L->viewY = vy; L->viewW = viewW; L->viewH = viewH;
|
||||
|
||||
// Surface dest rects (slots: 0 Heat, 1 Mfd2, 2 Mfd1, 3 Mfd3, 4 sec, 5 Comm).
|
||||
// Corner MFDs overlap the view corner by kCkOVL both axes; Mfd2 drops 25% of
|
||||
// its height into the top; sec is flush below (view bottom edge clean).
|
||||
// Sizes are the player-scaled ones (BTCkResolvedSizes), indexed in
|
||||
// kCkMfdBank order: 0 UL, 1 UC, 2 UR, 3 LL, 4 LR.
|
||||
int cx = vx + viewW / 2;
|
||||
// Heat TL
|
||||
L->surfX[0] = vx - kCkMFDW + kCkOVL; L->surfY[0] = vy - kCkMFDH + kCkOVL;
|
||||
L->surfW[0] = kCkMFDW; L->surfH[0] = kCkMFDH;
|
||||
L->surfX[0] = vx - S->mfdW[0] + kCkOVL; L->surfY[0] = vy - S->mfdH[0] + kCkOVL;
|
||||
L->surfW[0] = S->mfdW[0]; L->surfH[0] = S->mfdH[0];
|
||||
// Mfd2 top-center (25% into view)
|
||||
L->surfX[1] = cx - kCkMFDW / 2; L->surfY[1] = vy - (int)(0.75f * kCkMFDH);
|
||||
L->surfW[1] = kCkMFDW; L->surfH[1] = kCkMFDH;
|
||||
L->surfX[1] = cx - S->mfdW[1] / 2; L->surfY[1] = vy - (int)(0.75f * S->mfdH[1]);
|
||||
L->surfW[1] = S->mfdW[1]; L->surfH[1] = S->mfdH[1];
|
||||
// Mfd1 BL
|
||||
L->surfX[2] = vx - kCkMFDW + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
|
||||
L->surfW[2] = kCkMFDW; L->surfH[2] = kCkMFDH;
|
||||
L->surfX[2] = vx - S->mfdW[3] + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
|
||||
L->surfW[2] = S->mfdW[3]; L->surfH[2] = S->mfdH[3];
|
||||
// Mfd3 BR
|
||||
L->surfX[3] = vx + viewW - kCkOVL; L->surfY[3] = vy + viewH - kCkOVL;
|
||||
L->surfW[3] = kCkMFDW; L->surfH[3] = kCkMFDH;
|
||||
// sec center, flush below
|
||||
L->surfX[4] = cx - kCkRADW / 2; L->surfY[4] = vy + viewH;
|
||||
L->surfW[4] = kCkRADW; L->surfH[4] = kCkRADH;
|
||||
L->surfW[3] = S->mfdW[4]; L->surfH[3] = S->mfdH[4];
|
||||
// Comm TR
|
||||
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - kCkMFDH + kCkOVL;
|
||||
L->surfW[5] = kCkMFDW; L->surfH[5] = kCkMFDH;
|
||||
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - S->mfdH[2] + kCkOVL;
|
||||
L->surfW[5] = S->mfdW[2]; L->surfH[5] = S->mfdH[2];
|
||||
|
||||
// --- MFD red buttons: 4 top (bankHi-i) + 4 bottom (bankHi-4-i) ---
|
||||
int slotW = kCkMFDW / 4; // 80
|
||||
for (int m = 0; m < 5; m++)
|
||||
//-------------------------------------------------------------------
|
||||
// sec (the map). Bottom centre as the pod had it, either bottom
|
||||
// corner, or halfway up either side (BT_RADAR_POS). Only when it
|
||||
// takes a bottom CORNER does a lower MFD have to move -- it slides
|
||||
// inboard beside the map; on a side the map is out of that row's way
|
||||
// already.
|
||||
//-------------------------------------------------------------------
|
||||
{
|
||||
int s = kCkMfdBank[m].slot, hi = kCkMfdBank[m].bankHi;
|
||||
int sx = L->surfX[s], sy = L->surfY[s];
|
||||
for (int i = 0; i < 4; i++)
|
||||
int radarPos = CkRadarPosition();
|
||||
L->surfW[4] = S->radW;
|
||||
L->surfH[4] = S->radH;
|
||||
// A corner map goes FLUSH to the canvas edge and the lower MFD whose
|
||||
// corner it took sits BESIDE it -- measured off the canvas edge, not
|
||||
// the view edge, or the two overlap by the band width (they did:
|
||||
// 232px of map drawn over the Right Weapons MFD).
|
||||
int canvasLeft = vx - S->sideBandL;
|
||||
int canvasRight = vx + viewW + S->sideBandR;
|
||||
switch (radarPos)
|
||||
{
|
||||
int bx = sx + i * slotW;
|
||||
// Lamps sit fully OUTSIDE the MFD (a small gap off the edge) so nothing
|
||||
// occludes them or the DISPLAY/PROGRAM legends -- 4 above, 4 below.
|
||||
CkPushBtn(L, hi - i, bx, sy - kCkRedGap - kCkRedH, slotW - kCkGap, kCkRedH, 0, 0, 0);
|
||||
CkPushBtn(L, hi - 4 - i, bx, sy + kCkMFDH + kCkRedGap, slotW - kCkGap, kCkRedH, 0, 0, 0);
|
||||
case CkRadarBottomLeft:
|
||||
L->surfX[4] = canvasLeft;
|
||||
L->surfY[4] = vy + viewH;
|
||||
L->surfX[2] = L->surfX[4] + S->radW; // LL slides inboard
|
||||
break;
|
||||
case CkRadarBottomRight:
|
||||
L->surfX[4] = canvasRight - S->radW;
|
||||
L->surfY[4] = vy + viewH;
|
||||
L->surfX[3] = L->surfX[4] - S->mfdW[4]; // LR slides inboard
|
||||
break;
|
||||
case CkRadarMidLeft:
|
||||
L->surfX[4] = vx - S->radW + kCkOVL;
|
||||
L->surfY[4] = vy + (viewH - S->radH) / 2;
|
||||
break;
|
||||
case CkRadarMidRight:
|
||||
L->surfX[4] = vx + viewW - kCkOVL;
|
||||
L->surfY[4] = vy + (viewH - S->radH) / 2;
|
||||
break;
|
||||
default:
|
||||
L->surfX[4] = cx - S->radW / 2;
|
||||
L->surfY[4] = vy + viewH;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- radar yellow rails (6/side) + 4 bottom cells ---
|
||||
// --- MFD buttons + radar columns: L4RIOBANK, the shared under-glass field ---
|
||||
// Each bank is laid out at ITS display's scaled size, so the lamp strips and
|
||||
// the under-glass reach track a resized display instead of drifting off it.
|
||||
{
|
||||
int rx = L->surfX[4], ry = L->surfY[4];
|
||||
int slotH = kCkRADH / 6;
|
||||
for (int i = 0; i < 6; i++)
|
||||
for (int m = 0; m < 5; m++)
|
||||
{
|
||||
int y = ry + i * slotH;
|
||||
CkPushBtn(L, 0x10 + i, rx - kCkLamp, y, kCkLamp + kCkRailW, slotH - kCkGap, 1, 0, 0);
|
||||
CkPushBtn(L, 0x18 + i, rx + kCkRADW - kCkRailW, y, kCkRailW + kCkLamp, slotH - kCkGap, 1, 0, 0);
|
||||
int s = kCkMfdBank[m].slot;
|
||||
BTRioBankMetrics mfdMetrics;
|
||||
BTRioBankMetricsFor(L->surfW[s], L->surfH[s], &mfdMetrics);
|
||||
|
||||
BTRioBank bank;
|
||||
BTRioBankLayout(BTRioBankMfd, L->surfX[s], L->surfY[s],
|
||||
L->surfW[s], L->surfH[s], kCkMfdBank[m].bankHi, 0, &mfdMetrics, 0, &bank);
|
||||
BTRioBankDump(kCkMfdBank[m].name, &bank);
|
||||
for (int i = 0; i < bank.buttonCount; i++)
|
||||
{
|
||||
const BTRioButton &b = bank.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
|
||||
}
|
||||
}
|
||||
|
||||
static const int bottomAddr[4] = { 0x16, 0x17, 0x1F, 0x1E };
|
||||
int botLeft = rx + kCkRailW, botSpan = kCkRADW - 2 * kCkRailW, botW = botSpan / 4;
|
||||
for (int i = 0; i < 4; i++)
|
||||
CkPushBtn(L, bottomAddr[i], botLeft + i * botW, ry + kCkRADH - 14, botW - kCkGap, 14 + kCkLamp, 1, 0, 0);
|
||||
BTRioBankMetrics radarMetrics;
|
||||
BTRioBankMetricsFor(L->surfW[4], L->surfH[4], &radarMetrics);
|
||||
|
||||
BTRioBank radar;
|
||||
BTRioBankLayout(BTRioBankRadar, L->surfX[4], L->surfY[4],
|
||||
L->surfW[4], L->surfH[4], 0x10, 0x18, &radarMetrics, bottomAddr, &radar);
|
||||
BTRioBankDump("surround/radar", &radar);
|
||||
for (int i = 0; i < radar.buttonCount; i++)
|
||||
{
|
||||
const BTRioButton &b = radar.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
|
||||
}
|
||||
}
|
||||
|
||||
// --- flight blue blocks: THROTTLE/AUX (0x38-0x3F) under Mfd1, JOYSTICK
|
||||
@@ -314,20 +551,36 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
|
||||
{
|
||||
static const char *thrLabel[8] = { 0, 0, 0, 0, 0, "Panic", 0, "Throttle" }; // 0x38..0x3F
|
||||
static const int thrInert[8] = { 1, 1, 1, 1, 1, 0, 1, 0 };
|
||||
static const char *joyLabel[8] = { "Main", "Hat Bk", "Hat Up", "Hat R",
|
||||
"Hat L", "Pinky", "Middle", "Upper" }; // 0x40..0x47
|
||||
const int cw = 68, ch = 30, g = 4;
|
||||
int blockW = 4 * cw + 3 * g; // 284
|
||||
int belowLamp = kCkMFDH + kCkRedGap + kCkRedH + 8; // clear the MFD's bottom red lamps
|
||||
int thrX = L->surfX[2], thrY = L->surfY[2] + belowLamp;
|
||||
int joyX = L->surfX[3] + kCkMFDW - blockW, joyY = L->surfY[3] + belowLamp;
|
||||
for (int i = 0; i < 8; i++)
|
||||
static const char *joyLabel[8] = { "Main", "Look Bk", "Torso Ctr", "Look R",
|
||||
"Look L", "Pinky", "Middle", "Upper" }; // 0x40..0x47
|
||||
// These cells carry TEXT, so the surround runs them bigger than the
|
||||
// exploded window's 58x28 -- the caller owns the cell size
|
||||
// (l4riobank.h), only the grid walk is shared.
|
||||
BTRioBankMetrics metrics;
|
||||
metrics.strip = 0;
|
||||
metrics.gap = 4;
|
||||
metrics.cellW = 68;
|
||||
metrics.cellH = 30;
|
||||
|
||||
// Hang each block below ITS lower MFD, clearing that display's own
|
||||
// (scaled) bottom lamp strip -- a shrunken MFD used to leave the block
|
||||
// floating in the gap, an enlarged one to overlap it.
|
||||
int blockW = 4 * metrics.cellW + 3 * metrics.gap; // 284
|
||||
int thrX = L->surfX[2];
|
||||
int thrY = L->surfY[2] + L->surfH[2] + kCkRedGap + kCkRedH + 8;
|
||||
int joyX = L->surfX[3] + L->surfW[3] - blockW;
|
||||
int joyY = L->surfY[3] + L->surfH[3] + kCkRedGap + kCkRedH + 8;
|
||||
|
||||
BTRioBank flight;
|
||||
memset(&flight, 0, sizeof(flight));
|
||||
BTRioBankFlightGrid(thrX, thrY, 0x38, 8, 4, thrLabel, thrInert, &metrics, &flight);
|
||||
BTRioBankFlightGrid(joyX, joyY, 0x40, 8, 4, joyLabel, 0, &metrics, &flight);
|
||||
BTRioBankDump("surround/flight", &flight);
|
||||
for (int i = 0; i < flight.buttonCount; i++)
|
||||
{
|
||||
int c = i % 4, r = i / 4;
|
||||
CkPushBtn(L, 0x38 + i, thrX + c * (cw + g), thrY + r * (ch + g), cw, ch, 2,
|
||||
thrInert[i], thrLabel[i] ? thrLabel[i] : "");
|
||||
CkPushBtn(L, 0x40 + i, joyX + c * (cw + g), joyY + r * (ch + g), cw, ch, 2,
|
||||
0, joyLabel[i]);
|
||||
const BTRioButton &b = flight.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert,
|
||||
b.label ? b.label : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -959,8 +1212,33 @@ int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButton
|
||||
if (!gBTGaugeCockpit) return 0;
|
||||
int bbW = gBTCockpitCanvasW, bbH = gBTCockpitCanvasH;
|
||||
if (bbW <= 0 || bbH <= 0 || clientW <= 0 || clientH <= 0) return 0;
|
||||
int bx = cx * bbW / clientW;
|
||||
int by = cy * bbH / clientH;
|
||||
|
||||
// Client -> canvas. Under the letterbox the canvas occupies a centred
|
||||
// sub-rect, so the click has to come back through the SAME transform
|
||||
// Present used -- mapping against the full client would drift the hit test
|
||||
// off every button by the bar width. A click on a bar hits nothing.
|
||||
int bx, by;
|
||||
if (gBTCockpitLetterbox)
|
||||
{
|
||||
RECT fit;
|
||||
if (BTCockpitFitRect(clientW, clientH, &fit))
|
||||
{
|
||||
if (cx < fit.left || cx >= fit.right || cy < fit.top || cy >= fit.bottom)
|
||||
return 0;
|
||||
bx = (cx - fit.left) * bbW / (fit.right - fit.left);
|
||||
by = (cy - fit.top) * bbH / (fit.bottom - fit.top);
|
||||
}
|
||||
else
|
||||
{
|
||||
bx = cx * bbW / clientW;
|
||||
by = cy * bbH / clientH;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bx = cx * bbW / clientW;
|
||||
by = cy * bbH / clientH;
|
||||
}
|
||||
|
||||
BTCockpitLayout L;
|
||||
BTCockpitComputeLayout(bbW, bbH, &L);
|
||||
|
||||
+44
-11
@@ -53,6 +53,30 @@ struct BTCockpitLayout
|
||||
extern int gBTGaugeCockpit; // mode select; set once by btl4main
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH; // backbuffer canvas dims (for the aspect calc)
|
||||
|
||||
//
|
||||
// LETTERBOX FIT (2026-07-26). The cockpit canvas is a FIXED size -- D3D9
|
||||
// stretches it into whatever the client area is, so a window dragged to a
|
||||
// different shape used to squash the instruments (the projection was
|
||||
// aspect-corrected, but the panels, lamps and MFD glass were not).
|
||||
//
|
||||
// Now the canvas is fitted at ONE uniform scale, centred, with the leftover
|
||||
// left black: the cockpit scales both ways and never distorts, whatever the
|
||||
// window (drag, maximise, -fit borderless). D3D9 does the scaling at Present
|
||||
// time via a destination rect, which D3DSWAPEFFECT_DISCARD forbids -- so the
|
||||
// WINDOWED swap effect becomes D3DSWAPEFFECT_COPY when the surround is up and
|
||||
// multisampling is off (COPY cannot multisample). gBTCockpitLetterbox says
|
||||
// whether that happened; when it is 0 the old full-client stretch stands, so
|
||||
// an MSAA run still works, just without the letterbox.
|
||||
//
|
||||
extern int gBTCockpitLetterbox; // 1 = the swap chain can take a dest rect
|
||||
|
||||
// The uniform-scale centred rect for a client area. Returns 1 when it differs
|
||||
// from the full client (letterboxing needed), 0 when it fills it exactly.
|
||||
int BTCockpitFitRect(int clientW, int clientH, RECT *out);
|
||||
|
||||
// Present destination for the main device window, or NULL for the full client.
|
||||
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage);
|
||||
|
||||
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH);
|
||||
void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out);
|
||||
void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device);
|
||||
@@ -60,21 +84,30 @@ int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButto
|
||||
void BTCockpitMouseUp(void);
|
||||
|
||||
// Lamp brightness decode (RIOBase::LampState) -- shared by the cockpit draw and
|
||||
// the glass per-display windows. level = max of the two 2-bit fields; the low 2
|
||||
// bits pick a flash half-period (500/250/125 ms). Returns 0..3.
|
||||
// the glass per-display windows. Returns 0 (off) / 1 (dim) / 3 (bright).
|
||||
//
|
||||
// The wire word is TWO brightness fields plus a flash mode (L4RIO.h [T0]):
|
||||
// bits 0-1 solid=0 / flashSlow=1 / flashMed=2 / flashFast=3
|
||||
// bits 2-3 state 1 brightness (state1Off=0, state1Dim=1, state1Bright=3)
|
||||
// bits 4-5 state 2 brightness (state2Off=0, state2Dim=1, state2Bright=3)
|
||||
// Solid shows state 1; flashing ALTERNATES state 1 and state 2 at a
|
||||
// 500/250/125 ms half-period.
|
||||
//
|
||||
// ⚠ FIXED 2026-07-26: this used to return max(state1,state2) and blank to 0 on
|
||||
// the alternate phase. That agrees with the enum only when one state is Off --
|
||||
// true for the Panic lamp (L4CTRL.cpp flashFast+state1Off+state2Bright) and for
|
||||
// L4LAMP's off/dim pulse, which is why it went unnoticed -- but L4LAMP.cpp:252
|
||||
// commands flashFast + state1Dim + state2Bright, a DIM-to-BRIGHT pulse that
|
||||
// rendered as a hard BRIGHT-to-OFF blink. RP412's L4MFDVIEW LampLevel has the
|
||||
// faithful formula; this is it.
|
||||
inline int BTLampBrightnessOf(int state, unsigned long tick)
|
||||
{
|
||||
int level1 = (state >> 2) & 0x3;
|
||||
int level2 = (state >> 4) & 0x3;
|
||||
int level = (level1 > level2) ? level1 : level2;
|
||||
if (level == 0) return 0;
|
||||
int flash = state & 0x3;
|
||||
if (flash != 0)
|
||||
{
|
||||
unsigned long half = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
if ((tick / half) & 1) return 0;
|
||||
}
|
||||
return level;
|
||||
int flash = state & 0x3;
|
||||
if (flash == 0) return level1;
|
||||
unsigned long half = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
return ((tick / half) & 1) ? level2 : level1;
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
|
||||
@@ -3419,6 +3419,34 @@ DPLRenderer::DPLRenderer(
|
||||
mPresentParams.MultiSampleQuality--;
|
||||
}
|
||||
mPresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
|
||||
//
|
||||
// COCKPIT LETTERBOX (2026-07-26): the fixed canvas is fitted into the client
|
||||
// at Present time via a destination rect, which DISCARD forbids -- COPY is
|
||||
// the swap effect that takes one. Windowed + surround only, and only with
|
||||
// multisampling off (COPY cannot multisample). Full contract: l4vb16.h.
|
||||
//
|
||||
// btl4main sets the INTENT (it has to: the first WM_SIZE beats the device
|
||||
// here); this is where it becomes real, or gets withdrawn.
|
||||
{
|
||||
extern int gBTGaugeCockpit;
|
||||
extern int gBTCockpitLetterbox;
|
||||
if (!fullscreen && gBTGaugeCockpit && gBTCockpitLetterbox &&
|
||||
mPresentParams.MultiSampleType == D3DMULTISAMPLE_NONE)
|
||||
mPresentParams.SwapEffect = D3DSWAPEFFECT_COPY;
|
||||
else
|
||||
gBTCockpitLetterbox = 0; // withdrawn -- the canvas stretches
|
||||
// only meaningful when there IS a cockpit canvas to fit
|
||||
if (gBTGaugeCockpit)
|
||||
DEBUG_STREAM << "[cockpit] letterbox " << (gBTCockpitLetterbox ? "ON" : "off")
|
||||
<< " (swap effect "
|
||||
<< (mPresentParams.SwapEffect == D3DSWAPEFFECT_COPY ? "COPY" : "DISCARD")
|
||||
<< (mPresentParams.MultiSampleType != D3DMULTISAMPLE_NONE
|
||||
? "; multisampling on -- COPY unavailable, canvas stretches to the client"
|
||||
: "")
|
||||
<< ")\n" << std::flush;
|
||||
}
|
||||
|
||||
mPresentParams.hDeviceWindow = hWnd;
|
||||
mPresentParams.Flags = 0;
|
||||
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
||||
@@ -3462,8 +3490,30 @@ DPLRenderer::DPLRenderer(
|
||||
// while the cockpit-surround window is sized independently -- pinning to them
|
||||
// would shrink the backbuffer and break the composite.)
|
||||
//
|
||||
//
|
||||
// COCKPIT SURROUND: the backbuffer IS THE CANVAS, whatever the window
|
||||
// happens to be (2026-07-26). Everything downstream assumes it --
|
||||
// BTApplyWorldViewport and BTDrawCockpitPanels lay out from the
|
||||
// BACKBUFFER size, while the letterbox and the mouse hit-test map
|
||||
// through gBTCockpitCanvasW/H. Pinning to the client instead is fine
|
||||
// only while client == canvas, which is true for a normal boot and
|
||||
// FALSE for -fit: the window is the whole monitor before the device
|
||||
// exists, so the backbuffer came out 3440x1440 against a 1452x1059
|
||||
// canvas. Field report: tiny MFDs hugging the corners, an over-wide
|
||||
// world view, the scene squashed horizontally as Present scaled the
|
||||
// oversized backbuffer into the letterbox -- and, unseen, every button
|
||||
// hit-tested in a different space from the one it was drawn in (the
|
||||
// exact #50 divergence the note above warns about).
|
||||
//
|
||||
extern int gBTGaugeCockpit;
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH;
|
||||
RECT rc;
|
||||
if (hWnd != NULL && GetClientRect(hWnd, &rc)
|
||||
if (gBTGaugeCockpit && gBTCockpitCanvasW > 0 && gBTCockpitCanvasH > 0)
|
||||
{
|
||||
mPresentParams.BackBufferWidth = (UINT)gBTCockpitCanvasW;
|
||||
mPresentParams.BackBufferHeight = (UINT)gBTCockpitCanvasH;
|
||||
}
|
||||
else if (hWnd != NULL && GetClientRect(hWnd, &rc)
|
||||
&& rc.right > rc.left && rc.bottom > rc.top)
|
||||
{
|
||||
mPresentParams.BackBufferWidth = (UINT)(rc.right - rc.left);
|
||||
@@ -8799,7 +8849,11 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
// DIAG (turn-hitch hunt): draw CPU is _rt0..here; Present blocks on the GPU.
|
||||
LARGE_INTEGER _rt1; QueryPerformanceCounter(&_rt1);
|
||||
hr = mDevice->Present(NULL, NULL, NULL, NULL);
|
||||
// COCKPIT LETTERBOX: uniform-scale the canvas into the client (NULL = the
|
||||
// old full-client stretch, which is what every non-cockpit run gets).
|
||||
RECT _ckDestStorage;
|
||||
const RECT *_ckDest = BTCockpitPresentDest(mPresentParams.hDeviceWindow, &_ckDestStorage);
|
||||
hr = mDevice->Present(NULL, _ckDest, NULL, NULL);
|
||||
// The wait-screen overlay stands down PERMANENTLY the moment the first
|
||||
// REAL scene frame presents (user report 2026-07-22: the launch handoff
|
||||
// briefly interleaved idle-overlay presents with game frames = flicker).
|
||||
@@ -9125,7 +9179,12 @@ void DPLRenderer::ExecuteIdle()
|
||||
}
|
||||
backbuffer->Release();
|
||||
}
|
||||
HRESULT present_hr = mDevice->Present(NULL, NULL, NULL, NULL);
|
||||
// same letterbox as the scene present -- the wait overlay paints into
|
||||
// this backbuffer, so it has to land in the same rect
|
||||
RECT wait_dest_storage;
|
||||
const RECT *wait_dest =
|
||||
BTCockpitPresentDest(mPresentParams.hDeviceWindow, &wait_dest_storage);
|
||||
HRESULT present_hr = mDevice->Present(NULL, wait_dest, NULL, NULL);
|
||||
static int s_wait_path_logged = 0;
|
||||
if (!s_wait_path_logged)
|
||||
{
|
||||
@@ -9772,12 +9831,18 @@ static float BTWorldAspectOf(int client_w, int client_h)
|
||||
// ON-SCREEN aspect = (viewW/canvasW * client_w) / (viewH/canvasH * client_h).
|
||||
extern int gBTGaugeCockpit;
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH;
|
||||
extern int gBTCockpitLetterbox;
|
||||
if (gBTGaugeCockpit && gBTCockpitCanvasW > 0 && gBTCockpitCanvasH > 0)
|
||||
{
|
||||
BTCockpitLayout L;
|
||||
BTCockpitComputeLayout(gBTCockpitCanvasW, gBTCockpitCanvasH, &L);
|
||||
if (L.viewW > 0 && L.viewH > 0)
|
||||
{
|
||||
// LETTERBOXED: the canvas is scaled UNIFORMLY, so the view's
|
||||
// on-screen aspect is just its own -- the client no longer enters
|
||||
// into it (2026-07-26).
|
||||
if (gBTCockpitLetterbox)
|
||||
return (float)L.viewW / (float)L.viewH;
|
||||
float w = (float)L.viewW * (float)client_w / (float)gBTCockpitCanvasW;
|
||||
float h = (float)L.viewH * (float)client_h / (float)gBTCockpitCanvasH;
|
||||
if (h > 0.0f) return w / h;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//===========================================================================//
|
||||
// File: l4keylight.h //
|
||||
// Project: MUNGA Brick: RGB keyboard lamp mirror //
|
||||
// Contents: Windows Dynamic Lighting bridge (vRIO's KeyboardLampMirror) //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#pragma once
|
||||
|
||||
//########################################################################
|
||||
// Mirrors the game-commanded RIO lamp states onto per-key RGB keyboards
|
||||
// through Windows Dynamic Lighting (the WinRT LampArray API), following
|
||||
// vRIO's KeyboardLampMirror: keys bound to lamp addresses glow with the
|
||||
// panel palette (red for the banks, yellow for the Secondary/Screen
|
||||
// columns), flash modes blink at the panel's rates, other keys are
|
||||
// blacked out so the keyboard reads as the button field. Zone-lit
|
||||
// keyboards mirror the strongest lamp board-wide. Dynamic Lighting
|
||||
// grants LED control to the FOREGROUND app - which is the game itself,
|
||||
// so no Windows settings dance is needed.
|
||||
//
|
||||
// The implementation is C++/WinRT on a private worker thread, compiled
|
||||
// with default struct packing (the engine builds /Zp1, which would
|
||||
// break the WinRT ABI) - hence this interface is scalars only. All
|
||||
// functions are safe to call when Dynamic Lighting is unavailable;
|
||||
// failures log once and the mirror stays dormant.
|
||||
//########################################################################
|
||||
|
||||
// Where the mirror's status lines go (the caller owns the sink).
|
||||
void
|
||||
KeyLight_SetLogger(void (*logger)(const char *line));
|
||||
|
||||
// The key map: parallel arrays of virtual keys, their RIO lamp
|
||||
// addresses (0x00-0x47), and whether each paints yellow (Secondary /
|
||||
// Screen columns) instead of red.
|
||||
void
|
||||
KeyLight_SetMap(
|
||||
const int *virtual_keys,
|
||||
const int *addresses,
|
||||
const unsigned char *yellow,
|
||||
int count
|
||||
);
|
||||
|
||||
// Latest game-commanded lamp bytes (indexed by RIO lamp number).
|
||||
void
|
||||
KeyLight_UpdateLamps(const unsigned char *lamp_state, int count);
|
||||
|
||||
// Claim keyboards and start painting / release the LEDs to Windows.
|
||||
void
|
||||
KeyLight_Start();
|
||||
void
|
||||
KeyLight_Stop();
|
||||
@@ -0,0 +1,149 @@
|
||||
//===========================================================================//
|
||||
// File: l4riobank.h //
|
||||
// Project: MUNGA_L4 Brick: cockpit RIO button-bank geometry //
|
||||
// Contents: the ONE layout rule for the buttons around a pod display //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#ifndef _L4RIOBANK_H_
|
||||
#define _L4RIOBANK_H_
|
||||
|
||||
//###########################################################################
|
||||
// Every pod secondary display carries a bank of illuminated RIO pushbuttons
|
||||
// mounted around its glass. We render that field TWICE -- once composited
|
||||
// into the main window (the COCKPIT SURROUND, L4VB16.cpp) and once per
|
||||
// display in its own desktop window (the EXPLODED view, L4GLASSWIN.cpp) --
|
||||
// and until 2026-07-26 each renderer carried its own copy of the geometry.
|
||||
// They drifted: the exploded windows put the buttons UNDER the glass (a big
|
||||
// click target, only a thin lamp strip clearing the edge) while the surround
|
||||
// left the MFD lamps entirely outside the surface, so the same button was a
|
||||
// 156x138 target in one mode and a 76x24 sliver in the other.
|
||||
//
|
||||
// This is that geometry, once. A caller supplies a display rect and the
|
||||
// bank's anchor address(es); it gets back the button rects IN THAT SAME
|
||||
// SPACE plus the bounding box the bank needs. Placement -- WHERE each
|
||||
// display sits -- stays with the renderer, because that genuinely differs
|
||||
// (one composited canvas vs seven desktop windows).
|
||||
//
|
||||
// THE UNDER-GLASS RULE (ported from RP412 L4MFDVIEW, which took it from the
|
||||
// pod): a button reaches half the display in BEHIND the glass and only
|
||||
// `strip` clears the edge. The renderer draws the buttons FIRST and the
|
||||
// display imagery OVER them, so the lamp reads as a slim bar while
|
||||
// practically the whole display is the press target. Nothing is occluded --
|
||||
// the picture wins every pixel it covers.
|
||||
//
|
||||
// Addresses per [[pod-hardware]] "The button space + lamps"; the anchors are
|
||||
// the caller's -- this file never hardcodes a bank address.
|
||||
//###########################################################################
|
||||
|
||||
enum BTRioBankStyle
|
||||
{
|
||||
BTRioBankMfd = 0, // 4 buttons above the glass + 4 below, addresses
|
||||
// DESCENDING from anchorA row-major (0x2F..0x28)
|
||||
BTRioBankRadar // 6 down each side (anchorA left, anchorB right)
|
||||
// + an optional 4 along the bottom
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
BTRioBankMaxButtons = 24
|
||||
};
|
||||
|
||||
struct BTRioButton
|
||||
{
|
||||
int address; // RIO buttonGroup address (0x00-0x47)
|
||||
int x, y, w, h; // caller-space rect (the FULL hit target)
|
||||
int colorClass; // 0 red (MFD) / 1 yellow (radar) / 2 blue (flight)
|
||||
int inert; // 1 = no authored .CTL mapping -> drawn greyed
|
||||
const char *label; // non-NULL -> the face is drawn + labelled
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// How much of a button shows, and how the field is spaced. Both renderers
|
||||
// work at different scales (the surround halves the glass; the exploded
|
||||
// windows are native), so this is DERIVED from the display size -- the pod
|
||||
// rule applied at whatever size you hand it.
|
||||
//---------------------------------------------------------------------------
|
||||
struct BTRioBankMetrics
|
||||
{
|
||||
int strip; // how much of a button clears the display edge (the lamp)
|
||||
int gap; // gap between neighbouring buttons in a row/column
|
||||
int cellW, cellH; // the labelled no-glass cell (flight bank, map foot row)
|
||||
};
|
||||
|
||||
struct BTRioBank
|
||||
{
|
||||
BTRioButton buttons[BTRioBankMaxButtons];
|
||||
int buttonCount;
|
||||
|
||||
// The bank + display bounding box, in the caller's space. A window
|
||||
// caller sizes its client to boundsW/H and passes an origin that puts
|
||||
// boundsX/Y at zero; the composited caller ignores these and just uses
|
||||
// the button rects, which are already in canvas space.
|
||||
int boundsX, boundsY, boundsW, boundsH;
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// The pod rule at a given display size. The lamp strip scales off the SHORT
|
||||
// axis against the native 480 (an MFD is 640x480 landscape, the map 480x640
|
||||
// portrait -- both have a 480 short axis) and never drops below a readable
|
||||
// floor, because a 5px lamp on a half-scale surround MFD is a smudge.
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// Lay out one display's bank. originX/Y is the DISPLAY's top-left; button
|
||||
// rects come back around it, reaching `strip` outside the glass (so they can
|
||||
// land at negative coordinates relative to the origin -- see boundsX/Y).
|
||||
// anchorA/B bank base addresses (style-dependent; see BTRioBankStyle)
|
||||
// bottomAddrs BTRioBankRadar only: the 4 foot-row addresses, or NULL
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankLayout(
|
||||
BTRioBankStyle style,
|
||||
int originX,
|
||||
int originY,
|
||||
int displayW,
|
||||
int displayH,
|
||||
int anchorA,
|
||||
int anchorB,
|
||||
const BTRioBankMetrics *metrics,
|
||||
const int *bottomAddrs,
|
||||
BTRioBank *out
|
||||
);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// The no-display bank (throttle/panic 0x38-0x3F, joystick/fire 0x40-0x47):
|
||||
// a grid of labelled cells, drawn in full since there is no glass to hide
|
||||
// behind. APPENDS to `out` (so both columns share one bank) and grows its
|
||||
// bounds; zero `out` before the first call. `labels`/`inert` are `count`
|
||||
// long and may be NULL.
|
||||
//
|
||||
// These cells carry TEXT, so their size is the caller's call, not a scale of
|
||||
// some glass -- fill metrics->cellW/cellH yourself (the surround runs bigger
|
||||
// cells than the exploded window because its canvas is half-scale and the
|
||||
// labels still have to read).
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankFlightGrid(
|
||||
int originX,
|
||||
int originY,
|
||||
int baseAddress,
|
||||
int count,
|
||||
int columns,
|
||||
const char *const *labels,
|
||||
const int *inert,
|
||||
const BTRioBankMetrics *metrics,
|
||||
BTRioBank *out
|
||||
);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// One-shot geometry dump: `BT_RIOBANK_LOG=1` prints every button's address and
|
||||
// rect as the banks are built, so the field can be checked for coverage and
|
||||
// address shadowing without driving a mouse. Capped at the first few banks so
|
||||
// a per-frame caller (the surround recomputes its layout every draw) prints
|
||||
// one pass and then goes quiet.
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankDump(const char *tag, const BTRioBank *bank);
|
||||
|
||||
#endif
|
||||
+418
-55
@@ -142,10 +142,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
// new client aspect so the scene doesn't stretch fat/skinny (the D3D9
|
||||
// backbuffer stays at the configured size and is stretched into the
|
||||
// client area; rendering with the client aspect cancels the stretch).
|
||||
// Under the cockpit LETTERBOX the canvas is scaled uniformly instead,
|
||||
// so the aspect comes from the view rect -- BTWorldAspectOf handles it.
|
||||
if (wParam != SIZE_MINIMIZED && LOWORD(lParam) > 0 && HIWORD(lParam) > 0)
|
||||
{
|
||||
extern void L4NotifyWindowResized(int client_w, int client_h);
|
||||
L4NotifyWindowResized((int)LOWORD(lParam), (int)HIWORD(lParam));
|
||||
// Repaint the letterbox bars: a COPY present only writes its dest
|
||||
// rect, so whatever the old bars held would otherwise persist. The
|
||||
// class brush is black, so an erase is all it takes.
|
||||
extern int gBTCockpitLetterbox;
|
||||
if (gBTCockpitLetterbox)
|
||||
InvalidateRect(hWnd, NULL, TRUE);
|
||||
}
|
||||
return 0;
|
||||
case WM_CLOSE:
|
||||
@@ -215,6 +223,276 @@ static LONG WINAPI
|
||||
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
// environ.ini -- the player's settings file (cwd = content\). One KEY=VALUE
|
||||
// per line; the real environment always WINS, so a launcher .bat or a shell
|
||||
// export overrides the file rather than fighting it.
|
||||
//
|
||||
// ⚠ Loaded EARLY (2026-07-26). It used to be read ~300 lines into WinMain,
|
||||
// after the platform-profile block had already run its getenv()s -- so every
|
||||
// setting the profile reads (BT_PLATFORM, BT_COCKPIT, BT_GLASS_PANELS,
|
||||
// BT_DEV_GAUGES, L4CONTROLS...) was silently ignored from the file and only
|
||||
// worked as a real env var. It also putenv()'d comment lines verbatim, which
|
||||
// turned a commented-out option into an environment variable literally named
|
||||
// "#BT_MFD_SCALE" -- harmless, but it meant shipping a self-documenting file
|
||||
// would have littered the environment with junk.
|
||||
//===========================================================================//
|
||||
//
|
||||
// The shipped default, written on first run when the file is absent -- the
|
||||
// bindings.txt convention ([[glass-cockpit]] §bindings.txt is a COMPATIBILITY
|
||||
// SURFACE): the player customizes it, it stays UNTRACKED, and an
|
||||
// extract-over-top upgrade therefore never clobbers their settings. Every
|
||||
// option ships COMMENTED OUT, so a fresh install applies nothing and behaves
|
||||
// exactly as it did before the file existed.
|
||||
//
|
||||
static const char *kEnvironIniDefault =
|
||||
"# ============================================================================\n"
|
||||
"# environ.ini -- BattleTech 4.11 settings\n"
|
||||
"# ============================================================================\n"
|
||||
"# One KEY=VALUE per line, read at game start. Lines starting with # or ;\n"
|
||||
"# are comments. A real environment variable always WINS over this file, so\n"
|
||||
"# a launcher .bat can override anything here. Delete a line to fall back to\n"
|
||||
"# the built-in default.\n"
|
||||
"#\n"
|
||||
"# Input bindings live in bindings.txt beside this file (written with the\n"
|
||||
"# full documented layout on first run; delete it to restore defaults).\n"
|
||||
"\n"
|
||||
"# ---- The cockpit ------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# Where the five MFDs and the map go:\n"
|
||||
"# (unset) the COCKPIT SURROUND -- instruments composited around\n"
|
||||
"# the viewscreen in one window (the default)\n"
|
||||
"# BT_GLASS_PANELS=1 one desktop window PER display, arranged around the\n"
|
||||
"# game window (the 'exploded' view -- handy for reading\n"
|
||||
"# or screenshotting a display at full size)\n"
|
||||
"# BT_COCKPIT=0 the plain docked gauge strip along the bottom\n"
|
||||
"#BT_GLASS_PANELS=1\n"
|
||||
"#BT_COCKPIT=0\n"
|
||||
"\n"
|
||||
"# Size of the secondary displays in the cockpit surround, as a percentage of\n"
|
||||
"# their pod size. The pod bolted them down at one size; on a big panel there\n"
|
||||
"# is room to trade viewscreen for instrument, so turn these up if you want to\n"
|
||||
"# actually read the other displays while you fly. 100 = as the pod had them.\n"
|
||||
"# Range 25-200 (out-of-range or unreadable values fall back to the group\n"
|
||||
"# setting, then to 100). The surround grows to fit whatever you ask for.\n"
|
||||
"#\n"
|
||||
"# BT_MFD_SCALE sets all five MFDs at once...\n"
|
||||
"#BT_MFD_SCALE=100\n"
|
||||
"# ...and any single display can override it:\n"
|
||||
"# UL upper left (heat) UC upper center (engineering)\n"
|
||||
"# UR upper right (comm) LL lower left (left weapons)\n"
|
||||
"# LR lower right (right weapons)\n"
|
||||
"#BT_MFD_SCALE_UL=100\n"
|
||||
"#BT_MFD_SCALE_UC=100\n"
|
||||
"#BT_MFD_SCALE_UR=100\n"
|
||||
"#BT_MFD_SCALE_LL=100\n"
|
||||
"#BT_MFD_SCALE_LR=100\n"
|
||||
"\n"
|
||||
"# The portrait map/radar, sized on its own.\n"
|
||||
"#BT_RADAR_SCALE=100\n"
|
||||
"\n"
|
||||
"# Where the map sits:\n"
|
||||
"# CENTER bottom centre, under the viewscreen, as the pod had it\n"
|
||||
"# (default; BOTTOM and CENTRE mean the same)\n"
|
||||
"# LEFT bottom left corner (or BOTTOMLEFT)\n"
|
||||
"# RIGHT bottom right corner (or BOTTOMRIGHT)\n"
|
||||
"# MIDLEFT left edge, halfway up (or LEFTCENTER)\n"
|
||||
"# MIDRIGHT right edge, halfway up (or RIGHTCENTER)\n"
|
||||
"# Anywhere but CENTER stops it sitting where the road is. In a bottom corner\n"
|
||||
"# the lower MFD whose corner it takes slides inboard beside it; halfway up a\n"
|
||||
"# side it leaves the bottom row entirely.\n"
|
||||
"#BT_RADAR_POS=CENTER\n"
|
||||
"\n"
|
||||
"# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n"
|
||||
"#BT_COCKPIT_TINT=21FF42\n"
|
||||
"\n"
|
||||
"# ---- Display ----------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# The cockpit is fitted into the window at ONE uniform scale, centred, with\n"
|
||||
"# the leftover black -- drag the window to any shape and nothing distorts.\n"
|
||||
"# Launch with -fit (or -windowed-fullscreen) for a borderless window over the\n"
|
||||
"# whole monitor.\n"
|
||||
"\n"
|
||||
"# Simulation/render frame rate, integer frames per second. The desktop\n"
|
||||
"# default is 60; the arcade pods shipped at 25.\n"
|
||||
"#TARGETFPS=60\n"
|
||||
"\n"
|
||||
"# Anti-aliasing sample count, passed straight to Direct3D 9: 0 = off, else\n"
|
||||
"# 2..16 as the GPU supports. NOTE: turning this on disables the letterbox\n"
|
||||
"# fit above (the two use incompatible swap modes) and the cockpit stretches\n"
|
||||
"# to the window instead.\n"
|
||||
"#MULTISAMPLE=0\n"
|
||||
"\n"
|
||||
"# Particle budget, integer. Default 8192.\n"
|
||||
"#MAXPARTICLES=8192\n"
|
||||
"\n"
|
||||
"# ---- Optional ---------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to a lamp\n"
|
||||
"# button in bindings.txt glow with the panel -- yellow for the map's side\n"
|
||||
"# columns, red for the rest -- and flash in step with the on-screen buttons.\n"
|
||||
"# Per-key keyboards light each bound key; zone-lit boards mirror the\n"
|
||||
"# strongest lamp board-wide. Unset or nonzero = on; 0 = off. A machine\n"
|
||||
"# without Dynamic Lighting logs once and stays dormant.\n"
|
||||
"#BT_KEYLIGHT=0\n"
|
||||
"\n"
|
||||
"# ---- Multiplayer ------------------------------------------------------------\n"
|
||||
"\n"
|
||||
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
|
||||
"# running; without it the game logs the reason and falls back to Winsock.\n"
|
||||
"#BT_STEAM_NET=1\n";
|
||||
|
||||
//===========================================================================//
|
||||
// CWD GUARD (2026-07-26, field report).
|
||||
//
|
||||
// The engine resolves BTL4.RES, VIDEO\, BTDPL.INI, the eggs AND both player
|
||||
// config files (bindings.txt, environ.ini) RELATIVE TO CWD -- every launcher
|
||||
// .bat does `cd content` first, so this was invisible. A BARE launch of the
|
||||
// exe (double-click, or a shell sitting in build\Release) then:
|
||||
// - found no resources ("Resource file btl4.res v1.0.0.0 is obsolete!"),
|
||||
// - wrote a stray bindings.txt / environ.ini / btl4.log NEXT TO THE EXE
|
||||
// (so the player's real ones in content\ looked like they never appeared),
|
||||
// - and killed the mission generation the menu launched.
|
||||
// Reported live: `.\btl4.exe -fit` from build\Release -> menu opens -> crash
|
||||
// on Launch.
|
||||
//
|
||||
// So find content\ ourselves rather than trusting whoever started us. Runs
|
||||
// BEFORE the log file opens, so the log lands in content\ too -- same place
|
||||
// the launchers put it. Returns what happened for the boot line.
|
||||
//===========================================================================//
|
||||
static const char *gBTCwdFixNote = NULL;
|
||||
|
||||
//
|
||||
// -fit / -windowed-fullscreen, parsed EARLY: the miniconsole front end exits
|
||||
// this process long before the window-creation code runs, and it rebuilds the
|
||||
// mission child's command line from scratch -- so without this the flag was
|
||||
// silently dropped the moment you launched from the menu (field report:
|
||||
// `btl4.exe -fit` gave a borderless menu and then a windowed mission).
|
||||
// btl4console.cpp reads it when composing the relaunch.
|
||||
//
|
||||
int gBTFitDisplay = 0;
|
||||
|
||||
static void BTEnsureContentDirectory(void)
|
||||
{
|
||||
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return; // already in the right place
|
||||
}
|
||||
|
||||
char exeDir[MAX_PATH];
|
||||
if (GetModuleFileNameA(NULL, exeDir, MAX_PATH) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
char *slash = strrchr(exeDir, '\\');
|
||||
if (slash == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
*slash = 0;
|
||||
|
||||
// build\Release\btl4.exe -> ..\..\content is the shipped layout; the
|
||||
// others cover a flattened install and an exe dropped into content\.
|
||||
static const char *kProbe[] = {
|
||||
"\\..\\..\\content", "\\..\\content", "\\content",
|
||||
"", "\\..", "\\..\\.."
|
||||
};
|
||||
for (int i = 0; i < (int)(sizeof(kProbe) / sizeof(kProbe[0])); ++i)
|
||||
{
|
||||
char candidate[MAX_PATH * 2];
|
||||
char resource[MAX_PATH * 2];
|
||||
sprintf_s(candidate, sizeof(candidate), "%s%s", exeDir, kProbe[i]);
|
||||
sprintf_s(resource, sizeof(resource), "%s\\BTL4.RES", candidate);
|
||||
if (GetFileAttributesA(resource) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
if (SetCurrentDirectoryA(candidate))
|
||||
{
|
||||
gBTCwdFixNote = "found the content directory from the exe path";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
gBTCwdFixNote = "NO content directory found (BTL4.RES is nowhere near the "
|
||||
"exe) -- resources and settings will not load";
|
||||
}
|
||||
|
||||
static void BTWriteDefaultEnvironIni(void)
|
||||
{
|
||||
FILE *probe;
|
||||
if (fopen_s(&probe, "environ.ini", "r") == 0) // already there -- leave it alone
|
||||
{
|
||||
fclose(probe);
|
||||
return;
|
||||
}
|
||||
// Only ever write it BESIDE THE RESOURCES. If the cwd guard could not
|
||||
// find content\, writing here would scatter a stray settings file next to
|
||||
// whatever the player happened to launch from -- which is exactly the
|
||||
// confusion the guard exists to end.
|
||||
if (GetFileAttributesA("BTL4.RES") == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return;
|
||||
}
|
||||
FILE *out;
|
||||
if (fopen_s(&out, "environ.ini", "w") != 0)
|
||||
return;
|
||||
fputs(kEnvironIniDefault, out);
|
||||
fclose(out);
|
||||
std::cout << "[boot] wrote a default content\\environ.ini (all options "
|
||||
"commented out -- edit to taste)" << std::endl << std::flush;
|
||||
}
|
||||
|
||||
static void BTLoadEnvironIni(void)
|
||||
{
|
||||
BTWriteDefaultEnvironIni();
|
||||
|
||||
FILE *file;
|
||||
if (fopen_s(&file, "environ.ini", "r") != 0)
|
||||
return;
|
||||
|
||||
char line[1024];
|
||||
int applied = 0, skipped = 0;
|
||||
while (fgets(line, sizeof(line), file))
|
||||
{
|
||||
// strip EOL + trailing blanks
|
||||
int n = (int)strlen(line);
|
||||
while (n > 0 && (line[n-1] == '\n' || line[n-1] == '\r' ||
|
||||
line[n-1] == ' ' || line[n-1] == '\t'))
|
||||
line[--n] = 0;
|
||||
|
||||
const char *p = line;
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (*p == 0 || *p == '#' || *p == ';') // blank or comment
|
||||
continue;
|
||||
|
||||
const char *eq = strchr(p, '=');
|
||||
if (eq == NULL || eq == p) // no key, or no '=' at all
|
||||
{
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The real environment wins: only set what is not already set, so a
|
||||
// launcher or a shell export beats the file.
|
||||
char key[256];
|
||||
size_t klen = (size_t)(eq - p);
|
||||
if (klen >= sizeof(key)) { ++skipped; continue; }
|
||||
memcpy(key, p, klen);
|
||||
key[klen] = 0;
|
||||
while (klen > 0 && (key[klen-1] == ' ' || key[klen-1] == '\t'))
|
||||
key[--klen] = 0;
|
||||
if (getenv(key) != NULL)
|
||||
continue;
|
||||
|
||||
putenv(p);
|
||||
++applied;
|
||||
}
|
||||
fclose(file);
|
||||
|
||||
std::cout << "[boot] environ.ini: " << applied << " setting(s) applied"
|
||||
<< (skipped ? ", " : "") << (skipped ? std::to_string(skipped) : std::string())
|
||||
<< (skipped ? " line(s) ignored" : "") << std::endl << std::flush;
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
// Boot tick for the relaunch storm damper (btl4console.cpp, issue #33):
|
||||
@@ -228,6 +506,10 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
// else so even boot crashes leave a stack in the log.
|
||||
SetUnhandledExceptionFilter(BTCrashFilter);
|
||||
|
||||
// Sit in content\ before ANYTHING resolves a relative path -- the log file
|
||||
// below included, so a bare launch logs where the launchers log.
|
||||
BTEnsureContentDirectory();
|
||||
|
||||
// BT_CRASHTEST=1: deliberately AV after the log opens -- verifies the
|
||||
// crash filter's forensic path end-to-end on any machine.
|
||||
|
||||
@@ -279,6 +561,26 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
<< (getenv("BT_FE_LOOP") ? " (relaunched generation)" : " (first process)")
|
||||
<< std::endl << std::flush;
|
||||
|
||||
// Say so when we had to go looking for content\ -- a player who launched
|
||||
// the exe directly should see WHY their settings appeared where they did.
|
||||
if (gBTCwdFixNote != NULL)
|
||||
{
|
||||
char here[MAX_PATH] = { 0 };
|
||||
GetCurrentDirectoryA(MAX_PATH, here);
|
||||
std::cout << "[boot] cwd: " << gBTCwdFixNote << " -> " << here
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
|
||||
// The player's settings, before ANYTHING reads the environment (the
|
||||
// platform profile, the joystick wizard, the layout resolver...).
|
||||
BTLoadEnvironIni();
|
||||
|
||||
if (lpCmdLine && (strstr(lpCmdLine, "-fit") != 0 ||
|
||||
strstr(lpCmdLine, "-windowed-fullscreen") != 0))
|
||||
{
|
||||
gBTFitDisplay = 1;
|
||||
}
|
||||
|
||||
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
|
||||
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
|
||||
// the player moves for each pod control and write the joystick section
|
||||
@@ -490,21 +792,12 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1");
|
||||
if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN");
|
||||
|
||||
// DEFAULT LAYOUT = the COCKPIT SURROUND (single window: the six gauge
|
||||
// surfaces + clickable button lamps composited AROUND the centered 3D
|
||||
// view). Opt out with BT_COCKPIT=0 (dock-bottom strip) or
|
||||
// BT_GLASS_PANELS=1 (the per-display windows). When cockpit is the
|
||||
// default the buttons live IN the main window, so no separate pad/panel
|
||||
// window is created (leave BT_PAD_PANEL / BT_GLASS_PANELS unset).
|
||||
int cockpitDefault = 1;
|
||||
if (getenv("BT_COCKPIT") && getenv("BT_COCKPIT")[0] == '0') cockpitDefault = 0;
|
||||
if (getenv("BT_GLASS_PANELS") && getenv("BT_GLASS_PANELS")[0] != '0') cockpitDefault = 0;
|
||||
if (getenv("BT_DEV_GAUGES_WINDOW") || getenv("BT_DEV_GAUGES_DOCK")) cockpitDefault = 0;
|
||||
if (!cockpitDefault)
|
||||
{
|
||||
if (getenv("BT_PAD_PANEL") == NULL) putenv("BT_PAD_PANEL=1");
|
||||
if (getenv("BT_GLASS_PANELS") == NULL) putenv("BT_GLASS_PANELS=1");
|
||||
}
|
||||
// The SECONDARY-DISPLAY LAYOUT (surround / per-display windows / docked
|
||||
// strip / separate window) is resolved once, below, after the shared
|
||||
// defaults land -- it used to be half-decided here, which is how
|
||||
// BT_COCKPIT=0 came to mean "per-display windows" despite being
|
||||
// documented as the dock-bottom opt-out (the dock was unreachable under
|
||||
// glass). This block now only sets the profile's env presets.
|
||||
}
|
||||
#endif
|
||||
else
|
||||
@@ -531,12 +824,64 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
if (getenv("BT_DEV_GAUGES") != NULL && getenv("L4GAUGE") == NULL)
|
||||
putenv("L4GAUGE=640x480x16");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SECONDARY-DISPLAY LAYOUT -- resolved ONCE, here, now that every profile
|
||||
// putenv has landed. It used to be decided TWICE (the glass profile block
|
||||
// picked BT_PAD_PANEL/BT_GLASS_PANELS; the window-sizing block further down
|
||||
// re-derived cockpit-vs-dock from the same env with its own copy of the
|
||||
// precedence) and the boot banner read NEITHER -- it announced "per-display
|
||||
// cockpit windows" for every glass boot, including the surround default.
|
||||
// The split also broke BT_COCKPIT=0: documented as the dock-bottom opt-out,
|
||||
// it actually landed on the per-display windows, leaving the docked strip
|
||||
// unreachable under the glass profile.
|
||||
//
|
||||
// One resolver, one answer, consumed by the banner, the pad-panel decision
|
||||
// and the sizing block. Precedence:
|
||||
// BT_GLASS_PANELS!=0 per-display cockpit windows (explicit only)
|
||||
// BT_DEV_GAUGES_WINDOW the legacy separate MFD window
|
||||
// BT_DEV_GAUGES_DOCK the docked bottom strip
|
||||
// BT_COCKPIT=0 ...also the docked strip (the documented opt-out)
|
||||
// (nothing) COCKPIT SURROUND -- the glass default
|
||||
// -------------------------------------------------------------------------
|
||||
enum BTGlassLayout { GlassLayoutNone, GlassLayoutCockpit, GlassLayoutPanels,
|
||||
GlassLayoutDock, GlassLayoutWindow };
|
||||
int glassLayout = GlassLayoutNone;
|
||||
if (!fe_menu_mode && getenv("BT_DEV_GAUGES") != NULL)
|
||||
{
|
||||
int panels = 0;
|
||||
#ifdef BT_GLASS
|
||||
{ extern int BTGlassPanelsActive(); panels = BTGlassPanelsActive(); }
|
||||
#endif
|
||||
const char *ck = getenv("BT_COCKPIT");
|
||||
if (panels) glassLayout = GlassLayoutPanels;
|
||||
else if (getenv("BT_DEV_GAUGES_WINDOW")) glassLayout = GlassLayoutWindow;
|
||||
else if (getenv("BT_DEV_GAUGES_DOCK")) glassLayout = GlassLayoutDock;
|
||||
else if (ck != NULL && ck[0] == '0') glassLayout = GlassLayoutDock;
|
||||
else glassLayout = GlassLayoutCockpit;
|
||||
|
||||
// The surround and the per-display windows carry their own buttons; the
|
||||
// dock strip and the separate MFD window don't, so the single combined
|
||||
// pad panel supplies them (else the whole 72-button field is unclickable).
|
||||
if ((glassLayout == GlassLayoutDock || glassLayout == GlassLayoutWindow)
|
||||
&& getenv("BT_PAD_PANEL") == NULL)
|
||||
putenv("BT_PAD_PANEL=1");
|
||||
}
|
||||
static const char *kGlassLayoutName[] = {
|
||||
"no dev gauges",
|
||||
"cockpit surround",
|
||||
"per-display cockpit windows [BT_GLASS_PANELS]",
|
||||
"docked gauge strip",
|
||||
"separate MFD window"
|
||||
};
|
||||
|
||||
std::cout << "[boot] platform profile: "
|
||||
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
|
||||
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
|
||||
: gBTPlatformGlass ? "GLASS (PadRIO + per-display cockpit windows [BT_GLASS_PANELS] + plasma window)"
|
||||
: "DEV (single window + keyboard)")
|
||||
<< std::endl << std::flush;
|
||||
: gBTPlatformGlass ? "GLASS (PadRIO + plasma window)"
|
||||
: "DEV (single window + keyboard)");
|
||||
if (!fe_menu_mode)
|
||||
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
|
||||
std::cout << std::endl << std::flush;
|
||||
|
||||
#ifdef BT_GLASS
|
||||
//
|
||||
@@ -656,22 +1001,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
}
|
||||
if (getenv("BT_FORCE_FIRE") != NULL) gBTDrive.fireForced = 1;
|
||||
|
||||
// Optional environ.ini overrides (same convention as RP).
|
||||
FILE *file;
|
||||
char line[1024];
|
||||
if (fopen_s(&file, "environ.ini", "r") == 0)
|
||||
{
|
||||
while (!feof(file))
|
||||
{
|
||||
if (fgets(line, sizeof(line), file))
|
||||
{
|
||||
for (int i = (int)strlen(line); i >= 0; i--)
|
||||
if (line[i] == '\n' || line[i] == '\r') line[i] = 0;
|
||||
putenv(line);
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
// (environ.ini is loaded much earlier now -- see BTLoadEnvironIni.)
|
||||
|
||||
// Version scheme: 4.10 = the 1995 arcade release; 4.11 = this win32
|
||||
// reconstruction; build = git commit count, hash pins exact source
|
||||
@@ -783,25 +1113,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH;
|
||||
extern int BTGaugeStripHeightFor(int width);
|
||||
extern void BTCockpitCanvasFor(int viewW, int viewH, int *cw, int *ch);
|
||||
// BT_GLASS_PANELS breaks the gauges into their own per-display windows, so
|
||||
// the world window keeps its normal size (no cockpit / bottom strip).
|
||||
int glassOwnsGauges = 0;
|
||||
#ifdef BT_GLASS
|
||||
{ extern int BTGlassPanelsActive(); glassOwnsGauges = BTGlassPanelsActive(); }
|
||||
#endif
|
||||
if (!glassOwnsGauges && getenv("BT_DEV_GAUGES") != 0)
|
||||
// The mode came from the ONE resolver above (glassLayout) -- BT_GLASS_PANELS
|
||||
// keeps the world window at its normal size (the per-display windows own the
|
||||
// gauges: no cockpit surround, no bottom strip).
|
||||
{
|
||||
// Mode resolution (precedence): explicit BT_COCKPIT > separate-window /
|
||||
// legacy-inset opt-out > COCKPIT SURROUND default under BT_DEV_GAUGES.
|
||||
int cockpitOn;
|
||||
const char *ck = getenv("BT_COCKPIT");
|
||||
if (ck != NULL)
|
||||
cockpitOn = (ck[0] != '0');
|
||||
else
|
||||
cockpitOn = (getenv("BT_DEV_GAUGES_WINDOW") == 0 &&
|
||||
getenv("BT_DEV_GAUGES_DOCK") == 0);
|
||||
|
||||
if (cockpitOn)
|
||||
if (glassLayout == GlassLayoutCockpit)
|
||||
{
|
||||
gBTGaugeCockpit = 1;
|
||||
// -res W H = the WORLD VIEW size in cockpit mode; default 900x500.
|
||||
@@ -822,10 +1138,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
winW = cw; winH = ch;
|
||||
gBTCockpitCanvasW = winW; // windowed backbuffer = client canvas
|
||||
gBTCockpitCanvasH = winH;
|
||||
//
|
||||
// LETTERBOX INTENT, decided HERE rather than at device creation:
|
||||
// the first WM_SIZE arrives before the device exists, and the
|
||||
// world-aspect calc needs to know whether the canvas will be
|
||||
// scaled uniformly (aspect = the view's own) or stretched to
|
||||
// the client (aspect = the stretched one). Deciding it late
|
||||
// gave a -fit boot aspect=3.14 on an ultrawide -- applied on
|
||||
// the first frame, since nothing resizes the window again.
|
||||
// L4VIDEO clears the flag if it cannot actually get COPY.
|
||||
{
|
||||
extern int gBTCockpitLetterbox;
|
||||
const char *ms = getenv("MULTISAMPLE");
|
||||
gBTCockpitLetterbox = (ms == NULL || atoi(ms) == 0) ? 1 : 0;
|
||||
}
|
||||
std::cout << "[cockpit] view " << viewW << "x" << viewH
|
||||
<< " canvas " << winW << "x" << winH << std::endl << std::flush;
|
||||
}
|
||||
else if (getenv("BT_DEV_GAUGES_WINDOW") == 0)
|
||||
else if (glassLayout == GlassLayoutDock)
|
||||
{
|
||||
gBTGaugeDockBottom = 1;
|
||||
// Readability default (user-reported: the strip at 800 wide is a
|
||||
@@ -843,9 +1173,42 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// -fit (alias -windowed-fullscreen): borderless over the whole monitor.
|
||||
// The cockpit canvas letterboxes inside it at one uniform scale, so a
|
||||
// wider-than-canvas monitor gets black bars rather than a stretch -- the
|
||||
// same deal a dragged window gets, just without the chrome. Only useful
|
||||
// with a layout that HAS a canvas to fit; other modes keep their window.
|
||||
//
|
||||
extern int gBTFitDisplay; // parsed early (the menu exits before here)
|
||||
int fitDisplay = gBTFitDisplay;
|
||||
|
||||
RECT wr = { 0, 0, winW, winH };
|
||||
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
|
||||
DWORD winStyle = WS_OVERLAPPEDWINDOW;
|
||||
int winX = 0, winY = 0;
|
||||
if (fitDisplay)
|
||||
{
|
||||
RECT mon = { 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN) };
|
||||
MONITORINFO mi;
|
||||
memset(&mi, 0, sizeof(mi));
|
||||
mi.cbSize = sizeof(mi);
|
||||
POINT origin = { 0, 0 };
|
||||
HMONITOR hmon = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
|
||||
if (GetMonitorInfo(hmon, &mi))
|
||||
mon = mi.rcMonitor;
|
||||
winStyle = WS_POPUP | WS_CLIPCHILDREN;
|
||||
winX = mon.left;
|
||||
winY = mon.top;
|
||||
wr.left = 0; wr.top = 0;
|
||||
wr.right = mon.right - mon.left;
|
||||
wr.bottom = mon.bottom - mon.top;
|
||||
std::cout << "[cockpit] -fit: borderless " << (wr.right - wr.left) << "x"
|
||||
<< (wr.bottom - wr.top) << " (canvas letterboxes inside)"
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
|
||||
//
|
||||
// Window identity (MP dev): tag the title with the -net port so a player
|
||||
// running two nodes side-by-side can tell the windows apart when
|
||||
@@ -863,8 +1226,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
else
|
||||
swprintf(winTitle, 64, L"BattleTech %S", BT_VERSION_STRING);
|
||||
}
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, WS_OVERLAPPEDWINDOW,
|
||||
0, 0, wr.right - wr.left, wr.bottom - wr.top,
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, winStyle,
|
||||
winX, winY, wr.right - wr.left, wr.bottom - wr.top,
|
||||
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd) return FALSE;
|
||||
|
||||
|
||||
@@ -189,6 +189,23 @@ void
|
||||
command_line[n++] = (WCHAR)*s;
|
||||
}
|
||||
}
|
||||
//
|
||||
// Carry -fit into the child. This command line is built from scratch, so
|
||||
// a display flag the player passed US is not in `arguments` -- without
|
||||
// this, `btl4.exe -fit` gave a borderless menu and then a WINDOWED
|
||||
// mission (field report 2026-07-26).
|
||||
//
|
||||
{
|
||||
extern int gBTFitDisplay;
|
||||
if (gBTFitDisplay)
|
||||
{
|
||||
static const WCHAR fit[] = L" -fit";
|
||||
for (const WCHAR *s = fit; *s && n < MAX_PATH + 250; ++s)
|
||||
{
|
||||
command_line[n++] = *s;
|
||||
}
|
||||
}
|
||||
}
|
||||
command_line[n] = 0;
|
||||
|
||||
STARTUPINFOW startup;
|
||||
|
||||
@@ -465,6 +465,7 @@ const Receiver::HandlerEntry
|
||||
MESSAGE_ENTRY(Mech, TakeDamage),
|
||||
MESSAGE_ENTRY(Mech, PlayerLink),
|
||||
MESSAGE_ENTRY(Mech, BalanceCoolant), // id 0x16 @0049f728 (issue #20)
|
||||
MESSAGE_ENTRY(Mech, DuckRequest), // id 0x1a @0049fa00 (CROUCH, 2026-07-26)
|
||||
};
|
||||
|
||||
//
|
||||
@@ -486,6 +487,32 @@ void
|
||||
DEBUG_STREAM << "[valve] BALANCE (all valves -> 1)" << std::endl;
|
||||
}
|
||||
|
||||
//
|
||||
// @0049fa00 -- DuckRequest (id 0x1a): the CROUCH button (RIO 0x13). The
|
||||
// binary's whole body, verbatim:
|
||||
//
|
||||
// if (0 < *(int *)(message + 0xc)) *(undefined4 *)(this + 0x398) = 1;
|
||||
//
|
||||
// this+0x398 is duckState (attribute 0x37) -- the mech reset is the only other
|
||||
// writer in the binary (part_012.c:9439, the same reset that zeroes
|
||||
// incomingLock and sets distanceToMissile = FLT_MAX, which is how we already
|
||||
// map that region). Nothing in the decompiled code READS it: it is published
|
||||
// as an attribute, so its consumer is a databinding (the SQUAT clips), not a
|
||||
// call. So this handler is complete as-is -- the flag IS the feature's code
|
||||
// side, and a crouch implementation here would be a stand-in for data we have
|
||||
// not yet identified. See context/pod-hardware.md.
|
||||
//
|
||||
void
|
||||
Mech::DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message)
|
||||
{
|
||||
if (message->dataContents <= 0) // press only (msg+0xc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
duckState = 1;
|
||||
DEBUG_STREAM << "[duck] DuckRequest: duckState -> 1" << std::endl << std::flush;
|
||||
}
|
||||
|
||||
Receiver::MessageHandlerSet
|
||||
Mech::MessageHandlers(
|
||||
ELEMENTS(Mech::MessageHandlerEntries),
|
||||
|
||||
@@ -1193,6 +1193,22 @@ protected:
|
||||
enum { BalanceCoolantMessageID = 0x16 };
|
||||
void BalanceCoolantMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||
|
||||
// @0049fa00 -- "DuckRequest" (Mech handler table @0x50BDF8, id 0x1a; the
|
||||
// pod's CROUCH button, RIO address 0x13 -- the manual gives it a whole
|
||||
// section). The last unregistered id in the Mech table; until now a
|
||||
// press was silently swallowed by Receiver::Receive.
|
||||
//
|
||||
// The binary's ENTIRE body is two lines: press-only (msg+0xc > 0), then
|
||||
// duckState@0x398 = 1. It is a one-shot REQUEST flag, not a posture
|
||||
// toggle -- the handler never clears it, and the only other writer in
|
||||
// the whole binary is the mech reset (0). duckState has NO code reader
|
||||
// anywhere in the decomp; it is published as attribute 0x37, so whatever
|
||||
// consumes it consumes it through DATABINDING (the SQUAT clips), not
|
||||
// through a call. Reconstructed exactly: setting the flag is the
|
||||
// authentic behaviour, and inventing a crouch here would be a stand-in.
|
||||
enum { DuckRequestMessageID = 0x1a };
|
||||
void DuckRequestMessageHandler(ReceiverDataMessageOf<int> *message);
|
||||
|
||||
// --- damage-routing support (mechdmg / mech4) -----------------------
|
||||
// Typed access to the inherited Entity::damageZones[] (engine stores DamageZone*;
|
||||
// our entries are Mech__DamageZone, populated by the Mech ctor). Defined in
|
||||
|
||||
@@ -104,6 +104,18 @@ control (stick, twist, throttle, fire buttons), writes the bindings,
|
||||
and drops you into the solo menu to try them. Rebind keys in
|
||||
content\bindings.txt (regenerates with defaults if deleted).
|
||||
|
||||
FULL CONTROLS REFERENCE: see CONTROLS.txt next to this file -- every one of
|
||||
the pod's 72 cockpit buttons, what it does, and which display it sits on.
|
||||
|
||||
COCKPIT LAYOUT: content\environ.ini (written on first run, every option
|
||||
documented in the file itself) sizes the MFDs and the map, moves the map out
|
||||
of the middle of the road, or breaks each display into its own window. The
|
||||
cockpit now scales to ANY window shape without distorting -- drag it, maximise
|
||||
it, or launch with -fit for borderless full screen.
|
||||
|
||||
RGB KEYBOARDS: if yours supports Windows Dynamic Lighting, the keys you have
|
||||
bound to cockpit buttons glow in the panel's colours and flash along with it.
|
||||
|
||||
This build: {VERSION}. Private -- do not redistribute.
|
||||
{EXPIRE}Test builds STOP WORKING 14 days after they are made -- if the game
|
||||
{EXPIRE}says it has expired, just grab the operator's newest zip.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Verify the RIO button field from a BT_RIOBANK_LOG=1 run.
|
||||
|
||||
The banks now reach UNDER the glass and deliberately overlap (the map's foot
|
||||
row sits inside both side columns' reach), so "the address is in the list" is
|
||||
not enough -- the hit test takes the FIRST match, so an address whose rect is
|
||||
entirely shadowed by earlier buttons is dead however big it looks.
|
||||
|
||||
Reads btl4.log, groups by bank tag, and for each button asks: is there any
|
||||
point in my rect that no EARLIER button in this bank covers? Reports the
|
||||
address census per bank and any shadowed address.
|
||||
|
||||
python checkbank.py <log> [--expect 0x00-0x47]
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
BANK_RE = re.compile(r"\[riobank\] (.+?) bounds=\((-?\d+),(-?\d+),(-?\d+),(-?\d+)\) (\d+) buttons")
|
||||
BTN_RE = re.compile(r"\[riobank\] addr=0x([0-9a-f]+) rect=\((-?\d+),(-?\d+),(-?\d+),(-?\d+)\) class=(\d+)")
|
||||
|
||||
|
||||
def parse(path):
|
||||
banks = OrderedDict()
|
||||
current = None
|
||||
with open(path, "r", errors="replace") as handle:
|
||||
for line in handle:
|
||||
match = BANK_RE.search(line)
|
||||
if match:
|
||||
current = match.group(1)
|
||||
# a re-run appends; keep the FIRST pass of each tag
|
||||
banks.setdefault(current, [])
|
||||
if banks[current]:
|
||||
current = None # already captured this tag
|
||||
continue
|
||||
match = BTN_RE.search(line)
|
||||
if match and current is not None:
|
||||
addr = int(match.group(1), 16)
|
||||
x, y, w, h = (int(match.group(i)) for i in range(2, 6))
|
||||
banks[current].append((addr, x, y, w, h, int(match.group(6))))
|
||||
return banks
|
||||
|
||||
|
||||
def reachable(rect, earlier):
|
||||
"""Any point of `rect` not covered by an earlier rect?
|
||||
|
||||
Sampled on the coordinate grid induced by the earlier rects' edges -- if a
|
||||
gap exists at all, one of these sample points lands in it.
|
||||
"""
|
||||
_, x, y, w, h = rect[:5]
|
||||
xs = {x, x + w - 1}
|
||||
ys = {y, y + h - 1}
|
||||
for _, ex, ey, ew, eh, _ in earlier:
|
||||
for candidate in (ex - 1, ex, ex + ew - 1, ex + ew):
|
||||
if x <= candidate < x + w:
|
||||
xs.add(candidate)
|
||||
for candidate in (ey - 1, ey, ey + eh - 1, ey + eh):
|
||||
if y <= candidate < y + h:
|
||||
ys.add(candidate)
|
||||
for px in sorted(xs):
|
||||
for py in sorted(ys):
|
||||
hit = None
|
||||
for button in earlier:
|
||||
_, ex, ey, ew, eh, _ = button
|
||||
if ex <= px < ex + ew and ey <= py < ey + eh:
|
||||
hit = button
|
||||
break
|
||||
if hit is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else "btl4.log"
|
||||
banks = parse(path)
|
||||
if not banks:
|
||||
print("no [riobank] lines -- was BT_RIOBANK_LOG=1 set?")
|
||||
return 1
|
||||
|
||||
seen = set()
|
||||
failures = []
|
||||
for tag, buttons in banks.items():
|
||||
addresses = [b[0] for b in buttons]
|
||||
duplicates = {a for a in addresses if addresses.count(a) > 1}
|
||||
shadowed = []
|
||||
for index, button in enumerate(buttons):
|
||||
if not reachable(button, buttons[:index]):
|
||||
shadowed.append(button[0])
|
||||
seen.update(addresses)
|
||||
status = "ok"
|
||||
if duplicates or shadowed:
|
||||
status = "FAIL"
|
||||
failures.append(tag)
|
||||
print("%-22s %2d buttons 0x%02x-0x%02x %s"
|
||||
% (tag, len(buttons), min(addresses), max(addresses), status))
|
||||
if duplicates:
|
||||
print(" duplicate addresses: %s"
|
||||
% ", ".join("0x%02x" % a for a in sorted(duplicates)))
|
||||
if shadowed:
|
||||
print(" SHADOWED (no first-hit point): %s"
|
||||
% ", ".join("0x%02x" % a for a in sorted(shadowed)))
|
||||
|
||||
print("\n%d distinct addresses across %d banks" % (len(seen), len(banks)))
|
||||
missing = [a for a in range(0x00, 0x48) if a not in seen]
|
||||
if missing:
|
||||
print("not placed by any bank: %s"
|
||||
% ", ".join("0x%02x" % a for a in missing))
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Click every RIO button the layout claims, and check each one dispatched.
|
||||
|
||||
Reads a BT_RIOBANK_LOG=1 dump for the rects, posts a real WM_LBUTTONDOWN/UP at
|
||||
each button's centre, then compares the addresses the game LOGGED against the
|
||||
addresses the layout placed. This is the end-to-end check the geometry dump
|
||||
alone cannot give: it proves the hit test, the coordinate mapping and the RIO
|
||||
dispatch agree with the rects.
|
||||
|
||||
python clickbank.py <log> <window-title-substring> [--mode surround|exploded]
|
||||
|
||||
surround: one window, buttons are in backbuffer canvas space (== client space
|
||||
for a windowed run), so the centres go straight to the main window.
|
||||
exploded: one window per bank, each with its own client space; the bank tag
|
||||
names the window.
|
||||
"""
|
||||
import ctypes
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
|
||||
WM_LBUTTONDOWN = 0x0201
|
||||
WM_LBUTTONUP = 0x0202
|
||||
MK_LBUTTON = 0x0001
|
||||
|
||||
BANK_RE = re.compile(r"\[riobank\] (.+?) bounds=")
|
||||
BTN_RE = re.compile(r"\[riobank\] addr=0x([0-9a-f]+) rect=\((-?\d+),(-?\d+),(-?\d+),(-?\d+)\)")
|
||||
|
||||
|
||||
def parse(path):
|
||||
banks, current, done = {}, None, set()
|
||||
with open(path, "r", errors="replace") as handle:
|
||||
for line in handle:
|
||||
match = BANK_RE.search(line)
|
||||
if match:
|
||||
current = match.group(1)
|
||||
if current in done:
|
||||
current = None
|
||||
else:
|
||||
done.add(current)
|
||||
banks[current] = []
|
||||
continue
|
||||
match = BTN_RE.search(line)
|
||||
if match and current is not None:
|
||||
addr = int(match.group(1), 16)
|
||||
x, y, w, h = (int(match.group(i)) for i in range(2, 6))
|
||||
banks[current].append((addr, x + w // 2, y + h // 2))
|
||||
return banks
|
||||
|
||||
|
||||
def find_windows(substring):
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
def callback(hwnd, _):
|
||||
length = user32.GetWindowTextLengthW(hwnd)
|
||||
if length:
|
||||
buffer = ctypes.create_unicode_buffer(length + 1)
|
||||
user32.GetWindowTextW(hwnd, buffer, length + 1)
|
||||
if substring.lower() in buffer.value.lower() and user32.IsWindowVisible(hwnd):
|
||||
found.append((hwnd, buffer.value))
|
||||
return True
|
||||
|
||||
user32.EnumWindows(callback, 0)
|
||||
return found
|
||||
|
||||
|
||||
class RECT(ctypes.Structure):
|
||||
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
|
||||
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
|
||||
|
||||
|
||||
def canvas_size(path):
|
||||
"""The cockpit canvas from the boot line, e.g. '[cockpit] view 900x500 canvas 1452x1059'."""
|
||||
with open(path, "r", errors="replace") as handle:
|
||||
for line in handle:
|
||||
match = re.search(r"\[cockpit\] view \d+x\d+ canvas (\d+)x(\d+)", line)
|
||||
if match:
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
return None
|
||||
|
||||
|
||||
def fit_rect(client_w, client_h, canvas_w, canvas_h):
|
||||
"""Same uniform-scale centred fit the game computes (BTCockpitFitRect)."""
|
||||
w = client_w
|
||||
h = client_w * canvas_h // canvas_w
|
||||
if h > client_h:
|
||||
h = client_h
|
||||
w = client_h * canvas_w // canvas_h
|
||||
return ((client_w - w) // 2, (client_h - h) // 2, max(w, 1), max(h, 1))
|
||||
|
||||
|
||||
def click(hwnd, x, y):
|
||||
lparam = (y << 16) | (x & 0xFFFF)
|
||||
user32.PostMessageW(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lparam)
|
||||
time.sleep(0.02)
|
||||
user32.PostMessageW(hwnd, WM_LBUTTONUP, 0, lparam)
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def main():
|
||||
log = sys.argv[1]
|
||||
title = sys.argv[2]
|
||||
mode = "surround"
|
||||
if "--mode" in sys.argv:
|
||||
mode = sys.argv[sys.argv.index("--mode") + 1]
|
||||
|
||||
banks = parse(log)
|
||||
if not banks:
|
||||
print("no [riobank] lines in", log)
|
||||
return 1
|
||||
|
||||
# exploded: each bank is its OWN window with its own caption, so match
|
||||
# across every visible window rather than filtering by one title
|
||||
windows = find_windows("" if mode == "exploded" else title)
|
||||
if not windows:
|
||||
print("no window matching", title)
|
||||
return 1
|
||||
|
||||
# surround: the dump is in CANVAS space; if the window has been resized the
|
||||
# canvas is letterboxed inside the client, so map through the same fit the
|
||||
# game uses or every click lands off its button
|
||||
transform = None
|
||||
if mode == "surround":
|
||||
canvas = canvas_size(log)
|
||||
client = RECT()
|
||||
user32.GetClientRect(windows[0][0], ctypes.byref(client))
|
||||
if canvas and client.right > 0 and client.bottom > 0:
|
||||
ox, oy, fw, fh = fit_rect(client.right, client.bottom, canvas[0], canvas[1])
|
||||
if (fw, fh) != canvas:
|
||||
transform = (ox, oy, fw, fh, canvas[0], canvas[1])
|
||||
print("client %dx%d, canvas %dx%d -> fit %dx%d at (%d,%d)"
|
||||
% (client.right, client.bottom, canvas[0], canvas[1], fw, fh, ox, oy))
|
||||
|
||||
posted = set()
|
||||
for tag, buttons in banks.items():
|
||||
if mode == "surround":
|
||||
target = windows[0][0]
|
||||
else:
|
||||
# the bank tag is the window caption
|
||||
match = [h for h, caption in windows if caption.strip() == tag.strip()]
|
||||
if not match:
|
||||
print(" no window for bank %r -- skipped" % tag)
|
||||
continue
|
||||
target = match[0]
|
||||
for addr, cx, cy in buttons:
|
||||
if transform is not None:
|
||||
ox, oy, fw, fh, cw, ch = transform
|
||||
cx = ox + cx * fw // cw
|
||||
cy = oy + cy * fh // ch
|
||||
click(target, cx, cy)
|
||||
posted.add(addr)
|
||||
print("posted clicks for %d addresses" % len(posted))
|
||||
print(" ".join("0x%02x" % a for a in sorted(posted)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Measure the map's PAINTED legend grid, and compare the lamp bars against it.
|
||||
|
||||
The side button columns should line up with the legend cells the map imagery
|
||||
paints beside them. RP412 measured ITS map at 13 + 6x102 on a 105 pitch of 640
|
||||
-- a different game's art, so those numbers do not transfer. This finds ours.
|
||||
|
||||
Two scans over a native Secondary/Radar capture:
|
||||
* the LEGEND: horizontal rules of the six bordered cells in the map imagery
|
||||
* the LAMPS: the runs of the yellow button strip at the window's left edge
|
||||
and reports the offset between them, which is what a misalignment looks like.
|
||||
|
||||
py measurelegend.py <capture.png> <caption_h> <lamp_x> <legend_x0> <legend_x1>
|
||||
"""
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
caption = int(sys.argv[2]) if len(sys.argv) > 2 else 30
|
||||
lamp_x = int(sys.argv[3]) if len(sys.argv) > 3 else 12
|
||||
lx0 = int(sys.argv[4]) if len(sys.argv) > 4 else 22
|
||||
lx1 = int(sys.argv[5]) if len(sys.argv) > 5 else 118
|
||||
|
||||
image = Image.open(path).convert("RGB")
|
||||
w, h = image.size
|
||||
px = image.load()
|
||||
print("capture %dx%d (client starts at y=%d)" % (w, h, caption))
|
||||
|
||||
# --- the legend: rows carrying a long horizontal ORANGE rule -------------
|
||||
def orange(p):
|
||||
return p[0] > 110 and p[1] > 45 and p[2] < 90 and p[0] > p[2] + 60
|
||||
|
||||
rules = []
|
||||
for y in range(caption, h):
|
||||
run = 0
|
||||
best = 0
|
||||
for x in range(lx0, min(lx1, w)):
|
||||
if orange(px[x, y]):
|
||||
run += 1
|
||||
best = max(best, run)
|
||||
else:
|
||||
run = 0
|
||||
if best >= (lx1 - lx0) * 0.55:
|
||||
rules.append(y - caption)
|
||||
|
||||
# collapse adjacent rows into single rules
|
||||
grouped = []
|
||||
for y in rules:
|
||||
if grouped and y - grouped[-1][-1] <= 2:
|
||||
grouped[-1].append(y)
|
||||
else:
|
||||
grouped.append([y])
|
||||
rule_tops = [g[0] for g in grouped]
|
||||
print("\nlegend rules (client y): %s" % rule_tops)
|
||||
if len(rule_tops) >= 2:
|
||||
print("gaps: %s" % [rule_tops[i+1] - rule_tops[i]
|
||||
for i in range(len(rule_tops) - 1)])
|
||||
|
||||
# --- the lamps: runs of lit pixels in the button strip -------------------
|
||||
lit = []
|
||||
for y in range(caption, h):
|
||||
p = px[lamp_x, y]
|
||||
lit.append(p[0] > 55 and p[1] > 40 and p[2] < 90)
|
||||
runs = []
|
||||
start = None
|
||||
for i, on in enumerate(lit):
|
||||
if on and start is None:
|
||||
start = i
|
||||
elif not on and start is not None:
|
||||
if i - start >= 4:
|
||||
runs.append((start, i - start))
|
||||
start = None
|
||||
if start is not None and len(lit) - start >= 4:
|
||||
runs.append((start, len(lit) - start))
|
||||
|
||||
print("\nlamp bars (client y, height): %s" % runs)
|
||||
if len(runs) >= 2:
|
||||
print("lamp pitch: %s" % [runs[i+1][0] - runs[i][0]
|
||||
for i in range(len(runs) - 1)])
|
||||
|
||||
# --- compare ------------------------------------------------------------
|
||||
if rule_tops and runs:
|
||||
print("\ncell top vs lamp top:")
|
||||
for i, (top, height) in enumerate(runs[:len(rule_tops)]):
|
||||
print(" lamp %d top=%4d nearest rule=%4d offset=%+d"
|
||||
% (i, top, rule_tops[i], top - rule_tops[i]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -119,6 +119,35 @@ def main():
|
||||
lines = [l for l in lines if "{EXPIRE}" not in l]
|
||||
readme = "".join(lines)
|
||||
z.writestr(name + "/README.txt", readme)
|
||||
|
||||
# The full controls reference travels with the game, so players get the
|
||||
# 72-button panel map without the repo. Flattened to ASCII so it reads
|
||||
# correctly in Notepad -- the markdown source keeps its typography.
|
||||
if os.path.exists("docs/CONTROLS.md"):
|
||||
controls = open("docs/CONTROLS.md", encoding="utf-8").read()
|
||||
for bad, good in ((u"—", "-"), (u"–", "-"), (u"‘", "'"),
|
||||
(u"’", "'"), (u"“", '"'), (u"”", '"'),
|
||||
(u"×", "x"), (u"→", "->"), (u"…", "..."),
|
||||
(u"↑", "Up"), (u"↓", "Down"),
|
||||
(u"←", "Left"), (u"→", "Right"),
|
||||
(u"⚠", "!"), (u"·", "-")):
|
||||
controls = controls.replace(bad, good)
|
||||
controls = controls.encode("ascii", "replace").decode("ascii")
|
||||
z.writestr(name + "/CONTROLS.txt", controls)
|
||||
print(" controls reference -> CONTROLS.txt")
|
||||
|
||||
# The same map as a page, for anyone who would rather look at the
|
||||
# diagrams than read them. The source is a fragment (no doctype or
|
||||
# <head>), so wrap it: without a doctype the browser drops into quirks
|
||||
# mode, and without a charset the typography arrives as mojibake.
|
||||
if os.path.exists("docs/CONTROLS.html"):
|
||||
page = open("docs/CONTROLS.html", encoding="utf-8").read()
|
||||
page = ('<!doctype html>\n<html lang="en">\n<head>\n'
|
||||
'<meta charset="utf-8">\n'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
||||
'</head>\n<body>\n' + page + '\n</body>\n</html>\n')
|
||||
z.writestr(name + "/CONTROLS.html", page.encode("utf-8"))
|
||||
print(" controls page -> CONTROLS.html")
|
||||
for p in bats: # bats at the zip ROOT (next to
|
||||
arc = os.path.basename(p) # content\ + build\, per the guard)
|
||||
z.write(p, name + "/" + arc)
|
||||
|
||||
Reference in New Issue
Block a user