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:
parent
fc62cb6397
commit
67fe754dd6
15 changed files with 1133 additions and 50 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -59,6 +60,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly StackSplitQuantityState? _stackSplitQuantity;
|
||||
private readonly Func<bool> _dragOnPlayerOpensSecureTrade;
|
||||
private readonly Action<string>? _systemMessage;
|
||||
private readonly Action<string, RetailLogTextType>? _interfaceText;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
private readonly Action<uint, ItemUseRequestReservation>? _requestUse;
|
||||
// Slice 6.3: vendorGuid, itemGuid, amount, alternateCurrencyId -> true
|
||||
|
|
@ -115,7 +117,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<uint, ItemUseRequestReservation>? requestUse = null,
|
||||
Func<uint, uint, int, uint, bool>? sendBuy = null,
|
||||
Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, uint, bool>? sendBuyAll = null,
|
||||
Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, bool>? sendSell = null)
|
||||
Func<uint, IReadOnlyList<(int Amount, uint ItemGuid)>, bool>? sendSell = null,
|
||||
Action<string, RetailLogTextType>? interfaceText = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -144,6 +147,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_stackSplitQuantity = stackSplitQuantity;
|
||||
_dragOnPlayerOpensSecureTrade = dragOnPlayerOpensSecureTrade ?? (() => true);
|
||||
_systemMessage = systemMessage;
|
||||
_interfaceText = interfaceText;
|
||||
_requestUse = requestUse;
|
||||
_sendBuy = sendBuy;
|
||||
_sendBuyAll = sendBuyAll;
|
||||
|
|
@ -171,7 +175,9 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_transactions.StateChanged += OnTransactionStateChanged;
|
||||
_transactions.RequestCompleted += OnInventoryRequestCompleted;
|
||||
_transactions.RequestFailed += OnInventoryRequestFailed;
|
||||
_transactions.ObjectTableCleared += OnInventoryObjectsCleared;
|
||||
_objects.MoveRequestFailed += OnMoveRequestFailedNotice;
|
||||
}
|
||||
|
||||
public event Action? StateChanged;
|
||||
|
|
@ -1338,12 +1344,70 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_pendingBackpackPlacement = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>ACCWeenieObject::ServerSaysAttemptFailed @ 0x0058EAE0</c>: the
|
||||
/// server rejected the latched inventory request — compose
|
||||
/// "The <item> can't be <verb>" and route it as LogTextType
|
||||
/// 0x1A (ClientLocal, the SpewBox-only channel). No resolvable item or no
|
||||
/// latched kind → no text, exactly retail's silence.
|
||||
/// </summary>
|
||||
private void OnInventoryRequestFailed(
|
||||
PendingInventoryRequest request,
|
||||
uint weenieError)
|
||||
{
|
||||
if (_interfaceText is null)
|
||||
return;
|
||||
ClientObject? item = request.ItemIdentity ?? _objects.Get(request.ItemId);
|
||||
if (item is null)
|
||||
return;
|
||||
// NAME_PLURAL for merge/split, NAME_APPROPRIATE otherwise (the
|
||||
// ServerSaysAttemptFailed name-style column); a wire-omitted plural
|
||||
// falls back to the appropriate form.
|
||||
bool plural = request.Kind
|
||||
is InventoryRequestKind.Merge
|
||||
or InventoryRequestKind.SplitToContainer
|
||||
or InventoryRequestKind.SplitToWorld;
|
||||
string name = plural && !string.IsNullOrEmpty(item.PluralName)
|
||||
? item.PluralName
|
||||
: item.GetAppropriateName();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return;
|
||||
if (InventoryFailureMessages.Compose(request.Kind, name, weenieError)
|
||||
is { } text)
|
||||
{
|
||||
_interfaceText(text, RetailLogTextType.ClientLocal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The 0x00A0 dispatcher's second leg (<c>case 0xA0 @ 0x0055B342</c>):
|
||||
/// unless the error is in the exclusion set, the generic
|
||||
/// <c>HandleFailureEvent</c> table also runs — independent of whether a
|
||||
/// request was latched. Codes without a table row (0x426 AttunedItem
|
||||
/// among them) resolve to null text and stay silent.
|
||||
/// </summary>
|
||||
private void OnMoveRequestFailedNotice(MoveRequestFailure failure)
|
||||
{
|
||||
if (_interfaceText is null
|
||||
|| failure.WeenieError == 0u
|
||||
|| InventoryFailureMessages.SuppressesGenericFailureText(
|
||||
failure.WeenieError))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var (text, type) = WeenieErrorMessages.Resolve(failure.WeenieError, null);
|
||||
if (text is not null)
|
||||
_interfaceText(text, type);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_interactionState.Changed -= OnInteractionModeChanged;
|
||||
_objects.MoveRequestFailed -= OnMoveRequestFailedNotice;
|
||||
_transactions.ObjectTableCleared -= OnInventoryObjectsCleared;
|
||||
_transactions.RequestFailed -= OnInventoryRequestFailed;
|
||||
_transactions.RequestCompleted -= OnInventoryRequestCompleted;
|
||||
_transactions.StateChanged -= OnTransactionStateChanged;
|
||||
WorldDropDispatched = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue