diff --git a/CLAUDE.md b/CLAUDE.md index 7591be6..cc2bf0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ The first consumer is [dw2003recomp](https://github.com/gascarcella/dw2003recomp | `psyq/` | The Psy-Q shim, one file per library, plus the hardware models `gpu.c`, `gte.c`, `mdec.c`, `xa.c`; `check.sh` | | `shaders/` | The hardware renderer's HLSL (DXC to SPIR-V, for Windows also DXIL, at build time) | | `mods/fast_forward/`, `mods/fast_loading/` | The stack's own mods, which every game has; a game's own mods live in the game | -| `launcher/` | The launcher (C++, SDL3 + Dear ImGui): its own CMake project; every string from the game description (`launcher/src/brand.h`); `ui/` the Settings, Controls and Mods screens, the style and the in-game menu, compiled (with `src/`'s data layer) into a game built with `PSXSTACK_MENU` (`include/psxstack/menu.h`, `runtime/menu.c` the stubs); the update check (`src/http.cpp`, `src/update.cpp`), Update now and the self-update (`src/app.cpp`, `src/update_apply.cpp`, its self-test `src/selftest_update.cpp`); the Steam shortcut (`src/vdf.cpp`, `src/steam.cpp`, the Settings section `src/steam_section.cpp`, the game's artwork `src/steam_art.cpp`, its self-test `src/selftest_steam.cpp`) | +| `launcher/` | The launcher (C++, SDL3 + Dear ImGui): its own CMake project; every string from the game description (`launcher/src/brand.h`); `ui/` the Video, Graphics, Audio, Controls, Mods and System screens, the style and the in-game menu, compiled (with `src/`'s data layer) into a game built with `PSXSTACK_MENU` (`include/psxstack/menu.h`, `runtime/menu.c` the stubs); the update check (`src/http.cpp`, `src/update.cpp`), Update now and the self-update (`src/app.cpp`, `src/update_apply.cpp`, its self-test `src/selftest_update.cpp`); the Steam shortcut (`src/vdf.cpp`, `src/steam.cpp`, the System screen's section `src/steam_section.cpp`, the game's artwork `src/steam_art.cpp`, its self-test `src/selftest_steam.cpp`) | | `cmake/` | `psxstack.cmake` (`psxstack_add_game()`, documented inline), `imgui.cmake` (the Dear ImGui target), `version.cmake`, `embed.cmake`, `windows-x86_64.cmake` (llvm-mingw) | | `windows/` | The game's Windows resource and manifest templates | | `tools/` | `game_gen.py` (`game.json` → header), `port_gen.py` (the build's generators), `port_inventory.py` (the host-compile gate a game configures), `replay/` (the emulator and port replay runners a game configures: `emulator.py`, `run.lua`, `boot_check.lua`, `port_test.py`, `redux.sh`; docs/RUNTIME.md "The replay runners"), `mcp/` (the debug channel's MCP server, client, symbols; `tools/mcp/README.md`); built tools under `tools/` (gitignored) | @@ -68,7 +68,7 @@ python3 tests/hello_test.py [--record] # the smoke test: examples/hel python3 tests/tasks_test.py [--sanitize|--m32] # the fibers: examples/tasks (three tasks, one preempted), its picture, its log twice, a state saved on a task's fiber and resumed python3 tests/shim_test.py [--sanitize|--m32] # the shim alone (psyq/*.c + tests/shim/shim_test.c): LIBAPI's events and root counter 3, the card's BIOS file calls and LIBCARD, CdSearchFile over a built ISO, CdRead, LIBSPU, LIBSND's SEQs python3 tests/hello_test.py --exe build/hello-win/hello.exe --wine --gpu # its Windows build (-DPSXSTACK_SDL=ON, cross-built) under Wine; --gpu: the hardware renderer's picture too (D3D12) -python3 tests/hello_test.py --menu [--gpu] # hello with the window and the in-game menu (-DPSXSTACK_MENU=ON): the input self-test offscreen, ending with the menu's round trip (unmuted live: the device opens), and the menu's pictures (Settings; Mods at 1920x1080, 1280x800, 640x480), none wider than its window; --gpu: Settings on the hardware renderer too (lavapipe), the software one but for edge pixels +python3 tests/hello_test.py --menu [--gpu] # hello with the window and the in-game menu (-DPSXSTACK_MENU=ON): the input self-test offscreen, ending with the menu's round trip (unmuted live: the device opens), and the menu's pictures (Graphics; Video, Audio, System at 640x480; Mods at 1920x1080, 1280x800, 640x480), none wider than its window; --gpu: Graphics on the hardware renderer too (lavapipe), the software one but for edge pixels cmake -S launcher -B build/launcher -G Ninja [-DPSXSTACK_GAME_JSON=path/to/game.json] && cmake --build build/launcher SDL_VIDEO_DRIVER=offscreen build/launcher/-launcher --self-test build/selftest # from game.json (dw2003 by default) cmake -S launcher -B build/launcher-win -G Ninja -DCMAKE_TOOLCHAIN_FILE="$PWD/cmake/windows-x86_64.cmake" -DCMAKE_BUILD_TYPE=Release # the toolchain path must be absolute diff --git a/README.md b/README.md index 929c491..3897d5f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ consumer is [dw2003recomp](https://github.com/gascarcella/dw2003recomp), the Dig (docs/LAUNCHER.md "Updates"). - **Add to Steam:** the game as a non-Steam shortcut with its artwork, for Steam's library, Game Mode on the Steam Deck and its controller configuration (docs/LAUNCHER.md "Steam"). -- **The in-game menu:** the launcher's Settings, Controls and Mods inside the game's window on Escape or Back+Start, +- **The in-game menu:** the launcher's Video, Graphics, Audio, Controls, Mods and System screens inside the game's window on Escape or Back+Start, applied live (docs/LAUNCHER.md "In-game menu"). ### Mods diff --git a/docs/GAME_CONTRACT.md b/docs/GAME_CONTRACT.md index 29c8d04..9080f06 100644 --- a/docs/GAME_CONTRACT.md +++ b/docs/GAME_CONTRACT.md @@ -64,7 +64,7 @@ the port and the launcher stay single binaries. Contents: filter sets, one for its 2D scenes and one for its 3D scenes, and the runtime switches between them at the vsync where the adapter's `game_state_scene()` changes ("4. The adapter units"). A set is a filter and its crt values; the sub-pixel mode, the scale, fullscreen, the scaling and the internal scale stay global. `label` names each set in - the launcher's Filter rows (`"the field and the menus"`, `"battles"`); `2d.filter` (optional) is the 2D set's + the launcher's filter tabs (`"the field and the menus"`, `"battles"`); `2d.filter` (optional) is the 2D set's default filter, with the default crt values (absent: the 2D set starts as a copy of the 3D set). The settings' `video.filter` and `video.crt` are the 3D set, `video.scene_2d` the 2D set (docs/LAUNCHER.md "Settings file"). The sets are fixed, `2d` and `3d`. Absent: one set, and the probe is never read. diff --git a/docs/LAUNCHER.md b/docs/LAUNCHER.md index 342122d..44e056e 100644 --- a/docs/LAUNCHER.md +++ b/docs/LAUNCHER.md @@ -100,10 +100,10 @@ and absolute paths and exits 0, or exits 64 naming the bad key. | `video.renderer` | `"software"` or `"gpu"` | `"software"` | The window's renderer: SDL_Renderer, or the hardware renderer (SDL_GPU; it falls back to software when no device can present). The launcher's "Renderer" choice. A game built before it logs the key and ignores it | | `video.internal_scale` | integer 1-8 | 1 | The hardware renderer's resolution, N times the PS1's (1: the software picture). The launcher's "Resolution" slider, shown for the GPU renderer | | `video.subpixel` | `"off"`, `"on"` or `"perspective"` | `"on"` | Above internal scale 1, the hardware renderer draws the 3D at the GTE's sub-pixel positions (`docs/PORT.md` "Sub-pixel precision") instead of the PS1's whole pixels; `"perspective"` also textures it perspective-correct (not the PS1's look: its affine textures swim on large polygons). The launcher's "3D vertices" choice, shown for the GPU renderer above x1. A game built before it logs the key and ignores it | -| `video.filter` | `"none"`, `"sharp"`, `"scanlines"`, `"crt"` or `"smooth"` | `"none"` | The hardware renderer's present filter (docs/PORT.md "Rendering"): `none` shows the picture's pixels as whole blocks, `sharp` is sharp bilinear, `scanlines` sharp with dark lines, `crt` a TV, `smooth` xBR on the 1x picture (the internal scale does not apply to it). The software renderer shows the picture unfiltered and logs it. The launcher's "Filter" combo, shown for the GPU renderer; it writes the member only once it is set. A game with scenes (GAME_CONTRACT.md "1. game.json" Scenes): the set of its 3D scenes, the row "Filter: " | +| `video.filter` | `"none"`, `"sharp"`, `"scanlines"`, `"crt"` or `"smooth"` | `"none"` | The hardware renderer's present filter (docs/PORT.md "Rendering"): `none` shows the picture's pixels as whole blocks, `sharp` is sharp bilinear, `scanlines` sharp with dark lines, `crt` a TV, `smooth` xBR on the 1x picture (the internal scale does not apply to it). The software renderer shows the picture unfiltered and logs it. The launcher's "Filter" combo, shown for the GPU renderer; it writes the member only once it is set. A game with scenes (GAME_CONTRACT.md "1. game.json" Scenes): the set of its 3D scenes, the Graphics screen's tab named by the 3D label | | `video.crt` | `{ "scanlines", "mask", "curvature" }`, integers 0-100 | 50, 30, 0 | The filters' parameters: `scanlines` (the lines' strength) for `scanlines` and `crt`, `mask` (the aperture grille) and `curvature` for `crt`. The launcher's sliders under the Filter combo; written once set, as `video.filter` | -| `video.scene_2d` | `{ "filter", "crt" }`, as `video.filter` and `video.crt` | game.json's `video.scenes.2d.filter` with the default `crt`; without one, `video.filter` and `video.crt` | A game with scenes: the present-filter set of its 2D scenes (`video.filter` and `video.crt` are then its 3D scenes'); the window switches between the two at the vsync where the game's scene changes. A member it lacks is the default's. The launcher's second Filter row, "Filter: "; it writes the member whole once the set differs from the default, and keeps it once the file has it. A game without scenes logs the key and ignores it (the launcher leaves it as it is) | -| `video.menu_size` | `"smaller"`, `"auto"` or `"larger"` | `"auto"` | The in-game menu's size ("In-game menu"): its fit to the window, a step smaller or larger. The Settings screen's "Menu size"; written once set. The menu reads it, the runtime does not (`--print-settings` leaves it out); a game built before it logs the key and ignores it | +| `video.scene_2d` | `{ "filter", "crt" }`, as `video.filter` and `video.crt` | game.json's `video.scenes.2d.filter` with the default `crt`; without one, `video.filter` and `video.crt` | A game with scenes: the present-filter set of its 2D scenes (`video.filter` and `video.crt` are then its 3D scenes'); the window switches between the two at the vsync where the game's scene changes. A member it lacks is the default's. The Graphics screen's second filter tab, named by the 2D label; it writes the member whole once the set differs from the default, and keeps it once the file has it. A game without scenes logs the key and ignores it (the launcher leaves it as it is) | +| `video.menu_size` | `"smaller"`, `"auto"` or `"larger"` | `"auto"` | The in-game menu's size ("In-game menu"): its fit to the window, a step smaller or larger. The Video screen's "Menu size"; written once set. The menu reads it, the runtime does not (`--print-settings` leaves it out); a game built before it logs the key and ignores it | | `audio.mute` | bool | false | No audio device (switched off in the in-game menu: the device opens then) | | `memcard1`, `memcard2` | string or `null` | `"card1.mcd"`, `"card2.mcd"` | A `.mcd` image, created formatted when missing; `null`: no card in that slot. An empty string is an error | | `watchdog` | integer 0-3600 | 0 | Seconds without a vsync before the game exits 4; 0 is off. The bare binary (no `--config`) keeps its 10 s | @@ -283,10 +283,12 @@ game stays C. Every name, string, hash and number it shows comes from the game's over the generated header). It finds the settings directory, edits `settings.json` (keeping every member it does not edit, unknown ones included), and starts the game. Screens: **Disc** (file dialog, drag-and-drop, or a typed path; only the described discs' SHA-1s are accepted), **Play** (starts the game; on an error shows the exit status, the crash -report and the last lines of output, "Crash report"), **Settings** (scale, fullscreen, the rate when the game offers -two, renderer, mute, memory cards, the update check when the game has one: "Updates", Add to Steam: "Steam"), **Controls** (keyboard, gamepad -with the second player's gamepad, hotkeys), **Mods** (an on/off switch per mod and a page generated from its manifest; the data mods in the settings -directory's `mods/` in priority order, "Data mods"). Build, run, details and the self-test: `launcher/README.md`. +report and the last lines of output, "Crash report"), **Video** (scale, fullscreen, scaling, the rate when the game +offers two, the in-game menu's size), **Graphics** (the renderer, then the hardware renderer's resolution, sub-pixel +mode and filters, greyed out under the software renderer), **Audio** (mute), **Controls** (keyboard, gamepad with the +second player's gamepad, hotkeys), **Mods** (an on/off switch per mod and a page generated from its manifest; the data +mods in the settings directory's `mods/` in priority order, "Data mods"), **System** (memory cards, the update check +when the game has one: "Updates", Add to Steam: "Steam", the settings file's location and state). Build, run, details and the self-test: `launcher/README.md`. ## Updates @@ -300,7 +302,7 @@ the release's page in the browser. ### The check - **When:** at the launcher's start, on a worker thread, when `launcher.updates.check` is true (the default) and the - launcher was not started with `--no-update-check`; and at the Settings screen's **Check now**, whatever the setting. + launcher was not started with `--no-update-check`; and at the System screen's **Check now**, whatever the setting. - **What:** one GET of `https://api.github.com/repos//releases/latest` (GitHub leaves drafts and prereleases out) through the system's `curl` as a child process (`launcher/src/http.cpp`; DECISIONS "Updates: GitHub releases, curl as a process"): `-fsSL`, the User-Agent `-launcher/`, 10 s at most. The answer is read with the @@ -317,12 +319,12 @@ the release's page in the browser. the browser), **Later** (or Escape: asked again at the next start) and **Skip this version** (writes `launcher.updates.skipped`: that release and older ones are not offered again, a newer one is). When the install cannot update itself, a line says why ("This build cannot update itself. ..."; the plan's reason, "Applying one") - and Release page is the action. Check now only reports, on the Settings screen. -- **The Settings screen** ("Updates"): the checkbox "Check for updates when the launcher starts" + and Release page is the action. Check now only reports, on the System screen. +- **The System screen** ("Updates"): the checkbox "Check for updates when the launcher starts" (`launcher.updates.check`), the launcher's version, the last check's result and time, the skipped release (**Forget** clears it), **Update now** when a newer release was found and the install can update itself (else the - line saying why), **Check now** and **Release page**. The launcher's window only: the shared Settings screen - (`launcher/ui/screens.cpp`) leaves a place after "Memory cards" that its host fills (`Screens::set_settings_extra`), + line saying why), **Check now** and **Release page**. The launcher's window only: the shared System screen + (`launcher/ui/screens.cpp`) leaves a place after "Memory cards" that its host fills (`Screens::set_system_extra`), and the in-game menu fills none, so the game's menu has no update check. - **Failures** are reported in words and never retried by themselves: curl missing (Windows 10 before 1803, a Linux without it; **Release page** then opens the repository's latest release), no network (curl's own error), an HTTP @@ -350,7 +352,7 @@ update check is started while it goes on (Check now is disabled). A failure at any step (curl's error, a wrong size, a digest that does not match, the swap's own failures) leaves the old install as it was, removes the downloaded file and shows the message in the prompt with **Release page** and -**Close** (Escape); Update now can be pressed again from the Settings screen. If the swap succeeded but the new +**Close** (Escape); Update now can be pressed again from the System screen. If the swap succeeded but the new launcher could not be started, the prompt says so (the update is in place; start the launcher again). `$_LAUNCHER_UPDATE_NOW=1` makes the check at start answer its own prompt with Update now: for a test run of @@ -400,7 +402,7 @@ The launcher can put the game in the player's Steam library as a non-Steam short of it runs until the player asks: the launcher never looks for Steam, reads its files or checks that it is installed at its start or in the self-test's normal path. -- **The Settings section** (`launcher/src/steam_section.cpp`; the launcher's window only, through the shared Settings +- **The System screen's section** (`launcher/src/steam_section.cpp`; the launcher's window only, through the shared System screen's hook: the in-game menu has none). **Add to Steam** looks for Steam (below) and shows what it found: the Steam folder and the account, each a choice when there is more than one (the most recent account first), then **Add** or **Cancel**; nothing found, or no account yet, is said with where it looked. **Add** writes the shortcut @@ -450,7 +452,7 @@ at its start or in the self-test's normal path. pressed (counted), a running Steam refuses Add, Update and Remove with the files unchanged, Add keeps the other shortcuts byte for byte and the old file as `.bak`, the record is saved at once and read back at the next start (still nothing looked up), a moved launcher gets Update shortcut, Remove gives back the file as it was before Add, - and the not-found, already-removed and damaged-file cases; pictures of each state (`20*-Settings-steam*.png`). The + and the not-found, already-removed and damaged-file cases; pictures of each state (`20*-System-steam*.png`). The artwork: written by appid with the icon the shortcut's, moved by Update, removed by Remove with another shortcut's left; the build's own pictures checked as PNGs. CI also builds the launcher of `examples/hello`, whose game.json names artwork of its own (`examples/hello/steam/`, drawn by its `make_art.py`), and runs its self-test, so the path @@ -477,14 +479,16 @@ at a time), where leaving the game for the launcher's window is a chore. with the keyboard, the mouse and every gamepad; the pause, save and load hotkeys do not act while it is up. The hotkey again, or **Resume**, closes it and the game goes on; **Quit** ends the run with status 0. - **The pages** (the navigation on the left; the shoulder buttons or Ctrl+PageUp/PageDown step through them): **Game** - (the title, Resume, Quit) and the launcher's own **Settings**, **Controls** and **Mods** screens + (the title, Resume, Quit) and the launcher's own **Video**, **Graphics**, **Audio**, **Controls**, **Mods** and + **System** screens (`launcher/ui/screens.cpp`, the one implementation both draw; the update check, "Updates", is the window's own), over the settings file the game was started with (`--config`: the runtime's `settings.c` read it; the menu reads it again through the launcher's `SettingsFile` and writes it as the launcher would, keeping every member it does not edit) and the mods beside the executable plus the user's data mods (`--mods-dir`). A change is saved when no widget is in use (and when the menu closes), and the runtime then re-reads the file and applies it (`runtime/live.c` `port_live_apply`): the window's scale and fullscreen, the hardware renderer's filter (both sets, for a game with - scenes: the Settings page marks the one on screen, `port_video_scene()`) and sub-pixel mode, + scenes: on the Graphics page a filter tab each, the one on screen marked and chosen when the scene changes, + `port_video_scene()`) and sub-pixel mode, `audio.mute` (a game started muted opens its audio device when the mute goes off), the pad map and every hotkey (the table starts over, the enabled mods' bindings registered again) and the mods (`port_mods_apply`: one switched on gets its hotkeys and its `start`, one switched off its `stop`, one that stays on with other values its `start` @@ -496,7 +500,7 @@ at a time), where leaving the game for the launcher's window is a chore. - **The style** (`launcher/ui/style.cpp`, shared with the launcher's window) is scaled to fit the launcher's canvas, its window at scale 1 (960x620, `launcher/ui/style.h` `CANVAS_W`/`CANVAS_H`), into the game's window, so a page the launcher fits fits the menu too: about 1.74 at 1920x1080, 1.29 at 1280x800, at least 1. `video.menu_size` steps - it ("smaller" 0.8 times, "larger" 1.25 times; Settings, "Menu size", applied as soon as it is chosen), never past + it ("smaller" 0.8 times, "larger" 1.25 times; Video, "Menu size", applied as soon as it is chosen), never past the scale that leaves a 640x480 canvas. The shared screens fit that smallest canvas: the Mods screen's list becomes a combo above the mod's page when the two do not fit side by side, a mod's options stack (the name and Default on a line, the widget across the page) when the table's widget would be narrower than 160, rows of @@ -508,7 +512,8 @@ at a time), where leaving the game for the launcher's window is a chore. window's output with the menu up as a PNG, on either renderer (the hardware renderer's swapchain cannot be read back, so the present that takes it draws the picture and the menu into a texture of the swapchain's format, blits that into the swapchain and reads it back: `runtime/render_gpu.c`), on the page - `_PORT_MENU_SCREEN=game|settings|controls|mods` names (`mods:`: with that mod chosen; + `_PORT_MENU_SCREEN=game|video|graphics|audio|controls|mods|system` names (`settings`, the one page before + the split, is `video`; `mods:`: with that mod chosen; `controls:keyboard|gamepad|hotkeys`: on that tab), with the window first sized to `_PORT_MENU_WINDOW=WxH` when it is set (SDL's software renderer follows it: `SDL_RENDER_DRIVER=software`). The shot's log line names the windows whose content reaches past their right edge ("nothing wider than its window" when none does). @@ -516,9 +521,10 @@ at a time), where leaving the game for the launcher's window is a chore. `_PORT_MENU_LIVE_FILE=PATH` that file is put in place of the settings first (the hotkey pressed next is the file's binding, so a rebinding must take). `tests/hello_test.py --menu` builds `examples/hello` that way and runs both on SDL's offscreen driver with a settings file of its own, muted (the live file rebinds the menu hotkey, - switches fast-forward on and the mute off, so the audio device must open), the picture its Settings page (with + switches fast-forward on and the mute off, so the audio device must open), the picture its Graphics page (with `--gpu` also on the hardware renderer, on Mesa's lavapipe, which presents to the offscreen driver: the software - picture but for the anti-aliased edges, at most 0.1 % of the pixels off by more than 8 levels), then the Mods page's + picture but for the anti-aliased edges, at most 0.1 % of the pixels off by more than 8 levels), then the Video, + Audio and System pages' at 640x480, then the Mods page's at 1920x1080, 1280x800 and 640x480 and at 1280x800 "larger" (`tests/fixtures/mods/multipliers` chosen, a page larger than any a game ships so far), none of them wider than its window; CI's `launcher` job runs it and keeps the pictures. The screens' logic is the launcher self-test's. diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index 6de09a8..c182da5 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -71,7 +71,7 @@ cmake -S port -B build/port-sdl -G Ninja -DPSXSTACK_SDL=ON -DPSXSTACK_MENU=ON links as C++ (`launcher/ui/`, Dear ImGui from `tools/imgui`); without the option nothing of it is in the binary. A change made in the menu is written to the settings file and applied live where the runtime can (`runtime/live.c`). `_PORT_MENU_SHOT=PATH` writes the window with the menu up as a PNG (either renderer), on the page -`_PORT_MENU_SCREEN` names (`mods:` with a mod chosen, `controls:` on a tab of Controls), at the size `_PORT_MENU_WINDOW=WxH` gives +`_PORT_MENU_SCREEN` names (`video`, `graphics`, `audio`, `controls`, `mods`, `system`; `mods:` with a mod chosen, `controls:` on a tab of Controls), at the size `_PORT_MENU_WINDOW=WxH` gives the window (with `SDL_RENDER_DRIVER=software`). SDL's backends follow the `-dev` headers present when `setup.sh sdl3` runs: missing optional ones are turned off one by one (logged), and with no X11/Wayland headers at all only the `offscreen` and `dummy` video drivers are built (enough diff --git a/launcher/README.md b/launcher/README.md index 0233cfe..6d8e0ff 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -48,7 +48,7 @@ The first of these that applies: 4. The per-user directory, `SDL_GetPrefPath("", "")`: `~/.local/share//` on Linux (`$XDG_DATA_HOME` when set), `%APPDATA%\\` on Windows. -The status bar and the Settings screen show the directory and which rule chose it. The directory holds +The status bar and the System screen show the directory and which rule chose it. The directory holds `settings.json`, the memory cards and `mods/` (data mods: texture packs; docs/LAUNCHER.md "Data mods"). ## settings.json @@ -111,10 +111,12 @@ A game build from before `--config` or `--crash-dir` (its usage, exit 64, at the ## Settings and controls -**Settings:** the window's scale (1-16, with its size), fullscreen, the rate when the description offers two (the -nominal one named by its TV standard; the other shows the description's note), the renderer, mute, the two memory -card slots (a file name relative to the settings directory, or no card), the update check when the description names a -repository ("Updates"), and the settings file's location and state. +**Video:** the window's scale (1-16, with its size), fullscreen, the scaling, the rate when the description offers +two (the nominal one named by its TV standard; the other shows the description's note), the in-game menu's size. +**Graphics:** the renderer, then the hardware renderer's resolution, sub-pixel mode and filters (greyed out under the +software renderer, with a line that says so). **Audio:** mute. **System:** the two memory card slots (a file name +relative to the settings directory, or no card), the update check when the description names a repository +("Updates"), Add to Steam ("Steam"), and the settings file's location and state. **Controls**, three tabs: **Keyboard** and **Gamepad** list the 14 PS1 buttons with their inputs as chips (x removes one, + adds one, Default restores the game's); **Hotkeys** has the port's actions (pause, fullscreen) with their @@ -159,7 +161,7 @@ latest release at start, on a worker thread (`src/update.cpp`; docs/LAUNCHER.md A release newer than the launcher's version stamp (an exact `X.Y.Z`: a development build is only told the latest) brings up a prompt over the Play screen, never while the game runs: **Update now** (the default), **Release page** (the browser), **Later** (Escape), **Skip this version** (`launcher.updates.skipped`: not offered again, a newer one -is). The Settings screen has the checkbox "Check for updates when the launcher starts", the launcher's version, the +is). The System screen has the checkbox "Check for updates when the launcher starts", the launcher's version, the last check's result, **Update now**, **Check now** (whatever the checkbox says; it only reports) and **Release page**. `--no-update-check` skips the check at start for one run. Without curl the check says so, and **Release page** opens the repository's latest release. `$_LAUNCHER_FAKE_CURL` names a program to run in curl's place (the @@ -205,7 +207,7 @@ with and without `--config`, one that rejects the file, one that crashes, one th the window, walks every screen with injected key events and a virtual gamepad, plays with the stand-in (a file of the first described disc's size stands for the verified disc), rebinds a key through the prompt and checks the file, shows the Gamepad tab with player 2 set and two virtual gamepads connected, the GPU renderer's rows (for a game with scenes -the two Filter rows, the 2D one marked as the in-game menu marks the set on screen), drops a wrong file on the window, +the two filter tabs, the 2D one marked and chosen as the in-game menu marks the set on screen), drops a wrong file on the window, renders the mods screen over the fixture manifests (`tests/fixtures/mods/`: the first game's `fast_forward` as shipped, the documented `skip_dialogues` example, one with every option type, one with presets and a slider whose toggle makes it a typed number; three unusable ones written at test time) and rebinds a mod's key through the prompt, and saves a picture of @@ -218,7 +220,7 @@ version, a cut answer), `launcher.updates` in the file, `src/http.cpp` (a GET, c download with its progress, a cancel, a wrong size), then in the window: the prompt over Play (a picture), Escape as Later, the Release page, Skip and a newer release after it, the setting off with Check now, `--no-update-check`, the prompt waiting for a running game, curl missing, a cut answer, a development build and a description without a -repository (pictures of the Settings screen's part). Then **Update now** in the window, the stand-in "downloading" +repository (pictures of the System screen's part). Then **Update now** in the window, the stand-in "downloading" fixture packages (`_SELFTEST_CURL_DOWNLOAD` is its answer for a download, the release's JSON being the check's): the line instead of the button when the plan is not possible (no `$APPIMAGE`), the progress and Cancel on a slow download (no check started meanwhile, the file removed, the install unchanged), a wrong digest and a missing one @@ -234,7 +236,7 @@ again, unchanged), damaged files refused (cut short, bytes after the end, an unk are, the save's backup (once, as the file was), `loginusers.vdf` and the text form's comments, escapes and errors, and Steam's folders and accounts in a fixture home (a link to a folder counted once, Flatpak's, the most recent account first; Windows' order on the same folders); on Windows, the registry reader on a value of its own; a broken -`launcher.steam` kept; the process list (this launcher found by its name). Then the Settings screen's section in the +`launcher.steam` kept; the process list (this launcher found by its name). Then the System screen's section in the window, with a fixture lookup, a running check and launcher paths of its own: nothing looked up before Add to Steam, a running Steam refusing every button, Add, the next start, a moved launcher's Update shortcut, Remove (the file as it was before Add), and the not-found, already-removed and damaged-file cases, with a picture of each state; the artwork @@ -293,9 +295,9 @@ libgcc static) against an SDL3 with the desktop backends (`scripts/setup.sh sdl3 | `src/vdf.cpp`, `vdf.h` | Valve's KeyValues files: the binary form read and written back byte for byte, the text form read | | `src/steam.cpp`, `steam.h` | The Steam shortcut: where Steam is, its accounts, the shortcut added to and removed from `shortcuts.vdf` (docs/LAUNCHER.md "Steam") | | `src/steam_art.cpp` | The game's Steam artwork, embedded (`gen/steam_art.h`, from game.json `launcher.steam_art`) | -| `src/steam_section.cpp`, `steam_section.h` | The Settings screen's Steam section: Add to Steam, Remove from Steam, Update shortcut, the running-Steam check, `launcher.steam` | +| `src/steam_section.cpp`, `steam_section.h` | The System screen's Steam section: Add to Steam, Remove from Steam, Update shortcut, the running-Steam check, `launcher.steam` | | `src/selftest_steam.cpp` | The self-test's part for the Steam shortcut, on `tests/fixtures/steam/` and fixture folders, the section in the window included | -| `ui/screens.cpp`, `screens.h` | The Settings, Controls and Mods screens and the input prompt, drawn by the window and by the game's in-game menu | +| `ui/screens.cpp`, `screens.h` | The Video, Graphics, Audio, Controls, Mods and System screens and the input prompt, drawn by the window and by the game's in-game menu | | `ui/widgets.cpp`, `widgets.h`, `ui/style.cpp`, `style.h` | Small shared pieces and the look | | `ui/menu.cpp` | The in-game menu (`include/psxstack/menu.h`): compiled into a game built with `PSXSTACK_MENU`, never into the launcher; docs/LAUNCHER.md "In-game menu" | | `windows/launcher.rc.in`, `launcher.manifest.in` | The Windows resources, configured for the game's id | diff --git a/launcher/src/app.cpp b/launcher/src/app.cpp index 0f376e7..c65636e 100644 --- a/launcher/src/app.cpp +++ b/launcher/src/app.cpp @@ -19,7 +19,7 @@ extern "C" const char launcher_commit[]; namespace psxstack { -static const char *const SCREEN_NAMES[] = { "Play", "Disc", "Settings", "Controls", "Mods" }; +static const char *const SCREEN_NAMES[] = { "Play", "Disc", "Video", "Graphics", "Audio", "Controls", "Mods", "System" }; static_assert(sizeof(SCREEN_NAMES) / sizeof(SCREEN_NAMES[0]) == (size_t)Screen::Count, "one name per screen"); const char *screen_name(Screen s) { @@ -101,12 +101,12 @@ App::App(SDL_Window *window, SDL_Renderer *renderer, const SettingsDir &location update_.start(); update_auto_ = true; } - // The Settings screen's own parts (the in-game menu has neither): Updates when the game has a repository, Steam. + // The System screen's own parts (the in-game menu has neither): Updates when the game has a repository, Steam. steam_.configure(options.steam, &settings_, [this] { screens_.mark_dirty(); screens_.flush(); }); - screens_.set_settings_extra([this](float label_w) { + screens_.set_system_extra([this](float label_w) { if (update_.available()) { draw_updates(label_w); } @@ -607,8 +607,8 @@ void App::draw_update_prompt() { ImGui::EndPopup(); } -// The Settings screen's part (only when the description names a repository): the launcher's own, drawn through the -// shared screen's hook (Screens::set_settings_extra), so the in-game menu has none of it. +// The System screen's part (only when the description names a repository): the launcher's own, drawn through the +// shared screen's hook (Screens::set_system_extra), so the in-game menu has none of it. void App::draw_updates(float label_w) { Settings &s = settings_.values; section_title("Updates"); @@ -828,8 +828,14 @@ void App::draw() { case Screen::Disc: draw_disc(); break; - case Screen::Settings: - screens_.draw_settings(); + case Screen::Video: + screens_.draw_video(); + break; + case Screen::Graphics: + screens_.draw_graphics(); + break; + case Screen::Audio: + screens_.draw_audio(); break; case Screen::Controls: screens_.draw_controls(); @@ -837,6 +843,9 @@ void App::draw() { case Screen::Mods: screens_.draw_mods(); break; + case Screen::System: + screens_.draw_system(); + break; case Screen::Count: break; } diff --git a/launcher/src/app.h b/launcher/src/app.h index dce3d4f..15dc9b1 100644 --- a/launcher/src/app.h +++ b/launcher/src/app.h @@ -21,7 +21,7 @@ namespace psxstack { -enum class Screen { Play, Disc, Settings, Controls, Mods, Count }; +enum class Screen { Play, Disc, Video, Graphics, Audio, Controls, Mods, System, Count }; const char *screen_name(Screen s); // SDL video, the window, the renderer and ImGui's context and backends. False with the reason in `err`. @@ -54,7 +54,7 @@ struct AppOptions { std::vector args; std::function update_system; bool update_now = false; - // The Settings screen's Steam section (docs/LAUNCHER.md "Steam"): this system's Steam; the self-test's fixtures. + // The System screen's Steam section (docs/LAUNCHER.md "Steam"): this system's Steam; the self-test's fixtures. SteamOptions steam; }; @@ -134,7 +134,7 @@ class App { const std::string &update_message() const { return update_message_; } const HttpDownload &update_download() const { return download_; } void update_cancel(); // the download's Cancel: stopped, its file removed, the prompt closed (Later) - // The Settings screen's Steam section (its buttons; nothing looks for Steam before one is pressed). + // The System screen's Steam section (its buttons; nothing looks for Steam before one is pressed). SteamSection &steam() { return steam_; } private: diff --git a/launcher/src/brand.h b/launcher/src/brand.h index 37ebf26..b8a4165 100644 --- a/launcher/src/brand.h +++ b/launcher/src/brand.h @@ -44,13 +44,13 @@ constexpr const char *ENV_UPDATE_NOW = PSXSTACK_GAME_ENV_PREFIX "_LAUNCHER_UPDAT constexpr const char *ENV_SELFTEST_DISC = PSXSTACK_GAME_ENV_PREFIX "_SELFTEST_DISC"; constexpr const char *ENV_SELFTEST_GAME = PSXSTACK_GAME_ENV_PREFIX "_SELFTEST_GAME"; constexpr const char *ENV_SELFTEST_VIDEO_DRIVER = PSXSTACK_GAME_ENV_PREFIX "_SELFTEST_VIDEO_DRIVER"; -// The rates: the nominal one (the settings' default) and the ones the Settings screen offers. +// The rates: the nominal one (the settings' default) and the ones the Video screen offers. constexpr int GAME_RATE = PSXSTACK_GAME_RATE; constexpr int GAME_RATE_COUNT = PSXSTACK_GAME_RATE_COUNT; inline constexpr int GAME_RATES[PSXSTACK_GAME_RATE_COUNT] = PSXSTACK_GAME_RATES; constexpr const char *GAME_RATE_NOTE = PSXSTACK_GAME_RATE_NOTE; // The scenes (the description's video.scenes): a present-filter set for the 2D scenes and one for the 3D scenes, -// named by their labels in the Filter rows; the 2D set's default filter ("" = a copy of the 3D set's). +// named by their labels in the Graphics screen's filter tabs; the 2D set's default filter ("" = a copy of the 3D set's). constexpr bool GAME_SCENES = PSXSTACK_GAME_SCENES != 0; constexpr const char *GAME_SCENE_2D_LABEL = PSXSTACK_GAME_SCENE_2D_LABEL; constexpr const char *GAME_SCENE_3D_LABEL = PSXSTACK_GAME_SCENE_3D_LABEL; diff --git a/launcher/src/selftest.cpp b/launcher/src/selftest.cpp index 556de2f..7d07de8 100644 --- a/launcher/src/selftest.cpp +++ b/launcher/src/selftest.cpp @@ -1259,7 +1259,7 @@ static void drop_file(SDL_Window *w, const std::string &path) { } // The play path with the game's stand-in and a verified disc (a file of the BIN's size and the right disc.sha1). -// The mouse over the page, its wheel turned down: the Settings screen scrolled to its end (the Updates part), or by +// The mouse over the page, its wheel turned down: the System screen scrolled to its end (the Updates part), or by // `notches` of the wheel. static void scroll_page_down(SDL_Window *w, App &app, float notches = 40) { int width = 0, height = 0; @@ -1431,28 +1431,29 @@ static void test_play(SDL_Window *window, const std::string &root) { app.screens().show_controls_tab("keyboard"); pump(app, 2); } - app.set_screen(Screen::Settings); + // The Graphics screen under the Software renderer: the GPU renderer's settings greyed out (a picture); then the + // GPU renderer at x4 with them in use (a picture), then back to the default. + app.set_screen(Screen::Graphics); pump(app, 2); - png = path_join(shots, "9-Settings.png"); + png = path_join(shots, "9-Graphics.png"); app.frame(png.c_str()); - // The GPU renderer shows its internal-resolution slider (a picture), then back to the default. app.settings().values.renderer = "gpu"; app.settings().values.internal_scale = 4; pump(app, 2); - png = path_join(shots, "9b-Settings-gpu.png"); + png = path_join(shots, "9b-Graphics-gpu.png"); app.frame(png.c_str()); // The CRT filter shows its sliders (a picture). app.settings().values.present.filter = "crt"; pump(app, 2); - png = path_join(shots, "9c-Settings-crt.png"); + png = path_join(shots, "9c-Graphics-crt.png"); app.frame(png.c_str()); - // A game with scenes: the Filter row for its 3D scenes and the one for its 2D scenes, the latter marked as the one - // on screen as the in-game menu marks it (a picture; the launcher marks none). + // A game with scenes: the filters' tabs, the 2D scenes' marked as the one on screen as the in-game menu marks it + // and chosen by the scene's change (a picture; the launcher marks none). if (GAME_SCENES) { app.settings().values.present_2d.filter = "scanlines"; app.screens().set_scene(Screens::SCENE_2D); - scroll_page_down(window, app, 4); - png = path_join(shots, "9d-Settings-scenes.png"); + pump(app, 2); + png = path_join(shots, "9d-Graphics-scenes.png"); app.frame(png.c_str()); app.screens().set_scene(Screens::SCENE_NONE); app.settings().values.present_2d = Settings::scene_2d_default(app.settings().values.present); @@ -1555,7 +1556,7 @@ static void test_mods_window(SDL_Window *window, const std::string &root) { } // ---- the update check (docs/LAUNCHER.md "Updates"): the version rules, GitHub's answer, curl through its stand-in, -// the prompt and the Settings screen. Never the network: the stand-in answers. +// the prompt and the System screen. Never the network: the stand-in answers. static void set_env(const char *name, const std::string &value) { SDL_SetEnvironmentVariable(SDL_GetEnvironment(), name, value.c_str(), true); @@ -1818,7 +1819,7 @@ static void test_http(const std::string &root) { // The flow in the window: the check at start, the prompt over Play (Escape is Later, the Release page, Skip and a // newer release), the setting off and Check now, --no-update-check, the game running, curl missing, a cut answer, a -// development build, no repository; pictures of the prompt and of the Settings screen's part. +// development build, no repository; pictures of the prompt and of the System screen's part. static void test_update_window(SDL_Window *window, const std::string &root) { const std::string dir = path_join(root, "updates-ui"), shots = path_join(root, "screens"); const std::string files = path_join(root, "updates-files"); @@ -1867,10 +1868,10 @@ static void test_update_window(SDL_Window *window, const std::string &root) { pump(app, 3); check(!app.update_prompt() && !app.update_prompt_shown() && app.settings().values.update_skipped.empty(), "Escape is Later: the prompt closes, nothing is written"); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); pump(app, 3); scroll_page_down(window, app); - png = path_join(shots, "18-Settings-update.png"); + png = path_join(shots, "18-System-update.png"); app.frame(png.c_str()); check(opened.empty(), "no page opened by itself"); } @@ -1931,10 +1932,10 @@ static void test_update_window(SDL_Window *window, const std::string &root) { wait_check(app); check(app.update_check().state() == UpdateCheck::State::Newer && !app.update_prompt(), "Check now reports the release without a prompt: " + app.update_check().summary()); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); pump(app, 3); scroll_page_down(window, app); - png = path_join(shots, "18b-Settings-update-off.png"); + png = path_join(shots, "18b-System-update-off.png"); app.frame(png.c_str()); app.settings().values.update_check = true; // the checkbox: written back app.settings().values.update_skipped.clear(); // the Forget button @@ -1984,10 +1985,10 @@ static void test_update_window(SDL_Window *window, const std::string &root) { app.update_check().summary().find("curl was not found") != std::string::npos && app.update_check().release_page() == "https://github.com/owner/game/releases/latest", "curl missing: reported, the releases page offered: " + app.update_check().summary()); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); pump(app, 3); scroll_page_down(window, app); - png = path_join(shots, "18c-Settings-update-no-curl.png"); + png = path_join(shots, "18c-System-update-no-curl.png"); app.frame(png.c_str()); } set_env(ENV_FAKE_CURL, self_exe()); @@ -2017,13 +2018,13 @@ static void test_update_window(SDL_Window *window, const std::string &root) { AppOptions none = o; none.update_repo = ""; App app(window, renderer, location, none); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); pump(app, 3); scroll_page_down(window, app); check(!app.update_check().available() && app.update_check().state() == UpdateCheck::State::Idle && !app.update_prompt(), "no repository: no update check"); - png = path_join(shots, "18d-Settings-no-repo.png"); + png = path_join(shots, "18d-System-no-repo.png"); app.frame(png.c_str()); check(app.last_error().empty(), "no error in the status bar: " + app.last_error()); } @@ -2159,10 +2160,10 @@ static void test_update_flow(SDL_Window *window, const std::string &root) { check(app.update_step() == UpdateStep::None && !app.update_prompt() && !app.update_prompt_shown() && !path_is_file(download) && read(image) == old_image && !app.quit_requested(), "Cancel (Escape): the download stopped and removed, the prompt closed, the install unchanged"); - app.set_screen(Screen::Settings); // the Settings screen's Update now + app.set_screen(Screen::System); // the System screen's Update now pump(app, 3); scroll_page_down(window, app); - png = path_join(shots, "19e-Settings-update-now.png"); + png = path_join(shots, "19e-System-update-now.png"); app.frame(png.c_str()); } set_env(ENV_SELFTEST_CURL_DOWNLOAD, new_image); diff --git a/launcher/src/selftest_steam.cpp b/launcher/src/selftest_steam.cpp index 7802445..b89e31f 100644 --- a/launcher/src/selftest_steam.cpp +++ b/launcher/src/selftest_steam.cpp @@ -319,7 +319,7 @@ static void test_system() { "the shortcut's path is this executable: " + steam_launcher_path()); } -// ---- the Settings screen's section +// ---- the System screen's section static void pump(App &app, int frames) { for (int i = 0; i < frames; i++) { @@ -420,9 +420,9 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch const uint32_t appid = steam_shortcut_appid(launcher, GAME_TITLE); { App app(window, renderer, location, o); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); pump(app, 3); - shot(window, app, shots, "20-Settings-steam.png"); + shot(window, app, shots, "20-System-steam.png"); check(lookups == 0 && !app.steam().choosing(), "nothing looks for Steam before Add to Steam is pressed"); SteamSection &st = app.steam(); st.begin_add(); @@ -430,12 +430,12 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch check(lookups == 1 && st.installs().size() == 2 && st.install() == 0 && st.account() == 1 && st.message().empty(), "Add to Steam: two Steam folders, the most recent account chosen"); - shot(window, app, shots, "20b-Settings-steam-choose.png"); + shot(window, app, shots, "20b-System-steam-choose.png"); running = true; check(!st.add() && st.message().find("Steam is running") != std::string::npos && !path_is_file(recent) && !app.settings().values.steam.added(), "Steam running: nothing is written, the player is asked to quit it"); - shot(window, app, shots, "20c-Settings-steam-running.png"); + shot(window, app, shots, "20c-System-steam-running.png"); running = false; st.choose(0, 0); // the account whose file has two shortcuts already check(st.add() && !st.choosing() && app.settings().values.steam.appid == appid, @@ -456,13 +456,13 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch check(record_in_file(location.dir) == "39734383 " + std::to_string(appid) + " " + launcher, "launcher.steam saved at once: the account, the appid, the path"); pump(app, 2); - shot(window, app, shots, "20d-Settings-steam-added.png"); + shot(window, app, shots, "20d-System-steam-added.png"); } { // The next start: the record read back, still nothing looked up; the launcher moved: Update shortcut. lookups = 0; App app(window, renderer, location, o); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); SteamSection &st = app.steam(); check(app.settings().values.steam.added() && !st.moved() && lookups == 0, "the next start reads the record and does not look for Steam"); @@ -470,7 +470,7 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch launcher = path_join(path_join(dir, "Moved"), LAUNCHER_EXE); pump(app, 3); check(st.moved(), "the launcher moved: Update shortcut offered"); - shot(window, app, shots, "20e-Settings-steam-moved.png"); + shot(window, app, shots, "20e-System-steam-moved.png"); running = true; check(!st.update_shortcut() && get(first + ".bak") == fixture && names_in(first).size() == 3 && app.settings().values.steam.exe == old_launcher, @@ -493,7 +493,7 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch check(art_of(moved_id) == no_art && get(path_join(grid, "12345p.png")) == "another game's", "Remove: its pictures gone, another shortcut's left"); pump(app, 2); - shot(window, app, shots, "20f-Settings-steam-removed.png"); + shot(window, app, shots, "20f-System-steam-removed.png"); // Removed in Steam meanwhile: Remove forgets it and says so. st.begin_add(); @@ -514,12 +514,12 @@ void self_test_steam_window(SDL_Window *window, const std::string &root, int *ch } { App app(window, renderer, location, o); - app.set_screen(Screen::Settings); + app.set_screen(Screen::System); app.steam().begin_add(); pump(app, 2); check(app.steam().installs().empty() && app.steam().message().find("not found") != std::string::npos, "no Steam: said, with where the launcher looked"); - shot(window, app, shots, "20g-Settings-steam-not-found.png"); + shot(window, app, shots, "20g-System-steam-not-found.png"); } } diff --git a/launcher/src/settings.h b/launcher/src/settings.h index 28acff9..da97b48 100644 --- a/launcher/src/settings.h +++ b/launcher/src/settings.h @@ -72,9 +72,9 @@ struct FilterSet { } bool operator!=(const FilterSet &o) const { return !(*this == o); } }; -// video.scaling's values (the game's runtime/video_filter.c port_scaling_names), in the Settings screen's order. +// video.scaling's values (the game's runtime/video_filter.c port_scaling_names), in the Video screen's order. inline const char *const SCALING_NAMES[] = { "fit", "integer", "stretch" }; -// video.menu_size's values (the in-game menu's text, launcher/ui/menu.cpp), in the Settings screen's order. +// video.menu_size's values (the in-game menu's text, launcher/ui/menu.cpp), in the Video screen's order. inline const char *const MENU_SIZE_NAMES[] = { "smaller", "auto", "larger" }; // input.player2's values (the game's runtime/input.c): none, or the gamepad by connection order that is player 2. inline const char *const PLAYER2_NAMES[] = { "off", "first", "second" }; @@ -106,7 +106,7 @@ struct Settings { FilterSet present; // video.scene_2d (a game with scenes only): its 2D scenes' set, scene_2d_default(present) when the file has none. // Written whole once it differs from that, or once the file has it. It is held as values: where the default is a - // copy of the 3D set, a change to the 3D set leaves it as it was (as the Filter rows show it), and it is written. + // copy of the 3D set, a change to the 3D set leaves it as it was (as the filter tabs show it), and it is written. FilterSet present_2d = scene_2d_default(FilterSet()); std::string menu_size = "auto"; // the in-game menu's size (video.menu_size): one of MENU_SIZE_NAMES bool mute = false; diff --git a/launcher/src/steam_section.h b/launcher/src/steam_section.h index cfae87a..34b62c1 100644 --- a/launcher/src/steam_section.h +++ b/launcher/src/steam_section.h @@ -1,6 +1,6 @@ -// The Settings screen's Steam section (docs/LAUNCHER.md "Steam"): Add to Steam, Remove from Steam, Update shortcut, -// over steam.h and the settings' launcher.steam. The launcher window's alone (drawn through the shared Settings -// screen's hook, Screens::set_settings_extra; the in-game menu has none). Nothing looks for Steam until a button is +// The System screen's Steam section (docs/LAUNCHER.md "Steam"): Add to Steam, Remove from Steam, Update shortcut, +// over steam.h and the settings' launcher.steam. The launcher window's alone (drawn through the shared System +// screen's hook, Screens::set_system_extra; the in-game menu has none). Nothing looks for Steam until a button is // pressed: the lookup, the running check and the files are all behind the actions below. #pragma once diff --git a/launcher/src/update.h b/launcher/src/update.h index 89baa2f..2daa2bb 100644 --- a/launcher/src/update.h +++ b/launcher/src/update.h @@ -87,7 +87,7 @@ class UpdateCheck { const Release &release() const { return release_; } // UpToDate, Newer, DevBuild const std::string &message() const { return message_; } // NoCurl, Failed SDL_Time checked_at() const { return checked_at_; } // when the last answer came (0: never) - // The state in words, for the Settings screen ("0.5.0 is available (you have 0.4.1)"). + // The state in words, for the System screen ("0.5.0 is available (you have 0.4.1)"). std::string summary() const; // The page to open: the release's, else the repository's latest release. std::string release_page() const; diff --git a/launcher/ui/menu.cpp b/launcher/ui/menu.cpp index 22f229d..96ca755 100644 --- a/launcher/ui/menu.cpp +++ b/launcher/ui/menu.cpp @@ -1,16 +1,16 @@ // The in-game menu (include/psxstack/menu.h; docs/LAUNCHER.md "In-game menu"): Dear ImGui inside the game's window, // on the present path the window chose (SDL_Renderer, or SDL_GPU for the hardware renderer), shown while the pump // holds the game paused. Its pages: Game (the title, Resume, Quit) and the launcher's shared screens (screens.h: -// Settings, Controls, Mods) over the settings file the game was started with (--config) and the mods beside the +// Video, Graphics, Audio, Controls, Mods, System) over the settings file the game was started with (--config) and the mods beside the // game, so a change made here is the same change the launcher would make; it takes effect at the next start. The // runtime stays C: this file is the C++ behind the C API, compiled into the game only with PSXSTACK_MENU // (cmake/psxstack.cmake). // // _PORT_MENU_SHOT=PATH writes the window's output with the menu up, once, as a PNG: the second present after // it is shown, on either present path (the hardware renderer's through a texture render_gpu.c reads back: menu.h -// port_menu_shot_due). _PORT_MENU_SCREEN=game|settings|controls|mods picks the page it opens on (the picture -// of a page; default game), `mods:` with that mod chosen, `controls:` on that tab (keyboard, gamepad, -// hotkeys). +// port_menu_shot_due). _PORT_MENU_SCREEN=game|video|graphics|audio|controls|mods|system picks the page it +// opens on (the picture of a page; default game; `settings`, the page before the split, is video), `mods:` with +// that mod chosen, `controls:` on that tab (keyboard, gamepad, hotkeys). #include "menu.h" #include @@ -43,8 +43,8 @@ static_assert(psxstack::Screens::SCENE_3D == PORT_SCENE_3D && psxstack::Screens: namespace { -enum Page { PAGE_GAME, PAGE_SETTINGS, PAGE_CONTROLS, PAGE_MODS, PAGE_COUNT }; -const char *const PAGE_NAMES[PAGE_COUNT] = { "Game", "Settings", "Controls", "Mods" }; +enum Page { PAGE_GAME, PAGE_VIDEO, PAGE_GRAPHICS, PAGE_AUDIO, PAGE_CONTROLS, PAGE_MODS, PAGE_SYSTEM, PAGE_COUNT }; +const char *const PAGE_NAMES[PAGE_COUNT] = { "Game", "Video", "Graphics", "Audio", "Controls", "Mods", "System" }; struct Menu { SDL_Window *window = nullptr; @@ -100,6 +100,9 @@ bool menu_open_common(SDL_Window *window) { mn.start_page = i; } } + if (SDL_strcasecmp(name.c_str(), "settings") == 0) { + mn.start_page = PAGE_VIDEO; + } } int win_w = 0, win_h = 0; if (const char *size = SDL_getenv(env_name("_PORT_MENU_WINDOW").c_str()); @@ -239,8 +242,14 @@ int menu_draw() { } ImGui::BeginChild("page", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding); switch (mn.page) { - case PAGE_SETTINGS: - mn.screens.draw_settings(); + case PAGE_VIDEO: + mn.screens.draw_video(); + break; + case PAGE_GRAPHICS: + mn.screens.draw_graphics(); + break; + case PAGE_AUDIO: + mn.screens.draw_audio(); break; case PAGE_CONTROLS: mn.screens.draw_controls(); @@ -248,6 +257,9 @@ int menu_draw() { case PAGE_MODS: mn.screens.draw_mods(); break; + case PAGE_SYSTEM: + mn.screens.draw_system(); + break; default: action = menu_draw_game(); break; @@ -401,7 +413,7 @@ int port_menu_frame(void) { return PORT_MENU_NONE; } menu_style_for_window(); - // The scenes' set the present uses now, marked on the Settings page (a game with scenes; the game is paused, so it + // The scenes' set the present uses now, marked on the Graphics page (a game with scenes; the game is paused, so it // holds while the menu is up). mn.screens.set_scene(PSXSTACK_GAME_SCENES ? (int)port_video_scene() : psxstack::Screens::SCENE_NONE); if (mn.renderer != nullptr) { diff --git a/launcher/ui/screens.cpp b/launcher/ui/screens.cpp index d679934..9ee42ed 100644 --- a/launcher/ui/screens.cpp +++ b/launcher/ui/screens.cpp @@ -59,9 +59,8 @@ static float item_width(float label_w) { // A present-filter set (video.filter and video.crt, or a scene's set: video.scene_2d; under the GPU renderer only: the // Software one shows the picture unfiltered): a combo in FILTER_NAMES' order, the filter's sliders (its crt), a line -// of help for the choice. `scene` names the set's scenes ("Filter: battles", on a line of its own above its rows), -// nullptr for a game with one set; `in_use` marks the set the game shows now (the in-game menu). -void Screens::draw_filter(float label_w, FilterSet &set, const char *id, const char *scene, bool in_use) { +// of help for the choice. A game with scenes draws one per tab (draw_graphics). +void Screens::draw_filter(float label_w, FilterSet &set, const char *id) { static const char *const labels[] = { "None (the PS1's pixels)", "Sharp", "Scanlines", "CRT", "Smooth (2D)" }; static const char *const help[] = { "Each pixel of the picture as a block of whole window pixels, as it is.", @@ -83,19 +82,7 @@ void Screens::draw_filter(float label_w, FilterSet &set, const char *id, const c } } ImGui::PushID(id); - if (scene != nullptr) { - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("Filter: %s", scene); - ImGui::PopTextWrapPos(); - if (in_use) { - ImGui::SameLine(); - wrap_for(ImGui::CalcTextSize("(on screen now)").x); - ImGui::TextColored(rgb(ACCENT), "(on screen now)"); - } - ImGui::Dummy(ImVec2(0, 0)); - } else { - ImGui::TextDisabled("Filter"); - } + ImGui::TextDisabled("Filter"); ImGui::SameLine(label_w); ImGui::SetNextItemWidth(item_width(label_w)); if (ImGui::BeginCombo("##filter", labels[current])) { @@ -132,13 +119,21 @@ void Screens::draw_filter(float label_w, FilterSet &set, const char *id, const c ImGui::PopID(); } -void Screens::draw_settings() { +// A line of help under a setting's widget, in its value column. +static void help_line(float label_w, const char *text) { + ImGui::Indent(label_w); + ImGui::PushTextWrapPos(0); + ImGui::TextDisabled("%s", text); + ImGui::PopTextWrapPos(); + ImGui::Unindent(label_w); +} + +// Video: the window and the picture's place in it, the rate, the in-game menu's size. +void Screens::draw_video() { Settings &s = settings_.values; const float label_w = label_width(); draw_restart_note(); ImGui::BeginDisabled(!settings_.writable()); - - section_title("Video"); ImGui::TextDisabled("Window size"); ImGui::SameLine(label_w); ImGui::SetNextItemWidth(item_width(label_w)); @@ -167,32 +162,8 @@ void Screens::draw_settings() { dirty_ = true; } } - ImGui::Indent(label_w); - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("Fit: as large as the screen allows. Integer: every line the same height, with black bars " - "where the screen is not a whole multiple. Stretch: the whole screen, out of shape."); - ImGui::PopTextWrapPos(); - ImGui::Unindent(label_w); - } - // video.menu_size: the in-game menu's size, applied as soon as it is chosen (launcher/ui/menu.cpp). - { - static const char *const labels[] = { "Smaller", "Auto", "Larger" }; - static_assert(SDL_arraysize(labels) == SDL_arraysize(MENU_SIZE_NAMES), "a label per menu size"); - ImGui::TextDisabled("Menu size"); - if (!in_game_) { - ImGui::SetItemTooltip("The in-game menu's: Auto fits it to the game's window"); - } - ImGui::SameLine(label_w); - for (int i = 0; i < (int)SDL_arraysize(labels); i++) { - if (i > 0) { - ImGui::SameLine(); - wrap_for(radio_width(labels[i]), label_w); - } - if (ImGui::RadioButton(labels[i], s.menu_size == MENU_SIZE_NAMES[i])) { - s.menu_size = MENU_SIZE_NAMES[i]; - dirty_ = true; - } - } + help_line(label_w, "Fit: as large as the screen allows. Integer: every line the same height, with black bars " + "where the screen is not a whole multiple. Stretch: the whole screen, out of shape."); } // The rates the game offers (brand.h GAME_RATES): the nominal one named by its TV standard; one rate: no choice. if (GAME_RATE_COUNT > 1) { @@ -213,7 +184,41 @@ void Screens::draw_settings() { } } restart_mark(label_w); + if (s.refresh != GAME_RATE && GAME_RATE_NOTE[0] != '\0') { + help_line(label_w, GAME_RATE_NOTE); + } } + // video.menu_size: the in-game menu's size, applied as soon as it is chosen (launcher/ui/menu.cpp). + { + static const char *const labels[] = { "Smaller", "Auto", "Larger" }; + static_assert(SDL_arraysize(labels) == SDL_arraysize(MENU_SIZE_NAMES), "a label per menu size"); + ImGui::TextDisabled("Menu size"); + if (!in_game_) { + ImGui::SetItemTooltip("The in-game menu's: Auto fits it to the game's window"); + } + ImGui::SameLine(label_w); + for (int i = 0; i < (int)SDL_arraysize(labels); i++) { + if (i > 0) { + ImGui::SameLine(); + wrap_for(radio_width(labels[i]), label_w); + } + if (ImGui::RadioButton(labels[i], s.menu_size == MENU_SIZE_NAMES[i])) { + s.menu_size = MENU_SIZE_NAMES[i]; + dirty_ = true; + } + } + } + ImGui::EndDisabled(); +} + +// Graphics: the renderer and what the hardware one does with the picture. Under the Software renderer its settings +// are shown greyed out under a line that says so (the page keeps its shape when the renderer changes). +void Screens::draw_graphics() { + Settings &s = settings_.values; + const float label_w = label_width(); + const bool gpu = s.renderer == "gpu"; + draw_restart_note(); + ImGui::BeginDisabled(!settings_.writable()); ImGui::TextDisabled("Renderer"); ImGui::SameLine(label_w); if (ImGui::RadioButton("Software", s.renderer == "software")) { @@ -222,76 +227,105 @@ void Screens::draw_settings() { } ImGui::SameLine(); wrap_for(radio_width("GPU (experimental)"), label_w); - if (ImGui::RadioButton("GPU (experimental)", s.renderer == "gpu")) { + if (ImGui::RadioButton("GPU (experimental)", gpu)) { s.renderer = "gpu"; dirty_ = true; } restart_mark(label_w); - if (s.renderer == "gpu") { - char res[64]; - ImGui::TextDisabled("Resolution"); - ImGui::SameLine(label_w); - ImGui::SetNextItemWidth(item_width(label_w)); - SDL_snprintf(res, sizeof(res), "x%d (%d x %d)", s.internal_scale, 320 * s.internal_scale, - 240 * s.internal_scale); - dirty_ |= ImGui::SliderInt("##internal_scale", &s.internal_scale, 1, 8, res, ImGuiSliderFlags_AlwaysClamp); - restart_mark(label_w); - ImGui::Indent(label_w); - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("SDL_GPU (Vulkan; Direct3D 12 on Windows). x1 is the Software picture, pixel for pixel; above it the game is drawn " - "at that resolution, without dithering, in 8-bit colour. Without a usable GPU the game " - "falls back to Software."); - ImGui::PopTextWrapPos(); - ImGui::Unindent(label_w); + help_line(label_w, gpu ? "SDL_GPU (Vulkan; Direct3D 12 on Windows). Without a usable GPU the game falls back to " + "Software." + : "The PS1's picture as it drew it. The settings below need the GPU renderer."); + ImGui::Spacing(); + + ImGui::BeginDisabled(!gpu); + char res[64]; + ImGui::TextDisabled("Resolution"); + ImGui::SameLine(label_w); + ImGui::SetNextItemWidth(item_width(label_w)); + SDL_snprintf(res, sizeof(res), "x%d (%d x %d)", s.internal_scale, 320 * s.internal_scale, 240 * s.internal_scale); + dirty_ |= ImGui::SliderInt("##internal_scale", &s.internal_scale, 1, 8, res, ImGuiSliderFlags_AlwaysClamp); + restart_mark(label_w); + help_line(label_w, "x1 is the Software picture, pixel for pixel; above it the game is drawn at that resolution, " + "without dithering, in 8-bit colour."); + // video.subpixel (docs/LAUNCHER.md): the 3D at the GTE's sub-pixel positions, above x1 only. + ImGui::BeginDisabled(s.internal_scale <= 1); + ImGui::TextDisabled("3D vertices"); + ImGui::SameLine(label_w); + if (ImGui::RadioButton("Sub-pixel", s.subpixel == "on")) { + s.subpixel = "on"; + dirty_ = true; } - if (s.renderer == "gpu" && s.internal_scale > 1) { - // video.subpixel (docs/LAUNCHER.md): the 3D at the GTE's sub-pixel positions, above x1 only. - ImGui::TextDisabled("3D vertices"); - ImGui::SameLine(label_w); - if (ImGui::RadioButton("Sub-pixel", s.subpixel == "on")) { - s.subpixel = "on"; - dirty_ = true; - } - ImGui::SameLine(); - wrap_for(radio_width("Perspective"), label_w); - if (ImGui::RadioButton("Perspective", s.subpixel == "perspective")) { - s.subpixel = "perspective"; - dirty_ = true; - } - ImGui::SameLine(); - wrap_for(radio_width("PS1 pixels"), label_w); - if (ImGui::RadioButton("PS1 pixels", s.subpixel == "off")) { - s.subpixel = "off"; - dirty_ = true; + ImGui::SameLine(); + wrap_for(radio_width("Perspective"), label_w); + if (ImGui::RadioButton("Perspective", s.subpixel == "perspective")) { + s.subpixel = "perspective"; + dirty_ = true; + } + ImGui::SameLine(); + wrap_for(radio_width("PS1 pixels"), label_w); + if (ImGui::RadioButton("PS1 pixels", s.subpixel == "off")) { + s.subpixel = "off"; + dirty_ = true; + } + help_line(label_w, s.internal_scale <= 1 + ? "Above Resolution x1 only: at x1 the 3D steps a whole PS1 pixel at a time." + : "Sub-pixel: the 3D moves smoothly. Perspective: also textured perspective-correct (not " + "the PS1's look). PS1 pixels: the 3D steps a whole PS1 pixel at a time."); + ImGui::EndDisabled(); + ImGui::Spacing(); + if (!GAME_SCENES) { + draw_filter(label_w, s.present, "filter"); + } else if (ImGui::BeginTabBar("filters")) { + // A game with scenes (docs/LAUNCHER.md "Members"): a tab for the set of its 3D scenes (video.filter, video.crt) + // and one for its 2D scenes' (video.scene_2d), each named by its scenes' label; the game switches between them. + // The set on screen (the in-game menu) says so in its tab's name, and its tab is chosen when the scene changes. + const struct { + const char *label, *id; + int scene; + FilterSet *set; + } tabs[] = { { GAME_SCENE_3D_LABEL, "filter_3d", SCENE_3D, &s.present }, + { GAME_SCENE_2D_LABEL, "filter_2d", SCENE_2D, &s.present_2d } }; + for (const auto &t : tabs) { + std::string name = t.label; + if (!name.empty() && (unsigned char)name[0] < 0x80) { + name[0] = (char)SDL_toupper(name[0]); + } + if (gpu && scene_ == t.scene) { + name += " (on screen)"; + } + name += std::string("###") + t.id; // the tab's id stays as its name changes + if (ImGui::BeginTabItem(name.c_str(), nullptr, filter_tab_ == t.scene ? ImGuiTabItemFlags_SetSelected : 0)) { + draw_filter(label_w, *t.set, t.id); + ImGui::EndTabItem(); + } } - ImGui::Indent(label_w); - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("Sub-pixel: the 3D moves smoothly. Perspective: also textured perspective-correct (not the " - "PS1's look). PS1 pixels: the 3D steps a whole PS1 pixel at a time."); - ImGui::PopTextWrapPos(); - ImGui::Unindent(label_w); - } - if (s.renderer == "gpu" && !GAME_SCENES) { - draw_filter(label_w, s.present, "filter", nullptr, false); - } else if (s.renderer == "gpu") { - // A game with scenes (docs/LAUNCHER.md "Members"): a set for its 3D scenes (video.filter, video.crt) and one - // for its 2D scenes (video.scene_2d), each under its scenes' label; the game switches between them. - draw_filter(label_w, s.present, "filter_3d", GAME_SCENE_3D_LABEL, scene_ == SCENE_3D); - draw_filter(label_w, s.present_2d, "filter_2d", GAME_SCENE_2D_LABEL, scene_ == SCENE_2D); - } - if (s.refresh != GAME_RATE && GAME_RATE_NOTE[0] != '\0') { - ImGui::Indent(label_w); - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("%s", GAME_RATE_NOTE); - ImGui::PopTextWrapPos(); - ImGui::Unindent(label_w); + filter_tab_ = SCENE_NONE; + ImGui::EndTabBar(); } + ImGui::EndDisabled(); + ImGui::EndDisabled(); +} - section_title("Audio"); +// Audio: the mute (the page for the audio settings to come). +void Screens::draw_audio() { + Settings &s = settings_.values; + const float label_w = label_width(); + draw_restart_note(); + ImGui::BeginDisabled(!settings_.writable()); ImGui::TextDisabled("Mute"); ImGui::SameLine(label_w); dirty_ |= ImGui::Checkbox("##mute", &s.mute); + help_line(label_w, "No sound: the game leaves the audio device closed."); + ImGui::EndDisabled(); +} +// System: the memory cards, the host's own sections (set_system_extra: the launcher's Updates and Steam), the +// settings file. +void Screens::draw_system() { + Settings &s = settings_.values; + const float label_w = label_width(); + draw_restart_note(); + ImGui::BeginDisabled(!settings_.writable()); section_title("Memory cards"); if (in_game_) { ImGui::TextDisabled("(restart)"); @@ -317,16 +351,12 @@ void Screens::draw_settings() { ImGui::EndDisabled(); ImGui::PopID(); } - ImGui::Indent(label_w); - ImGui::PushTextWrapPos(0); - ImGui::TextDisabled("A .mcd card image, relative to the settings directory; the game creates a formatted card when " - "the file is missing. Unticked: no card in the slot."); - ImGui::PopTextWrapPos(); - ImGui::Unindent(label_w); + help_line(label_w, "A .mcd card image, relative to the settings directory; the game creates a formatted card " + "when the file is missing. Unticked: no card in the slot."); ImGui::EndDisabled(); - if (settings_extra_) { - settings_extra_(label_w); + if (system_extra_) { + system_extra_(label_w); } section_title("Settings file"); diff --git a/launcher/ui/screens.h b/launcher/ui/screens.h index 3d8c070..92ba17a 100644 --- a/launcher/ui/screens.h +++ b/launcher/ui/screens.h @@ -1,5 +1,5 @@ // The screens the launcher's window (launcher/src/app.cpp) and the game's in-game menu (launcher/ui/menu.cpp) share: -// Settings, Controls and Mods, over the settings file (settings.h) and the mods' manifests (mods.h), with the +// Video, Graphics, Audio, Controls, Mods and System, over the settings file (settings.h) and the mods' manifests (mods.h), with the // controls' "press an input" prompt (input.h InputCapture). Both hosts drive it the same way: load() once, // handle_event() for every SDL event before ImGui's backend sees it, begin_frame() before ImGui::NewFrame, a draw_* // per frame, end_frame() after ImGui::Render (the save, once no widget is in use). @@ -32,13 +32,19 @@ class Screens { bool game_found); // In the game's menu: every screen says that changes take effect at the next start. void set_in_game(bool on) { in_game_ = on; } - // The scenes' present-filter set the game shows now, which the Settings screen marks (a game with scenes; the + // The scenes' present-filter set the game shows now, which the Graphics screen marks (a game with scenes; the // in-game menu, every frame, from the runtime): SCENE_3D or SCENE_2D (game.h PORT_SCENE_*), SCENE_NONE unmarked. + // A change also chooses that set's tab. static constexpr int SCENE_NONE = -1, SCENE_3D = 0, SCENE_2D = 1; - void set_scene(int scene) { scene_ = scene; } - // A part of the Settings screen its host draws itself, after "Memory cards" (the launcher's Updates); given the - // label column's width. The in-game menu sets none. - void set_settings_extra(std::function draw) { settings_extra_ = std::move(draw); } + void set_scene(int scene) { + if (scene != scene_) { + filter_tab_ = scene; + } + scene_ = scene; + } + // A part of the System screen its host draws itself, after "Memory cards" (the launcher's Updates and Steam); + // given the label column's width. The in-game menu sets none. + void set_system_extra(std::function draw) { system_extra_ = std::move(draw); } // True when the event belonged to the controls' prompt (it then does not reach ImGui). bool handle_event(const SDL_Event &e); @@ -55,7 +61,10 @@ class Screens { return s; } - void draw_settings(); + void draw_video(); + void draw_graphics(); + void draw_audio(); + void draw_system(); void draw_controls(); void draw_mods(); @@ -78,7 +87,7 @@ class Screens { private: void draw_restart_note(); void restart_mark(float indent = 0); // "(restart)" after the last item, in the game's menu - void draw_filter(float label_w, FilterSet &set, const char *id, const char *scene, bool in_use); + void draw_filter(float label_w, FilterSet &set, const char *id); void draw_names_table(bool pad); void draw_players(); void draw_hotkeys(); @@ -102,8 +111,9 @@ class Screens { bool saved_ = false; bool in_game_ = false; int scene_ = SCENE_NONE; + int filter_tab_ = SCENE_NONE; // the Graphics screen's filter tab to choose at its next frame (set_scene) std::string controls_tab_; // show_controls_tab's, until the Controls screen is drawn - std::function settings_extra_; + std::function system_extra_; std::string error_; // the last save error // The controls: the "press a key" prompt and what it is for (section 0 keyboard, 1 gamepad, 2 hotkeys). diff --git a/schema/game.schema.json b/schema/game.schema.json index 8943886..0468b2b 100644 --- a/schema/game.schema.json +++ b/schema/game.schema.json @@ -60,14 +60,14 @@ "2d": { "type": "object", "additionalProperties": false, "required": ["label"], "properties": { - "label": { "type": "string", "minLength": 1, "description": "What the 2D scenes are, for the launcher's Filter row: \"the field and the menus\"." }, + "label": { "type": "string", "minLength": 1, "description": "What the 2D scenes are, for the launcher's filter tab (its first letter capitalised): \"the field and the menus\"." }, "filter": { "enum": ["none", "sharp", "scanlines", "crt", "smooth"], "description": "The 2D set's default filter (with the default crt values). Absent: a copy of the 3D set." } } }, "3d": { "type": "object", "additionalProperties": false, "required": ["label"], "properties": { - "label": { "type": "string", "minLength": 1, "description": "What the 3D scenes are, for the launcher's Filter row: \"battles\"." } + "label": { "type": "string", "minLength": 1, "description": "What the 3D scenes are, for the launcher's filter tab (its first letter capitalised): \"battles\"." } } } } diff --git a/tests/hello_test.py b/tests/hello_test.py index 450d5df..e4ff1f4 100644 --- a/tests/hello_test.py +++ b/tests/hello_test.py @@ -120,9 +120,10 @@ def script_check(runner, exe, build, env): def menu_check(build, env, gpu): """--menu: hello with the window and the in-game menu (-DPSXSTACK_SDL=ON -DPSXSTACK_MENU=ON: tools/sdl3, dxc and imgui), its input self-test on SDL's offscreen driver, which ends with the menu's round trip (runtime/input.c), and - the picture of the menu up on its Settings page (HELLO_PORT_MENU_SHOT and _SCREEN, launcher/ui/menu.cpp) at - build/menu.png; with gpu the same on the hardware renderer, build/menu-gpu.png (menu_gpu); then the Mods page's at - three window sizes and one menu size (MENU_SIZES), build/menu-mods-*.png; then the input self-test again with each + the picture of the menu up on its Graphics page (HELLO_PORT_MENU_SHOT and _SCREEN, launcher/ui/menu.cpp) at + build/menu.png; with gpu the same on the hardware renderer, build/menu-gpu.png (menu_gpu); then the other settings + pages' at 640x480 (MENU_PAGES), build/menu-.png; then the Mods page's at three window sizes and one menu size + (MENU_SIZES), build/menu-mods-*.png; then the input self-test again with each input.player2 that gives a second pad (PLAYER2: its players' part), the first also with the picture of the Controls screen's Gamepad tab at 640x480, build/menu-controls-gamepad.png.""" for tool in ("cmake", "ninja", "gcc", "g++", "objcopy", "objdump", "nm"): @@ -151,7 +152,7 @@ def menu_check(build, env, gpu): print("\n".join(lines[-15:])) fit = shot_fit(lines) if fit != "nothing wider than its window": - print(f"FAIL the Settings page at 640x480: {fit}") + print(f"FAIL the Graphics page at 640x480: {fit}") ok = False if shot.exists() and shot.stat().st_size > 1000 and shot.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n": print(f"ok the menu's picture: {shot} ({shot.stat().st_size} bytes)") @@ -160,6 +161,7 @@ def menu_check(build, env, gpu): ok = False if gpu: ok &= menu_gpu(exe, cfg, shot, env) + ok &= menu_pages(exe, cfg, env) ok &= menu_sizes(exe, cfg, env) ok &= players_check(exe, cfg, env) print("hello: " + ("pass" if ok else "FAIL")) @@ -167,7 +169,8 @@ def menu_check(build, env, gpu): def menu_run(exe, cfg, shot, env, *extra): - """The input self-test with the menu on its Settings page, its picture to `shot`. The game starts muted, from a + """The input self-test with the menu on its Graphics page (the largest of the settings pages: every setting of + the hardware renderer, greyed out under the software one), its picture to `shot`. The game starts muted, from a settings file of its own, so the menu's pages have something to edit. The live apply with a change (runtime/input.c HELLO_PORT_MENU_LIVE_FILE): a second file, put in place while the menu is up, rebinds the menu hotkey, switches fast-forward on and the mute off; the round trip then closes the menu with the new key, the mod's @@ -177,7 +180,7 @@ def menu_run(exe, cfg, shot, env, *extra): (cfg / "live.json").write_text('{ "schema": 1, "input": { "hotkeys": { "menu": "F10" } }, ' '"mods": { "fast_forward": { "enabled": true } }, "audio": { "mute": false } }\n') run_env = dict(env, SDL_VIDEO_DRIVER="offscreen", SDL_AUDIO_DRIVER="dummy", HELLO_PORT_MENU_SHOT=str(shot), - HELLO_PORT_MENU_SCREEN="settings", HELLO_PORT_MENU_LIVE_FILE=str(cfg / "live.json")) + HELLO_PORT_MENU_SCREEN="graphics", HELLO_PORT_MENU_LIVE_FILE=str(cfg / "live.json")) return subprocess.run([str(exe), "--config", str(cfg / "settings.json"), "--mods-dir", str(cfg / "mods"), "--input-test", "--fps", "0", *extra], capture_output=True, text=True, env=run_env, timeout=600) @@ -270,6 +273,33 @@ def png_rgb(path): return w, h, bytes(out) +# The settings pages menu_run does not take (it takes Graphics), at the smallest window: nothing may be wider than it. +MENU_PAGES = ["video", "audio", "system"] + + +def menu_pages(exe, cfg, env): + ok = True + pages = cfg / "pages" + pages.mkdir(exist_ok=True) + (pages / "settings.json").write_text('{ "schema": 1 }\n') + for page in MENU_PAGES: + shot = exe.parent / f"menu-{page}.png" + shot.unlink(missing_ok=True) + run_env = dict(env, SDL_VIDEO_DRIVER="offscreen", SDL_AUDIO_DRIVER="dummy", SDL_RENDER_DRIVER="software", + HELLO_PORT_MENU_SHOT=str(shot), HELLO_PORT_MENU_SCREEN=page, HELLO_PORT_MENU_WINDOW="640x480") + r = subprocess.run([str(exe), "--config", str(pages / "settings.json"), "--input-test", "--fps", "0"], + capture_output=True, text=True, env=run_env, timeout=600) + lines = r.stderr.strip().splitlines() + fit = shot_fit(lines) + good = (r.returncode == 0 and fit == "nothing wider than its window" and shot.exists() and + shot.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n") + print(("ok " if good else "FAIL ") + f"the {page.capitalize()} page at 640x480: {fit}; {shot}") + if not good and r.returncode != 0: + print("\n".join(lines[-15:])) + ok &= good + return ok + + # The Mods page's pictures (#92): a desktop's 1920x1080, the Steam Deck's 1280x800 and the smallest window, 640x480, # at the fitted size, and the Deck's at video.menu_size "larger" (the narrow layout at a larger scale), with # tests/fixtures/mods/multipliers selected (a page at least as large as the widest a game ships), its values set so