F1 (MUST-FIX): RetailWaitDialogView was the ONE dialog view the 0a7dc7d6
EffectiveCanvasSize sweep missed - the Entering World wait dialog (fires
on ENTER, the char screen primary action) still centered against the raw
window and landed off the visible canvas. Same three-line fix as its three
siblings; the enter-wait test now grows the window over the fixed canvas
and asserts canvas-space centering.
F3/F4: two stale assertions about the DELETED first AD-98 substitution
(the register section-2 header line and the live-DAT oracle test doc) now
describe the completed FixedCanvasSize mechanism - the C4-closeout failure
mode, caught before it cost anything.
F5: RetailDialogData.Confirmation sets ElementAttribute40 itself (retail
MakeConfirmExitDialog writes 0x8E=1, 0xAC=1, 0xC5); the manual set in
GameplayConfirmationController is gone.
F6: MapWindowToCanvas truncates instead of rounding - rounding mapped the
window far edge one past the canvas last valid coordinate, a 1px dead
hit-test band; test updated to truncation semantics + far-edge case.
F7: AD-98 records that the no-letterbox aspect claim has no decomp
citation and is confirmed by the user live gate pass 2026-08-15.
F8: the durable world-name read in StartCore is IsCurrent-gated like every
neighbouring step.
F2 filed as #401 (invert RetailUi to opt-out - product-default decision,
not a gate fix).
App 5100+6 skips, Runtime 1666, green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
4.5 KiB
C#
112 lines
4.5 KiB
C#
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.Net.Messages;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Ports the server-driven confirmation ownership in
|
|
/// <c>gmGamePlayUI @ 0x004E9D70..0x004E9EB6</c>. The dialog factory broadcasts
|
|
/// completion; this semantic owner retains the server type/context and sends the
|
|
/// matching confirmation response.
|
|
/// </summary>
|
|
public sealed class GameplayConfirmationController : IDisposable
|
|
{
|
|
private readonly RetailDialogFactory _dialogs;
|
|
private readonly Action<uint, uint, bool> _sendResponse;
|
|
private readonly Func<uint, string, string?>? _composeMessage;
|
|
private uint _dialogContext;
|
|
private uint _serverType;
|
|
private uint _serverContext;
|
|
private bool _disposed;
|
|
|
|
public GameplayConfirmationController(
|
|
RetailDialogFactory dialogs,
|
|
Action<uint, uint, bool> sendResponse,
|
|
Func<uint, string, string?>? composeMessage = null)
|
|
{
|
|
_dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
|
|
_sendResponse = sendResponse ?? throw new ArgumentNullException(nameof(sendResponse));
|
|
_composeMessage = composeMessage;
|
|
_dialogs.DialogClosed += OnDialogClosed;
|
|
}
|
|
|
|
public uint ActiveDialogContext => _dialogContext;
|
|
|
|
public bool HandleRequest(GameEvents.CharacterConfirmationRequest request)
|
|
{
|
|
// ClientUISystem::Handle_Character__ConfirmationRequest @ 0x005640A0
|
|
// routes types 2/3/5/6 to handlers that append " Continue?"; the generic
|
|
// type-7 YesNo request preserves the server text verbatim. Types 1 and 4
|
|
// have allegiance/fellowship semantic owners but use the same response
|
|
// tuple, so this controller retains that tuple until those panels exist.
|
|
// Each retail RecvNotice_* handler stores the tuple before calling the
|
|
// shared maker, including the already-open case.
|
|
_serverType = request.Type;
|
|
_serverContext = request.ContextId;
|
|
|
|
// gmGamePlayUI::MakeGameplayConfirmationDialog @ 0x004EB890 refuses a
|
|
// second gameplay confirmation while its context is non-zero.
|
|
if (_dialogContext != 0u)
|
|
return false;
|
|
|
|
// Types 1/4 arrive as ACE's bare player name; retail's own client
|
|
// wraps it through the StringInfo template mechanism
|
|
// (RecvNotice_SwearAllegiance / gmFellowshipUI's FellowshipRequest —
|
|
// the injected composer owns the resolve). A null compose falls back
|
|
// to the bare wire message rather than invented English.
|
|
string message = request.Type is 2u or 3u or 5u or 6u
|
|
? request.Message + " Continue?"
|
|
: _composeMessage?.Invoke(request.Type, request.Message)
|
|
?? request.Message;
|
|
// ElementAttribute40 now comes from RetailDialogData.Confirmation
|
|
// itself (batch review F5 — retail's confirmation builders all set it).
|
|
var data = RetailDialogData.Confirmation(message);
|
|
_dialogContext = _dialogs.MakeDialog(data);
|
|
return _dialogContext != 0u;
|
|
}
|
|
|
|
public bool HandleDone(GameEvents.CharacterConfirmationDone done)
|
|
{
|
|
// gmGamePlayUI::RecvNotice_AbortConfirmationRequest @ 0x004E9D70
|
|
// matches both server fields before closing the local dialog context.
|
|
if (_dialogContext == 0u
|
|
|| done.Type != _serverType
|
|
|| done.ContextId != _serverContext)
|
|
return false;
|
|
return _dialogs.CloseDialog(_dialogContext);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forget any tuple left after the dialog factory has completed its normal
|
|
/// retail close/reset callbacks.
|
|
/// </summary>
|
|
public void ResetSession()
|
|
{
|
|
_dialogContext = 0u;
|
|
_serverType = 0u;
|
|
_serverContext = 0u;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_dialogs.DialogClosed -= OnDialogClosed;
|
|
}
|
|
|
|
private void OnDialogClosed(uint context, RetailDialogData data)
|
|
{
|
|
if (context != _dialogContext
|
|
|| data.GetUInt32(RetailDialogProperty.Type) !=
|
|
(uint)RetailDialogType.Confirmation)
|
|
return;
|
|
|
|
bool accepted = data.GetBoolean(RetailDialogProperty.ConfirmationResult);
|
|
// gmGamePlayUI::CloseGameplayConfirmationDialog @ 0x004E9E80 sends
|
|
// before clearing its retained server/dialog context fields.
|
|
_sendResponse(_serverType, _serverContext, accepted);
|
|
_dialogContext = 0u;
|
|
_serverType = 0u;
|
|
_serverContext = 0u;
|
|
}
|
|
}
|