fix(chat): CH4 re-review fixes — dialog-queue reentrancy, settings option-bit chokepoint

Should-fix 1: RetailDialogFactory.CloseDialog's queued branch removed the
active DialogInfo, ran DialogDone (whose callback can synchronously open a
new dialog under the SAME queue key — the two-stage house-abandon
confirmation does exactly this), then called OpenNextDialog, which did an
unconditional Dictionary.Add on a key the reentrant dialog had already
re-occupied. Retail's HashTable::add tolerates the duplicate; Dictionary
throws. OpenNextDialog now returns early when the queue key is already
active — the reentrant dialog's own eventual close drains the queue.

Should-fix 2: @join/@leave wrote the local RuntimeCharacterOptionsState bit
before sending, but the Settings Chat toggles reached a second binding
(SendSingleCharacterOption) that only sent the wire message, leaving the
Turbine membership gate stale until the next PlayerDescription.
LiveSessionRuntimeFactory.CreateCommandBindings now has one shared local
function for both entrances.

Should-fix 3: corrected TS-68/#360 wording again — retail's DoAllegiance
dispatcher table EXECUTES boot/ban/officer/title/motd/name/lock/house/
chat/broadcast locally through their own handlers; acdream shows the
unrecognized-subcommand refusal for all nine pending the #360 port. What
matches retail is the ownership rule (the verb never reaches
DoChannelCommand/the server), not the subcommand behavior itself. Removed
the inaccurate "matching retail, not merely harmless" / "now matches
this" claims from both the register row and the issue.

Nits: corrected the HouseAbandonDialogCallback_First citation (0x00580E1A
is DoHouse's load site for the callback pointer, not the function entry —
the entry is 0x00580240, with the stage-2 confirmation string built at
0x005802D8) in both ClientCommandController.cs and the mirrored test
comment; added an InlineData case pinning "@clist allegiance" to
RequestChannelList(0x02000000); converted RetailClientCommandCatalog.
KnownVerbs from a plain array to a FrozenSet<string> with
StringComparer.OrdinalIgnoreCase, matching the file's other lookup tables.

Suite: 12,221 passed / 4 skipped / 0 failed (Release), up from CH4's
12,216/4/0 — net +5 tests, no removals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-09 22:24:43 +02:00
parent 59c053ee47
commit 5d247d5518
10 changed files with 204 additions and 34 deletions

View file

@ -342,15 +342,18 @@ public sealed class ClientCommandController
// GameActionHouseAbandon — "@house abandon". Retail's abandon
// branch (DoHouse @ 0x00580D58) opens a FIRST confirmation
// dialog (DialogFactory::MakeCallbackDialogInCurrentUI →
// HouseAbandonDialogCallback_First @0x00580E1A); only on
// accept does that callback open a SECOND dialog
// (HouseAbandonDialogCallback_Second @0x0057BE90), and only
// THAT callback's accept calls Event_AbandonHouse()
// HouseAbandonDialogCallback_First, function entry @0x00580240
// — 0x00580E1A is only the load site inside DoHouse where the
// callback pointer is fetched); only on accept does that
// callback open a SECOND dialog (its own stage-2 string site
// @0x005802D8 → HouseAbandonDialogCallback_Second @0x0057BE90),
// and only THAT callback's accept calls Event_AbandonHouse()
// (0x0057BF01 — the ONLY call site). Both strings recovered
// verbatim from acclient_2013_pseudo_c.txt (data_7e1460 /
// data_7e1370). CH4 REJECT-review Blocker 2 (2026-08-09):
// acdream previously sent 0x021F immediately with NO
// confirmation at all.
// confirmation at all. Citation corrected 2026-08-09, CH4
// re-review nit 4.
case ClientCommandId.HouseAbandon:
_bindings.ShowConfirmation(
"Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",

View file

@ -322,6 +322,16 @@ public sealed class RetailDialogFactory : IDisposable
private void OpenNextDialog(uint queueKey)
{
// A callback invoked from DialogDone (above, in CloseDialog) may
// synchronously make a new dialog under this same queue key before
// control returns here — MakeDialog's queued branch will have
// already re-occupied _activeQueued[queueKey]. Retail's HashTable::add
// tolerates the duplicate; Dictionary.Add does not. Bail out: the
// re-entrant dialog's own eventual CloseDialog will drain the
// pending queue via its own OpenNextDialog call.
if (_activeQueued.ContainsKey(queueKey))
return;
if (!_pending.TryGetValue(queueKey, out LinkedList<DialogInfo>? queue)
|| queue.First is null)
return;