Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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,10 +206,25 @@ jobs:
- name: Build
run: xmake build -y stfc-community-mod

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

- name: Report compiler cache
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 @@ -506,6 +521,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
98 changes: 98 additions & 0 deletions docs/MOD_SETTINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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 historical `ModConfirmationSettings` debug patch key remains compatible;
its C++ name is `installNativeSettings`.

## 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]` |

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).

## Native views

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, timer or global localization hook.
Platform guards, managed signature checks and hook ownership checks remain part
of installation on Windows and macOS.
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.

Shortcut editing and automatic action discovery follow in a separate PR.
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.
164 changes: 164 additions & 0 deletions docs/MOD_SETTINGS_CONTROLS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# 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]` |
| Future separate branch: Hotkeys | Rebind existing 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 these controls on Windows x64.
Hotkey editing remains a separate branch. 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. A Windows-only `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.

Windows installs the existing fleet-label and Forbidden Tech hooks when the mod
settings UI is enabled, so changing their values does not require a restart.
Each FT hook consults the current bypass flag; hook availability is separate from
the value. Other platforms retain startup-controlled installation and omit this UI.
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.

Hotkey editing follows the first real selection and persistence checks. Reuse the
current parser and binding map; add an explicit capture mode with Escape to cancel,
conflict feedback and a deliberate unbind action. Gameplay shortcuts must not fire
while a chord is being captured. Do not serialize display labels as key identities.

## 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