From 77cbdec8b6ffb5f2410c0511c9988b112eb74d0b Mon Sep 17 00:00:00 2001 From: ejams1 Date: Wed, 21 Oct 2026 23:52:35 -0600 Subject: [PATCH] Phase 7: GUI The upstream GUI of xEdit.dpr is ported onto the existing command layer with egui, in nine steps: the crate and the main window, the command-layer gaps, the view and navigation, the flow shell, the tree and grid bindings, the selection dialogs, the viewers and editors, the settings and modal tools, the menu and binding audit, and the gate. The gate closed with the ledger shut (cargo xtask check: 663 gui entries, 0 pending; 240 covered, 423 presentation), the ten popup menus walked through against the Delphi build (tools/menu_diff.py diffs xEdit/xeMainForm.dfm of the 4.1.5q source against crates/xedit-gui/src/menu_model.rs: caption tree, nesting, Enabled, Checked, RadioItem and Visible; problems: 0), and the dialogs witnessed (nineteen captures of the port's own dialogs, the module selection captured on both sides). The walkthrough record is docs/history/phase-7.md. The parity harness rerun at the tip covered the dump, check and clean steps (246 of 249, 246 of 249, 235 of 238); its saves step and the rest of the chain continue separately, and the record says which numbers belong to which tip. Co-Authored-By: Claude Code --- .claude/skills/checking-parity/SKILL.md | 1 + .../references/running-the-harness.md | 12 + .../references/running-the-oracle.md | 2 + .../references/screen-checks.md | 72 + .../using-xedit-cli/references/commands.md | 7 +- .../using-xedit-cli/references/known-gaps.md | 2 +- .../references/loading-plugins.md | 2 +- .../using-xedit-cli/references/scripts.md | 4 +- .gitattributes | 3 + Cargo.lock | 4965 +++++++++-- Cargo.toml | 17 + NOTICE | 55 + README.md | 7 +- coverage/gui_audit.toml | 1694 ++++ coverage/ledger.toml | 220 +- crates/xedit-analysis/src/check.rs | 27 +- crates/xedit-analysis/src/cleaning.rs | 4 + crates/xedit-analysis/src/conflict.rs | 26 + crates/xedit-analysis/src/filter.rs | 26 + crates/xedit-assets/src/lod/generate.rs | 2 + crates/xedit-assets/src/lod/mod.rs | 11 + crates/xedit-cli/src/main.rs | 237 +- crates/xedit-core/src/implementation/add.rs | 77 +- crates/xedit-core/src/implementation/refs.rs | 5 + crates/xedit-core/src/interface/element.rs | 11 +- crates/xedit-core/src/interface/globals.rs | 61 + crates/xedit-core/src/interface/mod.rs | 1 + crates/xedit-core/src/interface/wait_form.rs | 312 + crates/xedit-gui/Cargo.toml | 33 + .../xedit-gui/assets/icons/xEdit_FO_Icon.ico | Bin 0 -> 101894 bytes .../xedit-gui/assets/icons/xEdit_SF_Icon.ico | Bin 0 -> 64764 bytes .../xedit-gui/assets/icons/xEdit_TES_Icon.ico | Bin 0 -> 61829 bytes crates/xedit-gui/resources/dev_image.bmp | Bin 0 -> 120054 bytes crates/xedit-gui/resources/dev_message.rtf | 228 + .../resources/dev_message_thanks.rtf | 237 + crates/xedit-gui/resources/tips.txt | 62 + crates/xedit-gui/resources/whats_new.rtf | 2010 +++++ crates/xedit-gui/src/command.rs | 153 + .../src/dialogs/developer_message_form.rs | 344 + .../src/dialogs/edit_warning_form.rs | 190 + .../xedit-gui/src/dialogs/file_select_form.rs | 353 + .../src/dialogs/filter_options_form.rs | 788 ++ .../xedit-gui/src/dialogs/form_id_prompt.rs | 132 + .../xedit-gui/src/dialogs/game_select_form.rs | 134 + crates/xedit-gui/src/dialogs/legend_form.rs | 310 + .../src/dialogs/localization_form.rs | 483 ++ .../src/dialogs/localize_plugin_form.rs | 217 + crates/xedit-gui/src/dialogs/lodgen_form.rs | 644 ++ .../src/dialogs/log_analyzer_form.rs | 712 ++ crates/xedit-gui/src/dialogs/mod.rs | 245 + .../src/dialogs/mod_group_edit_form.rs | 1059 +++ .../src/dialogs/mod_group_select_form.rs | 934 +++ .../src/dialogs/module_select_form.rs | 494 ++ crates/xedit-gui/src/dialogs/options_form.rs | 1050 +++ crates/xedit-gui/src/dialogs/path_prompt.rs | 200 + crates/xedit-gui/src/dialogs/prompts.rs | 282 + .../xedit-gui/src/dialogs/rich_edit_form.rs | 213 + crates/xedit-gui/src/dialogs/script_form.rs | 1024 +++ crates/xedit-gui/src/dialogs/script_host.rs | 285 + crates/xedit-gui/src/dialogs/selection.rs | 693 ++ crates/xedit-gui/src/dialogs/text_prompt.rs | 138 + crates/xedit-gui/src/dialogs/tip_form.rs | 208 + .../src/dialogs/view_elements_form.rs | 572 ++ crates/xedit-gui/src/dialogs/viewers.rs | 849 ++ crates/xedit-gui/src/dialogs/wait_form.rs | 411 + .../dialogs/worldspace_cell_details_form.rs | 159 + crates/xedit-gui/src/element_detail.rs | 568 ++ crates/xedit-gui/src/icon.rs | 182 + crates/xedit-gui/src/information_text.txt | 215 + crates/xedit-gui/src/job_runner.rs | 396 + crates/xedit-gui/src/lib.rs | 61 + crates/xedit-gui/src/main.rs | 226 + crates/xedit-gui/src/main_form.rs | 7399 +++++++++++++++++ crates/xedit-gui/src/menu_model.rs | 3100 +++++++ crates/xedit-gui/src/nav_flows.rs | 662 ++ crates/xedit-gui/src/nav_tree.rs | 1913 +++++ crates/xedit-gui/src/panes.rs | 2494 ++++++ crates/xedit-gui/src/screenshot.rs | 278 + crates/xedit-gui/src/selection.rs | 246 + crates/xedit-gui/src/settings.rs | 584 ++ crates/xedit-gui/src/startup.rs | 1395 ++++ crates/xedit-gui/src/theme.rs | 281 + crates/xedit-gui/src/view_grid.rs | 1080 +++ crates/xedit-gui/src/widgets/conflict.rs | 354 + crates/xedit-gui/src/widgets/grid.rs | 910 ++ crates/xedit-gui/src/widgets/mod.rs | 120 + .../xedit-gui/src/widgets/push_like_button.rs | 167 + crates/xedit-gui/src/widgets/virtual_tree.rs | 812 ++ crates/xedit-gui/tests/menu_model.rs | 153 + crates/xedit-gui/tests/nav_tree.rs | 291 + crates/xedit-gui/tests/panes.rs | 412 + crates/xedit-gui/tests/selection_dialogs.rs | 416 + crates/xedit-gui/tests/step7.rs | 485 ++ crates/xedit-gui/tests/view_grid.rs | 298 + crates/xedit-loadorder/src/load_order.rs | 107 +- crates/xedit-session/src/check.rs | 11 + crates/xedit-session/src/clean.rs | 22 +- crates/xedit-session/src/commands.rs | 179 +- crates/xedit-session/src/compare_to.rs | 170 + crates/xedit-session/src/conflicts.rs | 325 +- crates/xedit-session/src/edit.rs | 20 + crates/xedit-session/src/filter.rs | 163 +- crates/xedit-session/src/formids.rs | 40 +- crates/xedit-session/src/lib.rs | 8 +- crates/xedit-session/src/localization.rs | 34 +- crates/xedit-session/src/lodgen.rs | 50 +- crates/xedit-session/src/log_analyzer.rs | 8 +- crates/xedit-session/src/modgroups.rs | 46 +- crates/xedit-session/src/module_select.rs | 292 +- crates/xedit-session/src/refs.rs | 53 +- crates/xedit-session/src/resources.rs | 13 +- crates/xedit-session/src/save.rs | 16 + crates/xedit-session/src/script.rs | 308 +- crates/xedit-session/src/settings.rs | 339 + crates/xedit-session/src/tool_modes.rs | 3 +- crates/xedit-session/tests/check.rs | 15 + crates/xedit-session/tests/conflicts.rs | 15 + crates/xedit-session/tests/formids.rs | 28 + crates/xedit-session/tests/gui_commands.rs | 16 + .../tests/gui_commands_batch4.rs | 847 ++ crates/xedit-session/tests/refs.rs | 27 + crates/xedit-session/tests/roundtrip.rs | 25 + crates/xedit-session/tests/script.rs | 191 +- crates/xtask/Cargo.toml | 3 +- crates/xtask/src/gui/mod.rs | 34 + crates/xtask/src/gui/windows/keys.rs | 347 + crates/xtask/src/gui/windows/mod.rs | 1757 ++++ crates/xtask/src/gui/windows/png.rs | 89 + crates/xtask/src/gui/windows/win32.rs | 886 ++ crates/xtask/src/main.rs | 210 +- crates/xtask/src/parity.rs | 2 +- crates/xtask/src/parity/hidden.rs | 158 +- deny.toml | 15 +- docs/PLAN.md | 40 + docs/history/phase-7.md | 109 + tools/menu_diff.py | 203 + upstream-map.toml | 104 +- 137 files changed, 55581 insertions(+), 954 deletions(-) create mode 100644 .claude/skills/checking-parity/references/screen-checks.md create mode 100644 coverage/gui_audit.toml create mode 100644 crates/xedit-core/src/interface/wait_form.rs create mode 100644 crates/xedit-gui/Cargo.toml create mode 100644 crates/xedit-gui/assets/icons/xEdit_FO_Icon.ico create mode 100644 crates/xedit-gui/assets/icons/xEdit_SF_Icon.ico create mode 100644 crates/xedit-gui/assets/icons/xEdit_TES_Icon.ico create mode 100644 crates/xedit-gui/resources/dev_image.bmp create mode 100644 crates/xedit-gui/resources/dev_message.rtf create mode 100644 crates/xedit-gui/resources/dev_message_thanks.rtf create mode 100644 crates/xedit-gui/resources/tips.txt create mode 100644 crates/xedit-gui/resources/whats_new.rtf create mode 100644 crates/xedit-gui/src/command.rs create mode 100644 crates/xedit-gui/src/dialogs/developer_message_form.rs create mode 100644 crates/xedit-gui/src/dialogs/edit_warning_form.rs create mode 100644 crates/xedit-gui/src/dialogs/file_select_form.rs create mode 100644 crates/xedit-gui/src/dialogs/filter_options_form.rs create mode 100644 crates/xedit-gui/src/dialogs/form_id_prompt.rs create mode 100644 crates/xedit-gui/src/dialogs/game_select_form.rs create mode 100644 crates/xedit-gui/src/dialogs/legend_form.rs create mode 100644 crates/xedit-gui/src/dialogs/localization_form.rs create mode 100644 crates/xedit-gui/src/dialogs/localize_plugin_form.rs create mode 100644 crates/xedit-gui/src/dialogs/lodgen_form.rs create mode 100644 crates/xedit-gui/src/dialogs/log_analyzer_form.rs create mode 100644 crates/xedit-gui/src/dialogs/mod.rs create mode 100644 crates/xedit-gui/src/dialogs/mod_group_edit_form.rs create mode 100644 crates/xedit-gui/src/dialogs/mod_group_select_form.rs create mode 100644 crates/xedit-gui/src/dialogs/module_select_form.rs create mode 100644 crates/xedit-gui/src/dialogs/options_form.rs create mode 100644 crates/xedit-gui/src/dialogs/path_prompt.rs create mode 100644 crates/xedit-gui/src/dialogs/prompts.rs create mode 100644 crates/xedit-gui/src/dialogs/rich_edit_form.rs create mode 100644 crates/xedit-gui/src/dialogs/script_form.rs create mode 100644 crates/xedit-gui/src/dialogs/script_host.rs create mode 100644 crates/xedit-gui/src/dialogs/selection.rs create mode 100644 crates/xedit-gui/src/dialogs/text_prompt.rs create mode 100644 crates/xedit-gui/src/dialogs/tip_form.rs create mode 100644 crates/xedit-gui/src/dialogs/view_elements_form.rs create mode 100644 crates/xedit-gui/src/dialogs/viewers.rs create mode 100644 crates/xedit-gui/src/dialogs/wait_form.rs create mode 100644 crates/xedit-gui/src/dialogs/worldspace_cell_details_form.rs create mode 100644 crates/xedit-gui/src/element_detail.rs create mode 100644 crates/xedit-gui/src/icon.rs create mode 100644 crates/xedit-gui/src/information_text.txt create mode 100644 crates/xedit-gui/src/job_runner.rs create mode 100644 crates/xedit-gui/src/lib.rs create mode 100644 crates/xedit-gui/src/main.rs create mode 100644 crates/xedit-gui/src/main_form.rs create mode 100644 crates/xedit-gui/src/menu_model.rs create mode 100644 crates/xedit-gui/src/nav_flows.rs create mode 100644 crates/xedit-gui/src/nav_tree.rs create mode 100644 crates/xedit-gui/src/panes.rs create mode 100644 crates/xedit-gui/src/screenshot.rs create mode 100644 crates/xedit-gui/src/selection.rs create mode 100644 crates/xedit-gui/src/settings.rs create mode 100644 crates/xedit-gui/src/startup.rs create mode 100644 crates/xedit-gui/src/theme.rs create mode 100644 crates/xedit-gui/src/view_grid.rs create mode 100644 crates/xedit-gui/src/widgets/conflict.rs create mode 100644 crates/xedit-gui/src/widgets/grid.rs create mode 100644 crates/xedit-gui/src/widgets/mod.rs create mode 100644 crates/xedit-gui/src/widgets/push_like_button.rs create mode 100644 crates/xedit-gui/src/widgets/virtual_tree.rs create mode 100644 crates/xedit-gui/tests/menu_model.rs create mode 100644 crates/xedit-gui/tests/nav_tree.rs create mode 100644 crates/xedit-gui/tests/panes.rs create mode 100644 crates/xedit-gui/tests/selection_dialogs.rs create mode 100644 crates/xedit-gui/tests/step7.rs create mode 100644 crates/xedit-gui/tests/view_grid.rs create mode 100644 crates/xedit-session/src/compare_to.rs create mode 100644 crates/xedit-session/src/settings.rs create mode 100644 crates/xedit-session/tests/gui_commands_batch4.rs create mode 100644 crates/xtask/src/gui/mod.rs create mode 100644 crates/xtask/src/gui/windows/keys.rs create mode 100644 crates/xtask/src/gui/windows/mod.rs create mode 100644 crates/xtask/src/gui/windows/png.rs create mode 100644 crates/xtask/src/gui/windows/win32.rs create mode 100644 docs/history/phase-7.md create mode 100644 tools/menu_diff.py diff --git a/.claude/skills/checking-parity/SKILL.md b/.claude/skills/checking-parity/SKILL.md index 4aecfe3b..022c1d8f 100644 --- a/.claude/skills/checking-parity/SKILL.md +++ b/.claude/skills/checking-parity/SKILL.md @@ -41,5 +41,6 @@ Read [running the oracle](references/running-the-oracle.md) before invoking a GU - [Checks](references/checks.md) - [Investigating a difference](references/investigating-a-difference.md) - [Quirks found and reproduced](references/quirks-found-and-reproduced.md) +- [Screen checks](references/screen-checks.md) - [Background and compatibility scope](references/background.md) diff --git a/.claude/skills/checking-parity/references/running-the-harness.md b/.claude/skills/checking-parity/references/running-the-harness.md index f498d1ef..2d845862 100644 --- a/.claude/skills/checking-parity/references/running-the-harness.md +++ b/.claude/skills/checking-parity/references/running-the-harness.md @@ -37,3 +37,15 @@ An oracle assertion failure while loading (`EAssertionFailed` in `wbImplementati The oracle is slow on large masters: it writes about 80 MB of dump per minute and needs more than ten minutes for `Fallout4.esm`. `--oracle-only` fills the cache without running the port. The port output is compared while the port writes it; it is kept only for a file that differs, as `.port.txt.zst` up to the first difference (`zstd -dc` reads it), next to the port's log `.port.log`. + +## The full chain + +The full corpus rerun is a sequence of modes, each with its own report in `target/parity/.json`: + +``` +dump, check, clean, saves, roundtrip, oracle-save, oracle-edit, conflicts, refs, modgroups, merged, filter, tool-modes, clean-scenarios, resources, discovery, lodgen, bsarch +``` + +Run them from a script that logs the exit code and the summary line of each mode, and keep the logs outside the repository. Two modes need care on a live save folder: `saves` needs `--oracle-timeout` (the oracle can crash on a save and then never exit, see [running the oracle](running-the-oracle.md)), and `bsarch` is the long one (1472 archives). + +The phase-7 chain of 2026-10-21 covered `dump`, `check` and `clean` at the phase tip; its `saves` step and the rest of the chain were continued separately, and `docs/history/phase-7.md` says which numbers belong to which tip. diff --git a/.claude/skills/checking-parity/references/running-the-oracle.md b/.claude/skills/checking-parity/references/running-the-oracle.md index 5a44c87f..0fea829a 100644 --- a/.claude/skills/checking-parity/references/running-the-oracle.md +++ b/.claude/skills/checking-parity/references/running-the-oracle.md @@ -27,4 +27,6 @@ Treat the oracle output of a crashed run as valid up to the last complete record Skyrim SE files dump completely. +**A crashed oracle can hang instead of exit.** On a Fallout 4 save the oracle can raise `EAccessViolation` and then stay alive without ever exiting, spinning on nothing: the harness waits on its output forever and the step never ends. The `xDump.exe` process is then the thing to stop, and `--oracle-timeout ` is the harness's own remedy: it stops the oracle after that long, keeps its partial output as `..oracle.timeout.txt.zst` and compares it as a prefix (`equal-prefix`), which is the same evidence a crashed run gives. Give the saves mode a timeout whenever the corpus is a live save folder, because the folder grows and a new save is a new hang. + The `xEdit-llm` automation build is a secondary oracle for conflict, reference and cleaning results as JSON. It is a fork at a different upstream commit: use it to cross-check, never to close a gate. diff --git a/.claude/skills/checking-parity/references/screen-checks.md b/.claude/skills/checking-parity/references/screen-checks.md new file mode 100644 index 00000000..ba5ec5b2 --- /dev/null +++ b/.claude/skills/checking-parity/references/screen-checks.md @@ -0,0 +1,72 @@ +## Screen checks (`cargo xtask gui`) + +The phase-7 gate witnesses a GUI claim with a screenshot or a scripted screen interaction against the 4.1.5q binary. `cargo xtask gui` starts a program on a private desktop of the current window station, which is never shown: nothing it draws reaches the user's desktop. A **session** is a JSON file; a helper process (`gui session`, started by `launch` and run from a copy of `xtask`) creates the desktop, starts the program on it, holds the desktop open and serves the other commands through request files beside the session. Windows belong to their desktop, so every list, capture, key, click, menu and close request is executed by that helper, not by the command's own process. + +| Command | What it does | +| --- | --- | +| `gui launch [--args ]... [--desktop ] [--session ] [--wait ] [--json]` | Starts the program, waits for its first visible window, writes the session (default `$XEDIT_GUI_SESSIONS`, else `$XEDIT_PARITY_SCRATCH/gui`, else `/.xtask-gui/session.json`). A program that exits during startup fails the launch with its exit code. | +| `gui windows [--json]` | Every top-level window of the desktop: handle, pid, class, title, rect, visible, and whether it belongs to the session's program. | +| `gui shot (--title \|--handle )... --out [--json]` | Captures every matching window with `PrintWindow` (BitBlt fallback), writes PNGs. Several matches write `-.png`. | +| `gui keys (--title\|--handle) --keys "ctrl+s,enter,text:FO4" [--control ] [--delay ]` | Posts keys with `WM_KEYDOWN`/`WM_CHAR`/`WM_KEYUP`, modifiers in the target thread's keyboard state. `alt+f` sends `SC_KEYMENU` (a posted Alt+letter never opens a menu). `--control` names the control class that receives the keys (`TLabeledEdit`, `TVirtualStringTree`): **a dialog on a private desktop has no focused control**, so without it the keys go to the form and text is lost. | +| `gui click (--title\|--handle) --at , [--button left\|right\|middle]` | Posts a mouse click at a client point, passed down to the deepest child under it. | +| `gui click (--title\|--handle) --click-button ` | Finds a `TButton`/`Button`/`TSpeedButton` child by caption (an `&` is ignored) and sends `BM_CLICK`. Use it where the dialog is DPI-clipped and the button is outside the visible client area. | +| `gui menus (--title\|--handle) [--json]` | The window's Win32 menu tree (`GetMenu`/`GetMenuItemInfoW`): text, id, enabled, checked, nested submenus. | +| `gui menupopup --handle [--json]` | An open popup menu (a window of the class `#32768`): text, id, enabled, checked, in order. A right-click that opens a context menu creates one; `gui windows` finds it. | +| `gui children (--title \|--handle ) [--json]` | The child windows of one window (`EnumChildWindows`): class, text, rect, visible. The layout of a program that draws its own controls (the 4.1.5q form) is only visible in its children. | +| `gui resize (--title\|--handle) (--size x\|--at ,) [--json]` | Moves and resizes a window (`SetWindowPos`). A window that is too small for its own layout (the 4.1.5q form is cut off at the bottom of the screen, where its tab strip is) has to grow before a screen check can reach the controls at its edge. | +| `gui close [--timeout ]` | `WM_CLOSE` to the program's windows, then `TerminateProcess` on exactly that pid (checked by its creation time), then ends the helper and removes the session. | + +Notes paid for on the first run (2026-10-19): + +- The helper runs from a **copy** of `xtask` beside the session, so a rebuild while a session runs is safe, but a command whose protocol changed needs a **new session** (close and launch again). +- `xEdit 4.1.5q` has **no Win32 menu**: its File/View/... menu is `pmuMain`, a popup menu on the `bnMainMenu` hamburger speed button (`Caption = #926`), a `TGraphicControl` with no window handle. `gui menus` therefore answers `has no menu` for it, and `GetMenu` cannot be used for the walkthrough. The button is disabled until a file is loaded; the gate has to open the popup from the main form after loading and capture the `#32768` menu window. +- At 150 % display scaling the 4.1.5q dialogs are clipped: their OK/Cancel buttons lie outside the visible client area, so click them with `--click-button`, not `--at`. +- `C:\Windows\System32\notepad.exe` on Windows 11 is the packaged Notepad (a hardlink into `WindowsApps`): launched directly it exits with code 0 without a window (`launch` reports `the program exited during startup`). Use a classic Win32 target (`dxdiag.exe`, `charmap.exe`) for a screen-check smoke test on this machine. + +Notes paid for at the phase-7 gate (2026-10-21), on the 4.1.5q binary at 200 % display scaling: + +- **The capture is a quarter of the form.** The Delphi build is DPI aware, so its window is twice the size in pixels that the helper (DPI unaware) sees, and `gui shot` sizes its bitmap from the helper's rectangle: the PNG is the top left quarter of the form at one pixel per physical pixel. A PNG pixel is therefore **two** client units (the helper's coordinates are the form's own units), and the part of the form past the PNG's edge is not in the capture at all. `gui resize` grows the window, which moves what the quarter shows but never widens it. +- **The layout is in the children, not in the picture.** `gui children` lists every child window of the form with its class, rect and visibility, which is where a control's position is found when it is outside the capture. A right-click at a child rect's top edge is how the column-header popups (`pmuNavHeaderPopup`, the nav tree's header) are reached. +- **A popup menu is a window of its own.** A right-click that opens a context menu creates a `#32768` window: `gui windows` finds its handle, `gui menupopup` reads its items with their ids, and `gui shot --handle` captures the menu itself, which is the evidence the walkthrough record keeps. `gui keys --keys escape` closes it. +- **A stale popup reads as a menu.** `gui menupopup` answers for whatever `#32768` window is open, so a popup that was not dismissed is read again on the next probe. Send `escape` before every probe and check the handle. +- **Not every popup of the release can be opened.** The view tree of 4.1.5q has no header control at runtime (a right-click on its top edge opens the view's own popup), the `Referenced By` page stays hidden until the loader has reference data, and one popup holds no item at all. The walkthrough then rests on the `.dfm` of the release, which is the build's own menu definition: `tools/menu_diff.py` diffs its ten popup menus against `crates/xedit-gui/src/menu_model.rs` (caption tree, nesting, `Enabled`, `Checked`, `RadioItem`, `Visible`) and exits non-zero on a difference. + +Worked example (the oracle's module selection, evidence in `M:\projects\scratch\p7-screen-evidence`): + +``` +W='M:\projects\scratch\p7-screen-evidence\oracle-private' # Data\, plugins.txt, temp\, cache\, backup\, mygames\ +cargo xtask gui launch "$XEDIT_ORACLE_DIR/xFOEdit.exe" \ + --args -FO4 --args "-D:$W\Data\\" --args "-P:$W\plugins.txt" --args "-T:$W\temp\\" \ + --args "-C:$W\cache\\" --args "-B:$W\backup\\" --args "-S:$W\\" \ + --args "-M:$W\mygames\\" --args "-I:$W\mygames\game.ini" --args "-G:$W\mygames\Saves\\" \ + --session M:/projects/scratch/p7-screen-evidence/fo4edit-session.json --desktop xedit-gui-fo4 --wait 40 +cargo xtask gui windows M:/projects/scratch/p7-screen-evidence/fo4edit-session.json # TfrmMain + TfrmModuleSelect +cargo xtask gui click M:/projects/scratch/p7-screen-evidence/fo4edit-session.json --title "What's New" --at 820,600 +cargo xtask gui shot M:/projects/scratch/p7-screen-evidence/fo4edit-session.json --title "Module Selection" --out module-selection.png +cargo xtask gui keys M:/projects/scratch/p7-screen-evidence/fo4edit-session.json --title "Module Selection" \ + --control TLabeledEdit --keys "text:FO4" # the filter box shows FO4 +cargo xtask gui close M:/projects/scratch/p7-screen-evidence/fo4edit-session.json +``` + +`--json` prints the same result as the text output as JSON on every command. + +### Capturing the Rust GUI (`xedit-gui --screenshot`) + +Our GUI paints through eframe/wgpu, and on the private desktop `gui shot` cannot witness it: the window is hidden and has no composited surface, so `PrintWindow` and the `BitBlt` fallback both return a black client area. The app captures itself instead, through egui's own screenshot path (`ViewportCommand::Screenshot`, which renders the frame to a capture texture and reads it back to the CPU even when the window is hidden): + +``` +cargo xtask gui launch M:\projects\scratch\xedit-p7-target\debug\xedit-gui.exe \ + --args --game --args sse --args --load --args Update.esm \ + --args --screenshot --args 'M:\projects\scratch\p7-shot-evidence\sse-main.png' --args --exit \ + --session M:/projects/scratch/p7-shot-evidence/sse-main-session.json --desktop xedit-shot-sse --wait 90 +cargo xtask gui close M:/projects/scratch/p7-shot-evidence/sse-main-session.json +``` + +- `--screenshot ` writes one PNG of the window. It waits for the screen the other switches name: with `--load`, until the load finishes; with `--dialog `, the dialog is opened as part of that same finish, so it is in the capture; with neither, the game or module selection dialog. It also captures the `Fatal:` screen of a failed load. +- `--screenshot-frames ` (default 2) is the number of painted frames to wait after the screen is ready, and `--screenshot-delay ` a further wall-clock wait, for content that arrives on a worker thread. +- `--exit` closes the window once the PNG is written, so a test cannot hang; a screen that is not ready after 180 s closes without a PNG (the `XEDIT_GUI_LOG` line says why). The exit costs one `ViewportCommand::Close`; settings are saved on the close request as usual. +- The capture is the app's own pixels, so it proves what egui laid out, not what a Win32 compositor would show. Use `gui windows`/`gui keys`/`gui close` against the process as normal; only `gui shot` is replaced. `xtask gui shot` stays the right tool for the 4.1.5q Delphi binary and its GDI dialogs. +- A blank or black PNG means the app's frame was not painted before the read-back (or the GPU adapter refused the capture): check `XEDIT_GUI_LOG` for `screenshot: wrote ` and `screenshot: could not write ...`. The file only appears once the callback has run. +- On this machine the loaded screen opens the upstream developer message (step 6's `startup_dialogs`) on the first ready frame: the SSE settings file does not exist and FO4's has `[DeveloperMessage] Version=0`. The `--dialog ` runs suppress it (their dialog opens inside `finish_load`), and an unknown `--dialog main` opens nothing and leaves the bare shell; capture both states when the gate needs the shell without a modal. `sse-main-developer-message.png` and `sse-main.png` in the phase-7 evidence are the two. A step-6 `--dialog` name (`tip`, `script`, `rich-edit`, `view-elements`, `log-analyzer`, `localization`, `localize-plugin`) is capturable alone since step 6's ordering was fixed: the dialog-only path opens its own dialog and leaves the developer message behind it. The phase-7 evidence holds the nineteen dialog views (`port-dialog-*.png`). + +Idle rule that the capture work exposed: eframe 0.37 calls `App::ui` only when a repaint is requested, so an idle window costs no CPU — but every `send_viewport_cmd` requests a repaint of its viewport. Sending the window title/icon per frame repainted the loaded window continuously at a full core on the private desktop; they are now sent once. When adding a viewport command, send it on change, not per pass, and schedule `request_repaint_after` only while an animation or a job is in flight. A hidden window runs `App::logic` only; `--screenshot` polls the load and its own state there, and asks for painted frames with `ViewportCommand::RequestPaintWhileHidden`. diff --git a/.claude/skills/using-xedit-cli/references/commands.md b/.claude/skills/using-xedit-cli/references/commands.md index cd475490..469ce062 100644 --- a/.claude/skills/using-xedit-cli/references/commands.md +++ b/.claude/skills/using-xedit-cli/references/commands.md @@ -10,7 +10,7 @@ Inspection (never mutates): |---|---|---| | `session info` | `session.info` | Game tag, game name, data folder and the loaded files in load order. | | `files list [--name-filter TEXT] [--regex]` | `files.list` | Every loaded file with load order, masters, record count, ESM and localized flags; `--name-filter` is the navigation tree's "Filter by filename" (a case-insensitive substring, or a `preCaseLess` PCRE with `--regex`). | -| `session load [--plugin P]... [--mode M] [--force] [--dry-run]` | `session.load` | Loads another plugin list into the running session, or the same one with a tool mode's load settings (`--mode quickclean`, `quickautoclean`, `checkforerrors`, `translate`), as `xeInit.pas` applies them before anything loads. The plugins of a `serve` or MCP session are otherwise fixed at startup. A load discards the unsaved changes of the loaded files unless `--force`; a dry run reports the plugins, the mode's settings and what a reload would discard. Needs `--edit` unless `--dry-run`. See "Daemon and MCP server". | +| `session load [--plugin P]... [--mode M] [--compare-to F] [--force] [--dry-run]` | `session.load` | Loads another plugin list into the running session, or the same one with a tool mode's load settings (`--mode quickclean`, `quickautoclean`, `checkforerrors`, `translate`), as `xeInit.pas` applies them before anything loads. The plugins of a `serve` or MCP session are otherwise fixed at startup. A load discards the unsaved changes of the loaded files unless `--force`; a dry run reports the plugins, the mode's settings and what a reload would discard. `--compare-to F` is the tree's "Compare to...": the file loads as the last plugin's compare load (it has to sit in the same folder), and this request's file wins over the global `--compare-to`. Needs `--edit` unless `--dry-run`. See "Daemon and MCP server". | | `records list [--file F] [--signature SIG] [--offset N] [--limit N]` | `records.list` | Records of one plugin in file order; `total` counts the matches before paging. | | `records find [--file F] [--signature SIG] [--editor-id TEXT] [--name TEXT] [--limit N]` | `records.find` | Records whose editor ID or display name contains the text, compared without case. | | `records get [--file F] [--depth N]` | `records.get` | One record with its elements as a tree. | @@ -27,6 +27,9 @@ Inspection (never mutates): | `refs get [--file F] [--offset N] [--limit N]` | `refs.get` | The records that refer to a record (xEdit's "Referenced By" tab) and the FormIDs it refers to. Builds the reference index first. | | `refs build [--file F] [--only-load]` | `refs.build` | Builds the reference index of the loaded files, or loads it from the reference cache, and saves the cache (`BuildOrLoadRef`). | | `refs dump` | (none) | The referenced-by lists of every record as text, for the parity check. | +| `modules list [--all] [--active] [--masters-present]` | `modules.list` | The modules of the data folder in `wbModulesByLoadOrder` order, as the module selection dialog and the mod group editor's Insert list them: name, the ESM/light/medium/update/localized flags, active and valid, the masters, the load order and FileID, the path, the size, the write time and the CRC32. `--all` includes the game's executable and a plugin whose header did not read (the dialogs list only the valid ones); `--active` and `--masters-present` keep the active modules and the ones whose masters are there. | +| `signatures list` | `signatures.list` | The record and base-record signatures of the game's definitions (`wbRecordDefs`), the filter options dialog's `clbRecordSignatures` and `clbBaseRecordSignatures`: `SIGNATURE - Name` per record definition, the base list only for definitions with `ReferenceSignatureCount > 0`, both sorted by that text. | +| `settings get [--section S] [--key K] [--type T] [--default V]` | `settings.get` | xEdit's settings file (`xEdit.ini`): the section names, a section's keys or one key. `--type string\|integer\|bool\|float\|double` reads it as the matching `TMemIniFile.Read*` (the raw text is reported either way) and `--default` is that read's default. The GUI's keys live here: `[frmMain]` placement and `pnlNavWidth`/`vstNavColumnWidth`, `[ColorConflictThis]`/`[ColorConflictAll]` (a `TColor` per enum name), `[Nav] FilesSort`/`INFOSort`, `[View]`, and the `[Options]` switches `xeInit.pas` reads. | | `call system.version` | `system.version` | The version of the build. | | `dump --game G ` | (none) | The whole plugin as `xDump.exe` prints it, to stdout; progress goes to stderr. | | `tool modes` | `tool.modes` | The seventeen tool modes of xEdit with the switch of each, whether the game of the session has it, what it does and what of it is missing here. | @@ -57,9 +60,11 @@ Changing data (`mutates`, needs `--edit` or `--dry-run`): | `save [--file F] [--output PATH] [--no-backup]` | `files.save` | Writes a loaded plugin as xEdit saves it. Changes files, not data. | | `clean [--file F]... [--itm] [--udr] [--quick] [--output PATH] [--no-backup] [--udr-xesp B] [--udr-scale B] [--udr-scale-value N] [--udr-z B] [--udr-z-value N] [--udr-mstt B] [--udr-mstt-value HEX]` | `files.clean` | Cleans one plugin or a selection of several: removes the records identical to their master (ITM), undeletes and disables the deleted references (UDR) with the options of xEdit's Cleaning tab, or runs xEdit's quick auto clean mode, which saves. See "Cleaning". | | `call files.dirty_info` | `files.dirty_info` | The dirty information the session's cleaning steps stored, per plugin and CRC32, and its LOOT (and Oblivion's BOSS) masterlist entries. | +| `files compare-to --path PATH [--dry-run]` | `files.compare_to` | Copies the file at `--path` into the folder of the loaded plugin `` and loads the copy in the place of that plugin, at the plugin's load order, as a compare load (xEdit's "Compare to..."). The copy takes the plugin's load order and the plugin becomes its last master. The copy has to sit in the same folder as the plugin. Needs `--edit` unless `--dry-run`. | | `files new [--light] [--medium] [--template T]` | `files.new` | A new empty plugin in the data folder with the game master as its master (xEdit's `AddNewFile`); `--template` takes one of the module templates of xEdit's "Create New File" dialog (`esp`, `esp-esm`, `esp-light`, `esp-esm-light`, `esp-medium`, `esp-esm-medium`, `esp-update`, `esp-esm-update`, `esm`, `esm-light`, `esm-medium`, `esm-update`), which the game's modules offer. Save it with `save --file ` in the same `batch` or `serve` session. `file_exists` when the data folder or the session has it. | | `patch merged [--no-save] [--output PATH] [--no-backup]` | `patch.merged` | xEdit's "Create Merged Patch": a new `.esp` that merges the lists several plugins change, saved into the data folder (or `--output`). See "Merged patch". | | `records cleanup-injected [...] [--file F]` | `records.cleanup_injected` | Copies the records that refer to injected records of a plugin that is not their master into that plugin and removes those references from the originals (xEdit's "Cleanup injected records"). | +| `settings set --section S --key K --value V [--type T] [--dry-run]` | `settings.set` | Writes one value of xEdit's settings file as the GUI writes it (`--type` string, integer, bool (`1`/`0`), float or double (`FloatToStr`)); a missing section is added at the end. Reports the old and new text and whether it changed. Needs `--edit` unless `--dry-run`. | | `modgroups select [...] [--saved] [--all]` | `modgroups.select` | Chooses the active mod groups and saves the selection in xEdit's settings file. Changes a file. | | `modgroups create --module M... [--item LINE]... [--include-crcs] [--add-current-crcs] [--file M]` | `modgroups.create` | Adds a mod group to the `.modgroups` file of one of its modules. Changes a file. | | `modgroups edit [--file F] [--new-name N] [--item LINE]... [--add-current-crcs]` | `modgroups.edit` | Renames a mod group or replaces its items. Changes a file. | diff --git a/.claude/skills/using-xedit-cli/references/known-gaps.md b/.claude/skills/using-xedit-cli/references/known-gaps.md index 49db6336..67024207 100644 --- a/.claude/skills/using-xedit-cli/references/known-gaps.md +++ b/.claude/skills/using-xedit-cli/references/known-gaps.md @@ -19,7 +19,7 @@ The CLI's own sessions read no plugin list unless `--plugins-list` names one. - **References.** `refs build-reachable` is xEdit's "Build Reachable Info"; what it reaches is right for the groups a game always loads and for the references, scripting, quest-objective, furniture and NPC cases of `TwbMainRecord.Reached`, but a record whose references come from the reference cache takes only its editor ID and full name from it (its base record, grid cell and GUI names are read from the record when needed), and a record's own elements are not walked while its references are built by an edit (`UpdateRefs`). - **Mod groups.** The modules of the game's Creation Club list (`Skyrim.ccc`, `Fallout4.ccc`, `Starfield.ccc` next to the data folder) come after the official ones in the list's order (parity #41, `parity modgroups --file sse-ccc-order`); a loaded module's `Plugins.txt` index comes from the plugin list `--plugins-list` names (the CLI reads none otherwise, so the index stays unset as upstream's when the file is not there). -The presets of the *module* selection dialog (`frmModuleSelect_ActiveModules_Presets`) are not ported (issue #78); the mod group dialog's are. +The presets of the *module* selection dialog (`frmModuleSelect_ActiveModules_Presets`) and of the mod group dialog are `modules.presets`/`modules.preset_*` and `modgroups.presets`/`modgroups.preset_*` (parity #36). - **Conflicts.** The hidden-record state, the compare-to load (`--compare-to`), the raw data compare (`records.compare --raw-data`) and compare of selected records of different FormIDs (`records.compare_selected`) are in; `conflicts.for_elements` is the script function `ConflictAllForElements`. The release never sets `wbCompareRawData` (no switch assigns it), so `--raw-data` is an option over the dormant upstream code paths with no oracle run. diff --git a/.claude/skills/using-xedit-cli/references/loading-plugins.md b/.claude/skills/using-xedit-cli/references/loading-plugins.md index e0d2d506..d9b95ccb 100644 --- a/.claude/skills/using-xedit-cli/references/loading-plugins.md +++ b/.claude/skills/using-xedit-cli/references/loading-plugins.md @@ -7,7 +7,7 @@ xedit --game sse --load "\Skyrim.esm" --load "\Update.esm" xedit --game fo4 --load "\DLCRobot.esm" ``` -- `--compare-to ` is the navigation tree's "Compare to...": the file at `` loads in the place of the last `--load` plugin, takes its load order slot and is compared with it record by record (`wbFile` with `aCompareTo`, `AddMaster(flCompareTo)`, `fsCompareToHasSameMasters`, `IsCompareToSame`). +- `--compare-to ` is the navigation tree's "Compare to...": the file at `` loads in the place of the last `--load` plugin, takes its load order slot and is compared with it record by record (`wbFile` with `aCompareTo`, `AddMaster(flCompareTo)`, `fsCompareToHasSameMasters`, `IsCompareToSame`). It has to sit in the folder of that plugin, as the GUI copies the chosen file into the data folder. The flag is not global: it belongs to the startup load, before the command (`xedit --game G --compare-to F --load P ...`); `session load --compare-to` names the compare file of the load that command makes instead. It is not editable (a save refuses it as `[TES4:00000000] can not be edited`, as the release does). A record both files hold shows 2 columns where one is the compare load, identical records use the fast `ContentEquals` path (`caNoConflict`, the original `ctMaster`, the compare load `ctIdenticalToMaster`). It has to sit in the folder of that plugin, as the GUI copies the chosen file into the data folder; it is not editable (a save refuses it as `[TES4:00000000] can not be edited`, as the release does). diff --git a/.claude/skills/using-xedit-cli/references/scripts.md b/.claude/skills/using-xedit-cli/references/scripts.md index cbb694ae..9fe25e9d 100644 --- a/.claude/skills/using-xedit-cli/references/scripts.md +++ b/.claude/skills/using-xedit-cli/references/scripts.md @@ -24,7 +24,9 @@ Presentational members (`TCanvas` drawing and the paint methods) keep their stat | CLI | Registry name | What it does | |---|---|---| -| `script list [--scripts DIR]` | `script.list` | Every `*.pas` of the scripts folder with its line count, as xEdit's script list shows them (the form's `_newscript_.pas` template included). The folder is `--scripts`, else `XEDIT_SCRIPTS`, else the oracle's `Edit Scripts` (`XEDIT_ORACLE_DIR`), else `Edit Scripts` beside `xedit.exe` (upstream's `wbScriptsPath`, which `-S:` sets); only the top folder is read. Read-only; error code `io` when the folder cannot be read. | +| `script list [--scripts DIR] [--subdirectories]` | `script.list` | Every `*.pas` of the scripts folder with its line count, as xEdit's script list shows them (the form's `_newscript_.pas` template included). The folder is `--scripts`, else `XEDIT_SCRIPTS`, else the oracle's `Edit Scripts` (`XEDIT_ORACLE_DIR`), else `Edit Scripts` beside `xedit.exe` (upstream's `wbScriptsPath`, which `-S:` sets); only the top folder is read unless `--subdirectories` lists the scripts of the subdirectories too (the form's "Include scripts from subdirectories"; the names are relative to the folder, with a backslash between the parts). Read-only; error code `io` when the folder cannot be read. | +| `script read [--scripts DIR]` | `script.read` | Reads a script and prints its text, as the script editor opens it (`DoScriptSelectionChange`'s `LoadFromFile`): the path that was read, the unit name (the file name without its extension) and the line count. A relative name resolves against the scripts folder (`--scripts`). Read-only; error code `io` when the file cannot be read. | +| `script save [--text TEXT] [--from FILE] [--scripts DIR] [--dry-run]` | `script.save` | Writes a script, as the script editor's Save does (`btnSaveClick`): the tabs become two spaces, the previous file is copied to a timestamped `.backup.` name, and the text is written with CR LF line ends. The text comes from `--text` or from the file of `--from`; `--dry-run` reports the target and writes nothing. Needs `--edit` unless `--dry-run`; error code `io` when the file cannot be written. | | `script check ...` | `script.check` | Compiles each script and the units it uses; the response has `ok`, `scripts_folder`, `total`, `parsed`, `failed` and per file `ok` with `errors` (`file`, `line`, `message`). | | `script run [--scripts DIR] [--no-save] [--dry-run] [--build-refs] [--answer JSON]... [--answers FILE]` | `script.run` | Runs the script over the loaded plugins (`--game`, `--load`), as `DoRunScript`/`ApplyScript` do. `--scripts` is the folder a relative script name resolves against (the units a script `uses` always read from the script's own folder, as `DoRunScript` sets `wbScriptsPath`). `--no-save` leaves the changed plugins in memory for `xedit save`; by default they are saved in place with the GUI's save log lines (`Saving: .save.