Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0ee0d59
Resolve Windows layout shortcuts with cached physical chords
Guffawaffle Sep 10, 2026
e8a4883
Document and test German experimental shortcut overlap
Guffawaffle Sep 10, 2026
c3aa4bc
Check startup config output before replacing files
Guffawaffle Sep 12, 2026
3ba601e
Exercise startup save failures and permission retention
Guffawaffle Sep 12, 2026
a8ed7bc
Capture inherited permissions before the first save
Guffawaffle Sep 12, 2026
4d1a476
Retain missing-file creation coverage alongside ACL fixtures
Guffawaffle Sep 12, 2026
eb56fa0
Run config-save fixtures on native Windows and macOS CI
Guffawaffle Sep 12, 2026
227fb35
Contain keyboard diagnostic save failures at the input boundary
Guffawaffle Sep 13, 2026
24b9a53
Retry CI after runner acquisition failure
Guffawaffle Sep 13, 2026
1265f91
Merge branch 'dev' into feature/layout-aware-letter-shortcuts
Guffawaffle Sep 18, 2026
48b48e5
Merge branch 'dev' into feature/config-save-rewrite
Guffawaffle Sep 18, 2026
fd15fb5
Add shared IL2CPP runtime boundary helpers with contract tests
Guffawaffle Sep 22, 2026
733997c
Narrow shared invocation helpers around existing loading screens
Guffawaffle Sep 22, 2026
d6c10b7
Guard existing IL2CPP class lookup when metadata is unavailable
Guffawaffle Sep 22, 2026
c09cb04
Add reusable native settings framework with Fleet Commander confirmat…
Guffawaffle Sep 27, 2026
f15d1c7
Add reusable native Mod Settings navigation groundwork (without held …
Guffawaffle Sep 27, 2026
fb90890
Expand native Mod Settings with live controls and save feedback (with…
Guffawaffle Sep 27, 2026
0d08142
Add native shortcut editor with popup capture and immediate saves (wi…
Guffawaffle Sep 27, 2026
6373d77
Preserve player-authored TOML for runtime setting saves (without held…
Guffawaffle Sep 27, 2026
fe4d792
Align native settings foundation PR with upstream dev
Guffawaffle Sep 28, 2026
32cc0e2
Align settings navigation with updated foundation
Guffawaffle Sep 28, 2026
f306f5e
Align Mod Settings controls with updated navigation
Guffawaffle Sep 28, 2026
7bfcdb5
Align layout-aware shortcuts PR with upstream dev
Guffawaffle Sep 28, 2026
bb7abdb
Align shortcut editor with updated Mod Settings controls
Guffawaffle Sep 28, 2026
cb3a4d6
Align shortcut editor with updated layout shortcuts
Guffawaffle Sep 28, 2026
d024f4c
Align native settings hook installation with upstream patch switches
Guffawaffle Oct 2, 2026
5dda512
Refresh checked startup config saves against current upstream
Guffawaffle Oct 2, 2026
25d78b5
Protect staged configuration contents and follow dangling links
Guffawaffle Oct 2, 2026
834f7d1
Merge branch 'feature/config-save-rewrite' into feature/preserving-to…
Guffawaffle Oct 2, 2026
d9fcec1
Give runtime persistence its own patch registry switch
Guffawaffle Oct 2, 2026
2395ba5
Reject settings path retargeting during staged edits
Guffawaffle Oct 2, 2026
2d9039b
Merge branch 'fix/fc-ability-confirmation' into feature/mod-settings-…
Guffawaffle Oct 2, 2026
c0119a0
Merge branch 'feature/preserving-toml-editor' into feature/mod-settin…
Guffawaffle Oct 2, 2026
7f9ea43
Align settings groundwork documentation with current adapters
Guffawaffle Oct 2, 2026
fbb95bf
Merge branch 'feature/mod-settings-navigation' into feature/mod-setti…
Guffawaffle Oct 2, 2026
c8104d6
Document independent installation controls for native settings
Guffawaffle Oct 2, 2026
fe0e83d
Clarify shared native settings platform implementation
Guffawaffle Oct 2, 2026
8fde63f
Align navigation notes with registered task pages
Guffawaffle Oct 2, 2026
9488b8e
Merge branch 'feature/mod-settings-controls' into feature/mod-shortcu…
Guffawaffle Oct 2, 2026
e69206b
Align shortcut docs with current popup and publication behavior
Guffawaffle Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ jobs:
- name: Build
run: xmake build -y stfc-community-mod

- name: Test confirmation setting contracts
shell: pwsh
run: ./tests/run-confirmation-settings.ps1

- name: Test x64 trampoline relocation
shell: pwsh
run: |
Expand All @@ -218,6 +222,17 @@ jobs:
shell: pwsh
run: sccache --show-stats

- name: Test startup config saves
shell: pwsh
env:
PACKAGE_DIR: ${{ steps.xmake_cache_paths.outputs.package_dir }}
run: |
$header = Get-ChildItem -LiteralPath (Join-Path $env:PACKAGE_DIR 't/toml++') -Recurse -Filter toml.h |
Where-Object { $_.Directory.Name -eq 'toml++' } | Select-Object -First 1
if (-not $header) { throw 'Built toml++ package not found.' }
./tests/run-config-save.ps1 -TomlInclude $header.Directory.Parent.FullName
./tests/run-settings.ps1

- name: Package
shell: pwsh
run: |
Expand Down Expand Up @@ -521,6 +536,20 @@ jobs:
shell: bash
run: sccache --show-stats

- name: Test startup config saves
shell: bash
env:
PACKAGE_DIR: ${{ steps.xmake_cache_paths.outputs.package_dir }}
run: |
set -euo pipefail
TOML_HEADER=$(find "$PACKAGE_DIR/t/toml++" -path '*/include/toml++/toml.h' -print -quit)
test -n "$TOML_HEADER"
bash tests/run-config-save.sh "$(dirname "$(dirname "$TOML_HEADER")")"
bash tests/run-settings.sh
- name: Test confirmation setting contracts
shell: bash
run: bash tests/run-confirmation-settings.sh

- name: Report Swift module cache
shell: bash
run: |
Expand Down
121 changes: 121 additions & 0 deletions docs/MOD_SETTINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Mod settings: current architecture and behavior

This is the current contract for the expanded Windows x64 and macOS settings UI. The
[foundation notes](MOD_SETTINGS_FOUNDATION.md) describe the first FC-only slice;
their prototype counts and proposed budgets are historical, not current limits.

## Ownership

Feature adapters own live values, availability and persistence. A
`ValueSetting<T>` owns snapshot validation, guarded application and readback.
`PageCatalog` owns placement and presentation callbacks, without Unity objects
or a second copy of configuration. Registration freezes before native creation.
Each game settings context gets a fresh tree from that immutable plan.

The [native adapter map](MOD_SETTINGS_NATIVE_ADAPTER.md) identifies each hook
owner. Interop, value widgets, action widgets, navigation and styling are separate
concerns. Existing XMake source discovery builds them. Each detour has one owner.
The `ModConfirmationSettings` registry entry uses default-enabled
`[patches].nativesettingshooks` in every build; its C++ flag is
`installNativeSettings`. Persistence is independently controlled by
`[patches].runtimeconfighooks`. Forbidden Tech and fleet-label installation use
their owning patch switches, independent of native UI and feature values.

## Placement and summaries

Player tasks determine labels and navigation; TOML sections remain the storage
reference. Moving a page does not rename stored keys or change defaults.

| Mod Settings page | Contents | Storage reference |
| --- | --- | --- |
| Camera | Keyboard zoom speed, pan glide | `[graphics]` |
| Fleet Labels | Collapsible Player and Non-player profiles | `[graphics]` |
| Map & Travel | Instant warp mode, shared with its shortcut | `[ui]` |
| Previews & Cargo | Preview shortcuts and automatic cargo previews | `[ui]` |
| Shortcuts | Binding editors grouped by action, with current-binding summaries | `[shortcuts]` |

Empty groups are omitted. Camera and preview controls require their existing
consumer hooks to have installed successfully. FC and Forbidden Tech remain in
the game's native confirmation page. Fleet headings start collapsed and summarize
their current mode, with a two-decimal threshold where relevant. The warp page
row shows its mode. Summaries refresh on binding and existing setting notifications.
Cargo target rows appear only while Automatically open cargo is ON; hiding them
preserves each target preference.

## Honest state and persistence

Readback proves the live value, not file or cloud durability. Unknown state never
looks OFF. Failed application preserves authoritative readback and never issues
an automatic reverse write. Native indicators are suppressed when unknown.

A finite loaded value outside a slider's UI range shows `Out of range; edit TOML`.
A non-finite value shows `Invalid value; edit TOML`. Neither is clamped, saved or
fixed by reopening. TOML is loaded at startup: a manual correction takes effect
after restarting. Ordinary unavailable readers retain `Reopen to retry`.
Disabled sliders receive a feature-owned reason; only Fleet Labels says
`Select Threshold`. Keep suffixes short: the native row truncates long labels.
The shared slider widget rounds display values, including during dragging;
speed uses whole numbers and fractional controls use at most two decimals.
Rendering never rounds or saves a loaded preference.

The existing single writer serializes TOML edits and coalesces pending changes
per key. It preserves unrelated source and detects conflicting external edits.
Runtime failure/conflict leaves the live edit active. A failure-only notice at
the top of Mod Settings pages says `Active this session; couldn't save. See mod log.`
It concerns mod TOML saves, not the native FC cloud preference. Detailed key and
failure information stays in the log. No success notices or modal dialogs appear.

Failure state belongs to the writer's per-key records. Saving one key cannot
hide another key's failure. A later successful save of the failed key clears it.
Rejected submissions outside the writer leave a conservative session warning,
because they have no tracked completion. The existing runtime callback observes
aggregate status without taking the writer lock; native UI work happens only
when it changes, on the game thread. Opening settings never retries or writes.
F10's 500 ms best effort force close and the ordinary quit/drain path are
unchanged. See [persistence contracts](config-save.md).

## Editor lifetime and native views

Change and Add stay drafts until popup Confirm or Use anyway publishes the complete
action list. Remove, Restore and Undo publish immediately; there is no page-level
Apply step. Restore uses canonical defaults; `NONE` means unbound. Scrollable overlap
warnings are advisory and never remove another action's binding.
Uncategorized gives newly registered actions an editor before presentation
metadata is supplied; it does not discover arbitrary TOML values. See
[shortcut contracts and extension guidance](MOD_SHORTCUT_SETTINGS.md).

The page owns cancellation through `Page::leave`. Navigation, controller
destruction and session invalidation end the visit. Recycling a row, conditional
filtering and section folding preserve the draft. Escape and focus loss cancel
recording; input stays owned until a focused sample sees all keys released.
The existing ScreenManager dispatcher samples keys only during capture.

Widgets keep weak ownership records and restore text, tint, sprites, button
visibility and interactability before reuse. Callback identity and the currently
bound context gate commands. Value rows defer list rebinding until their
request/readback scope finishes. Headings and action rows do not persist
presentation state. The save notice uses the native button-row adapter with its
button hidden and invocation disabled; visibility is checked on refresh.
An unavailable action-widget family leaves controls usable and save details in
the log. Failure notices never create otherwise-empty groups.

There is no new polling hook, save worker or global localization hook.
Platform guards, managed signature checks and hook ownership checks remain part
of installation. Windows and macOS use the same settings adapter, while
layout-aware capture is Windows-only and macOS uses physical keys.
Platform builds alone do not establish runtime compatibility.

## Validation and follow-up

Run `tests/run-settings.ps1`, `tests/run-config-save.ps1` and the Windows build.
Fixtures cover guarded values, range preservation, conditional sections, command
identities, writer failures and shutdown. Native checks separately cover Back,
folding, conditional rows, notice layout and pooled stock-row restoration.

Run the keyboard layout, chord and dispatch XMake fixtures for input changes.
Native editor checks include scrolling/folding, Back, held-key focus changes,
overlap inspection, live dispatch and restart persistence on an identified build.

Numeric input boxes and a real client restart command
remain later work. Neither generic TOML editing nor exposing every config key is
implied by this catalog.
168 changes: 168 additions & 0 deletions docs/MOD_SETTINGS_CONTROLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Mod settings controls

Build real controls on the navigation foundation in small slices. Register only
working controls; omit empty groups. Stable setting keys and storage owners stay
independent of labels and placement.

See [the current architecture contract](MOD_SETTINGS.md) and
[native adapter ownership](MOD_SETTINGS_NATIVE_ADAPTER.md).

## Layout

| Location | Control | Existing owner |
| --- | --- | --- |
| Mod Settings > Map & Travel | Instant warp mode: Normal (ask), Warp, Jump | `ui.auto_confirm_instant_warp` and the Alt+I action |
| Mod Settings > Fleet Labels | Player label detail and zoom threshold | `graphics.zoom_label_player_detail`, `graphics.zoom_label_player_threshold` |
| Mod Settings > Fleet Labels | Non-player label detail and zoom threshold | `graphics.zoom_label_non_player_detail`, `graphics.zoom_label_non_player_threshold` |
| Mod Settings > Camera | Keyboard zoom speed and pan glide | `graphics.keyboard_zoom_speed`, `graphics.system_pan_momentum_falloff` |
| Mod Settings > Previews & Cargo | Locate/Recall while previewing; automatic cargo and target types | Existing preview/cargo keys in `[ui]` |
| Mod Settings > Shortcuts | Rebind registered actions | Existing shortcut parser and `MapKey` registrations |
| General > confirmation page | Confirm Forbidden Tech upgrades | Inverse of `ui.auto_confirm_ft_upgrade` |

The controls branch implements shared Windows x64 and macOS adapters; platform
qualification is recorded separately from implementation.
The shortcut editor shares that foundation and its existing dispatcher. Native
confirmation controls stay on the native page. FC retains its existing owner.

## Instant warp mode

Use one selection, not three independently stored flags. The UI and Alt+I call
the same live mutation function. Cycle order remains Normal > Warp > Jump > Normal.
Selecting the current value does not enqueue another save. Invalid choices do
not alter live state or the file. Existing per-ship overrides retain precedence;
the picker changes only the global fallback mode.

Reuse the existing single runtime writer, optimistic conflict handling and
source-preserving TOML edits. UI readback confirms the live value, not durable
storage; asynchronous failures produce a quiet session-only notice, with details
in the log. Reopening must
read the current owner, and shortcut changes must refresh a visible selector.
Native selection callbacks need the same rendering, stale-context and reentry
protection already exercised for boolean controls.

Selected options use bold text and the native checkmark on a normal background,
including instant warp and both Fleet Labels profiles. White fill is transient
pressed feedback, not persistent selection or keyboard focus. The scoped adapter
uses native sprites already rendered by settings rows and restores each Image's
previous override before pooling. The shared `Selectable.DoStateTransition`
hook observes input-state changes, calls the original once, then updates only
owned selection rows. Other controls take the native path; there is no frame
polling, animation replacement, asset loading or setting write in this hook.

## Fleet Labels and Forbidden Tech

One Fleet Labels page contains a collapsible Player heading, its Native /
Expanded / Compact / Threshold choices and percentage slider, followed by the
same controls under a Non-player heading. Each profile has its own owner and
selection. Click either heading to hide/show its controls independently, without
navigating away. Both sections start collapsed on each page visit; expansion is
temporary presentation state and never writes TOML or changes a setting value.
The native category arrow points down when expanded and right when collapsed.
Headings use larger bold cyan text and a darkened row background. An enabled
threshold slider uses a subtle cyan accent to connect it to the selected mode,
without a white selection fill. Tints affect the row's direct `BG` Image child
(`Background` for category headings),
when present; other prefab layouts retain the text styling. Native colors and
text are restored before refresh and pooling. Styling uses the existing bind,
refresh and release hooks, with no frame polling or shared-material changes.
Threshold is stored in [0, 1], edited in 1% steps,
and enabled only in Threshold mode. At 0% labels stay compact; at 100% they stay
expanded. Reading a player-authored fractional value does not round or save it.
Each user edit updates the existing live profile and refreshes tracked labels.
The native slider callbacks use the same typed snapshot/reentry guards as choices.
Unknown values suppress the slider and numeric label; disabled known values remain
visible. Releasing a pooled widget restores its label, active state and interaction.

The owning Zoom and Forbidden Tech patch switches control installation independently
of native settings and feature values. Supported callbacks install once; current
label profiles and the FT bypass flag are checked inside them, so live changes do
not require a restart. Windows x64 and macOS use shared adapters; native availability
requires validated metadata and completed hook families. Mac qualification remains
separate from the Windows evidence below.
Confirmation ON means the bypass flag is false. Toggling must never invoke an
upgrade callback by itself.

The existing TOML writer now registers these additional keys at startup. One
worker serializes changes to the same file, keeping the latest pending intent
**per key**. A 150 ms quiet period coalesces slider motion; normal quit flushes the
pending value without waiting out that delay. F10 retains its existing force-close
cancellation and 500 ms best effort bound. Save failures/conflicts log the affected
section and key and leave the live setting in place. Numeric edits use the TOML
serializer and the same source-preserving edit/reparse/external-edit checks.
Page opens, section folding and native rendering never enqueue saves.

Collapsible headings reuse the existing category bind/release and page-selection
hooks. A heading click gives the native option panel a filtered `OptionContext[]`
through the existing `BindDataContext(provider, object)` virtual slot. The original page's
children and navigation parent stay intact, including controls omitted from the
visible list. Native rebinding releases hidden widgets and refreshes expanded
ones through the same guarded readers as a normal page visit. Plain headings
remain non-interactive. No new detour, frame polling or persistence owner is added.

On entry, native navigation establishes the selected page and Back target first;
the same callback then applies the initial collapsed list before returning. If
that presentation bind fails, the adapter attempts to restore the expanded list
so controls remain accessible. Expanding either section reads its current values.

Exact Windows build261 unwind extents, checked before expanding installation:

| Native target | RVA | Bytes |
| --- | --- | --- |
| SliderOptionWidget.SetWidgetData | D09C50 | 592 |
| SliderOptionWidget.OnSliderValueChanged | D0A1E0 | 117 |
| SliderOptionWidget.OnAboutToReleaseContext | D09EA0 | 288 |
| NavigationLOD.UpdateLOD | F8ECF0 | 75 |
| NavigationFleetWidget.OnDidBindContext | F7C870 | 335 |
| NavigationFleetWidget.OnAboutToReleaseContext | F7CEA0 | 283 |
| NavigationFleetWidget.OnEnable | F7D8B0 | 344 |
| NavigationFleetWidget.OnDisable | F7DAF0 | 236 |
| MessageBox.Show(context) | 70B5F0 | 81 |
| MessageBox.Show(context, callback) | 70B650 | 257 |
| TextOptionWidget.SetWidgetData | D0A470 | 293 |
| TextOptionWidget.ClearWidgetData | D0A680 | 271 |
| Selectable.DoStateTransition | 47A9650 | 805 |

These historical measurements exceed the bundled x64 SPUD 24-byte overwrite.
Installation resolves current targets through managed metadata. Measured client SHA256:
`487af4bb9c697c353be9714359a97dddcece5dab872622a6c498a27bbfc44f40`.
This is Windows evidence, not proof of macOS hook fit or native widget behavior.

## Camera and previews

Keyboard zoom speed offers 0–1000 in steps of 25 with whole-number labels. Pan
glide offers 0–0.99 in steps of 0.01; it retains the existing pan formula. These
are UI editing ranges, not new TOML constraints. Out-of-range loaded values are
preserved and explained rather than clamped. The shared slider path controls
display precision without writing a loaded value.

Preview toggles and their existing hotkeys call the same owner, so live state,
readback and saving agree. Locate/Recall labels invert their stored disable
flags. Cargo targets are visible only while auto-open is ON, with their saved
preferences retained while hidden. Hook installation success gates each group.

## Future organization and commands (design notes)

Use player tasks for navigation and TOML sections as storage references.
Introduce a group only when it gains a working control and explicit apply path.
Changing placement must not change storage identity. Confirmations continue on
the native confirmation page; arbitrary TOML keys are not discovered as controls.

A future **Restart client** command could support controls that explicitly need
restart. It would perform an ordinary client restart, settle pending saves using
the existing lifecycle, and relaunch through a supported lifecycle owner. Cache
clearing is a separate operation and must not be called by this command. This is
an idea only: the current branch adds neither restart-only controls nor a restart
command. The ownership/relaunch details need their own design before implementation.

The delivered shortcut editor reuses the parser and binding map, with Escape to
cancel capture, overlap feedback and explicit Remove. Capture owns keyboard input
through release; display labels are never serialized as key identities. See
[shortcut contracts](MOD_SHORTCUT_SETTINGS.md) for current publication and undo
behavior before extending the editor.

## Runtime gate

Before promoting the Navigation slice, verify all three choices, Alt+I changes
while visible, Back/reopen, restart persistence, and an external TOML edit conflict.
Bind build receipts to the installed artifact. The existing synthetic navigation
probe is not evidence that a new selection widget or real persistence path works.
Loading
Loading