feat(ui): Campaign OP slice OP3 — Options panel shell, open paths, Gameplay tab

Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host
0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import
pattern CharacterController already validates, registered through
RetailPanelUiController so it shares retail's "one active gmPanelUI child"
mutual exclusion with every other sibling panel for free. F11 and the
toolbar's options button (0x1000019B, already authoring panel id 10) both
now open it; the close button fires the same ToggleOptionsPanel action.

OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact
Apply/Reset/Defaults/visibility semantics from
UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately,
Apply commits every row unconditionally + flushes the batched blob, Reset
reverts only Changed rows, Defaults restores without committing, and
tab-switch/window-hide revert uncommitted edits. Wired for all four tabs;
this slice registers real rows on none of them (Gameplay authentically has
none — a pure button list). UiTabPanel gains an ActivePageChanged event so
the page model can hook every tab transition, including the initial
default-tab activation.

The seven Gameplay-tab buttons: Exit Game reuses the existing graceful
window-close path; Exit to Character Selection gets retail's confirmation
dialog and byte-verified mid-air refusal but still behaves as Exit Game
(AD-74 — no pre-world character-select flow exists); Configure Keyboard
and In-Game Help Files are inert this slice (AD-76 for Help — the
plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse
short-circuit to their own byte-verified failure text through the
interface-text seam instead of ShellExecute against a dead URL (AD-75);
Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port,
persisting five new CameraTurningSettings preferences and sending
PlayerOption.UseMouseTurning — TS-74 records that acdream has no
persistent mouse-turning camera mode for the bit to drive yet.

Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline
12,871/4/0 — only new tests added).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 02:14:40 +02:00
parent 5242de9f15
commit 9d26ecc623
27 changed files with 25696 additions and 8 deletions

View file

@ -0,0 +1,52 @@
namespace AcDream.UI.Abstractions.Panels.Settings;
/// <summary>
/// The five CLIENT-LOCAL preferences retail's "Use Mouse Turning Settings"
/// Gameplay-tab button (<c>gmConfigUI::SetMouseTurningDefaults @0x0049E8F0</c>)
/// writes alongside the one server-synced bit
/// (<c>PlayerOption.UseMouseTurning</c>, carried through
/// <c>RuntimeCharacterOptionsState</c> / <c>SetSingleCharacterOption (0x0005)</c>
/// — NOT stored here). Persisted to <c>settings.json</c> the same way
/// <see cref="AudioSettings"/>/<see cref="DisplaySettings"/> are; this is the
/// forward-compatible home Campaign OP slice OP6's full Config tab (27 rows)
/// re-homes into its own preference groups without a storage-shape change —
/// these five keys ARE four of that tab's Camera/Input rows
/// (<c>Camera_Stiffness</c>, <c>Camera_AdjustmentSpeed</c>,
/// <c>Camera_AlignToSlope</c>, <c>Input_MouseLookSensitivity</c>,
/// <c>Input_InvertMouseLookYAxis</c> — research doc
/// <c>2026-08-10-options-panel-structure.md</c> §4).
/// </summary>
public sealed record CameraTurningSettings(
float Stiffness,
float AdjustmentSpeed,
float MouseLookSensitivity,
bool AlignToSlope,
bool InvertMouseLookYAxis)
{
/// <summary>
/// Retail's ORDINARY Config-tab defaults (NOT the mouse-turning macro's
/// targets — <c>gmConfigUI::InitOptions @0x0049E400</c>, research doc §4):
/// Stiffness 0.45 (<c>0x3EE66666</c>), AdjustmentSpeed 40.0
/// (<c>0x42200000</c>), MouseLookSensitivity 0.55 (<c>0x3F0CCCCD</c>),
/// AlignToSlope on, InvertMouseLookYAxis off.
/// </summary>
public static CameraTurningSettings Default { get; } = new(
Stiffness: 0.45f,
AdjustmentSpeed: 40.0f,
MouseLookSensitivity: 0.55f,
AlignToSlope: true,
InvertMouseLookYAxis: false);
/// <summary>
/// The mouse-turning macro's recommended targets
/// (<c>SetMouseTurningDefaults</c>, research doc §4.4): Stiffness 0.95,
/// AdjustmentSpeed 50.0, MouseLookSensitivity 0.7, AlignToSlope OFF,
/// InvertMouseLookYAxis ON.
/// </summary>
public static CameraTurningSettings MouseTurningTarget { get; } = new(
Stiffness: 0.95f,
AdjustmentSpeed: 50.0f,
MouseLookSensitivity: 0.7f,
AlignToSlope: false,
InvertMouseLookYAxis: true);
}

View file

@ -214,6 +214,43 @@ public sealed class SettingsStore
public void SaveChat(ChatSettings chat)
=> SaveSection("chat", BuildChatObject(chat));
/// <summary>
/// Load the five client-local Camera/Input preferences the "Use Mouse
/// Turning Settings" Gameplay-tab macro writes (Campaign OP slice OP3).
/// Same fall-back behaviour as <see cref="LoadDisplay"/>.
/// </summary>
public CameraTurningSettings LoadCameraTurning()
{
if (!File.Exists(_path)) return CameraTurningSettings.Default;
try
{
using var stream = File.OpenRead(_path);
var doc = JsonDocument.Parse(stream);
var root = doc.RootElement;
if (!root.TryGetProperty("cameraTurning", out var ct)
|| ct.ValueKind != JsonValueKind.Object)
return CameraTurningSettings.Default;
var d = CameraTurningSettings.Default;
return new CameraTurningSettings(
Stiffness: ReadFloat(ct, "stiffness", d.Stiffness),
AdjustmentSpeed: ReadFloat(ct, "adjustmentSpeed", d.AdjustmentSpeed),
MouseLookSensitivity: ReadFloat(ct, "mouseLookSensitivity", d.MouseLookSensitivity),
AlignToSlope: ReadBool (ct, "alignToSlope", d.AlignToSlope),
InvertMouseLookYAxis: ReadBool (ct, "invertMouseLookYAxis", d.InvertMouseLookYAxis));
}
catch (Exception ex)
{
Console.WriteLine($"settings: failed to load {_path}: {ex.Message} — using defaults");
return CameraTurningSettings.Default;
}
}
/// <summary>Save the camera-turning preferences, preserving all other
/// top-level keys.</summary>
public void SaveCameraTurning(CameraTurningSettings cameraTurning)
=> SaveSection("cameraTurning", BuildCameraTurningObject(cameraTurning));
/// <summary>
/// Load per-character settings keyed by <paramref name="toonKey"/>.
/// Missing file or missing toon entry → <see cref="CharacterSettings.Default"/>.
@ -594,6 +631,16 @@ public sealed class SettingsStore
["vsync"] = d.VSync,
};
private static SortedDictionary<string, object> BuildCameraTurningObject(CameraTurningSettings c)
=> new(StringComparer.Ordinal)
{
["adjustmentSpeed"] = c.AdjustmentSpeed,
["alignToSlope"] = c.AlignToSlope,
["invertMouseLookYAxis"] = c.InvertMouseLookYAxis,
["mouseLookSensitivity"] = c.MouseLookSensitivity,
["stiffness"] = c.Stiffness,
};
private static SortedDictionary<string, object> BuildAudioObject(AudioSettings a)
=> new(StringComparer.Ordinal)
{