acdream/src/AcDream.App/UI/GameplayConfirmationController.cs
Erik 67fe754dd6 fix: social gate round 2, part 2 - confirmation-dialog sentences + the
refused-drop yellow notice

Item 4 (confirmation dialogs missing text + names): the missing retail
mechanism was StringTable template substitution - an entry is N+1 literal
fragments interleaved with N named variables, composed by
StringTable::GetString @0x004300D0 (no-metalanguage branch @0x004303B7).
ACE sends the bare player name for types 1/4; retail's OWN CLIENT wraps
it. Ported as DatStringResolver.ResolveTemplate (PLAYER hash 0x05506DA2,
the exact compute_str_hash space; Chorizite stores the variable hashes
directly):

- Server-driven type 4 -> ID_Fellowship_FellowshipRequest, type 1 ->
  ID_Allegiance_AcceptSwearConfirmation, injected into
  GameplayConfirmationController; null resolve falls back to the bare
  wire message, never invented English. The 2/3/5/6 " Continue?" family
  never consults the composer.
- Local Swear/Break/Kick: the bind-time fragment-0 latch (which showed
  the dangling "Do you wish to swear to ") is replaced by click-time
  ResolveTemplate with the target's name.

All five templates verified token-free in the installed DAT - this is
NOT a StringTableMetaLanguage port (AD-81's engine caveat stands).

Item 5 (refused drop shows nothing; retail shows yellow top-center
text): the prevRequest latch was ALREADY ported (InventoryTransactionState);
what was missing was the consumer. InventoryTransactionState now raises
RequestFailed(request, weenieError) when a 0x00A0 clears the latch;
ItemInteractionController composes ServerSaysAttemptFailed @0x0058EAE0's
"The <item> can't be <verb>" (verb table + suffix map ported verbatim in
Core's InventoryFailureMessages, NAME_PLURAL for merge/split) and routes
it as LogTextType 0x1A ClientLocal -> the SpewBox, retail's yellow
top-center line. The dispatcher's second leg (@0x0055B342) also runs:
outside the 7-code exclusion set, WeenieErrorMessages resolves per-code
text/destination; 0x426 AttunedItem has no row in either place beyond
the verb line - faithful single-line output.

Register: AD-85 narrowed to its numeric-field item, AD-81 amended (the
token-free interleave is now ported; meta-token engine + FormatName
remain), AD-93 filed (wire-guid-match vs retail's latched-guid
preference; no Move/Wield latch kinds).

Tests: +2 InventoryTransactionState failure-latch, +5 ResolveTemplate
(constructed StringTable fixtures), +1 composer injection, +1 end-to-end
refused-drop line. Core 4,697/1 skip, App 4,983/3 skips.

Research: docs/research/2026-08-13-confirm-and-weenie-error-display.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 21:10:42 +02:00

111 lines
4.4 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;
var data = RetailDialogData.Confirmation(message)
.Set(RetailDialogProperty.ElementAttribute40, true);
_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;
}
}