Skip to content

Let settings change and save while the game runs - #65

Merged
Adubbz merged 4 commits into
TheMoonPeople:portfrom
carrdylan52:pr/settings-base-20261006
Oct 6, 2026
Merged

Adubbz merged 4 commits into
TheMoonPeople:portfrom
carrdylan52:pr/settings-base-20261006

Conversation

@carrdylan52

Copy link
Copy Markdown
Contributor

The groundwork the settings menu needs: config.json can now change while the game runs, applies at once, and is saved safely. Four commits on current port. Nothing under ps2/ changes, and there's no menu yet; the Options screen is the next PR, on top of this one.

What it is

  • One entry point. ConfigChange(Config) (port/src/platform/config.hpp) takes a whole Config, typically ConfigGet() with some fields edited. It does five things:

    1. It sends the settings through the serialiser and parser and back, so a bad value is reported and becomes its default, exactly as at start-up. There is no second copy of the range checks.
    2. It returns at once if nothing changed.
    3. It makes the new settings current.
    4. It saves config.json: written to config.json.tmp, then renamed over the old file, so a crash can't leave half a file.
    5. It calls each change hook with the old and the new settings.

    A failed save is retried the next time the same settings come in. ConfigChange and the hooks run on the main thread only; a hook that calls ConfigChange is refused.

  • Change hooks. ConfigAddChangeHook / ConfigRemoveChangeHook. They run over a copy of the list, so a hook may remove itself. When the game's own options move into config.json (the Options PR), they register a hook; ConfigChange doesn't change.

  • What applies live (ApplyConfigChange in main.cpp, where start-up already applied these):

    • master volume;
    • the input section (bindings, mouse, stick; InputApplyConfig, split out of InputInit; physical key state survives a rebind);
    • tick rate;
    • interpolation, max_fps and show_fps;
    • present mode and vsync (gfx::SetPresentMode recreates the swapchain as a resize does);
    • window size and fullscreen (WindowSetMode, the same placement as WindowInit; a maximised window is restored first);
    • aspect and ui_scale (gfx::SetFrameLayout, held until the next pump outside a frame).

    The detail and shadow distances already read live.

  • Restart only. game.debug_mode applies on the next start, and ConfigAppliesOnRestart says so. The debug-toggle chord now reads the value from start-up as well.

  • --width, --height and --show-fps still win over the file, and a headless window keeps its size.

As you said: no migration and no version field. A saved tick_rate of 50 still loads as 50, and that test is unchanged. Rewriting the file drops comments, which docs/PC.md now says. A separate commit fixes docs/PC.md where it still gave the tick rate as 50 Hz.

Testing

Windows (Debug, clang 23): 102 of 102. Linux (clang 23): a clean build; ctest 95 of 95 on RADV and on lavapipe; truncations.py --check passes; Doxygen gives no new warning; the two recorded replays (a debug item grant, a dungeon floor) run to the end. The new test PlatformConfig.ChangeAppliesAndSaves runs in about 0.06 s. It covers:

  • a bad value falling back to its default;
  • hook order, including a hook that removes itself;
  • a re-entrant change being refused;
  • an unchanged call doing nothing;
  • a failed save being retried, made to fail by putting a directory where config.json goes, before and after load;
  • no .tmp being left behind;
  • a reload giving the saved values.

Changing settings live was exercised through a temporary hook in a running game, which is not part of this PR:

  • ui_scale 1.5 and back;
  • fullscreen on a 3840×2160 monitor and back to 1280×720;
  • a maximised window resized to 1024×768;
  • a tick rate of 30, then 60 (measured at 30.00 and 59.94 ticks per second);
  • fifo → mailbox → immediate → fifo.

macOS is not built here.

Things you may want different

  1. Every key is written, defaults included, as before. Writing only what differs from the default would let a later change of default reach players (it's how tick_rate 50 stuck), but that is a separate policy.
  2. The render scale is still fixed at start-up: neither this nor a manual resize recalculates it. That belongs with the renderer.
  3. git clang-format wants <nlohmann/json.hpp> ahead of the standard headers in config.cpp. That is the existing order on port; this only adds <utility> next to them, so I left it.

ConfigChange(config) is the one entry point: it takes the settings as
config.json would read them back, makes them current, saves the file and
hands the old and new settings to the change hooks, which apply what
changed. main.cpp's hook applies the host's settings live: master volume,
the input section, the tick rate, interpolation, max_fps and show_fps, the
present mode (gfx::SetPresentMode recreates the swapchain), the window's
size and fullscreen state (WindowSetMode, through the same pixel-size
event as a resize by hand), and aspect and ui_scale (gfx::SetFrameLayout
at the next pump outside a frame). Only game.debug_mode waits for the next
start, which ConfigAppliesOnRestart tells a settings screen.

ConfigSave writes config.json.tmp and renames it over config.json, so a
crash cannot leave a truncated file. A rewrite drops comments.
A failed save left the new settings current, so the same settings again
returned true without writing. ConfigChange now tracks whether
config.json holds the current settings and retries the save for equal
settings, without running the hooks again.

The hooks are run over a copy of the list, so one may add or remove hooks
(they count from the next change), and a ConfigChange from inside a hook is
refused, so the hooks after it never see settings other than the ones they
were passed.

Leaving fullscreen for a windowed size restores a maximized window first,
which otherwise ignores the size and position, and the window calls report
their failures. The debug chord's permission is latched from the config at
start beside DebugMode, so game.debug_mode is wholly a next-start setting.
On Linux an ifstream opens a directory and reads it as empty, so
ConfigLoad took a directory named config.json for a file holding the
defaults and marked the settings saved. It now reads only a regular file;
anything else falls back to the defaults and a save attempt, as a missing
file does. The change test cleans up its hooks and directory even when an
assertion stops it early.
@Adubbz
Adubbz merged commit a98ee40 into TheMoonPeople:port Oct 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants