fix(ui): OP3 review fixes — byte-verified Magic chat lines, Gameplay/OptionPage shape, mid-air tri-state, shared geometry
Consolidated fix round for the two OP3 dual-lens reviews
(docs/research/2026-08-11-op3-review-{mechanism,blast}.md), both
APPROVE-WITH-FIXES.
MUST-FIX:
- The six "Use Mouse Turning Settings" chat lines were typed
RetailLogTextType.ClientLocal (0x1A); retail types them 0x07 (Magic).
BYTE-VERIFIED against the PDB-paired binary at all six
gmConfigUI::SetMouseTurningDefaults call sites (0x0049E972/E9E2/EA52/
EAA4/EAF6/EB48): every site pushes `6a 07` (type=7) immediately before
the text-pointer push and the AddTextToScroll call. Added a dedicated
OptionsRuntimeBindings.DisplayMouseTurningMacroLine seam routed at
Magic (scrolling chat transcript, light blue, timestamped) instead of
the 4-slot SpewBox ClientLocal uses; the mid-air refusal and UA/RA
keep ClientLocal (both independently confirmed correct).
- Filed AD-77: the client-wide floating-only gmPanelUI host divergence
(retail also exposes a docked 0x21000017 host) the plan §5 delegated
to this review, scoped to every main panel, not just Options.
SHOULD-FIX:
- gmGameplayOptionsUI is not an OptionPage in retail (acclient.h:55857,
UIElement_Field). OptionsPanelController now constructs the Gameplay
slot's OptionPage with AfterApply deliberately null, so entering/
leaving that tab never publishes SaveCharacterOptionsRuntimeCmd.
Corrected OptionPageModel's doc comment and rewrote the two tests
that pinned the wrong (Gameplay-flushes) shape.
- Added the OptionPage.OnOptionChanged seam (PlayerOptionPage::
OnOptionChanged @0x004F27D0) — fires as the last step of Apply/
Reset/Defaults, plus once per live LED edit via a new
IOptionRow.AttachPageNotify hook (BoolOptionRow wires it into
SetCurrentValue only, matching retail's Apply(1)-only
HandleDialogAndNotices path). OP4-6 will bind Apply/Reset enable
state to this.
- Exit to Character Selection's mid-air refusal is now tri-state
(Func<bool?> IsGrounded): retail's UseTime only reaches the airborne
test inside `else if (smartbox->player)`, so outside player mode (or
with no live controller) the button is a SILENT no-op, not a
refusal. Fixed the inverted comment at both call sites.
- Options panel geometry now matches its nine gmPanelUI siblings
sharing RetailPanelUiController's one main-panel rectangle
(ResizeX=false, bottom-edge-only resize, no invented Min/MaxWidth/
Height) instead of being the only all-four-edge/horizontal-resize
outlier whose width silently reverted whenever a sibling was shown.
- Added the three missing test pins: Options/Character mutual
exclusion through a REAL RetailPanelUiController registration,
RetailDialogFactory.MakeConfirmation's omitted-queueKey overload
sharing DefaultQueueKey, and UiTabPanel.ActivePageChanged never
firing on a dormant (non-activated) host.
- TS-74's What/Where now names the five store-only CameraTurning
preferences explicitly instead of only mentioning them in Risk.
- Test script gains the toolbar-button ghosted->enabled+highlight
check, UseMouseTurning-survives-relogin and the five prefs-survive-
relaunch steps, a UA/RA legibility eye-item, and the corrected
bottom-edge-only geometry description for step 5.
One-liners fixed in files already touched: symmetric close-button
resolve-failure logging in OptionsPanelController.Bind (blast NOTE 8).
Full Release suite: 12,947 passed / 4 skipped / 0 failed (baseline
12,935/4/0 post-OP7 — 12 net new tests; the two OptionPageModelTests
"wrong-shape" tests were renamed/rewritten in place, not removed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
64898f1301
commit
386076af0f
11 changed files with 505 additions and 72 deletions
|
|
@ -800,13 +800,20 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.Communication.AddText(text, RetailLogTextType.ClientLocal)),
|
||||
Options: new OptionsRuntimeBindings(
|
||||
CommandBus: () => late.Session.Commands,
|
||||
// Same playerOnGround shape SessionPlayerComposition
|
||||
// already computes for the diagnostic dumper (line
|
||||
// ~322): "on the ground" is meaningless outside player
|
||||
// mode, so the mid-air refusal never fires while flying/
|
||||
// spectating.
|
||||
IsGrounded: () => d.PlayerMode.IsPlayerMode
|
||||
&& d.PlayerController.Controller is { IsAirborne: false },
|
||||
// Tri-state per gmGamePlayUI::UseTime @0x004EA3A0's exact
|
||||
// branch structure (mechanism review S3 / blast review
|
||||
// SHOULD-FIX 1, 2026-08-11 fix round): retail's airborne
|
||||
// test only runs INSIDE `else if (smartbox->player)` —
|
||||
// with no player object (not in player mode, or no live
|
||||
// controller yet) neither the refusal nor the logoff
|
||||
// itself ever fires, i.e. a SILENT no-op, not a refusal.
|
||||
// The original two-way bool collapsed that third case
|
||||
// into "not grounded", which fired the mid-air message
|
||||
// outside player mode — the opposite of retail and the
|
||||
// opposite of what the old comment here claimed.
|
||||
IsGrounded: () => !d.PlayerMode.IsPlayerMode
|
||||
? (bool?)null
|
||||
: d.PlayerController.Controller is { IsAirborne: false },
|
||||
IsUseMouseTurningEnabled: () =>
|
||||
CharacterOptionTable.TryGet(
|
||||
CharacterOptionId.UseMouseTurning,
|
||||
|
|
@ -814,6 +821,16 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
&& (d.Character.Options.Options2 & entry.Mask) != 0u,
|
||||
DisplaySystemMessage: text =>
|
||||
d.Communication.AddText(text, RetailLogTextType.ClientLocal),
|
||||
// OP3 review-fix round (2026-08-11), MUST-FIX M1: BYTE-
|
||||
// VERIFIED against the PDB-paired binary — all six
|
||||
// gmConfigUI::SetMouseTurningDefaults chat-line sites
|
||||
// push `6a 07` (type=7=Magic) immediately before the
|
||||
// text-pointer push and the AddTextToScroll call
|
||||
// (0x0049E972/E9E2/EA52/EAA4/EAF6/EB48). Magic routes to
|
||||
// the scrolling chat transcript, not the 4-slot SpewBox
|
||||
// ClientLocal uses.
|
||||
DisplayMouseTurningMacroLine: text =>
|
||||
d.Communication.AddText(text, RetailLogTextType.Magic),
|
||||
LoadCameraTurning: d.Settings.LoadCameraTurning,
|
||||
SaveCameraTurning: d.Settings.SaveCameraTurning),
|
||||
StackSplitQuantity: d.StackSplitQuantity,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,21 @@ public interface IOptionRow
|
|||
/// <summary><c>RestoreDefaultValue</c>: <c>m_current = m_default</c>,
|
||||
/// then applies the default live.</summary>
|
||||
void RestoreDefaultValue();
|
||||
|
||||
/// <summary>
|
||||
/// Wires this row's owning-page notify hook — retail's
|
||||
/// <c>UIOption::m_pOCH</c> (option-change-handler) pointer, invoked by
|
||||
/// <c>UIOption::HandleDialogAndNotices @0x004EFB90</c>'s
|
||||
/// <c>m_pOCH->OnOptionChanged(this)</c> call after a LIVE user edit
|
||||
/// (a widget's own <c>SetCurrentValue</c>/<c>Apply(1)</c> path ONLY —
|
||||
/// <c>RestoreSavedValue</c>/<c>RestoreDefaultValue</c> use retail's
|
||||
/// <c>Apply(0)</c>, which skips this per-row notify because the OWNING
|
||||
/// VERB (<see cref="OptionPage.Reset"/>/<see cref="OptionPage.Defaults"/>)
|
||||
/// already calls <see cref="OptionPage.OnOptionChanged"/> once itself,
|
||||
/// at its own tail). Called once by <see cref="OptionPage.Register"/>;
|
||||
/// mechanism review S2, 2026-08-11 fix round.
|
||||
/// </summary>
|
||||
void AttachPageNotify(Action notify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -44,6 +59,7 @@ public interface IOptionRow
|
|||
public sealed class BoolOptionRow : IOptionRow
|
||||
{
|
||||
private readonly Action<bool>? _apply;
|
||||
private Action? _notifyPageOptionChanged;
|
||||
private bool _current;
|
||||
private bool _saved;
|
||||
private bool _default;
|
||||
|
|
@ -77,14 +93,19 @@ public sealed class BoolOptionRow : IOptionRow
|
|||
/// <summary>
|
||||
/// Retail <c>SetCurrentValue @0x00486970</c> — the LED-click entry point.
|
||||
/// Writes <c>m_current</c> and applies it live immediately; does NOT
|
||||
/// touch <see cref="Saved"/> (Apply is the only verb that commits).
|
||||
/// touch <see cref="Saved"/> (Apply is the only verb that commits). Also
|
||||
/// notifies the owning page (<see cref="AttachPageNotify"/>) — retail's
|
||||
/// <c>Apply(1)</c>-only <c>HandleDialogAndNotices</c> path.
|
||||
/// </summary>
|
||||
public void SetCurrentValue(bool value)
|
||||
{
|
||||
_current = value;
|
||||
_apply?.Invoke(value);
|
||||
_notifyPageOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void AttachPageNotify(Action notify) => _notifyPageOptionChanged = notify;
|
||||
|
||||
public void SaveCurrentValue() => _saved = _current;
|
||||
|
||||
public void RestoreSavedValue()
|
||||
|
|
@ -134,13 +155,20 @@ public sealed class BoolOptionRow : IOptionRow
|
|||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// An empty page (zero registered rows — the Gameplay tab's own model, which
|
||||
/// has no <c>UIOption</c> rows at all per research doc §6) makes every verb a
|
||||
/// no-op and <see cref="Changed"/> permanently false; <see cref="AfterApply"/>
|
||||
/// still fires on <see cref="Apply"/>/<see cref="OnShown"/> (retail's
|
||||
/// <c>SaveCurrentValues</c> flushes the blob regardless of whether THIS
|
||||
/// page's own rows changed anything — the module's dirty flag is global, not
|
||||
/// per-page).
|
||||
/// An empty page (zero registered rows) makes every verb a no-op and
|
||||
/// <see cref="Changed"/> permanently false; when <see cref="AfterApply"/> IS
|
||||
/// wired, it still fires on <see cref="Apply"/>/<see cref="OnShown"/> even
|
||||
/// with zero rows (retail's <c>SaveCurrentValues</c> flushes the blob
|
||||
/// regardless of whether THIS page's own rows changed anything — the
|
||||
/// module's dirty flag is global, not per-page). <b>This is a property of
|
||||
/// the generic empty-page shape, not a description of the Gameplay tab.</b>
|
||||
/// <c>gmGameplayOptionsUI</c> (<c>acclient.h:55857</c>) derives from
|
||||
/// <c>UIElement_Field</c>, not <c>OptionPage</c>/<c>PlayerOptionPage</c> at
|
||||
/// all, so retail never calls <c>SaveCurrentValues</c> for it in the first
|
||||
/// place — <see cref="OptionsPanelController"/> deliberately
|
||||
/// constructs the Gameplay slot's <see cref="OptionPage"/> instance with
|
||||
/// <see cref="AfterApply"/> left <c>null</c> so entering/leaving that tab
|
||||
/// never flushes (mechanism review S1, 2026-08-11 fix round).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class OptionPage
|
||||
|
|
@ -154,17 +182,33 @@ public sealed class OptionPage
|
|||
/// <see cref="OnShown"/>) — the seam a controller wires to the batched
|
||||
/// <c>SaveOptions</c>/blob-flush command. Never invoked by
|
||||
/// <see cref="Reset"/> or <see cref="Defaults"/> (retail's Reset/Defaults
|
||||
/// call <c>Apply(0)</c> per-row and re-run <c>OnOptionChanged(0)</c>
|
||||
/// call <c>Apply(0)</c> per-row and reach <see cref="OnOptionChanged"/>
|
||||
/// directly — they never reach <c>PlayerOptionPage::SaveCurrentValues</c>,
|
||||
/// so they never flush).
|
||||
/// </summary>
|
||||
public Action? AfterApply { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>OptionPage::OnOptionChanged(0)</c> — the ONLY thing that
|
||||
/// enable-gates Apply/Reset (<c>PlayerOptionPage::OnOptionChanged
|
||||
/// @0x004F27D0</c>: disabled when <see cref="Changed"/> is false, enabled
|
||||
/// otherwise; Defaults is NEVER gated — retail's override never fetches
|
||||
/// its child id at all). Retail runs this as the LAST statement of all
|
||||
/// three verbs (<c>0x004F2C95</c> Apply, <c>0x004F2CE5</c> Defaults,
|
||||
/// <c>0x004F2D4A</c> Reset); a live user edit
|
||||
/// (<see cref="IOptionRow.AttachPageNotify"/>'s
|
||||
/// <c>SetCurrentValue</c>-only path) also reaches it directly via
|
||||
/// <c>UIOption::HandleDialogAndNotices @0x004EFB90</c>. OP4-6 bind
|
||||
/// buttons to this seam; mechanism review S2, 2026-08-11 fix round.
|
||||
/// </summary>
|
||||
public Action? OnOptionChanged { get; set; }
|
||||
|
||||
/// <summary>Registers one row. Retail's <c>OptionPage::RegisterOption
|
||||
/// @0x004F2E90</c>, called from each page's <c>InitOptions</c>.</summary>
|
||||
public void Register(IOptionRow row)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(row);
|
||||
row.AttachPageNotify(() => OnOptionChanged?.Invoke());
|
||||
_rows.Add(row);
|
||||
}
|
||||
|
||||
|
|
@ -173,17 +217,19 @@ public sealed class OptionPage
|
|||
public bool Changed => _rows.Any(static row => row.Changed);
|
||||
|
||||
/// <summary><c>OptionPage::SaveCurrentValues @0x004F2C60</c> — Apply:
|
||||
/// commits every row's baseline unconditionally, then flushes via
|
||||
/// <see cref="AfterApply"/>.</summary>
|
||||
/// commits every row's baseline unconditionally, flushes via
|
||||
/// <see cref="AfterApply"/>, then re-evaluates <see cref="OnOptionChanged"/>.</summary>
|
||||
public void Apply()
|
||||
{
|
||||
foreach (IOptionRow row in _rows)
|
||||
row.SaveCurrentValue();
|
||||
AfterApply?.Invoke();
|
||||
OnOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary><c>OptionPage::RestoreSavedValues @0x004F2D00</c> — Reset:
|
||||
/// reverts only the rows that are currently <see cref="IOptionRow.Changed"/>.
|
||||
/// reverts only the rows that are currently <see cref="IOptionRow.Changed"/>,
|
||||
/// then re-evaluates <see cref="OnOptionChanged"/>.
|
||||
/// Snapshotted before iterating so a row's own revert (which flips
|
||||
/// <see cref="IOptionRow.Changed"/> back to false) cannot skip a later
|
||||
/// row.</summary>
|
||||
|
|
@ -191,15 +237,17 @@ public sealed class OptionPage
|
|||
{
|
||||
foreach (IOptionRow row in _rows.Where(static row => row.Changed).ToArray())
|
||||
row.RestoreSavedValue();
|
||||
OnOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary><c>OptionPage::RestoreDefaultValues @0x004F2CB0</c> —
|
||||
/// Defaults: restores every row unconditionally, live, without
|
||||
/// committing.</summary>
|
||||
/// committing, then re-evaluates <see cref="OnOptionChanged"/>.</summary>
|
||||
public void Defaults()
|
||||
{
|
||||
foreach (IOptionRow row in _rows)
|
||||
row.RestoreDefaultValue();
|
||||
OnOptionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary><c>PlayerOptionPage::OnVisibilityChanged(true)</c> — the page
|
||||
|
|
|
|||
|
|
@ -122,7 +122,21 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
private OptionsPanelController(UiTabPanel tabPanel, Action? afterApply)
|
||||
{
|
||||
_tabPanel = tabPanel;
|
||||
foreach (uint pageId in new[] { GameplayPageId, CharacterPageId, ChatPageId, ConfigPageId })
|
||||
|
||||
// Mechanism review S1 (2026-08-11 fix round): gmGameplayOptionsUI
|
||||
// (acclient.h:55857) derives from UIElement_Field, NOT
|
||||
// OptionPage/PlayerOptionPage at all — unlike the other three tabs
|
||||
// (gmCharacterSettingsUI/gmChatOptionsUI/gmConfigUI, all
|
||||
// : PlayerOptionPage). Retail never calls SaveCurrentValues for the
|
||||
// Gameplay page, so it never flushes the batched blob on show/hide.
|
||||
// The model still needs an (empty) page instance so
|
||||
// OnActivePageChanged's TryGetValue lookup and OnHidden/OnShown
|
||||
// (Reset/Apply over zero rows, both harmless no-ops) keep working
|
||||
// uniformly across all four tabs — only AfterApply is deliberately
|
||||
// left null here, so entering/leaving Gameplay never publishes
|
||||
// SaveCharacterOptionsRuntimeCmd.
|
||||
_pages.Add(GameplayPageId, new OptionPage { AfterApply = null });
|
||||
foreach (uint pageId in new[] { CharacterPageId, ChatPageId, ConfigPageId })
|
||||
{
|
||||
var page = new OptionPage { AfterApply = afterApply };
|
||||
_pages.Add(pageId, page);
|
||||
|
|
@ -159,6 +173,10 @@ public sealed class OptionsPanelController : IRetainedPanelController
|
|||
|
||||
if (layout.FindElement(CloseButtonId) is UiButton close)
|
||||
close.OnClick = callbacks.Toggle;
|
||||
else
|
||||
Console.WriteLine(
|
||||
$"[D.2b] OptionsPanelController: close button 0x{CloseButtonId:X8} "
|
||||
+ "not found in the built layout — its handler was not wired.");
|
||||
|
||||
BindButton(layout, ExitToCharacterSelectionId, callbacks.RequestExitToCharacterSelection);
|
||||
// ConfigureKeyboardId: INERT this slice — authored, clickable, no
|
||||
|
|
|
|||
|
|
@ -156,10 +156,14 @@ public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
|||
/// 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>
|
||||
/// (<c>transient_state & 1</c>), tri-state per
|
||||
/// <c>gmGamePlayUI::UseTime @0x004EA3A0</c>'s exact branch structure:
|
||||
/// <c>else if (smartbox->player) { if (!grounded) refuse; else proceed; }</c>
|
||||
/// — the airborne test is reached ONLY when a player object exists at all.
|
||||
/// <c>null</c> means "no player" (acdream: <c>!d.PlayerMode.IsPlayerMode ||
|
||||
/// d.PlayerController.Controller is null</c>), which retail's own structure
|
||||
/// reaches neither branch for — no refusal, no proceed (mechanism review S3
|
||||
/// / blast review SHOULD-FIX 1, 2026-08-11).</param>
|
||||
/// <param name="IsUseMouseTurningEnabled">Live read of
|
||||
/// <c>PlayerOption.UseMouseTurning</c> (<c>CharacterOptions2</c> bit
|
||||
/// <c>0x00400000</c>) from the canonical <c>RuntimeCharacterOptionsState</c> —
|
||||
|
|
@ -168,12 +172,28 @@ public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
|||
/// (<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>
|
||||
/// <see cref="AppraisalRuntimeBindings"/> already use. Used for the mid-air
|
||||
/// refusal (byte-verified <c>ClientLocal 0x1A</c>) and the Urgent
|
||||
/// Assistance/Report Abuse short-circuit (AD-75) — NOT the mouse-turning
|
||||
/// macro's own six lines, which use <see cref="DisplayMouseTurningMacroLine"/>
|
||||
/// instead.</param>
|
||||
/// <param name="DisplayMouseTurningMacroLine">The mouse-turning macro's own
|
||||
/// six retail chat lines — routed at <c>RetailLogTextType.Magic (0x07)</c>,
|
||||
/// BYTE-VERIFIED at all six <c>ClientSystem::AddTextToScroll</c> call sites
|
||||
/// inside <c>gmConfigUI::SetMouseTurningDefaults</c> (raw bytes: <c>6a 07</c>
|
||||
/// immediately before the text-pointer push and the call, at every one of
|
||||
/// <c>0x0049E972</c>/<c>0x0049E9E2</c>/<c>0x0049EA52</c>/<c>0x0049EAA4</c>/
|
||||
/// <c>0x0049EAF6</c>/<c>0x0049EB48</c>, byte-verified against the PDB-paired
|
||||
/// binary 2026-08-11 — the OP3 review-fix round). Magic routes through
|
||||
/// <c>RuntimeCommunicationState.AddText</c> to the scrolling chat transcript
|
||||
/// (light blue, timestamped, logged), NOT the 4-slot SpewBox
|
||||
/// <see cref="DisplaySystemMessage"/>'s <c>ClientLocal</c> uses.</param>
|
||||
public sealed record OptionsRuntimeBindings(
|
||||
Func<ICommandBus> CommandBus,
|
||||
Func<bool> IsGrounded,
|
||||
Func<bool?> IsGrounded,
|
||||
Func<bool> IsUseMouseTurningEnabled,
|
||||
Action<string> DisplaySystemMessage,
|
||||
Action<string> DisplayMouseTurningMacroLine,
|
||||
Func<CameraTurningSettings> LoadCameraTurning,
|
||||
Action<CameraTurningSettings> SaveCameraTurning);
|
||||
|
||||
|
|
@ -1855,11 +1875,20 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
/// 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.
|
||||
/// <c>gmGamePlayUI::UseTime @0x004EA3A0</c>'s drain, its EXACT three-way
|
||||
/// branch (review-fix round, 2026-08-11 — the original port collapsed
|
||||
/// this to a two-way `if/else` that fired the refusal outside player
|
||||
/// mode too):
|
||||
/// <code>
|
||||
/// if (m_bLogoutConfirmed) {
|
||||
/// if (m_shouldQuitOnLogout) { QueueUIMode(0x10000009); }
|
||||
/// else if (smartbox->player) {
|
||||
/// if (!grounded) refuse "Cannot log off while in mid-air.";
|
||||
/// else LogOffCharacter();
|
||||
/// }
|
||||
/// // no player at all: neither branch runs — SILENT no-op.
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
private void RequestExitToCharacterSelection()
|
||||
{
|
||||
|
|
@ -1867,13 +1896,20 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
if (!accepted) return;
|
||||
|
||||
if (_bindings.Options.IsGrounded())
|
||||
switch (_bindings.Options.IsGrounded())
|
||||
{
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
}
|
||||
else
|
||||
{
|
||||
_bindings.Options.DisplaySystemMessage(ClientTextRefusals.CantLogOffMidAir);
|
||||
case true:
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
break;
|
||||
case false:
|
||||
_bindings.Options.DisplaySystemMessage(ClientTextRefusals.CantLogOffMidAir);
|
||||
break;
|
||||
case null:
|
||||
// Retail's `else if (smartbox->player)` gate: outside
|
||||
// player mode (or with no live controller) there is no
|
||||
// player object for UseTime to test at all, so neither
|
||||
// the airborne refusal nor the logoff itself ever runs.
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1883,7 +1919,9 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
/// 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,
|
||||
/// live, send the wire bit if it changed, emit every retail chat line
|
||||
/// (byte-verified <c>Magic 0x07</c>, review-fix round 2026-08-11 — see
|
||||
/// <see cref="OptionsRuntimeBindings.DisplayMouseTurningMacroLine"/>),
|
||||
/// then commit the (currently empty) Config page exactly like retail's
|
||||
/// own trailing <c>SaveCurrentValues()</c>
|
||||
/// (<c>gmConfigUI::SetMouseTurningDefaults @0x0049EB57</c>).
|
||||
|
|
@ -1905,7 +1943,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
}
|
||||
|
||||
foreach (string line in result.ChatLines)
|
||||
_bindings.Options.DisplaySystemMessage(line);
|
||||
_bindings.Options.DisplayMouseTurningMacroLine(line);
|
||||
|
||||
OptionsPanelController?.ConfigPage.Apply();
|
||||
}
|
||||
|
|
@ -1967,17 +2005,27 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Left = 150f,
|
||||
Top = 80f,
|
||||
Visible = false,
|
||||
ResizeX = true,
|
||||
// Blast review SHOULD-FIX 2 (2026-08-11): match every OTHER
|
||||
// gmPanelUI sibling sharing RetailPanelUiController's ONE
|
||||
// main-panel geometry rectangle (Character/Inventory/
|
||||
// Spellbook/Effects/the four indicator-detail panels) —
|
||||
// ResizeX=false, bottom-edge-only resize, no invented
|
||||
// Min/MaxWidth/Height. Options previously authored the ONLY
|
||||
// divergent policy in that shared group (ResizeX=true,
|
||||
// all-four-edge resize, literal 300x200 minimums); since
|
||||
// every sibling forces `width = frame.Width` on the shared
|
||||
// rect (RetailWindowManager.ResizeTo), a user-widened
|
||||
// Options window silently reverted to 300 the next time ANY
|
||||
// sibling was shown — this makes the whole cohort consistent
|
||||
// again instead of one outlier with its own rules.
|
||||
ResizeX = false,
|
||||
ResizeY = true,
|
||||
ResizableEdges = ResizeEdges.Left | ResizeEdges.Right
|
||||
| ResizeEdges.Top | ResizeEdges.Bottom,
|
||||
ResizableEdges = 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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue