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>
405 lines
22 KiB
Markdown
405 lines
22 KiB
Markdown
# Confirmation-dialog text & refused-drop notification — research (2026-08-13 user gate)
|
|
|
|
**Scope:** two presentation gaps observed at the 2026-08-13 connected gate.
|
|
Read-only research; no source edits. Both questions end in an implementation
|
|
recipe. All retail addresses are the Sept 2013 EoR build
|
|
(`docs/research/named-retail/acclient_2013_pseudo_c.txt`, cited below as
|
|
`pseudo-c:<line>` `@<address>`); register-elided constants were byte-decoded
|
|
from the PDB-paired binary `C:\Users\erikn\Downloads\acclient.exe` (the
|
|
`reference_pe_byte_decode` method); dat template strings were dumped from the
|
|
**installed** `client_local_English.dat`
|
|
(`%USERPROFILE%\Documents\Asheron's Call\`, the same file the client runs
|
|
against — relevant per open issue #383 installed-vs-fixture drift).
|
|
|
|
---
|
|
|
|
## Q1 — Fellowship-invite / allegiance-swear confirmation dialog text
|
|
|
|
### 1.1 What ACE actually sends in `0x0274 Character.ConfirmationRequest`
|
|
|
|
Wire layout (`references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventConfirmationRequest.cs:7-13`):
|
|
|
|
```
|
|
uint32 confirmationType // 1 = SwearAllegiance, 4 = Fellowship (ConfirmationType.cs:5-12)
|
|
uint32 contextId // per-player UIntSequence (ConfirmationManager.cs:35)
|
|
String16L text
|
|
```
|
|
|
|
**The `text` field is the BARE PLAYER NAME in both campaign-FA cases — never a
|
|
composed sentence:**
|
|
|
|
| Type | Sender | `text` value |
|
|
|---|---|---|
|
|
| 4 Fellowship | `references/ACE/Source/ACE.Server/Entity/Fellowship.cs:126` — `EnqueueSend(new Confirmation_Fellowship(inviter.Guid, newMember.Guid), inviter.Name)` | the **inviter's** name |
|
|
| 1 SwearAllegiance | `references/ACE/Source/ACE.Server/WorldObjects/Player_Allegiance.cs:90` — `EnqueueSend(new Confirmation_SwearAllegiance(patron.Guid, Guid), Name)` | the **would-be vassal's** name |
|
|
|
|
Both route through `ConfirmationManager.EnqueueSend`
|
|
(`references/ACE/Source/ACE.Server/WorldObjects/Managers/ConfirmationManager.cs:33-51`),
|
|
which assigns the context id and arms a 30 s timeout. ACE's own comment in
|
|
`GameEventConfirmationRequest.cs:8` ("172 is the max seen in retail pcaps")
|
|
plus the retail client code below confirm retail servers sent the same
|
|
shape — a short string the CLIENT wraps.
|
|
|
|
### 1.2 What retail composes per type (byte-verified end to end)
|
|
|
|
Client dispatch: `ClientUISystem::Handle_Character__ConfirmationRequest
|
|
@0x005640A0` (`pseudo-c:368625-368669`) is a bare 7-way switch on the type:
|
|
|
|
- case **1** → `CM_Allegiance::SendNotice_SwearAllegianceRequest(text, ctx)` (`@0x006A7420`)
|
|
- case **4** → `CM_Fellowship::SendNotice_FellowshipRequest(text, ctx)` (`@0x006A6650`)
|
|
- cases 2/3/5/6/7 → the gmGamePlayUI family acdream already ports.
|
|
|
|
Receivers and dialog builders:
|
|
|
|
- `gmFellowshipUI::RecvNotice_FellowshipRequest @0x00490880`
|
|
(`pseudo-c:156924`) → `gmFellowshipUI::MakeFellowRequestDialog @0x00490620`
|
|
(`pseudo-c:156764`)
|
|
- `gmAllegianceUI::RecvNotice_SwearAllegianceRequest @0x00493110`
|
|
(`pseudo-c:159256`) → `gmAllegianceUI::MakeAcceptSwearConfirmationDialog
|
|
@0x00492990` (`pseudo-c:158824`)
|
|
|
|
Both builders are structurally identical (guarded by their own
|
|
one-outstanding context field — `m_fellowRequestContext` /
|
|
`m_acceptSwearContext` — and both store the server context id for the
|
|
eventual `CM_Character::Event_ConfirmationResponse` `0x0275`):
|
|
|
|
```
|
|
StringInfo si;
|
|
si.SetStringIDandTableEnum(<STRING_ID>, 0x10000001); // table enum → StringTable DID
|
|
si.AddVariable_String(<VAR_KEY>, wireText); // wireText = the bare name from 0x0274
|
|
PropertyCollection props;
|
|
props[0x8E] = 1; // dialog kind
|
|
props[0xC5] = si; // dialog text StringInfo
|
|
DialogFactory::MakeDialogInCurrentUI(props);
|
|
m_ui<...>ServerContextID = contextId;
|
|
```
|
|
|
|
The decompiler prints the `<STRING_ID>` and `<VAR_KEY>` arguments as literal
|
|
`0` — a known BN artifact (the globals' static initializer is 0; they are
|
|
hashed at startup). **Byte-decoded from the paired binary:**
|
|
|
|
- `MakeFellowRequestDialog` @0x0049065B: `A1 50 E1 83 00` =
|
|
`mov eax, [0x0083E150]` → **`ID_Fellowship_FellowshipRequest`**
|
|
(`pseudo-c:1147714`); @0x00490685: `8B 0D 70 E1 83 00` =
|
|
`mov ecx, [0x0083E170]` → **`ID_Player`** (`pseudo-c:1147722`).
|
|
- `MakeAcceptSwearConfirmationDialog` @0x004929CB: `A1 04 E2 83 00` →
|
|
`[0x0083E204]` = **`ID_Allegiance_AcceptSwearConfirmation`**
|
|
(`pseudo-c:1147783`); @0x004929F5: `8B 0D 34 E2 83 00` → `[0x0083E234]` =
|
|
**`ID_Player`** (`pseudo-c:1147795`).
|
|
|
|
The startup hash inits: `ID_Player = compute_str_hash("PLAYER")`
|
|
(`pseudo-c:767722 @0x006EE4DD`) — and `compute_str_hash("PLAYER")` =
|
|
**`0x05506DA2`**, which is EXACTLY the variable-name hash stored in the dat
|
|
entries below, closing the chain. Table enum `0x10000001` maps (via
|
|
`DBObj::GetDIDByEnum`, `StringInfo::SetStringIDandTableEnum @0x0042C760`,
|
|
`pseudo-c:48643-48656`, DivineType `0x25` = StringTable) to StringTable DID
|
|
**`0x23000001`** — empirically confirmed because the dump below found every
|
|
key there, and `SocialAllegiancePageController` already resolves its sibling
|
|
keys against `0x23000001` (`SocialAllegiancePageController.cs:139`).
|
|
|
|
**The substitution mechanism** (this is the part AD-81/AD-85 recorded as
|
|
unported): a StringTable entry is NOT "one string + variants" — for templated
|
|
entries it is **N+1 literal fragments interleaved with N variables**.
|
|
`StringInfo::InqStringInternal @0x0042E020` (`pseudo-c:50432`) builds a map
|
|
{variableNameHash → resolved value} from the StringInfo's `m_variables`, then
|
|
`StringTable::GetString @0x004300D0` (`pseudo-c:52601`) walks the entry —
|
|
the no-metalanguage branch @0x004303B7 is the canonical shape:
|
|
|
|
```
|
|
out = "";
|
|
for i in 0 .. numStrings-1:
|
|
out += strings[i]; // literal fragment
|
|
if i < numVariables:
|
|
out += varMap[variableNameHash[i]]; // substituted value ("" + fail flag if missing)
|
|
```
|
|
|
|
(The live path passes `useMetaLanguage=1` and goes through
|
|
`StringTableMetaLanguage::RenderString` @0x004302B1 + `StripMetaLetters`,
|
|
which additionally handles `%(...)`-style meta tokens — **none of the five
|
|
strings below contain any**, so plain interleave is byte-equivalent for
|
|
them. Decoding `RenderString` in general remains out of scope, exactly as
|
|
AD-81 warns.)
|
|
|
|
**The actual templates** (installed `client_local_English.dat`, StringTable
|
|
`0x23000001`; hash = `DatStringResolver.ComputeHash` of the key, the exact
|
|
`compute_str_hash @0x00413110` port):
|
|
|
|
| Key | Hash | Fragments | Variables |
|
|
|---|---|---|---|
|
|
| `ID_Fellowship_FellowshipRequest` | `0x08D09E44` | `[""]` + `[" has invited you to join their fellowship. Do you accept?"]` | `[PLAYER=0x05506DA2]` |
|
|
| `ID_Allegiance_AcceptSwearConfirmation` | `0x056EA6EE` | `[""]` + `[" would like to swear allegiance to you. Do you accept?"]` | `[PLAYER]` |
|
|
| `ID_Allegiance_SwearConfirmation` (local, vassal-side) | `0x048B3F2E` | `["Do you wish to swear to ", "?"]` | `[PLAYER]` |
|
|
| `ID_Allegiance_BreakConfirmation` (local) | `0x0BDA6FDE` | `["Are you sure you wish to break from ", "?"]` | `[PLAYER]` |
|
|
| `ID_Allegiance_KickConfirmation` (local) | `0x09692CBE` | `["Are you sure you wish to kick ", " from your allegiance?"]` | `[PLAYER]` |
|
|
|
|
So retail's composed dialogs are, exactly:
|
|
|
|
- Fellowship invite (recruit sees): **`<inviterName> has invited you to join their fellowship. Do you accept?`**
|
|
- Incoming swear (patron sees): **`<vassalName> would like to swear allegiance to you. Do you accept?`**
|
|
- Local swear (vassal clicks Swear): **`Do you wish to swear to <targetName>?`** (name from
|
|
`GetObjectName(NAME_APPROPRIATE)` — lane C `docs/research/2026-08-11-fa-allegiance-wire.md` §1.3 item 2)
|
|
- Local break / kick: analogous.
|
|
|
|
### 1.3 What acdream does today, and exactly what is missing
|
|
|
|
**Parse — present and correct.**
|
|
`GameEvents.ParseCharacterConfirmationRequest`
|
|
(`src/AcDream.Core.Net/Messages/GameEvents.cs:514-531`) reads
|
|
type/contextId/String16L. Registered at `GameEventWiring.cs:319-326`, routed
|
|
`LiveSessionRuntimeFactory.cs:315-316` →
|
|
`RetailUiRuntime.HandleConfirmationRequest` (`RetailUiRuntime.cs:749-750`) →
|
|
`GameplayConfirmationController.HandleRequest`
|
|
(`src/AcDream.App/UI/GameplayConfirmationController.cs:32-56`). The response
|
|
leg (`0x0275`) and Done leg (`0x0276`) are complete.
|
|
|
|
**Display — the server-driven dialogs show the bare name.**
|
|
`GameplayConfirmationController.HandleRequest:49-51` appends `" Continue?"`
|
|
for types 2/3/5/6 (correct — that's retail's gmGamePlayUI handlers) and shows
|
|
`request.Message` VERBATIM for everything else — so for types 1 and 4 the
|
|
dialog body is just `+Horan` (ACE's bare name). **Missing: the client-side
|
|
template resolve + PLAYER substitution of §1.2.** This is the gap register
|
|
rows AD-81/AD-85 recorded (AD-85 item 3 explicitly:
|
|
"ACE sends the target's bare `Name` as the ENTIRE confirmation message …
|
|
retail's own client wraps it via the identical `StringInfo` mechanism").
|
|
|
|
**Display — the LOCAL swear/break/kick dialogs show a dangling fragment.**
|
|
`SocialAllegiancePageController` resolves the templates at bind time
|
|
(`SocialAllegiancePageController.cs:364-369`) through
|
|
`DatStringResolver.Resolve` (`src/AcDream.App/UI/Layout/DatStringResolver.cs:27-45`)
|
|
— but `Resolve` returns **`entry.Strings[token]`, i.e. fragment 0 only**
|
|
(its "token selects one localized variant" model predates the
|
|
fragment-interleave finding above). `OnSwearClick`
|
|
(`SocialAllegiancePageController.cs:448-458`) then shows
|
|
`_swearConfirmationTemplate ?? name` → the user sees literally
|
|
**`Do you wish to swear to `** — truncated, no name, no `?`. That is the
|
|
observed "missing text and no player name" for the outgoing swear confirm.
|
|
Break/Kick (`:463-473`, `:480-490`) have the same shape.
|
|
|
|
**Adjacent, recorded, NOT part of the minimal fix:** retail keeps types 1/4
|
|
in their gm-UI owners with SEPARATE one-outstanding guards;
|
|
acdream's single generic dialog context (already documented in the
|
|
controller's class comment, `GameplayConfirmationController.cs:36-40`) can
|
|
refuse a type-4 while an unrelated gameplay confirm is open. Leave as-is.
|
|
|
|
### 1.4 Implementation recipe (Q1)
|
|
|
|
Minimal, faithful, and it fixes all five dialogs with ONE primitive:
|
|
|
|
1. **Add a template-substituting resolve to `DatStringResolver`** (it already
|
|
caches `StringTable`s and owns `ComputeHash`):
|
|
|
|
```csharp
|
|
// StringTable::GetString @0x004300D0, no-metalanguage branch @0x004303B7:
|
|
// fragments interleaved with variables; N vars, N or N+1 fragments.
|
|
public string? ResolveTemplate(uint tableId, string key,
|
|
IReadOnlyDictionary<uint, string> variables)
|
|
{
|
|
// look up entry by ComputeHash(key); return null if absent;
|
|
// sb: for i in 0..Strings.Count-1 { sb.Append(Strings[i].Value);
|
|
// if (i < Variables.Count)
|
|
// sb.Append(variables.TryGetValue(Variables[i], out var v) ? v : ""); }
|
|
}
|
|
public static readonly uint PlayerVariable = ComputeHash("PLAYER"); // 0x05506DA2
|
|
```
|
|
|
|
Guard: refuse (return null) if the entry contains metalanguage tokens is
|
|
NOT needed for these five (verified token-free), but do not advertise this
|
|
as a general `StringTableMetaLanguage` port — AD-81's scope note stands.
|
|
|
|
2. **Server-driven dialogs (types 1/4):** where
|
|
`RetailUiRuntime.HandleConfirmationRequest` (or
|
|
`GameplayConfirmationController.HandleRequest` via an injected
|
|
`Func<uint,string,string?>` — the controller is constructed at
|
|
`RetailUiRuntime.cs:2981-2983` where `_bindings.Assets.Dats` is in scope,
|
|
same as the 20 existing `new DatStringResolver(...)` sites) — resolve:
|
|
- type 4 → `ResolveTemplate(0x23000001, "ID_Fellowship_FellowshipRequest", {PLAYER: request.Message})`
|
|
- type 1 → `ResolveTemplate(0x23000001, "ID_Allegiance_AcceptSwearConfirmation", {PLAYER: request.Message})`
|
|
- null → fall back to the current bare `request.Message` (never invent
|
|
English — AD-85's disposition). Types 2/3/5/6/7 unchanged.
|
|
|
|
3. **Local swear/break/kick:** replace the bind-time
|
|
`ResolveString(...)`-fragment-0 latch with the same
|
|
`ResolveTemplate(0x23000001, key, {PLAYER: targetName})` at click time
|
|
(the name is already fetched: `SocialAllegiancePageController.cs:450`,
|
|
`:466`, `:483`). Fallback stays the bare name.
|
|
|
|
4. **Register bookkeeping (same commit as the fix):** narrow AD-85 (its
|
|
item 2 dialogs and item 3 wire-side gap become ported; its item 1 numeric
|
|
fields — 2-variable templates like `ID_Fellowship_FellowStats` — can now
|
|
ALSO be fixed by the same primitive, or stay recorded); narrow AD-81
|
|
accordingly (its `StringTableMetaLanguage` engine caveat remains for
|
|
meta-token templates; `ACCharGenData::FormatName` remains open).
|
|
|
|
---
|
|
|
|
## Q2 — Refused-drop yellow top-of-screen notification
|
|
|
|
### 2.1 What ACE sends when a drop is refused
|
|
|
|
`references/ACE/Source/ACE.Server/WorldObjects/Player_Inventory.cs:1371-1470`
|
|
(`HandleActionDropItem`):
|
|
|
|
| Refusal | What ACE sends |
|
|
|---|---|
|
|
| **Attuned item** (`:1389-1393`) | `GameEventInventoryServerSaveFailed(itemGuid, WeenieError.AttunedItem)` — **`0x00A0` with error `0x0426`**. Nothing else. |
|
|
| Busy/teleporting (`:1373-1378`) | `GameEventWeenieError(YoureTooBusy=0x1D)` + `0x00A0` with error None |
|
|
| Summoned-pet device (`:1395-1400`) | transient string + `0x00A0` error None |
|
|
| Item being traded (`:1402-1406`) | `0x00A0` with `TradeItemBeingTraded` |
|
|
| Teleported mid-chain (`:1412-1416`) | `0x00A0` with `ActionCancelled=0x36` |
|
|
|
|
Layout (`GameEventInventoryServerSaveFailed.cs:7-16`, opcode
|
|
`GameEventType.cs:18` = `0x00A0`): `uint32 itemGuid; uint32 weenieError`.
|
|
ACE's own comment: "client doesn't show this error mostly, and defaults to
|
|
specific error messages, depending on the item name + action" — which is
|
|
exactly the retail mechanism below.
|
|
|
|
### 2.2 Where retail displays it, and in what presentation
|
|
|
|
**Dispatch** — event case `0xA0` @0x0055B342 (`pseudo-c:359365-359384`):
|
|
|
|
1. Prefers the client's own latched `ACCWeenieObject::prevRequestObjectID`
|
|
over the wire guid, looks the item up, and calls
|
|
`ACCWeenieObject::ServerSaysAttemptFailed(item, err, 1)`.
|
|
2. Then, unless `err ∈ {0x1E, 0x2B, 0x3EF, 0x43E, 0x4CE, 0x4CF, 0x46A}`,
|
|
also calls `ClientCommunicationSystem::HandleFailureEvent(err, "")`
|
|
(`@0x00571990` — the 344-row WeenieError→text switch the CH campaign
|
|
ported). **`0x0426 AttunedItem` has NO case in that switch** (verified in
|
|
both the decomp and acdream's ported table), so this leg shows nothing
|
|
for an attuned drop — faithful silence.
|
|
|
|
**Composition** — `ACCWeenieObject::ServerSaysAttemptFailed @0x0058EAE0`
|
|
(`pseudo-c:409283-409448`) switches on the latched request KIND
|
|
(`ACCWeenieObject::prevRequest`, enum `InventoryRequest`,
|
|
`acclient.h:6812-6825`):
|
|
|
|
| `prevRequest` | Base text (`%s` = `GetObjectNameWide`) | Name style |
|
|
|---|---|---|
|
|
| `IR_MERGE=1` | `The %s can't be merged` | `NAME_PLURAL` |
|
|
| `IR_SPLIT=2` | `The %s can't be split` | `NAME_PLURAL` |
|
|
| `IR_MOVE=3` | `The %s can't be moved` | `NAME_APPROPRIATE` (=2, `acclient.h:6833`) |
|
|
| `IR_PICK_UP=4` | `The %s can't be picked up` | appropriate |
|
|
| `IR_PUT_IN_CONTAINER=5` | `The %s can't be put in the container` | appropriate |
|
|
| **`IR_DROP=6`** | **`The %s can't be dropped`** | appropriate |
|
|
| `IR_WIELD=7` | `The %s can't be wielded` | appropriate |
|
|
| `IR_GIVE=9` | `The %s can't be given` | appropriate |
|
|
|
|
then appends an error-code suffix (the decomp's `__return_addr` compares are
|
|
a BN artifact for the error argument):
|
|
|
|
| Error | Suffix |
|
|
|---|---|
|
|
| `0x1D` YoureTooBusy | ` - you're too busy` |
|
|
| `0x20` IllegalInventoryTransaction | ` - you must control both objects` |
|
|
| `0x28` Frozen | ` - the item is under someone else's control` |
|
|
| `0x2A` YouAreTooEncumbered | ` - you are too encumbered` |
|
|
| `0x36` ActionCancelled | ` - action cancelled` |
|
|
| `0x37-0x39` ObjectGone/NoObject/CantGetThere | ` - unable to move to object` |
|
|
| `0x3EE` TheContainerIsClosed | ` - the container is closed` |
|
|
| anything else (incl. **`0x426`**) | *(no suffix)* |
|
|
|
|
and displays it via **`ECM_UI::SendNotice_DisplayStringInfo(0x1A, si)`**
|
|
(@0x0058EE07), then clears the latch. Type `0x1A` = `ClientLocal`
|
|
(`src/AcDream.Core/Chat/RetailLogTextType.cs:60`) is precisely the SpewBox
|
|
filter: `gmSpewBoxUI`'s `RecvNotice_DisplayFinalStringInfo @0x004D60A0`
|
|
accepts ONLY `0x1A`, and every chat window is born with that bit CLEARED
|
|
(`ChatInterface::ChatInterface @0x004F4550`,
|
|
`m_llTextTypeFilter &= 0xFBFFFFFF` —
|
|
`docs/research/2026-08-09-chat-retail-color-table.md:351`). So the refused
|
|
drop is **SpewBox-only** — the transient top-center interface-text area —
|
|
which acdream renders in the user-pinned retail yellow `0x81C4C8`
|
|
`(1, 1, 0.247, 1)` (`src/AcDream.App/UI/SpewBoxController.cs:238`, pinned at
|
|
CH user-gate round 1 side-by-side vs retail; note the CHAT color table's
|
|
`0x1A` entry is bright red — a different element tree the SpewBox never
|
|
touches, `SpewBoxController.cs:213-237`).
|
|
|
|
**The latch** — `ACCWeenieObject::RecordRequest @0x0058C220`
|
|
(`pseudo-c:406362-406371`) stores `(objectId, kind, time)` in three globals;
|
|
the DROP send site is `ACCWeenieObject::UIAttemptPutIn3D @0x0058D700`
|
|
(`pseudo-c:407816-407836`): sends `CM_Inventory::Event_DropItem`, latches
|
|
`IR_DROP` when the item lives in a container (the normal case), `IR_MOVE` if
|
|
it was already in 3D. One global slot, overwritten per request, cleared on
|
|
success/failure/timeout.
|
|
|
|
Expected retail behavior for the gate case: drop an attuned item →
|
|
**`The <item name> can't be dropped`** in yellow, top-center, ~5 s.
|
|
|
|
### 2.3 What acdream is missing (exactly)
|
|
|
|
- **Parse — present.** `GameEvents.ParseInventoryServerSaveFailed`
|
|
(`src/AcDream.Core.Net/Messages/GameEvents.cs:445-455`) reads
|
|
`(itemGuid, weenieError)`.
|
|
- **Display surface — present.** The SpewBox pipeline is complete and
|
|
user-gated: `RuntimeCommunicationState.AddText` chokepoint routes
|
|
`RetailLogTextType.ClientLocal` to `SpewBoxState`
|
|
(`src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs:96,180-205`),
|
|
rendered yellow/top-center by `SpewBoxController`.
|
|
- **WeenieError table — present.** `WeenieErrorMessages.Resolve`
|
|
(`src/AcDream.Core/Chat/WeenieErrorMessages.cs:172-183`, 344 rows, no
|
|
`0x426` row — faithful) already serves the UseDone path
|
|
(`GameEventWiring.cs:752+`).
|
|
- **Routing — MISSING.** The `0x00A0` handler
|
|
(`src/AcDream.Core.Net/GameEventWiring.cs:726-740`) does ONLY the B-Drag
|
|
optimistic rollback (`ClientObjectTable.RejectMove`,
|
|
`src/AcDream.Core/Items/ClientObjectTable.cs:698-703`) plus a console
|
|
line. Every `MoveRequestFailed` subscriber is state-cleanup only
|
|
(`InventoryController.cs:338-345`, `AutoWieldController.cs:400-409`,
|
|
`InventoryWorldDropProjectionController.cs:100`,
|
|
`InventoryTransactionState.cs:72`). **No user-visible text is produced
|
|
anywhere on this path.**
|
|
- **Request-kind latch — MISSING.** Nothing records "the last inventory
|
|
request was a DROP of guid X" (retail's `prevRequest*` trio), so the
|
|
"can't be dropped" verb cannot be chosen today. acdream's drop send is
|
|
`WorldSession.SendDropItem` (`src/AcDream.Core.Net/WorldSession.cs:2557-2561`,
|
|
wired at `InteractionRetainedUiComposition.cs:322`).
|
|
- **Composer — MISSING.** No port of `ServerSaysAttemptFailed`'s verb table
|
|
+ suffix map.
|
|
- **`HandleFailureEvent` leg — MISSING on this path.** The 0x00A0 route
|
|
never consults `WeenieErrorMessages` (harmless for `0x426`, wrong for
|
|
codes that DO have rows and are not in the exclusion set).
|
|
|
|
### 2.4 Implementation recipe (Q2)
|
|
|
|
1. **Latch the request kind** — a Runtime-owned single-slot
|
|
`(itemGuid, InventoryRequest kind, time)` mirror of retail's
|
|
`RecordRequest @0x0058C220`. Natural home: `RuntimeInventoryState`
|
|
(J4.2 already owns the one-request-at-a-time gate). Write it at every
|
|
inventory send site (drop / pickup / wield / give / merge / split / move /
|
|
put-in-container); overwrite-per-request; clear on the success echo and in
|
|
the 0x00A0 handler after composing (retail clears at
|
|
`@0x0058EE43-0x0058EE63`).
|
|
2. **On `0x00A0`** (after the existing `RejectMove`):
|
|
- If a latch exists (prefer the latched guid over the wire guid, exactly
|
|
retail's `prevRequestObjectID` preference @0x0055B361) and the item
|
|
resolves in `ClientObjectTable`: compose
|
|
`"The {name} can't be {verb}"` from the §2.2 verb table + suffix map,
|
|
and route `RuntimeCommunicationState.AddText(text,
|
|
RetailLogTextType.ClientLocal)` → lands in the SpewBox, yellow,
|
|
top-center. If no latch/item: no text (retail shows nothing —
|
|
do NOT invent a fallback line).
|
|
- Then, mirroring the dispatcher, if `err ∉ {0x1E, 0x2B, 0x3EF, 0x43E,
|
|
0x4CE, 0x4CF, 0x46A}`, run `WeenieErrorMessages.Resolve(err, null)` and
|
|
display per its returned `RetailLogTextType` (null text → silent,
|
|
which covers `0x426`).
|
|
3. **Name style:** retail uses `GetObjectNameWide(NAME_APPROPRIATE)`
|
|
(plural-aware article-free form; `NAME_PLURAL` for merge/split). If the
|
|
first pass uses `ClientObject.Name` raw, file the (tiny) divergence row
|
|
for plural/appropriate handling in the same commit.
|
|
4. **Layer note:** the composer needs the item name (Core `ClientObjectTable`)
|
|
and the Runtime chokepoint — wire it where both are borrowed (the
|
|
App/Runtime composition that already owns the 0x00A0 consumer), not
|
|
inside `GameEventWiring`'s Core.Net registrar body, to keep Code
|
|
Structure Rule 2 intact.
|
|
|
|
---
|
|
|
|
## Verification checklist for the fix session
|
|
|
|
- Fellowship invite (two-client): recruit's dialog reads
|
|
`<inviter> has invited you to join their fellowship. Do you accept?`
|
|
- Incoming swear (two-client): patron's dialog reads
|
|
`<vassal> would like to swear allegiance to you. Do you accept?`
|
|
- Local swear click: `Do you wish to swear to <target>?` (full sentence).
|
|
- Drop an attuned item: yellow top-center `The <item> can't be dropped`,
|
|
no chat-transcript line, dialog-free.
|
|
- Register rows AD-81/AD-85 narrowed in the same commit; new row only if the
|
|
name-style approximation (recipe Q2 step 3) ships.
|