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

@ -0,0 +1,81 @@
using AcDream.Core.Items;
namespace AcDream.Core.Chat;
/// <summary>
/// Composes the client-side "The &lt;item&gt; can't be &lt;verb&gt;" refusal
/// line retail shows when the server rejects an inventory request
/// (InventoryServerSaveFailed 0x00A0).
/// </summary>
/// <remarks>
/// Exact port of <c>ACCWeenieObject::ServerSaysAttemptFailed @ 0x0058EAE0</c>:
/// the base text switches on the latched request kind
/// (<c>ACCWeenieObject::prevRequest</c>, enum <c>InventoryRequest</c>,
/// <c>acclient.h:6812-6825</c>), then an error-code suffix is appended, and
/// the result is displayed via <c>ECM_UI::SendNotice_DisplayStringInfo(0x1A)</c>
/// — LogTextType 0x1A (<see cref="RetailLogTextType.ClientLocal"/>), the
/// SpewBox-only channel. The event dispatcher (<c>case 0xA0 @ 0x0055B342</c>)
/// additionally routes most errors through
/// <c>ClientCommunicationSystem::HandleFailureEvent</c>; the exclusion set it
/// checks first lives in <see cref="SuppressesGenericFailureText"/>.
/// </remarks>
public static class InventoryFailureMessages
{
/// <summary>
/// The refusal line for a failed inventory request, or null when the
/// request kind has no retail verb (retail shows nothing rather than an
/// invented sentence). <paramref name="itemName"/> must already be the
/// retail-appropriate display name (plural for merge/split —
/// <c>NAME_PLURAL</c>; appropriate otherwise).
/// </summary>
public static string? Compose(
InventoryRequestKind kind,
string itemName,
uint weenieError)
{
// ServerSaysAttemptFailed's verb switch. acdream has no latched kind
// for retail's IR_MOVE ("moved") or IR_WIELD ("wielded") today —
// wields ride AutoWieldController without the single-request gate —
// so those rows are absent rather than guessed onto a wrong kind.
string? verb = kind switch
{
InventoryRequestKind.Merge => "merged",
InventoryRequestKind.SplitToContainer => "split",
InventoryRequestKind.SplitToWorld => "split",
InventoryRequestKind.Pickup => "picked up",
InventoryRequestKind.PutInContainer => "put in the container",
InventoryRequestKind.DropToWorld => "dropped",
InventoryRequestKind.Give => "given",
_ => null,
};
if (verb is null)
return null;
return $"The {itemName} can't be {verb}{Suffix(weenieError)}";
}
/// <summary>
/// ServerSaysAttemptFailed's error suffix map; every other code —
/// including 0x426 AttunedItem — gets no suffix.
/// </summary>
private static string Suffix(uint weenieError) => weenieError switch
{
0x1Du => " - you're too busy",
0x20u => " - you must control both objects",
0x28u => " - the item is under someone else's control",
0x2Au => " - you are too encumbered",
0x36u => " - action cancelled",
0x37u or 0x38u or 0x39u => " - unable to move to object",
0x3EEu => " - the container is closed",
_ => "",
};
/// <summary>
/// The 0x00A0 dispatcher's exclusion set (<c>@ 0x0055B342</c>): these
/// errors skip the <c>HandleFailureEvent</c> leg entirely (their text, if
/// any, is owned elsewhere).
/// </summary>
public static bool SuppressesGenericFailureText(uint weenieError)
=> weenieError is 0x1Eu or 0x2Bu or 0x3EFu or 0x43Eu or 0x4CEu
or 0x4CFu or 0x46Au;
}