diff --git a/.gitignore b/.gitignore index 69ba2de..6e27588 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ content/environ.ini content/frontend.egg content/steam_appid.txt content/fe_last.ini +content/glass_layout.cfg +glass_layout.cfg # local root copies of the player bats (players/ is the source of truth) /play_solo.bat /joyconfig.bat diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 0384914..88daf0d 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -114,6 +114,31 @@ each, so they cover the foot band too, and the hit test takes the first match what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests first-hit reachability, not mere presence. +## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified] + +The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window +(`ComputeLayout`, then a one-shot re-snap once the real main window is up). They carry +`WS_CAPTION` in every mode, so a dev can always drag them — but by default a drag does **not** +survive relaunch (and the menu→mission→menu loop relaunches the process each hop). **`BT_GLASS_LAYOUT`** +(`L4GLASSWIN.cpp`) adds opt-in persistence to a cwd-relative `glass_layout.cfg` (beside +`bindings.txt`; gitignored): + +| `BT_GLASS_LAYOUT` | Behaviour | +|---|---| +| unset / `off` / `0` | computed ring only, no file I/O (**default** — pod-faithful, unchanged) | +| `load` / `restore` | restore saved positions on startup (per-window fallback to computed for any not in the file); never writes | +| `save` / `adjust` / `on` / `1` | restore first, **then** re-write the whole file on each finished drag (`WM_EXITSIZEMOVE`) and on teardown — the round-trip "adjust" mode | + +Mechanics: `glass_layout.cfg` is `# comment`-tolerant, one `«window title»=x,y,w,h` line per +window (title is the stable key — titles carry no `=`). **Position is restored; size is not** — +each window's frame size is deterministic from its content (`AdjustWindowRect`), so honouring an +old saved `w,h` could distort a later geometry change; the `w,h` are written for reference only. +A restored window sets a `restored` flag that makes `ComputeLayout` skip it, so the post-main-window +re-snap leaves hand-placed windows alone (round-trip verified: a window saved at 777,333 came back +at exactly 777,333 after the re-snap fired). Save fires on `WM_EXITSIZEMOVE` (survives a hard kill, +since it writes as soon as a drag finishes) with a teardown write as backstop. This is the native +analogue of TeslaRel410 `emulator/pod-launch`'s per-rig `--bridge-pos`/`--layout` placement args. + **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 diff --git a/docs/GLASS_COCKPIT.md b/docs/GLASS_COCKPIT.md index 520b6dd..2208e71 100644 --- a/docs/GLASS_COCKPIT.md +++ b/docs/GLASS_COCKPIT.md @@ -732,3 +732,35 @@ mode). mkdist wraps the fragment in a doctype/charset shell and ships it beside - 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. + +## 2026-07-28 — Sticky per-display window placement (BT_GLASS_LAYOUT) + +The exploded (`BT_GLASS_PANELS`) windows self-place in the pod-faithful ring and re-snap once the +main window appears; a dev could always drag them (they carry `WS_CAPTION`) but the drag never +survived the menu→mission→menu relaunch loop. New **`BT_GLASS_LAYOUT`** gate (all in +`L4GLASSWIN.cpp`, ~160 lines, no shared-file churn) persists positions to a cwd-relative +`glass_layout.cfg` beside `bindings.txt` (gitignored): + +- **`off`/`0`/unset (default):** computed ring only, no file I/O — pod behaviour unchanged. +- **`load`/`restore`:** on `Create`, after `ComputeLayout`, `LoadLayout` overrides `wantX/wantY` + for each window named in the file (per-window fallback to computed) and sets a `restored` flag; + never writes. +- **`save`/`adjust`/`on`/`1`:** loads first, then `SaveLayout` rewrites the whole file on each + finished drag (`WM_EXITSIZEMOVE` in the WndProc) and once more on teardown (`Destroy`) as a + backstop. Writing on drag-finish means a hard kill still keeps the layout. + +Format: `# comment`-tolerant, one `«title»=x,y,w,h` per window; title is the stable key (no `=` in +any title). **Position restored, size ignored** — frame size is deterministic from content +(`AdjustWindowRect`), so an old `w,h` can't distort a later geometry change; `w,h` are written for +reference only. The `restored` flag makes `ComputeLayout` skip that window, so the post-main-window +re-snap leaves hand-placed windows put. Native analogue of TeslaRel410 `emulator/pod-launch`'s +per-rig `--bridge-pos`/`--layout` args. + +### Verified [T2, runtime round-trip] +- Build: Release links clean, only the 40 tolerated `/FORCE` externals, no compile errors. +- `load`: seeded a distinctive cfg → log `restored 7 window position(s)`, cockpit up, mission ran. +- `save`: moved *Flight Controls* to (777,333) + drove `WM_EXITSIZEMOVE` → cfg written with all 7 + rects, the moved one at `777,333` (proves `GetWindowRect` read the live window), others at ring. +- round-trip: relaunched `load` against the game-written cfg → after 12 s (past the re-snap) the + *Flight Controls* window was physically at (777,333), all 7 at their saved coords — the re-snap + guard held. diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index 771a3de..b17f096 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -113,6 +113,7 @@ struct GWin int clientW, clientH; int frameW, frameH; int wantX, wantY; + int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap }; static GWin gWins[8]; @@ -331,6 +332,8 @@ static void for (int i = 0; i < gWinCount; ++i) { GWin &w = gWins[i]; + if (w.restored) + continue; // sticky (BT_GLASS_LAYOUT) -- keep the saved spot int fw = w.frameW, fh = w.frameH; int x = m.left, y = m.top; switch (i) @@ -362,6 +365,152 @@ static void 0, 0, SWP_NOSIZE | SWP_NOACTIVATE); } +//########################################################################### +// Sticky layout -- BT_GLASS_LAYOUT persists each window's on-screen position +// to glass_layout.cfg (cwd-relative, beside bindings.txt), so a developer can +// drag the ring into place once and have it survive the menu->mission->menu +// relaunch loop and future launches. Modes: +// off / 0 / unset -- computed pod-faithful ring only, no file I/O (default) +// load / restore -- restore saved positions (per-window fallback to +// computed); never write +// save / adjust / -- restore first, then re-save on finished-drag +// on / 1 (WM_EXITSIZEMOVE) and on teardown [the round trip] +// Position is restored; size stays deterministic from the window's content, so +// a later geometry change can't distort an old save. Windows carry WS_CAPTION +// in every mode, so they are always draggable -- the gate only controls whether +// a drag STICKS. +//########################################################################### + +enum { LayoutOff = 0, LayoutLoad = 1, LayoutSave = 2 }; + +static const char *layoutFileName = "glass_layout.cfg"; + +static int + GlassLayoutMode() +{ + static int m = -1; + if (m < 0) + { + const char *e = getenv("BT_GLASS_LAYOUT"); + if (e == NULL || e[0] == '\0') + m = LayoutOff; + else if (_stricmp(e, "off") == 0 || _stricmp(e, "0") == 0) + m = LayoutOff; + else if (_stricmp(e, "load") == 0 || _stricmp(e, "restore") == 0) + m = LayoutLoad; + else // save / adjust / on / 1 / anything else truthy + m = LayoutSave; + if (m != LayoutOff) + DEBUG_STREAM << "[glasswin] BT_GLASS_LAYOUT=" + << (m == LayoutSave ? "save" : "load") << " (" << layoutFileName + << ")\n" << std::flush; + } + return m; +} + +// Find a window by its (stable) title. Titles carry no '=', so the cfg splits +// cleanly on the first '='. +static GWin * + FindGWinByTitle(const char *title) +{ + for (int i = 0; i < gWinCount; ++i) + if (gWins[i].title != NULL && strcmp(gWins[i].title, title) == 0) + return &gWins[i]; + return NULL; +} + +// Restore saved positions over the just-computed pod-faithful defaults. Called +// from Create AFTER ComputeLayout, so any window not named in the file keeps its +// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which +// re-runs ComputeLayout once the main window appears) leaves them alone. +static void + LoadLayout() +{ + if (GlassLayoutMode() == LayoutOff) + return; + + FILE *f = fopen(layoutFileName, "rt"); + if (f == NULL) + { + DEBUG_STREAM << "[glasswin] no " << layoutFileName + << " yet (using computed placement)\n" << std::flush; + return; + } + + int restored = 0; + char line[256]; + while (fgets(line, sizeof(line), f) != NULL) + { + char *s = line; + while (*s == ' ' || *s == '\t') ++s; + if (*s == '#' || *s == '\r' || *s == '\n' || *s == '\0') + continue; + + char *eq = strchr(s, '='); + if (eq == NULL) + continue; + *eq = '\0'; + // trim trailing space off the title + char *end = eq; + while (end > s && (end[-1] == ' ' || end[-1] == '\t')) --end; + *end = '\0'; + + int x = 0, y = 0, w = 0, h = 0; + if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2) + continue; + + GWin *gw = FindGWinByTitle(s); + if (gw == NULL) + continue; + gw->wantX = x; + gw->wantY = y; + gw->restored = 1; + ++restored; + } + fclose(f); + DEBUG_STREAM << "[glasswin] restored " << restored << " window position(s) from " + << layoutFileName << "\n" << std::flush; +} + +// Write every window's current on-screen frame rect. Whole-file rewrite (it is +// tiny), so partial/hard kills never leave a half-written file for long. Called +// on finished-drag and on teardown in save mode. +static void + SaveLayout() +{ + if (GlassLayoutMode() != LayoutSave) + return; + + FILE *f = fopen(layoutFileName, "wt"); + if (f == NULL) + { + DEBUG_STREAM << "[glasswin] could not write " << layoutFileName + << "\n" << std::flush; + return; + } + + fputs("# BT411 glass cockpit window layout (BT_GLASS_LAYOUT=save writes this\n" + "# on finished-drag/exit; =load restores it). =<x>,<y>,<w>,<h>\n", + f); + int wrote = 0; + for (int i = 0; i < gWinCount; ++i) + { + GWin &gw = gWins[i]; + if (gw.hwnd == NULL || !IsWindow(gw.hwnd)) + continue; + RECT r; + if (!GetWindowRect(gw.hwnd, &r)) + continue; + fprintf(f, "%s=%ld,%ld,%ld,%ld\n", gw.title, + (long)r.left, (long)r.top, + (long)(r.right - r.left), (long)(r.bottom - r.top)); + ++wrote; + } + fclose(f); + DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to " + << layoutFileName << "\n" << std::flush; +} + //########################################################################### // Painting //########################################################################### @@ -715,6 +864,13 @@ static LRESULT CALLBACK } return 0; + case WM_EXITSIZEMOVE: + // The user finished dragging (or resizing) a window. In save/adjust + // mode, persist the whole ring so the layout survives a hard kill even + // if teardown never runs. No-op in load/off mode. + SaveLayout(); + return 0; + case WM_CLOSE: ShowWindow(window, SW_HIDE); // hide; the device owns the lifetime return 0; @@ -781,6 +937,7 @@ void gWins[i].frameH = fr.bottom - fr.top; } ComputeLayout(); + LoadLayout(); // BT_GLASS_LAYOUT=load/save: restore saved positions over the ring for (int i = 0; i < gWinCount; ++i) { @@ -816,6 +973,9 @@ void void BTGlassPanels_Destroy() { + SaveLayout(); // backstop for a clean teardown (WM_EXITSIZEMOVE already + // caught every finished drag); no-op unless mode==save + for (int i = 0; i < gWinCount; ++i) { if (gWins[i].hwnd != NULL)