Combined mechanism-faithfulness x regression/blast-radius pass over
371197a3 (OP9 retirement of the dead F11 settings surface +
GameplaySettings), per the OP5/OP7 single-reviewer precedent for
closeout-shaped slices.
Retirement verified correct: zero production readers of GameplaySettings
or SettingsVM existed pre-commit (checked against the pre-commit tree,
not the diff), SetUiLocked first-call/repeat-call behavior is provably
unchanged by deleting _uiLockConverged, the wire paths
(SetAcceptLootPermits 0x0005, ToggleUiLock, PlayerDescription
convergence) are untouched, the settings.json unknown-key carry-forward
is real, and the AP-196 register edit reconciles (143 -> 142 active,
29 -> 30 retired, total unchanged).
MUST-FIX 1: SaveAudio -> ApplyAudio (OP6 live-apply, live consumer in
ConfigOptionsPageController) lost its only assertion when
SettingsViewModelSavePreservesSectionAndTargetOrder was deleted.
SHOULD-FIX 2-5: stale architecture-doc seam naming SettingsVM; three
dead residues (uncallable private SaveCharacter, orphaned
IngressShutdownRoots.Settings, writerless CharacterSettings path);
test delta enumerates to -84, not the claimed -80 "exactly"; dangling
comment referencing the deleted assertion. Two nits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 KiB
Campaign OP slice OP9 — combined-lens review (mechanism × blast radius)
Target: 371197a3 — refactor(settings): OP9 — retire the dead F11
settings surface + fully-superseded GameplaySettings
Reviewer: combined mechanism-faithfulness × regression/blast-radius pass
(one reviewer, OP5/OP7 precedent for closeout-shaped slices).
Base: 1a57f96e (campaign tip after the OP8 merge). Worktree clean at
review time; HEAD == the target commit.
VERDICT: APPROVE-WITH-FIXES
The retirement itself is correct. Every claim in the commit message's
per-file disposition list that bears on live behavior verifies against
the post-commit source: no production code path read GameplaySettings, no
production code path constructed SettingsVM, SetUiLocked's observable
contract is unchanged, the wire behavior of SetAcceptLootPermits /
ToggleUiLock / the PlayerDescription convergence is untouched, and the
settings.json unknown-key carry-forward mechanism is real. The register
edit is arithmetically and semantically correct.
Five findings, none of them a behavior regression: one lost regression guard on a live mechanism (MUST-FIX 1), one stale architecture doc seam (SF-2), three dead residues the retirement created and left (SF-3), a test-delta claim that does not reconcile (SF-4), and a dangling comment (SF-5). Plus two nits.
MUST-FIX 1 — SaveAudio → ApplyAudio (OP6's live-apply) lost its only test
RuntimeSettingsController.SaveAudio pushes the saved snapshot into the
live engine — src/AcDream.App/Settings/RuntimeSettingsController.cs:420
(_runtimeTargets?.ApplyAudio(audio);), documented at :407-413 as the
Campaign OP slice OP6 change that made Audio-tab edits take effect
without a relaunch. Its live production consumer is
ConfigOptionsPageController's Sound rows (ConfigOptionsPageController.cs:103).
Pre-commit, exactly one test asserted that push: the now-deleted
SettingsViewModelSavePreservesSectionAndTargetOrder, whose ordered event
list contained "target-audio" with an explicit OP6 comment
(371197a3~1:tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs:296-312).
Post-commit, grep -rn ApplyAudio src tests shows no assertion anywhere
that SaveAudio invokes IRuntimeSettingsTargets.ApplyAudio. The fake
still records the event —
tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs:817-820
(events.Add("target-audio");) — and nothing ever reads it. The two
surviving audio tests (ComputeEffectiveCategoryVolumes_*, :241-278)
test the static volume helper, not the controller push. ApplyStartup's
own audio call (RuntimeSettingsController.cs:226) is still covered by
StartupApplyIsOrderedExactlyOnceAndRuntimeBindingDoesNotReplay (:54),
so this gap is specific to the save path.
This is the class the slice contract cannot absorb: the commit asserts "no behavior change to anything live, verified", and one live mechanism is now unverified. Note the contrast — the other three target pushes were kept covered, deliberately or not:
| Push | Post-commit coverage |
|---|---|
SaveDisplay → ApplyDisplayWindowState |
implicit but real — DisplayTargetFailurePreservesEstablishedStoreThenPublishBoundary (:566) sets ThrowOnDisplay and asserts the resulting state boundary, which only happens if the target was called |
ReapplyQualityPreset → ApplyQuality |
positive (:633, :653) |
SetUiLocked → ApplyUiLock |
positive (:672-679, :694, :702) |
SaveChat → SetChatOpacity |
positive — SaveChat_PushesOpacityToRuntimeTargets_LiveApply_NoRestart (:338) |
SaveAudio → ApplyAudio |
none |
Fix (minimal): in RuntimeSettingsControllerTests, add the missing
positive assertion in the same shape as the chat-opacity test — bind a
FakeRuntimeTargets, call controller.SaveAudio(controller.Audio with { Master = 0.25f }), assert the recorded events contain "target-audio"
(ideally assert the value forwarded, per the :236-240 comment's own
lesson about M2's inversion).
SHOULD-FIX 2 — the architecture doc still names SettingsVM as a live seam
docs/architecture/code-structure.md:209-212:
WorldRenderFrameBuilder -> RuntimeWorldFrameSettingsPreview -> IRuntimeSettingsPreviewSource -> RuntimeSettingsController -> optional SettingsVMfor the live settings draft preview applied before world drawing.
This sits inside a present-tense list of current presentation seams
("These are presentation state/command seams…", :217), not a historical
narrative. It is the one surviving reference that would send a future
reader looking for a type that no longer exists, and CLAUDE.md's
architecture rule ("when the architecture doc and reality diverge, update
one or the other") makes it in-scope for the same commit.
Secondary, lower severity, same file: :738-739 — "The controller also
preserves unrelated unsaved Gameplay draft fields when combat preferences
change" is now false. It is inside the Slice-8 checkpoint-G/H ledger
narrative, so it reads as dated history; a one-clause "(retired at
Campaign OP slice OP9)" is enough.
Everything else the repo-wide sweep turned up is genuinely historical and
correctly left alone: docs/plans/2026-07-22-gamewindow-slice-8-*.md,
memory/project_gamewindow_decomposition.md:81/85/181,
docs/research/2026-08-1*-op{3,4,6,8}-*.md,
docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md,
docs/research/2026-08-10-character-options-map.md,
docs/ISSUES.md:15859, and the docs/research/2026-05-21-a6-captures/*.log
run logs.
SHOULD-FIX 3 — three dead residues left behind by the retirement
The slice deleted the callers without sweeping what became unreachable:
RuntimeSettingsController.SaveCharacter(CharacterSettings)—src/AcDream.App/Settings/RuntimeSettingsController.cs:547-566. Private, zero callers post-commit (its only caller was thesaveCharactercallback handed to the deletedCreateViewModel). C# emits no warning for an unused private method, so the build stays green while the method, its_defaultCharacterpromotion branch, and the whole write half of theCharacterSettingsstore are unreachable:IRuntimeSettingsStorage.SaveCharacter(:31),JsonRuntimeSettingsStorage.SaveCharacter(:68), andSettingsStore.SaveCharacternow have no production writer. (ActiveToonKeyis genuinely live —InteractionRetainedUiComposition.cs:507and:612key per-character window layouts off it — so only theCharacter/SaveCharacterhalf is dead.RuntimeSettingsController.Characterlikewise has no production reader.) The commit also deleted that method's only test (CharacterSaveUsesActiveToonAndDefaultSaveBecomesResetContext) without re-targeting it — consistent with the code being dead, but it means the code is now both uncallable and untested rather than removed.IngressShutdownRoots.Settings—src/AcDream.App/Rendering/GameWindowLifetime.cs:75, captured atsrc/AcDream.App/Rendering/GameWindow.cs:1670. Its only reader was theSoft("settings view model", …UnbindViewModel())stage this commit deleted;grepfinds no otheringress.Settingsuse. Harmless (a reference held until shutdown) but it is now a shutdown-roots member that roots nothing.
Either delete these in the OP9 follow-up, or — if CharacterSettings
persistence is meant to come back on a later Options-panel tab — say so in
a one-line comment so the next reader does not re-derive that it is dead.
The commit message's "CharacterSettings … UNTOUCHED per the campaign
contract" covers keeping the record and store; it does not explain the
now-uncallable controller method.
SHOULD-FIX 4 — the −80 test delta does not reconcile; static enumeration gives −84
The commit states the delta "is exactly the deleted SettingsVM/ SettingsPanel/GameplaySettings test surface". Enumerating the touched files at both commits:
| File | Removed | Added |
|---|---|---|
tests/.../Panels/Settings/GameplaySettingsTests.cs (deleted) |
3 | — |
tests/.../Panels/Settings/SettingsVMTests.cs (deleted) |
43 | — |
tests/.../Panels/Settings/SettingsPanelTests.cs (deleted) |
29 | — |
tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs |
13 | 6 |
tests/.../Panels/Settings/SettingsStoreTests.cs |
4 | 2 |
tests/AcDream.App.Tests/Input/GameplayInputCommandControllerTests.cs |
2 (InlineData) | 2 (InlineData) |
tests/.../Rendering/GameWindowSlice8BoundaryTests.cs, tests/.../Input/InputDispatcherCaptureTests.cs |
0 | 0 |
| Net | 94 | 10 |
−84, not −80. All three deleted files declare public sealed class
(discoverable), none carries [Theory]/[MemberData]/Skip, and the
skip count is 4 on both sides — so no discovery artifact explains the gap.
The baseline is quoted twice in the immediately preceding commits
(f1d50207, 1a57f96e: 13,155 passed / 4 skipped), so either that
baseline is off by 4 or the post number is. Nothing in the diff adds four
tests elsewhere.
This is an evidence-accounting defect, not a behavior defect — no live behavior test is unaccounted for by it (the one real coverage loss is MUST-FIX 1, which is inside the 13-removed RuntimeSettingsControllerTests bucket). Re-measure and restate, or drop the word "exactly".
For the record, the 13 removed RuntimeSettingsControllerTests cases
disposition cleanly: 6 were re-targeted onto the surviving API
(CharacterContext*, DraftPreview*, RuntimeTargetLoans*,
SetUiLocked_*, SyncChatFromServerOptions*, UnboundRuntimeTargets*),
5 were SettingsVM-draft-only, 1 (UiLockPersistenceFailureCanRetry…)
pinned a retry against a save that no longer exists — and its
target-failure sibling UiLockTargetFailureCanRetryTheSameRequestedValue
(:683) survives and still pins the real invariant — 1 covered the now-dead
SaveCharacter (SF-3), and 1 (SettingsViewModelSavePreservesSectionAndTargetOrder)
carried the "target-audio" assertion (MUST-FIX 1).
SHOULD-FIX 5 — dangling comment referencing a deleted assertion
tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs:236-240:
"The FakeRuntimeTargets-based
"target-audio"assertion above (and its predecessor before this rework) only ever checked that ApplyAudio fired…"
There is no longer any "target-audio" assertion above (or anywhere); what
is now immediately above is
ConcreteRuntimeTargetPublishesSetSingleCharacterOptionOntoTheBus. Reword
when fixing MUST-FIX 1 — the two are the same edit.
NIT 6 — "bound through … CharacterOptionsPageController" is imprecise for 2 of the 13
Commit §4 and the AP-196 retirement note both say all 13 members "were
ALREADY bound through CharacterOptionTable/CharacterOptionsPageController's
server-bit seam (the OP4 Character tab)". All 13 are in the table —
verified one by one at src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs
:117,119,121,123,125,127,130,131,132,158,160,161,162 — but two are
deliberately not Character-tab rows (CharacterOptionsPageControllerTests.cs:26
names the exclusion set: AppearOffline, UseMouseTurning, LockUI, per
research §2.7):
LockUI— bound through/lockui+ the radar control:LiveSessionRuntimeFactory.cs:419-421(server-bit read,0x0005send,SetUiLockedvisual push),InteractionRetainedUiComposition.cs:492(mount-time seed),:647(radar binding).UseMouseTurning— bound through the Gameplay-tab mouse macro:InteractionRetainedUiComposition.cs:843-845(IsUseMouseTurningEnabledlive server-bit read) andRetailUiRuntime.cs:2009-2019(wire send). Note this is the server bit, correctly distinct from the Config tab's own client-localCameraTurningSettings.UseMouseTurning(TS-74,ConfigOptionsPageController.cs:605-612).
The substance holds — all 13 have a live server-bit home and none was orphaned — only the sentence's channel attribution is loose. Worth correcting in the register row, which is the durable artifact.
NIT 7 — OP9's own bookkeeping is still outstanding
docs/plans/2026-08-10-options-panel-campaign.md:428 still reads
| OP9 | — | | | |, and :9 still says "ACTIVE — planned 2026-08-10. No
slices landed yet" (stale for the whole campaign, pre-existing). §OP9's
bullet list (:352-370) also owes the ISSUES sweep (#358 retest), the
CLAUDE.md Current-state paragraph, and the memory digest update. None of
that is a defect in this commit — it is the closeout half of the slice —
but it should not be forgotten when OP9 is declared complete.
Verified clean (mechanism lens)
-
RuntimeSettingsControllerlive core — all present and unaltered: storage load/save for Display/Audio/Chat/Character (:170-181),ResolvedQuality/ReapplyQualityPreset(:369-383),ApplyStartup(:213-235),SaveAudio's OP6 live-apply (:414-427), publicSaveDisplay(:391-405, still theConfigOptionsPageControllerseam),IRuntimeSettingsTargets.SetSingleCharacterOption(:115) andSetChatOpacity(:124),ServerOptionsSeeded/NotifyServerOptionsSeeded(:542-545),SetUiLocked(:292-299). The only removals areGameplay,SetAcceptLootPermits, the privateSaveGameplay, and the four_viewModel?notification calls.SetUiLockedfirst-call / repeat-call equivalence — verified. The old guard wasif (_lastAppliedUiLocked == locked && _uiLockConverged) return;with_uiLockConvergedinitialised true and_lastAppliedUiLockedinitialised null. On the first call(bool?)null == lockedisfalse, so the conjunct never mattered: the firstApplyUiLockpush happened then and happens now. Deleting_uiLockConvergedtherefore cannot have changed whether the first push fires. Repeat same-value calls returned early then (after a successfulSaveGameplayset_uiLockConverged = true) and return early now. Failure ordering is also preserved:_lastAppliedUiLockedis still assigned afterApplyUiLock, so a throwing target leaves the guard un-advanced and the same value can be retried — pinned byUiLockTargetFailureCanRetryTheSameRequestedValue(:683-704). The one behavioral difference is that a storage failure no longer forces a redundant re-push on the next same-value call; there is no storage call left to fail, andApplyUiLockis idempotent. All three call sites traced and unchanged: PlayerDescription convergence (LiveSessionRuntimeFactory.cs:339-340),ToggleUiLock(:419-421), radar/Character-tab path (InteractionRetainedUiComposition.cs:647). -
IRuntimeSettingsPreviewSource— the "already always false in production" claim is true. Searching the pre-commit tree (git grep … 371197a3~1) forCreateViewModel/CreateViewModelBinding/new SettingsVM/new SettingsPanelreturns only test files plus the controller's own definitions._viewModelwas therefore alwaysnullin production, soHasDraftPreviewwas alwaysfalseandDisplayPreview/AudioPreviewalways fell through toDisplay/Audio— exactly what they now return unconditionally. Both consumers see identical values:WorldRenderFrameBuilder.cs:370-380(the whole draft block was already dead in production) andLiveObjectFrameController.cs:59-62(SettingsParticleRangeSource, which readsDisplayPreviewunconditionally — unchanged). Third consumerInteractionRetainedUiComposition.cs:685(DisplayPreview.ShowFps) likewise unchanged. -
GameplayInputCommandController— diff confirms exactly three cases touched.AcdreamToggleDebugPanel(:190-197) andToggleChatEntry(:208-213) keepreturn true, matching the prior no-op's handled contract;ToggleOptionsPanel(:214-224) still calls_retained.ToggleOptionsPanel()with only the comment reworded. No otherInputActioncase changed, and the removed constructor parameter is the only signature change (SessionPlayerComposition.cs:1194updated). Worth recording: the return value is inert in production —GameplayInputActionRouter.HandleCommand(:170-171) discards it and the command controller is the last stage of the priority graph — so thetrue-vs-falsequestion could not have been observable either way. The newRetiredDevToolsCommand_IsConsumedWithoutClaimingATypedOwnertheory (GameplayInputCommandControllerTests.cs:43-56) re-targets the two deleted mapping rows rather than dropping them. NoToggleSettingsPanelInputActionexists (grepoverAcDream.UI.Abstractionsis empty), soKeyBindings.RetailDefaults()and the keymap fixtures needed no edit — claim confirmed. -
LiveSessionRuntimeFactory—SetAcceptLootPermitsstill sends0x0005viaSendSingleCharacterOption(:494-496); only the_interaction.Settings.SetAcceptLootPermits(value)write-behind line was removed, and it had no reader.AcceptLootPermitsstill reads the server bit (:491-493).ToggleUiLock(:415-422) and the PlayerDescriptionOnCharacterOptionsChangedconvergence (:337-345) are byte-identical apart from comments. -
settings.json migration — mechanism verified at
SettingsStore.SaveSection(src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs:643-687): every top-level key other than the target section and"version"is carried forward byprop.Value.GetRawText(), so an orphaned"gameplay"block survives every subsequent save, and noLoad*method reads it. The two new tests (SettingsStoreTests.cs:219-260) pin exactly that, not something weaker: the first proves a leftover block does not disturb Display/Audio/Chat loads; the second writes an unrelated section and asserts the raw"gameplay"/"lockUI": true/"showHelm": falsetext is still in the file afterwards. The re-targeted coexistence and window-position/layout tests correctly substitute a Display assertion for the deleted Gameplay one, preserving each test's actual point.
Verified clean (blast lens)
- Forward references. Repo-wide sweep (
src,tests,tools,scripts,docs,.claude) forSettingsVM,SettingsPanel,RuntimeSettingsViewModelBinding,CreateViewModel,IDevToolsGameplayCommands,DevToolsGameplayCommands,GameplaySettings,LoadGameplay,SaveGameplay,SetAcceptLootPermits(the controller method),ToggleSettingsPanel,UnbindViewModel: the only code hits are OP9's own explanatory comments, plusICombatGameplaySettingsSourcesubstring matches (RuntimeCharacterState.cs:850) and an unrelatedDebugVM CreateViewModel()local helper intests/AcDream.App.Tests/Combat/CombatFeedbackSlotTests.cs:29. One vacuous-but-harmless residue:GameWindowSlice8BoundaryTests.cs:319still assertsDoesNotContain(".LoadGameplay()")(and:328/:330_persistedGameplay/_settingsVm) — negative source-text guards that can no longer fail wrongly; leaving them is fine. - No string/reflection lookup of the deleted panel:
acdream.settings(the oldSettingsPanel.Id) appears nowhere in the repo. IPanel/IPanelRenderersurvive with real implementers —ChatPanel.cs:34,DebugPanel.cs:22,VitalsPanel.cs:16. Claim confirmed.- Headless / UI Studio / plugin surfaces (the three the implementer's
15-file list did not cover):
src/AcDream.Headless,src/AcDream.Plugin.Abstractions,src/AcDream.Runtime,src/AcDream.Core*,tools/,scripts/,.claude/— zero hits for any deleted type.src/AcDream.UI.ImGui/is an empty leftover directory, not inAcDream.slnx. SettingsDevToolsCompositionsurvives correctly. Despite its name it is a live composition phase (ApplyStartup,SettingsDevToolsComposition.cs:53); only its doc comment needed the update it got. The plan's "retire …SettingsDevToolsCompositionwiring" bullet referred to the ImGui wiring already removed at V11.- Register (§7 of the brief).
AP-196is struck through with a substantive retirement note (retail-divergence-register.md:192), the section-3 header moves 143 → 142 (:178), and the counts verify by enumeration: active| AP-rows 143 → 142, retired| ~~AP-rows 29 → 30, total row count unchanged. No other row cites a deleted type — the only otherGameplaySettingshit isTS-73(:367), a substring match insideICombatGameplaySettingsSource. - Gate script (§9 of the brief).
docs/research/2026-08-11-campaign-op-test-script.mdis clean: every "F11" and "Gameplay tab" reference is to the retail Options panel (post-OP3), not to the deleted ImGui surface. No step exercises anything this commit removed.
Fix list (ordered, for the fixer)
- Add the
SaveAudio→ApplyAudioassertion toRuntimeSettingsControllerTests; reword the:236-240comment in the same edit (MUST-FIX 1 + SF-5). - Update
docs/architecture/code-structure.md:209-212(drop the-> optional SettingsVMlink) and tag:738-739as OP9-retired (SF-2). - Delete
RuntimeSettingsController.SaveCharacter(:547-566) andIngressShutdownRoots.Settings(GameWindowLifetime.cs:75,GameWindow.cs:1670), or document why they stay (SF-3). - Re-measure and restate the test delta, or drop "exactly" (SF-4).
- Correct the LockUI/UseMouseTurning channel wording in the AP-196 retirement note (NIT 6); fill the OP9 plan-ledger row at closeout (NIT 7).