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:
parent
5242de9f15
commit
9d26ecc623
27 changed files with 25696 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
|
@ -143,6 +144,39 @@ public sealed record ToolbarRuntimeBindings(
|
|||
|
||||
public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP3 (2026-08-11): bindings the retail Options panel
|
||||
/// needs beyond what the generic <see cref="RetailPanelUiController"/> mount
|
||||
/// already supplies (retained-window registration, geometry, opacity — all
|
||||
/// shared with every other <c>gmPanelUI</c> sibling through
|
||||
/// <see cref="RetailUiRuntimeBindings"/> itself).
|
||||
/// </summary>
|
||||
/// <param name="CommandBus">Late-resolved live command surface — the SAME
|
||||
/// <c>Func<ICommandBus></c> provider pattern <see cref="ChatRuntimeBindings.CommandBus"/>
|
||||
/// already uses, since the live session's bus is not yet constructed at the
|
||||
/// point this binding record is built (composition order).</param>
|
||||
/// <param name="IsGrounded">Retail's mid-air logout-refusal check
|
||||
/// (<c>transient_state & 1</c>) — mirrors the SAME
|
||||
/// <c>d.PlayerMode.IsPlayerMode && d.PlayerController.Controller is
|
||||
/// { IsAirborne: false }</c> pattern already used for
|
||||
/// <c>InteractionRetainedUiComposition</c>'s own <c>playerOnGround</c> read.</param>
|
||||
/// <param name="IsUseMouseTurningEnabled">Live read of
|
||||
/// <c>PlayerOption.UseMouseTurning</c> (<c>CharacterOptions2</c> bit
|
||||
/// <c>0x00400000</c>) from the canonical <c>RuntimeCharacterOptionsState</c> —
|
||||
/// the mouse-turning macro's sixth, server-synced target.</param>
|
||||
/// <param name="DisplaySystemMessage">The interface-text seam
|
||||
/// (<c>RetailLogTextType.ClientLocal</c>, SpewBox-only) — the SAME
|
||||
/// <c>text => d.Communication.AddText(text, RetailLogTextType.ClientLocal)</c>
|
||||
/// delegate shape <see cref="VendorRuntimeBindings.DisplaySystemMessage"/>/
|
||||
/// <see cref="AppraisalRuntimeBindings"/> already use.</param>
|
||||
public sealed record OptionsRuntimeBindings(
|
||||
Func<ICommandBus> CommandBus,
|
||||
Func<bool> IsGrounded,
|
||||
Func<bool> IsUseMouseTurningEnabled,
|
||||
Action<string> DisplaySystemMessage,
|
||||
Func<CameraTurningSettings> LoadCameraTurning,
|
||||
Action<CameraTurningSettings> SaveCameraTurning);
|
||||
|
||||
public sealed record InventoryRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
|
|
@ -231,6 +265,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
RetailUiCursorBindings Cursor,
|
||||
ConfirmationRuntimeBindings Confirmations,
|
||||
AppraisalRuntimeBindings Appraisal,
|
||||
OptionsRuntimeBindings Options,
|
||||
StackSplitQuantityState StackSplitQuantity,
|
||||
BufferedUiRegistry? Plugins,
|
||||
RetailUiPersistenceBindings? Persistence,
|
||||
|
|
@ -309,6 +344,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
MountAppraisal();
|
||||
MountEffects();
|
||||
MountIndicatorDetailPanels();
|
||||
MountOptionsPanel();
|
||||
MountIndicators();
|
||||
MountJumpPowerbar();
|
||||
MountDialogFactory();
|
||||
|
|
@ -404,6 +440,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public RetailDialogFactory? DialogFactory { get; private set; }
|
||||
public ExternalContainerController? ExternalContainerController { get; private set; }
|
||||
public VendorUiController? VendorController { get; private set; }
|
||||
public OptionsPanelController? OptionsPanelController { get; private set; }
|
||||
|
||||
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
|
|
@ -1783,25 +1820,178 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Console.WriteLine("[UI] retail seven-control indicator bar from LayoutDesc 0x21000071.");
|
||||
}
|
||||
|
||||
private void RequestEndCharacterSession()
|
||||
/// <summary>
|
||||
/// Retail <c>ID_Client_EndCharacterSessionConfirm</c>
|
||||
/// (<c>CM_UI::SendNotice_EndCharacterSession</c>'s confirmation dialog
|
||||
/// text — research doc <c>2026-08-10-keyboard-config-and-gameplay-tab.md</c>
|
||||
/// §2.1), shared by both the indicator bar's own end-session control
|
||||
/// (<see cref="RequestEndCharacterSession"/>) and the Options panel's
|
||||
/// Exit to Character Selection button
|
||||
/// (<see cref="RequestExitToCharacterSelection"/>).
|
||||
/// </summary>
|
||||
private string ResolveEndCharacterSessionConfirmMessage()
|
||||
{
|
||||
const string fallback = "Are you sure you want to end this character session?";
|
||||
string message;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
message = new DatStringResolver(_bindings.Assets.Dats).Resolve(
|
||||
return new DatStringResolver(_bindings.Assets.Dats).Resolve(
|
||||
0x23000001u,
|
||||
DatStringResolver.ComputeHash("ID_Client_EndCharacterSessionConfirm"))
|
||||
?? fallback;
|
||||
}
|
||||
}
|
||||
|
||||
ShowConfirmation(message, accepted =>
|
||||
private void RequestEndCharacterSession()
|
||||
{
|
||||
ShowConfirmation(ResolveEndCharacterSessionConfirmMessage(), accepted =>
|
||||
{
|
||||
if (accepted)
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP3: the Options panel's Exit to Character
|
||||
/// Selection button (element <c>0x10000203</c>) — D6's "behaves as Exit
|
||||
/// Game" adaptation (register row, same commit), PLUS retail's
|
||||
/// confirmation dialog and mid-air refusal, which DO port exactly.
|
||||
/// <c>gmGamePlayUI::UseTime @0x004EA3A0</c>'s drain: on confirmation
|
||||
/// accept, refuse with <see cref="ClientTextRefusals.CantLogOffMidAir"/>
|
||||
/// via the interface-text seam while airborne
|
||||
/// (<c>transient_state & 1</c>); otherwise proceed exactly like Exit
|
||||
/// Game.
|
||||
/// </summary>
|
||||
private void RequestExitToCharacterSelection()
|
||||
{
|
||||
ShowConfirmation(ResolveEndCharacterSessionConfirmMessage(), accepted =>
|
||||
{
|
||||
if (!accepted) return;
|
||||
|
||||
if (_bindings.Options.IsGrounded())
|
||||
{
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
}
|
||||
else
|
||||
{
|
||||
_bindings.Options.DisplaySystemMessage(ClientTextRefusals.CantLogOffMidAir);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP3: the Options panel's Use Mouse Turning Settings
|
||||
/// button (element <c>0x100005CC</c>) — the pure
|
||||
/// <see cref="MouseTurningSettingsMacro"/> orchestrated against live
|
||||
/// bindings: load current values, compute the macro, persist + apply
|
||||
/// live, send the wire bit if it changed, emit every retail chat line,
|
||||
/// then commit the (currently empty) Config page exactly like retail's
|
||||
/// own trailing <c>SaveCurrentValues()</c>
|
||||
/// (<c>gmConfigUI::SetMouseTurningDefaults @0x0049EB57</c>).
|
||||
/// </summary>
|
||||
private void ApplyMouseTurningSettingsMacro()
|
||||
{
|
||||
CameraTurningSettings current = _bindings.Options.LoadCameraTurning();
|
||||
bool useMouseTurningCurrent = _bindings.Options.IsUseMouseTurningEnabled();
|
||||
MouseTurningSettingsMacro.Result result =
|
||||
MouseTurningSettingsMacro.Compute(current, useMouseTurningCurrent);
|
||||
|
||||
_bindings.Options.SaveCameraTurning(result.Updated);
|
||||
|
||||
if (result.UseMouseTurningChanged)
|
||||
{
|
||||
_bindings.Options.CommandBus().Publish(
|
||||
new SetSingleCharacterOptionRuntimeCmd(
|
||||
(uint)CharacterOptionId.UseMouseTurning, true));
|
||||
}
|
||||
|
||||
foreach (string line in result.ChatLines)
|
||||
_bindings.Options.DisplaySystemMessage(line);
|
||||
|
||||
OptionsPanelController?.ConfigPage.Apply();
|
||||
}
|
||||
|
||||
private void MountOptionsPanel()
|
||||
{
|
||||
ElementInfo? rootInfo;
|
||||
ImportedLayout? layout;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
rootInfo = LayoutImporter.ImportInfos(
|
||||
_bindings.Assets.Dats,
|
||||
Layout.OptionsPanelController.HostLayoutId,
|
||||
Layout.OptionsPanelController.SlotElementId);
|
||||
var resolver = new DatStringResolver(_bindings.Assets.Dats);
|
||||
layout = rootInfo is null
|
||||
? null
|
||||
: LayoutImporter.Build(
|
||||
rootInfo,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont,
|
||||
resolver.Resolve);
|
||||
}
|
||||
if (rootInfo is null || layout is null)
|
||||
{
|
||||
Console.WriteLine("[UI] options panel: LayoutDesc 0x2100006E slot 0x1000018D not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var callbacks = new Layout.OptionsPanelController.Callbacks(
|
||||
Toggle: () => ToggleWindow(WindowNames.Options),
|
||||
RequestExitToCharacterSelection: RequestExitToCharacterSelection,
|
||||
ExitGame: _bindings.Indicators.EndCharacterSession,
|
||||
UseMouseTurningSettings: ApplyMouseTurningSettingsMacro,
|
||||
DisplaySystemMessage: _bindings.Options.DisplaySystemMessage,
|
||||
AfterApply: () => _bindings.Options.CommandBus().Publish(
|
||||
new SaveCharacterOptionsRuntimeCmd()));
|
||||
|
||||
Layout.OptionsPanelController? controller =
|
||||
Layout.OptionsPanelController.Bind(layout, callbacks);
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine("[UI] options panel: required root did not build as UiTabPanel.");
|
||||
return;
|
||||
}
|
||||
|
||||
OptionsPanelController = controller;
|
||||
controller.ActivateTabs();
|
||||
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
controller.Root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Options,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 150f,
|
||||
Top = 80f,
|
||||
Visible = false,
|
||||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right
|
||||
| ResizeEdges.Top | ResizeEdges.Bottom,
|
||||
ConstrainDragToParent = true,
|
||||
ConstrainResizeToParent = true,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
MinWidth = 300f,
|
||||
MinHeight = 200f,
|
||||
DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo),
|
||||
Controller = controller,
|
||||
});
|
||||
_panelUi.RegisterMainPanel(
|
||||
RetailPanelCatalog.Options,
|
||||
WindowNames.Options,
|
||||
handle,
|
||||
rootInfo.TryGetEffectiveBool(
|
||||
RetailPanelUiController.RestorePreviousPropertyId,
|
||||
out bool restorePrevious)
|
||||
&& restorePrevious);
|
||||
Console.WriteLine("[UI] retail Options panel from LayoutDesc importer (0x2100006E slot 0x1000018D).");
|
||||
}
|
||||
|
||||
private void MountJumpPowerbar()
|
||||
{
|
||||
ElementInfo? info;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue