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>
This commit is contained in:
Erik 2026-08-13 21:10:42 +02:00
parent fc62cb6397
commit 67fe754dd6
15 changed files with 1133 additions and 50 deletions

View file

@ -2858,7 +2858,24 @@ public sealed class RetailUiRuntime : IDisposable
// an allegiance member, so the allegiance-profile accessors
// above cannot name them).
ResolveWorldObjectName: guid => _bindings.Inventory.Objects.Get(guid)?.GetAppropriateName(),
ShowConfirmation: (message, completed) => ShowConfirmation(message, completed)),
ShowConfirmation: (message, completed) => ShowConfirmation(message, completed),
// Swear/Break/Kick confirmation sentences: the 0x23000001
// templates' PLAYER slot filled at click time (2026-08-13
// confirm/weenie-error research §1.4 — the AD-85 dangling
// "Do you wish to swear to " fix).
ResolvePlayerTemplate: (key, playerName) =>
{
lock (_bindings.Assets.DatLock)
{
return fellowshipStrings.ResolveTemplate(
0x23000001u,
key,
new Dictionary<uint, string>
{
[Layout.DatStringResolver.PlayerVariable] = playerName,
});
}
}),
Friends: _bindings.Social.Friends,
Squelch: _bindings.Social.Squelch,
TemplateResolver: TemplateResolver,
@ -2978,9 +2995,37 @@ public sealed class RetailUiRuntime : IDisposable
}
DialogFactory = new RetailDialogFactory(Host.Root, CreateLayout);
// Types 1/4 carry ACE's bare player name; retail's client wraps it
// via the local StringTable templates (single PLAYER variable,
// token-free — 2026-08-13 confirm/weenie-error research §1).
var confirmationStrings = new DatStringResolver(_bindings.Assets.Dats);
string? ComposeConfirmation(uint type, string bareName)
{
string? key = type switch
{
// gmAllegianceUI::RecvNotice_SwearAllegiance
1u => "ID_Allegiance_AcceptSwearConfirmation",
// gmFellowshipUI's incoming FellowshipRequest
4u => "ID_Fellowship_FellowshipRequest",
_ => null,
};
if (key is null)
return null;
lock (_bindings.Assets.DatLock)
{
return confirmationStrings.ResolveTemplate(
0x23000001u,
key,
new Dictionary<uint, string>
{
[DatStringResolver.PlayerVariable] = bareName,
});
}
}
_gameplayConfirmationController = new GameplayConfirmationController(
DialogFactory,
_bindings.Confirmations.SendResponse);
_bindings.Confirmations.SendResponse,
ComposeConfirmation);
_itemConfirmationController = new RetailItemConfirmationController(
DialogFactory,
ItemInteraction);