fix(chat): consolidated-review fixes — retail /help Detail extraction, seam wiring test
SHOULD-FIX 1: RetailClientCommandCatalog's ~45 catalog leaf verbs were showing acdream-authored Summary text for /help <verb> instead of retail's own Detail_HelpType(2) text. Byte-swept every Help* handler against the PDB-paired acclient.exe (verified MATCH), confirmed each Detail/Summary branch by reading the actual decompiled if/else shape (address order and string length both proved unreliable alone), and fixed a sweep_weenie_strings.py 800-char truncation bug that silently dropped several longer Detail branches. Resolved every ambiguous CmdHashData-registered verb (hor/hr/hom/hoa/alh/ah/friends_add/ friends_remove/squelch/unsquelch) by reading for Binary Ninja's nullptr-4th-arg decompiler artifact instead of trusting it. Coverage: 42 of 47 distinct catalog Definitions verbatim-extracted, 4 confirmed-null (index/clist/on/off register with a genuinely null help pointer — DoHelp falls to UnknownCommand for these, now reproduced), 1 honest UNVERIFIED (messagetypes builds its text from a runtime enum table, not a static string). ChatCommandRouter now prefers retail Detail text over the catalog summary; RetailCommandHelpTable's class doc no longer overclaims its own scope. SHOULD-FIX 2: extracted the a5a7eb4f-class OnInterfaceText wiring into a testable CreateChatViewModel method and added ComposedChatViewModelWiresOnInterfaceTextToSpewBox, which the prior FakeFactory-based test suite could never exercise. SHOULD-FIX 3: retires register row AP-113. DoLifestone/DoMarketplace print their own 0x1A refusal text (byte-recovered, UTF-16LE) instead of falling through to the generic 0x26 fallback; ChatCommandRouter's comment corrected to state the fallback's real scope. SHOULD-FIX 4: corrected the divergence register's stale AP section header sentence about AP-190's opacity default (refuted bycc582899). NITs: (a) HeadlessStaticStateAudit routes through the injected HeadlessDiagnosticWriter instead of Console.WriteLine; (b) a bounded 300-pump liveness diagnostic on the IsQuiescent conductor gate (no retry, no behavior change); (c) fixed the #365 hydration test's doc comment contradiction against diagnosis §8; (d) the 0x26 fallback dispatches on WeenieErrorMessages' own Type instead of hardcoding ClientLocal. Full Release suite: 12,553 passed / 4 skipped / 0 failed (baseline03404b71: 12,542/4/0; net +11 tests, zero regressions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
03404b7121
commit
f7a6f46ba0
15 changed files with 947 additions and 85 deletions
File diff suppressed because one or more lines are too long
|
|
@ -289,6 +289,7 @@ implementer per slice against a pinned contract (per
|
||||||
| CH6b floating windows 1–4 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions |
|
| CH6b floating windows 1–4 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions |
|
||||||
| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) |
|
| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) |
|
||||||
| Goal-window #363/#367 interface-text seam | `09453eca` | 12,542 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox now flashes for the reclassified refusals (see "Goal-window follow-up" below) |
|
| Goal-window #363/#367 interface-text seam | `09453eca` | 12,542 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox now flashes for the reclassified refusals (see "Goal-window follow-up" below) |
|
||||||
|
| Consolidated review — retail `/help` Detail extraction, seam wiring test | this commit | 12,553 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — SHOULD-FIX 1 (42 of 47 catalog leaf verbs given retail Detail_HelpType text, 4 confirmed-null, 1 honest UNVERIFIED, retiring the class doc's overclaim), SHOULD-FIX 2 (`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` — the a5a7eb4f defect class had no test), SHOULD-FIX 3 (AP-113 RETIRED — Lifestone's and Marketplace's own bespoke bad-args refusal text recovered byte-exact), SHOULD-FIX 4 (register header's stale 0.5/1.0 sentence corrected), NITs (a)-(d) (`HeadlessDiagnosticWriter` instead of `Console.WriteLine`, bounded non-quiescent-pump liveness diagnostic, hydration test doc-comment contradiction, 0x26 fallback dispatches on its own `Type`) | pending — needs the next connected round to confirm `/help <verb>` now shows retail's exact wording (see "Consolidated review" below) |
|
||||||
|
|
||||||
### CH4 closeout (2026-08-09)
|
### CH4 closeout (2026-08-09)
|
||||||
|
|
||||||
|
|
@ -940,3 +941,133 @@ red instead of doing nothing; `/hslist badtype` and a bad-args catalog
|
||||||
command (e.g. `/ls now`) now flash red SpewBox text instead of showing a
|
command (e.g. `/ls now`) now flash red SpewBox text instead of showing a
|
||||||
green "Usage:" line in chat; `/help nonsenseverb` and a bare `/`/`@` now
|
green "Usage:" line in chat; `/help nonsenseverb` and a bare `/`/`@` now
|
||||||
flash on the SpewBox instead of appearing in the chat scroll.
|
flash on the SpewBox instead of appearing in the chat scroll.
|
||||||
|
|
||||||
|
## Consolidated review — goal-window round (2026-08-10)
|
||||||
|
|
||||||
|
Applied the consolidated-review findings against the `09453eca` goal
|
||||||
|
window under the same hard constraints (no subagents, no client launches,
|
||||||
|
one commit). Baseline `03404b71`, full Release suite 12,542/4/0.
|
||||||
|
|
||||||
|
**SHOULD-FIX 1 — retail `/help <verb>` Detail extraction.** Retail's
|
||||||
|
`DoHelp @0x0057F9E0` calls a resolved verb's OWN registered help callback
|
||||||
|
with `Detail_HelpType(2)`, never `Summary_HelpType(1)` —
|
||||||
|
`RetailClientCommandCatalog`'s ~45 leaf Definitions were all showing their
|
||||||
|
acdream-authored Summary-shaped `HelpText` for `/help <verb>` instead.
|
||||||
|
Located every `Help*` handler by name in `acclient_2013_pseudo_c.txt`,
|
||||||
|
swept its exact byte extent for `push imm32` string literals against the
|
||||||
|
PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH), and
|
||||||
|
confirmed each Detail-vs-Summary branch assignment by reading the actual
|
||||||
|
decompiled if/else shape — address order and string length both proved
|
||||||
|
UNRELIABLE indicators on their own (HelpDie's Detail sits at the HIGHER
|
||||||
|
address, HelpCorpse's at the LOWER; the "longest text" heuristic held for
|
||||||
|
every simple 2/3-branch case but AFK/Emote/Squelch turned out to
|
||||||
|
CONCATENATE multiple literals within one branch instead). Found and fixed
|
||||||
|
a real tool gap along the way: `sweep_weenie_strings.py`'s stock 800-char
|
||||||
|
cap silently dropped several longer Detail branches as a bare `None`
|
||||||
|
(HelpConsent 978 chars, HelpFillComponents 1036, HelpEndurance 2459) — a
|
||||||
|
custom unbounded pass recovered them.
|
||||||
|
|
||||||
|
`InitializeCommands @0x00581970`'s registration blocks always render the
|
||||||
|
CmdHashData help-pointer 4th argument as a literal `nullptr` in Binary
|
||||||
|
Ninja's pseudo-C — a confirmed, systematic decompiler artifact (the real
|
||||||
|
pointer is stored to a throwaway local immediately before the constructor
|
||||||
|
call, which BN fails to thread through). Reading for the PRESENCE of that
|
||||||
|
typed local (vs. a plain `int32_t = 0`) resolved every ambiguous verb:
|
||||||
|
hor/hr/hom/hoa and alh/ah share their Detail text with `HelpHouse`/
|
||||||
|
`HelpAllegiance` (i.e. `HouseOverview`/`AllegianceOverview`, already
|
||||||
|
extracted); friends_add/friends_remove share `HelpFriends`;
|
||||||
|
squelch/unsquelch share `HelpSquelch`. Four verbs
|
||||||
|
(index/clist/on/off) genuinely have no help pointer — retail's own
|
||||||
|
`DoHelp` falls straight to `UnknownCommand` for these, a CONFIRMED
|
||||||
|
behavior now reproduced (`RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp`),
|
||||||
|
not a guess. `messagetypes`/its 3 aliases stay an honest acdream summary —
|
||||||
|
`HelpMessageTypes` builds its text from a live enum table at runtime, not
|
||||||
|
a static string. Final coverage (measured by test, not hand-counted): 42
|
||||||
|
of 47 distinct catalog leaf Definitions verbatim-extracted, 4
|
||||||
|
confirmed-null, 1 unverified.
|
||||||
|
|
||||||
|
`ChatCommandRouter.EmitVerbHelp` now checks
|
||||||
|
`CatalogVerbsWithNoRetailHelp` → `RetailCommandHelpTable.TryGetCatalogVerbDetailText`
|
||||||
|
→ `RetailClientCommandCatalog.TryGetHelpText` (the summary fallback,
|
||||||
|
unchanged) → the pre-existing `RetailCommandHelpTable.TryGetHelpText`
|
||||||
|
(chat-alias/channel verbs, a disjoint key space). `RetailCommandHelpTable`'s
|
||||||
|
class doc, which claimed to cover only verbs the catalog "doesn't dispatch
|
||||||
|
directly," is corrected — it now overlaps the catalog's own leaf verbs by
|
||||||
|
design. A fresh full-length byte-sweep of `HelpAllegiance @0x0057ae10` (the same
|
||||||
|
function `AllegianceOverview` already cites) also turned up one
|
||||||
|
previously-dropped line (" WARNING! Officers banning...") the earlier
|
||||||
|
extraction missed — now split into `AllegianceWarningLine` and restored
|
||||||
|
in place at its correct position (between "ban list" and "info").
|
||||||
|
|
||||||
|
**SHOULD-FIX 2 — `OnInterfaceText` production wiring had no test.** The
|
||||||
|
a5a7eb4f defect class (a composed hook wired but never transferred) went
|
||||||
|
undetected because `InteractionRetainedUiCompositionTests`' `FakeFactory`
|
||||||
|
substitutes a stub `ChatVM` for every composition-order test and never
|
||||||
|
exercises `RetailInteractionRetainedUiCompositionFactory`'s real wiring.
|
||||||
|
Extracted the wiring (`InteractionRetainedUiComposition.cs`, previously
|
||||||
|
inline at the line the review cited) into its own testable
|
||||||
|
`CreateChatViewModel` method — no GPU/dat/UiHost dependency, so it is
|
||||||
|
callable directly against the existing null-heavy test `Fixture` without
|
||||||
|
a full `Compose()`. New test
|
||||||
|
`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` asserts the hook is
|
||||||
|
non-null AND that a `ShowInterfaceText` call actually lands in
|
||||||
|
`RuntimeCommunicationState.SpewBox` after a `Tick`.
|
||||||
|
|
||||||
|
**SHOULD-FIX 3 — AP-113 RETIRED.** `DoLifestone`/`DoMarketplace
|
||||||
|
@0x0056FC70`/`@0x0056FCE0` print their OWN 0x1A string for bad args and
|
||||||
|
return 1 — retail never reaches the generic `HandleFailureEvent(0x26)`
|
||||||
|
fallback for either. Both literals were mis-attributed by Binary Ninja to
|
||||||
|
unrelated vtable-slot symbols (the same pooled-string artifact class this
|
||||||
|
campaign has hit before); read directly off the raw `push imm32` operand
|
||||||
|
and decoded as UTF-16LE against the PDB-paired binary: `"Please see @help
|
||||||
|
lifestone for more information on how to use this command."` and the
|
||||||
|
`marketplace` analogue. `RetailClientCommandCatalog.Lifestone`/`Marketplace`
|
||||||
|
now carry them as `InvalidArgumentsText`. AP-113 is RETIRED (its only
|
||||||
|
named verb, lifestone, now has its exact wording); Marketplace was never a
|
||||||
|
filed divergence, so its fix is a plain accuracy improvement alongside it.
|
||||||
|
`ChatCommandRouter.cs`'s bad-args comment is corrected to name both
|
||||||
|
verbs and state the 0x26 fallback's actual scope (verbs with NO bespoke
|
||||||
|
retail refusal, not every bad-args case).
|
||||||
|
|
||||||
|
**SHOULD-FIX 4 — register header sentence fixed.** `retail-divergence-
|
||||||
|
register.md`'s AP section header still said AP-190 "ships one shared
|
||||||
|
0.5/1.0 default rather than gmMainChatUI's per-class 1.0/1.0 override" —
|
||||||
|
refuted by `cc582899`, which fixed the shipped default TO 1.0/1.0 (the
|
||||||
|
row's own REWORDED (2) already said so; only the header summary hadn't
|
||||||
|
caught up). Corrected in place; the active-row count drops to 132 with
|
||||||
|
AP-113's retirement.
|
||||||
|
|
||||||
|
**NITs (a)-(d).** (a) `HeadlessStaticStateAudit`'s single-session log line
|
||||||
|
now routes through the injected `HeadlessDiagnosticWriter` (constructed
|
||||||
|
before the audit call in `HeadlessProcessHost`, not after) instead of a
|
||||||
|
bare `Console.WriteLine` that bypassed the structured stream every other
|
||||||
|
headless diagnostic uses. (b) `HeadlessSessionWorldProjection.PumpFirstEntry`
|
||||||
|
gained a bounded, diagnostic-only trip-wire: 300 consecutive non-quiescent
|
||||||
|
pumps (generous — many seconds at the host's tick cadence) emit ONE
|
||||||
|
message naming the stuck landblock; no retry, no behavior change, the gate
|
||||||
|
itself is untouched. (c) The #365 hydration test's doc comment is
|
||||||
|
corrected — it claimed reverting the `IsQuiescent` gates makes the
|
||||||
|
conductor reach "`PublicationCommitted` — a non-null dormant controller,"
|
||||||
|
which contradicts the diagnosis doc's own §8 finding (the controller is
|
||||||
|
ALREADY built AND published, `CanExecuteLiveMovement = True`, when the
|
||||||
|
assertion actually fails); the comment now also states plainly that this
|
||||||
|
bounded xunit test proves the GATE, not the #365 stall's closure — that
|
||||||
|
broader claim's evidence is the separate live-ACE run §8 records. (d)
|
||||||
|
`ChatCommandRouter`'s 0x26 fallback now resolves
|
||||||
|
`WeenieErrorMessages.Resolve(0x026u, null)` once and dispatches on its own
|
||||||
|
`Type` field instead of assuming `ShowInterfaceText`'s hardcoded
|
||||||
|
`ClientLocal` is correct for it (it is, today — this just stops relying on
|
||||||
|
that being an unstated invariant).
|
||||||
|
|
||||||
|
Full Release suite: 12,553 passed / 4 skipped / 0 failed (net +11 tests
|
||||||
|
from this round, zero regressions; one `LandblockPresentationPipelineTests`
|
||||||
|
failure observed on one parallel full-solution run reproduced as PASS in
|
||||||
|
isolation and on two subsequent full-solution reruns — pre-existing
|
||||||
|
run-order flakiness in an unrelated streaming test, not caused by this
|
||||||
|
round's changes). No subagent review this session (hard constraint); no
|
||||||
|
connected user gate (hard constraint — no client launches). The next
|
||||||
|
connected round should confirm `/help die`, `/help lifestone`,
|
||||||
|
`/help endurance`, and a handful of the other 42 newly-extracted verbs
|
||||||
|
show retail's exact wording instead of the old acdream summaries, and that
|
||||||
|
`/help index`/`/clist`/`/on`/`/off` now show "Unknown command" via the
|
||||||
|
SpewBox.
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,33 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||||
canSend: () => late.Session.IsInWorld);
|
canSend: () => late.Session.IsInWorld);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consolidated-review round (2026-08-10), SHOULD-FIX 2: extracted out
|
||||||
|
/// of <see cref="CreateRetainedUi"/> so this specific wiring — the
|
||||||
|
/// composed <see cref="ChatVM.OnInterfaceText"/> hook that routes
|
||||||
|
/// <c>ChatCommandRouter</c>'s <c>0x1A</c> refusals to
|
||||||
|
/// <see cref="RuntimeCommunicationState.AddText"/>/SpewBox — is
|
||||||
|
/// independently testable without the rest of <see cref="CreateRetainedUi"/>'s
|
||||||
|
/// GPU/dat/UiHost dependencies (all null-safe in a pure unit test; this
|
||||||
|
/// method only touches <see cref="InteractionRetainedUiDependencies.Communication"/>).
|
||||||
|
/// The a5a7eb4f defect class (a composed hook wired but never
|
||||||
|
/// transferred) is exactly what this seam existing untested let slip
|
||||||
|
/// through — see <c>InteractionRetainedUiCompositionTests.ComposedChatViewModelWiresOnInterfaceTextToSpewBox</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal static ChatVM CreateChatViewModel(InteractionRetainedUiDependencies d) =>
|
||||||
|
new ChatVM(
|
||||||
|
d.Communication.Chat,
|
||||||
|
displayLimit: 200,
|
||||||
|
commandTargets: d.Communication.CommandTargets)
|
||||||
|
{
|
||||||
|
// Issue #363 / #367: routes ChatCommandRouter's 0x1A
|
||||||
|
// (ClientLocal) command refusals to the same SpewBox
|
||||||
|
// chokepoint every other interface-text producer uses,
|
||||||
|
// instead of the chat scroll.
|
||||||
|
OnInterfaceText = text =>
|
||||||
|
d.Communication.AddText(text, RetailLogTextType.ClientLocal),
|
||||||
|
};
|
||||||
|
|
||||||
public RetainedUiComposition CreateRetainedUi(
|
public RetainedUiComposition CreateRetainedUi(
|
||||||
InteractionRetainedUiDependencies d,
|
InteractionRetainedUiDependencies d,
|
||||||
InteractionUiLateBindings late,
|
InteractionUiLateBindings late,
|
||||||
|
|
@ -559,18 +586,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
|
|
||||||
host.Root.Width = d.Window.Size.X;
|
host.Root.Width = d.Window.Size.X;
|
||||||
host.Root.Height = d.Window.Size.Y;
|
host.Root.Height = d.Window.Size.Y;
|
||||||
var chat = new ChatVM(
|
var chat = CreateChatViewModel(d);
|
||||||
d.Communication.Chat,
|
|
||||||
displayLimit: 200,
|
|
||||||
commandTargets: d.Communication.CommandTargets)
|
|
||||||
{
|
|
||||||
// Issue #363 / #367: routes ChatCommandRouter's 0x1A
|
|
||||||
// (ClientLocal) command refusals to the same SpewBox
|
|
||||||
// chokepoint every other interface-text producer uses,
|
|
||||||
// instead of the chat scroll.
|
|
||||||
OnInterfaceText = text =>
|
|
||||||
d.Communication.AddText(text, RetailLogTextType.ClientLocal),
|
|
||||||
};
|
|
||||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
AcDream.UI.Abstractions.Panels.Settings.SettingsStore? layoutStore =
|
||||||
d.Settings.LayoutStore;
|
d.Settings.LayoutStore;
|
||||||
RetailUiPersistenceBindings? persistence = layoutStore is null
|
RetailUiPersistenceBindings? persistence = layoutStore is null
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,14 @@ internal sealed class HeadlessProcessHost : IDisposable
|
||||||
throw new HeadlessConfigurationException(
|
throw new HeadlessConfigurationException(
|
||||||
"Direct credentials require exactly one configured session.");
|
"Direct credentials require exactly one configured session.");
|
||||||
}
|
}
|
||||||
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
// Consolidated-review round (2026-08-10), NIT (a): construct the
|
||||||
configuration.Sessions.Count);
|
// diagnostics writer BEFORE the audit call so its single-session
|
||||||
|
// "probes enabled" line routes through the same structured stream
|
||||||
|
// every other headless diagnostic uses, instead of a bare
|
||||||
|
// Console.WriteLine that bypassed it.
|
||||||
_diagnostics = new HeadlessDiagnosticWriter(diagnostics);
|
_diagnostics = new HeadlessDiagnosticWriter(diagnostics);
|
||||||
|
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||||
|
configuration.Sessions.Count, _diagnostics);
|
||||||
var credentials = new HeadlessCredentialResolver(
|
var credentials = new HeadlessCredentialResolver(
|
||||||
standardInput,
|
standardInput,
|
||||||
paths.ConfigDirectory);
|
paths.ConfigDirectory);
|
||||||
|
|
|
||||||
|
|
@ -675,7 +675,14 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
Runtime,
|
Runtime,
|
||||||
content,
|
content,
|
||||||
_firstEntryDrive,
|
_firstEntryDrive,
|
||||||
_acceptedPositionDrive);
|
_acceptedPositionDrive,
|
||||||
|
// Consolidated-review round (2026-08-10), NIT (b): the
|
||||||
|
// SAME session-labelled diagnostics stream every other
|
||||||
|
// producer in this class writes into.
|
||||||
|
onNonQuiescentStall: message => _diagnostics.Message(
|
||||||
|
_descriptor.Id,
|
||||||
|
message,
|
||||||
|
Runtime.Generation.Value));
|
||||||
_worldProjection = projection;
|
_worldProjection = projection;
|
||||||
worldProjection = projection;
|
worldProjection = projection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -617,18 +617,34 @@ internal sealed class HeadlessSessionWorldProjection
|
||||||
/// resolves promptly.
|
/// resolves promptly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||||
|
private readonly Action<string>? _onNonQuiescentStall;
|
||||||
private uint _requestedLocalPlayerCell;
|
private uint _requestedLocalPlayerCell;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consolidated-review round (2026-08-10), NIT (b): a purely
|
||||||
|
/// diagnostic trip-wire, not a behavior change. At the host's per-tick
|
||||||
|
/// pump cadence this is on the order of many seconds — generous enough
|
||||||
|
/// that no legitimate multi-tick collision-generation sequence should
|
||||||
|
/// ever cross it, so crossing it means something is genuinely stuck
|
||||||
|
/// (a publication that never seals, an admission that never clears).
|
||||||
|
/// </summary>
|
||||||
|
private const int NonQuiescentStallPumpThreshold = 300;
|
||||||
|
|
||||||
|
private int _nonQuiescentPumpCount;
|
||||||
|
private bool _reportedNonQuiescentStall;
|
||||||
|
|
||||||
internal HeadlessSessionWorldProjection(
|
internal HeadlessSessionWorldProjection(
|
||||||
GameRuntime runtime,
|
GameRuntime runtime,
|
||||||
HeadlessProcessContentOwner.HeadlessProcessContentLease content,
|
HeadlessProcessContentOwner.HeadlessProcessContentLease content,
|
||||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
||||||
|
Action<string>? onNonQuiescentStall = null)
|
||||||
: this(
|
: this(
|
||||||
runtime,
|
runtime,
|
||||||
new HeadlessCollisionNeighborhood(runtime, content),
|
new HeadlessCollisionNeighborhood(runtime, content),
|
||||||
firstEntry,
|
firstEntry,
|
||||||
acceptedPositionDrive)
|
acceptedPositionDrive,
|
||||||
|
onNonQuiescentStall)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,7 +652,8 @@ internal sealed class HeadlessSessionWorldProjection
|
||||||
GameRuntime runtime,
|
GameRuntime runtime,
|
||||||
IHeadlessCollisionNeighborhood collision,
|
IHeadlessCollisionNeighborhood collision,
|
||||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null,
|
||||||
|
Action<string>? onNonQuiescentStall = null)
|
||||||
{
|
{
|
||||||
_runtime = runtime
|
_runtime = runtime
|
||||||
?? throw new ArgumentNullException(nameof(runtime));
|
?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
@ -644,6 +661,7 @@ internal sealed class HeadlessSessionWorldProjection
|
||||||
?? throw new ArgumentNullException(nameof(collision));
|
?? throw new ArgumentNullException(nameof(collision));
|
||||||
_firstEntry = firstEntry;
|
_firstEntry = firstEntry;
|
||||||
_acceptedPositionDrive = acceptedPositionDrive;
|
_acceptedPositionDrive = acceptedPositionDrive;
|
||||||
|
_onNonQuiescentStall = onNonQuiescentStall;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ProjectSpawn(
|
public void ProjectSpawn(
|
||||||
|
|
@ -776,7 +794,23 @@ internal sealed class HeadlessSessionWorldProjection
|
||||||
// publication work each tick (its own mutating side effect), so
|
// publication work each tick (its own mutating side effect), so
|
||||||
// gating it too would make the neighborhood itself never converge.
|
// gating it too would make the neighborhood itself never converge.
|
||||||
if (!_collision.IsQuiescent)
|
if (!_collision.IsQuiescent)
|
||||||
|
{
|
||||||
|
// NIT (b): diagnostic-only trip-wire, no retry and no behavior
|
||||||
|
// change -- the gate above keeps refusing to drive the
|
||||||
|
// conductor exactly as it always has. Fires ONCE per stall
|
||||||
|
// episode so a hang shows up in the diagnostics stream instead
|
||||||
|
// of reading as "still working" indefinitely.
|
||||||
|
if (++_nonQuiescentPumpCount >= NonQuiescentStallPumpThreshold
|
||||||
|
&& !_reportedNonQuiescentStall)
|
||||||
|
{
|
||||||
|
_reportedNonQuiescentStall = true;
|
||||||
|
_onNonQuiescentStall?.Invoke(FormattableString.Invariant(
|
||||||
|
$"collision neighborhood non-quiescent for {_nonQuiescentPumpCount} pumps, requested landblock=0x{_requestedLocalPlayerCell:X8}"));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
_nonQuiescentPumpCount = 0;
|
||||||
|
_reportedNonQuiescentStall = false;
|
||||||
_firstEntry?.DriveAll();
|
_firstEntry?.DriveAll();
|
||||||
_acceptedPositionDrive?.Advance();
|
_acceptedPositionDrive?.Advance();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Headless.Configuration;
|
using AcDream.Headless.Configuration;
|
||||||
|
using AcDream.Headless.Diagnostics;
|
||||||
|
|
||||||
namespace AcDream.Headless.Hosting;
|
namespace AcDream.Headless.Hosting;
|
||||||
|
|
||||||
|
|
@ -22,8 +23,18 @@ namespace AcDream.Headless.Hosting;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static class HeadlessStaticStateAudit
|
internal static class HeadlessStaticStateAudit
|
||||||
{
|
{
|
||||||
internal static void ValidateProcessIsolation(int sessionCount)
|
/// <summary>
|
||||||
|
/// Consolidated-review round (2026-08-10), NIT (a): <paramref name="diagnostics"/>
|
||||||
|
/// is the SAME structured writer every other headless diagnostic uses
|
||||||
|
/// (<see cref="HeadlessProcessHost"/>'s <c>_diagnostics</c> field, now
|
||||||
|
/// constructed before this call rather than after it) — the raw
|
||||||
|
/// <c>Console.WriteLine</c> this replaced bypassed the session-labelled
|
||||||
|
/// JSON stream every other producer writes into.
|
||||||
|
/// </summary>
|
||||||
|
internal static void ValidateProcessIsolation(
|
||||||
|
int sessionCount, HeadlessDiagnosticWriter diagnostics)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(diagnostics);
|
||||||
var enabled = new List<string>();
|
var enabled = new List<string>();
|
||||||
foreach (PropertyInfo property in typeof(PhysicsDiagnostics)
|
foreach (PropertyInfo property in typeof(PhysicsDiagnostics)
|
||||||
.GetProperties(BindingFlags.Public | BindingFlags.Static)
|
.GetProperties(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
|
@ -53,8 +64,10 @@ internal static class HeadlessStaticStateAudit
|
||||||
|
|
||||||
if (sessionCount == 1)
|
if (sessionCount == 1)
|
||||||
{
|
{
|
||||||
Console.WriteLine(FormattableString.Invariant(
|
diagnostics.Message(
|
||||||
$"[headless-audit] single-session process — process-global physics probes enabled: {string.Join(", ", enabled)}"));
|
sessionId: "process",
|
||||||
|
eventName: FormattableString.Invariant(
|
||||||
|
$"headless-audit: single-session process — process-global physics probes enabled: {string.Join(", ", enabled)}"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,21 +54,44 @@ public static class ChatCommandRouter
|
||||||
{
|
{
|
||||||
// #363 / register row AP-183: retail's bad-args refusal is
|
// #363 / register row AP-183: retail's bad-args refusal is
|
||||||
// ALWAYS 0x1A (ClientLocal / SpewBox-only) — verified
|
// ALWAYS 0x1A (ClientLocal / SpewBox-only) — verified
|
||||||
// against five decompiled handlers (DoDie, DoChannelList/
|
// against seven decompiled handlers (DoDie, DoChannelList/
|
||||||
// On/Off, DoAllegiance, DoHouseAvailableList). A Definition
|
// On/Off, DoAllegiance, DoHouseAvailableList, and — added at
|
||||||
// with its own InvalidArgumentsText is the handler's own
|
// the consolidated-review round 2026-08-10, retiring
|
||||||
// bespoke refusal string, printed before it returns
|
// register row AP-113 — DoLifestone/DoMarketplace). A
|
||||||
// "handled" (1) so retail's generic fallback never fires
|
// Definition with its own InvalidArgumentsText is the
|
||||||
// for it. A Definition with none falls to that generic
|
// handler's own bespoke refusal string (byte-recovered from
|
||||||
// fallback: ClientCommunicationSystem::DoCommand
|
// the PDB-paired acclient.exe; Binary Ninja mis-attributes
|
||||||
// @0x0057E46D calls HandleFailureEvent(0x26) when a
|
// both DoLifestone's and DoMarketplace's own literal to an
|
||||||
// registered handler returns 0 (bad args) —
|
// unrelated vtable-slot symbol, the same class of artifact
|
||||||
|
// this file already documents for the Help* family),
|
||||||
|
// printed before the handler returns "handled" (1) so
|
||||||
|
// retail's generic fallback never fires for it. The 0x26
|
||||||
|
// fallback below therefore covers ONLY verbs with no
|
||||||
|
// bespoke retail refusal of their own — not every bad-args
|
||||||
|
// case, and not a placeholder pending more extraction:
|
||||||
|
// ClientCommunicationSystem::DoCommand @0x0057E46D calls
|
||||||
|
// HandleFailureEvent(0x26) when a registered handler
|
||||||
|
// returns 0 (bad args) with no bespoke string, resolving to
|
||||||
// "That is not a valid command." (WeenieErrorMessages
|
// "That is not a valid command." (WeenieErrorMessages
|
||||||
// [0x026]) — never the acdream-invented "Usage: {Usage}"
|
// [0x026]) — never the acdream-invented "Usage: {Usage}"
|
||||||
// line this branch used to synthesize.
|
// line this branch used to synthesize. Consolidated-review
|
||||||
vm.ShowInterfaceText(clientCommand.InvalidArgumentsText
|
// NIT (d): dispatches on the resolved entry's own Type
|
||||||
?? WeenieErrorMessages.Resolve(0x026u, null).Text
|
// rather than assuming ShowInterfaceText's hardcoded
|
||||||
?? "That is not a valid command.");
|
// ClientLocal is correct for it.
|
||||||
|
if (clientCommand.InvalidArgumentsText is { } bespokeRefusal)
|
||||||
|
{
|
||||||
|
vm.ShowInterfaceText(bespokeRefusal);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(string? text, RetailLogTextType type) fallback =
|
||||||
|
WeenieErrorMessages.Resolve(0x026u, null);
|
||||||
|
string fallbackText = fallback.text ?? "That is not a valid command.";
|
||||||
|
if (fallback.type == RetailLogTextType.ClientLocal)
|
||||||
|
vm.ShowInterfaceText(fallbackText);
|
||||||
|
else
|
||||||
|
vm.ShowSystemMessage(fallbackText);
|
||||||
|
}
|
||||||
return SubmitOutcome.ClientHandled;
|
return SubmitOutcome.ClientHandled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +284,24 @@ public static class ChatCommandRouter
|
||||||
/// <see cref="RetailCommandHelpTable.UnknownCommand"/> text instead of
|
/// <see cref="RetailCommandHelpTable.UnknownCommand"/> text instead of
|
||||||
/// an acdream-invented "No help available" message.
|
/// an acdream-invented "No help available" message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Consolidated-review round (2026-08-10), SHOULD-FIX 1: lookup order
|
||||||
|
/// is now (1) <see cref="RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp"/>
|
||||||
|
/// — a catalog leaf verb retail itself registers with a NULL help
|
||||||
|
/// pointer (index/clist/on/off) goes straight to
|
||||||
|
/// <see cref="RetailCommandHelpTable.UnknownCommand"/>, matching
|
||||||
|
/// retail's own <c>DoHelp</c> exactly, and must never reach either
|
||||||
|
/// text source below; (2)
|
||||||
|
/// <see cref="RetailCommandHelpTable.TryGetCatalogVerbDetailText"/> —
|
||||||
|
/// retail's OWN Detail_HelpType text, byte-swept from the retail
|
||||||
|
/// binary, for the 42 catalog leaf verbs it covers; (3)
|
||||||
|
/// <see cref="RetailClientCommandCatalog.TryGetHelpText"/> — the
|
||||||
|
/// catalog's acdream-authored summary, now purely the fallback for
|
||||||
|
/// catalog verbs not yet extracted (currently only messagetypes and
|
||||||
|
/// its 3 aliases); (4) <see cref="RetailCommandHelpTable.TryGetHelpText"/>
|
||||||
|
/// — chat-alias/channel verbs and the group-topic nodes, a disjoint key
|
||||||
|
/// space from the catalog so this reordering changes nothing for them.
|
||||||
|
/// </remarks>
|
||||||
private static void EmitVerbHelp(string verb, ChatVM vm)
|
private static void EmitVerbHelp(string verb, ChatVM vm)
|
||||||
{
|
{
|
||||||
if (verb.Length == 0)
|
if (verb.Length == 0)
|
||||||
|
|
@ -270,6 +311,25 @@ public static class ChatCommandRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
string normalized = verb.TrimStart('/', '@');
|
string normalized = verb.TrimStart('/', '@');
|
||||||
|
|
||||||
|
if (RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp.Contains(
|
||||||
|
normalized.TrimEnd(',')))
|
||||||
|
{
|
||||||
|
// Retail types this 0x1A (ClientLocal) -> SpewBox-only, the
|
||||||
|
// SAME fallback an unregistered verb gets — DoHelp's help-
|
||||||
|
// pointer-null guard skips its callback branch entirely. See
|
||||||
|
// RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp's remarks.
|
||||||
|
vm.ShowInterfaceText(RetailCommandHelpTable.UnknownCommand);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RetailCommandHelpTable.TryGetCatalogVerbDetailText(normalized, out string retailDetailText))
|
||||||
|
{
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + retailDetailText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
||||||
{
|
{
|
||||||
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||||
|
|
|
||||||
|
|
@ -29,16 +29,36 @@ public static class RetailClientCommandCatalog
|
||||||
bool HasValidArguments,
|
bool HasValidArguments,
|
||||||
string? InvalidArgumentsText);
|
string? InvalidArgumentsText);
|
||||||
|
|
||||||
|
// ClientCommunicationSystem::DoLifestone @ 0x0056FC70. Consolidated
|
||||||
|
// review round (2026-08-10), SHOULD-FIX 3: retires register row
|
||||||
|
// AP-113. DoLifestone's own bad-args branch prints ITS OWN 0x1A string
|
||||||
|
// and returns 1 — retail never reaches the generic HandleFailureEvent
|
||||||
|
// (0x26) fallback for it. Binary Ninja mis-attributes the literal to
|
||||||
|
// an unrelated vtable-slot symbol
|
||||||
|
// (ClientCommunicationSystem::`vftable'.RecvNotice_AddItemToTrade,
|
||||||
|
// the classic pooled-string artifact); recovered byte-exact by reading
|
||||||
|
// the raw `push imm32` operand at 0x0056fc84 and decoding the UTF-16LE
|
||||||
|
// string at data VA 0x007d0578 directly against the PDB-paired
|
||||||
|
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH).
|
||||||
private static readonly Definition Lifestone = new(
|
private static readonly Definition Lifestone = new(
|
||||||
ClientCommandId.LifestoneRecall,
|
ClientCommandId.LifestoneRecall,
|
||||||
Usage: "/lifestone",
|
Usage: "/lifestone",
|
||||||
HelpText: "/lifestone (/lif, /ls) - Returns you to the last lifestone you used without killing you.",
|
HelpText: "/lifestone (/lif, /ls) - Returns you to the last lifestone you used without killing you.",
|
||||||
ValidateArguments: static arguments => arguments.Length == 0);
|
ValidateArguments: static arguments => arguments.Length == 0,
|
||||||
|
InvalidArgumentsText: "Please see @help lifestone for more information on how to use this command.");
|
||||||
|
|
||||||
private static readonly Definition Marketplace = NoArguments(
|
// ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0. Same
|
||||||
|
// methodology and same vtable-mislabeling artifact as Lifestone above
|
||||||
|
// (mis-attributed to RecvNotice_UpdateToolbarSelectionDisplay);
|
||||||
|
// operand at 0x0056fcf4, data VA 0x007d0610. Not a previously-filed
|
||||||
|
// register row — Marketplace was never flagged as diverging — this is
|
||||||
|
// a plain accuracy improvement alongside Lifestone's AP-113 retirement.
|
||||||
|
private static readonly Definition Marketplace = new(
|
||||||
ClientCommandId.MarketplaceRecall,
|
ClientCommandId.MarketplaceRecall,
|
||||||
"/marketplace",
|
Usage: "/marketplace",
|
||||||
"/marketplace (/mar, /mp) - Teleports you to the Marketplace of Dereth.");
|
HelpText: "/marketplace (/mar, /mp) - Teleports you to the Marketplace of Dereth.",
|
||||||
|
ValidateArguments: static arguments => arguments.Length == 0,
|
||||||
|
InvalidArgumentsText: "Please see @help marketplace for more information on how to use this command.");
|
||||||
|
|
||||||
private static readonly Definition PkArena = NoArguments(
|
private static readonly Definition PkArena = NoArguments(
|
||||||
ClientCommandId.PkArenaRecall,
|
ClientCommandId.PkArenaRecall,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,24 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign CH slice CH4 (2026-08-09): <c>/help <verb></c> text for
|
/// Campaign CH slice CH4 (2026-08-09): <c>/help <verb></c> text for
|
||||||
/// every retail-registry verb <see cref="RetailClientCommandCatalog"/>
|
/// every retail-registry verb — originally chat aliases and channel verbs
|
||||||
/// doesn't dispatch directly — chat aliases and channel verbs
|
|
||||||
/// (<see cref="ChatInputParser"/>), the null-func help-only nodes (retail
|
/// (<see cref="ChatInputParser"/>), the null-func help-only nodes (retail
|
||||||
/// registers these with NO handler; typing them bare reaches the server,
|
/// registers these with NO handler; typing them bare reaches the server,
|
||||||
/// only <c>@help <verb></c> shows anything locally), and the
|
/// only <c>@help <verb></c> shows anything locally), and the
|
||||||
/// allegiance/house command overviews (the per-subcommand detail lives
|
/// allegiance/house command overviews (the per-subcommand detail lives
|
||||||
/// here too, even though most subcommands are not yet locally executed —
|
/// here too, even though most subcommands are not yet locally executed —
|
||||||
/// see TS-68).
|
/// see TS-68). <b>Corrected at the consolidated-review round
|
||||||
|
/// (2026-08-10), SHOULD-FIX 1:</b> the sentence above previously claimed
|
||||||
|
/// this table covers only verbs <see cref="RetailClientCommandCatalog"/>
|
||||||
|
/// "doesn't dispatch directly" — that framing is now FALSE and was itself
|
||||||
|
/// an overclaim-by-omission: <see cref="CatalogVerbDetailByVerb"/> (see
|
||||||
|
/// its own remarks, and the "Catalog leaf-verb Detail extraction" region
|
||||||
|
/// below) supplies retail Detail_HelpType text for 42 of the catalog's OWN
|
||||||
|
/// dispatched leaf verbs too, which <c>ChatCommandRouter</c> now prefers
|
||||||
|
/// over the catalog's acdream-authored summaries. This table is the single
|
||||||
|
/// <c>/help <verb></c> retail-text source for BOTH catalog-dispatched
|
||||||
|
/// and non-catalog verbs; <see cref="RetailClientCommandCatalog.TryGetHelpText"/>
|
||||||
|
/// is now purely the fallback for the catalog verbs not yet extracted.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// The named constants above <see cref="ByVerb"/> (<see cref="Tell"/>,
|
/// The named constants above <see cref="ByVerb"/> (<see cref="Tell"/>,
|
||||||
|
|
@ -193,13 +203,27 @@ public static class RetailCommandHelpTable
|
||||||
public const string Motd =
|
public const string Motd =
|
||||||
"@allegiance motd - Displays the message of the day for your allegiance. @allegiance motd set <text> - Sets the MOTD. Can only be used by monarchs. @allegiance motd clear - Clears the MOTD. Can only be used by monarchs. NOT YET IMPLEMENTED in acdream (TS-68) — the command reaches the server as literal text.";
|
"@allegiance motd - Displays the message of the day for your allegiance. @allegiance motd set <text> - Sets the MOTD. Can only be used by monarchs. @allegiance motd clear - Clears the MOTD. Can only be used by monarchs. NOT YET IMPLEMENTED in acdream (TS-68) — the command reaches the server as literal text.";
|
||||||
|
|
||||||
|
// Split out of AllegianceOverview's own data block (data_0x7dfb58) at
|
||||||
|
// the consolidated review round (2026-08-10) so the const declaration
|
||||||
|
// order matches use; see the class remarks on AllegianceOverview below.
|
||||||
|
public const string AllegianceWarningLine =
|
||||||
|
" WARNING! Officers banning or booting a character by account could wind up in a situation where they are no longer in the allegiance if they boot a character that is above them in the hierarchy.\n";
|
||||||
|
|
||||||
// acclient_2013_pseudo_c.txt:1031234 (data_7e03d4) plus the full
|
// acclient_2013_pseudo_c.txt:1031234 (data_7e03d4) plus the full
|
||||||
// per-subcommand block at 1031210-1031230 (data_7dfb58).
|
// per-subcommand block at 1031210-1031230 (data_7dfb58). Consolidated
|
||||||
|
// review round (2026-08-10), SHOULD-FIX 1 byproduct: a fresh full-length
|
||||||
|
// byte sweep of HelpAllegiance @0x0057ae10 (0x57ae10-0x57aef0, no
|
||||||
|
// max_chars truncation this time) turned up the " WARNING! Officers
|
||||||
|
// banning..." line between "ban list" and "info" that the original
|
||||||
|
// extraction dropped — confirmed present in the SAME data block
|
||||||
|
// (data_0x7dfb58) this const already cites, not a new source. Restored
|
||||||
|
// verbatim, including the retail leading space.
|
||||||
public const string AllegianceOverview =
|
public const string AllegianceOverview =
|
||||||
"@allegiance - Commands to help manage your allegiance.\n"
|
"@allegiance - Commands to help manage your allegiance.\n"
|
||||||
+ "@allegiance boot [-account] <name> - Removes a character from your allegiance.\n"
|
+ "@allegiance boot [-account] <name> - Removes a character from your allegiance.\n"
|
||||||
+ "@allegiance ban <add/remove> <name> - Bans all characters on the given character's account from your allegiance (and boots them too!)\n"
|
+ "@allegiance ban <add/remove> <name> - Bans all characters on the given character's account from your allegiance (and boots them too!)\n"
|
||||||
+ "@allegiance ban list - List the characters whose accounts are banned from your allegiance.\n"
|
+ "@allegiance ban list - List the characters whose accounts are banned from your allegiance.\n"
|
||||||
|
+ AllegianceWarningLine
|
||||||
+ "@allegiance info <name> - Requests information on a member of your allegiance. [IMPLEMENTED]\n"
|
+ "@allegiance info <name> - Requests information on a member of your allegiance. [IMPLEMENTED]\n"
|
||||||
+ "@allegiance chat <on/off> - Turn allegiance chat on and off.\n"
|
+ "@allegiance chat <on/off> - Turn allegiance chat on and off.\n"
|
||||||
+ "@allegiance chat kick <name>[, <reason>] - Kick a player temporarily from the allegiance chat room.\n"
|
+ "@allegiance chat kick <name>[, <reason>] - Kick a player temporarily from the allegiance chat room.\n"
|
||||||
|
|
@ -416,6 +440,287 @@ public static class RetailCommandHelpTable
|
||||||
+ "@help text - Commands that help you manage your text window.\n"
|
+ "@help text - Commands that help you manage your text window.\n"
|
||||||
+ CommandsGroupSummary + "\n";
|
+ CommandsGroupSummary + "\n";
|
||||||
|
|
||||||
|
// ── Catalog leaf-verb Detail extraction (consolidated review round,
|
||||||
|
// 2026-08-10, SHOULD-FIX 1) ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// RetailClientCommandCatalog's 47 top-level Definitions each carry an
|
||||||
|
// acdream-authored HelpText — a SUMMARY-shaped one-liner, not retail's
|
||||||
|
// Detail_HelpType(2) text. DoHelp @0x0057F9E0 calls a resolved verb's
|
||||||
|
// OWN registered help callback with Detail_HelpType for /help <verb>
|
||||||
|
// (the class remarks above trace this call precisely) — retail never
|
||||||
|
// shows the Summary text there. Every Help* handler below was located
|
||||||
|
// by name in acclient_2013_pseudo_c.txt, its exact byte extent (this
|
||||||
|
// function's start VA to the next function's start VA, read from the
|
||||||
|
// pseudo-C's own function-header addresses) swept for `push imm32`
|
||||||
|
// string literals against the PDB-paired
|
||||||
|
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH via
|
||||||
|
// check_exe_pdb.py), and the resulting branch assignment confirmed by
|
||||||
|
// reading the actual decompiled if/else shape rather than guessing
|
||||||
|
// from string length or address order — those two heuristics
|
||||||
|
// DISAGREED between HelpDie (Detail at the higher address) and
|
||||||
|
// HelpCorpse (Detail at the LOWER address), so neither is trustworthy
|
||||||
|
// alone. sweep_weenie_strings.py's stock 800-char cap silently dropped
|
||||||
|
// several longer Detail branches (HelpConsent 978 chars, HelpFillComponents
|
||||||
|
// 1036, HelpEndurance 2459) as a `None` result with no error — a custom
|
||||||
|
// unbounded-length pass (same PE-parsing logic, no cap) recovered them;
|
||||||
|
// this is a tool limitation worth fixing generally, not specific to
|
||||||
|
// this sweep.
|
||||||
|
//
|
||||||
|
// <b>CmdHashData help-pointer verification.</b> Several verbs the
|
||||||
|
// catalog dispatches (hor/hr, hom/hoa, friends_add, friends_remove,
|
||||||
|
// squelch, unsquelch, alh/ah) have no distinctly-NAMED Help* function of
|
||||||
|
// their own. `ClientCommunicationSystem::InitializeCommands
|
||||||
|
// @0x00581970`'s per-verb block ALWAYS shows the registered help
|
||||||
|
// pointer as a literal `nullptr` 4th argument to
|
||||||
|
// `CmdHashData::CmdHashData(...)` in Binary Ninja's rendering — a
|
||||||
|
// confirmed, systematic decompiler artifact, not per-verb truth: the
|
||||||
|
// REAL help pointer is instead stored to a throwaway local
|
||||||
|
// (`uint8_t (__stdcall* var_3c_NN)(...) = ClientCommunicationSystem::HelpXxx;`)
|
||||||
|
// immediately before the constructor call, which Binary Ninja fails to
|
||||||
|
// thread through as the actual argument. Presence of that typed
|
||||||
|
// function-pointer local (not a plain `int32_t var_3c_NN = 0;`) is the
|
||||||
|
// ground truth: hor/hr/hom/hoa share `HelpHouse` (the SAME function
|
||||||
|
// "house"/"hou" use — <see cref="HouseOverview"/> already covers it),
|
||||||
|
// friends_add/friends_remove share `HelpFriends` (same as "friends"),
|
||||||
|
// squelch/unsquelch share `HelpSquelch`, and alh/ah share
|
||||||
|
// `HelpAllegiance` (the SAME function "allegiance"/"all" use — <see
|
||||||
|
// cref="AllegianceOverview"/> already covers it). Four verbs
|
||||||
|
// (index/clist/on/off) genuinely DO have a plain `int32_t var_3c_NN = 0;`
|
||||||
|
// local with no preceding Help-pointer assignment — CONFIRMED NULL,
|
||||||
|
// not merely undecoded: retail's own `DoHelp` skips straight to its
|
||||||
|
// "Unknown command" fallback for these (the `if (eax_35 != 0)` guard
|
||||||
|
// around the help-pointer call), even though the verb itself dispatches
|
||||||
|
// fine. <see cref="CatalogVerbsWithNoRetailHelp"/> names them so
|
||||||
|
// <c>ChatCommandRouter</c> can reproduce that exact behavior instead of
|
||||||
|
// falling back to the catalog's invented summary.
|
||||||
|
//
|
||||||
|
// <c>messagetypes</c>/<c>message_types</c>/<c>msgtypes</c>/<c>msg_types</c>
|
||||||
|
// are the one leaf verb NOT extracted this round:
|
||||||
|
// `HelpMessageTypes @0x0056e5d0` calls
|
||||||
|
// `gmCCommunicationSystem::GetListofSquelchChannels(arg4)` to build its
|
||||||
|
// text from a live enum table at runtime — there is no static string to
|
||||||
|
// sweep. It keeps its acdream summary, honestly UNVERIFIED rather than
|
||||||
|
// guessed at.
|
||||||
|
//
|
||||||
|
// Coverage this round: 42 of the 47 catalog Definitions got a verbatim
|
||||||
|
// Detail override (<see cref="CatalogVerbDetailByVerb"/>, several
|
||||||
|
// sharing one extraction per the CmdHashData note above), 4 are
|
||||||
|
// CONFIRMED-NULL (<see cref="CatalogVerbsWithNoRetailHelp"/>), and 1
|
||||||
|
// (messagetypes and its 3 aliases) remains an honest acdream summary —
|
||||||
|
// <c>RetailClientCommandHelpCoverageTests</c> pins these counts so a
|
||||||
|
// future extraction pass (or an accidental regression) is caught.
|
||||||
|
public const string LifestoneDetail =
|
||||||
|
"@lifestone - Returns you to the last lifestone you used without killing you.\n";
|
||||||
|
|
||||||
|
public const string MarketplaceDetail =
|
||||||
|
"@marketplace - Teleports you to the Marketplace of Dereth.\n";
|
||||||
|
|
||||||
|
public const string PkArenaDetail =
|
||||||
|
"@pkarena - Teleports you to the PK Arena. You must be PK to use this command.\n";
|
||||||
|
|
||||||
|
public const string PkLiteArenaDetail =
|
||||||
|
"@pklarena - Teleports you to the PKL Arena. You must be PKL to use this command.\n";
|
||||||
|
|
||||||
|
public const string PkLiteDetail =
|
||||||
|
"@pklite - Sets your status to Player Killer Lite (PK Lite). PK Lite characters can attack other PK Lite characters. They cannot, however, attack Player Killer (PK) characters. PK Lite characters operate under the same combat rules as PK characters, except that if you are killed in a PK Lite battle, you will not accrue vitae and you will not drop any coins or items. Only Non-Player Killers may use this command to enter PK Lite. Dying in a PK Lite battle and logging off will restore your status to Non-Player Killer.\n";
|
||||||
|
|
||||||
|
public const string AgeDetail =
|
||||||
|
"@age - Displays your total gameplay time.\n";
|
||||||
|
|
||||||
|
public const string BirthDetail =
|
||||||
|
"@birth - Displays when your character was created.\n";
|
||||||
|
|
||||||
|
public const string FrameRateDetail =
|
||||||
|
"@framerate - Toggles the framerate display.\n";
|
||||||
|
|
||||||
|
public const string LockUiDetail =
|
||||||
|
"@lockui - Toggles the locked state of the UI layout.\n";
|
||||||
|
|
||||||
|
public const string VersionDetail =
|
||||||
|
"@version - Tells you what version of the software you are using.\n";
|
||||||
|
|
||||||
|
public const string LocDetail =
|
||||||
|
"@loc - Displays your current position in your chat window. Use this information when you wish to submit a bug report.\n";
|
||||||
|
|
||||||
|
public const string CorpseDetail =
|
||||||
|
"@corpse - Displays the location of your last outdoor death. Even if your corpse has disappeared or if you have subsequently died indoors, typing this command will display your last outdoor corpse location.\n";
|
||||||
|
|
||||||
|
public const string DieDetail =
|
||||||
|
"@die - If you wish to kill your character and leave a corpse, you may use the @die command. This will result in your character's death, you will leave behind a corpse with some of your items, and you will appear at your lifestone. If you wish to travel to your lifestone without leaving behind a corpse, you may use the @lifestone command.\n";
|
||||||
|
|
||||||
|
public const string ClearDetail =
|
||||||
|
"@clear - Clears the chat box of all text.\n";
|
||||||
|
|
||||||
|
public const string SaveUiDetail =
|
||||||
|
"@saveui <filename> - Saves the current user interface layout to disk using the provided file name. If no file name is provided the layout is saved with a name that is unique for your server, character and resolution.\n";
|
||||||
|
|
||||||
|
public const string LoadUiDetail =
|
||||||
|
"@loadui <filename> - Loads a previously saved user interface layout from disk using the provided file name";
|
||||||
|
|
||||||
|
public const string SaveAutoUiDetail =
|
||||||
|
"@saveautoui - Stores the current layout to a character and resolution specific file. This layout will automatically be used when the resolution changes for this character to the current size.\n";
|
||||||
|
|
||||||
|
public const string LoadAutoUiDetail =
|
||||||
|
"@loadautoui - Forces a previously saved layout to load for this user and resolution.";
|
||||||
|
|
||||||
|
public const string AfkDetail =
|
||||||
|
"@afk - Turns on AFK (away-from-keyboard) mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n@afk on - Turns on AFK mode. When set to AFK, other players that send you directed chatyou will receive a customizable message that your are not currently at the keyboard.\n@afk off - Turn off AFK mode.\n@afk msg <message> - Set the message that will be sent to players that send you directed chat while you are in AFK mode. Issuing \"@afk msg\" with no message will set your AFK message back to the default. Your custom AFK message is limited to 192 characters.\n";
|
||||||
|
|
||||||
|
public const string ConsentDetail =
|
||||||
|
"The @consent commands allow you to display and manage your corpse-looting consent list. This list lets you control whether others may permit you to loot their corpse and also allows you to monitor who has given you permission. You may have a maximum of 20 separate permissions at any given time. You will not be able to loot a corpse that was the victim of a player killer, even if its owner has given you permission. Also, players who have squelched you are not able to permit you to loot their corpse. Note that you can toggle your consent on/off via the Character Options panel as well as through these commands.\n@consent on - Turns on your ability to accept permissions from other players.\n@consent off - Turns off your ability to accept permissions from other players.\n@consent who - Lists those who have given you permission to loot their corpses.\n@consent remove <name> - Removes the permission a player granted to you.\n@consent clear - Clears your entire consent list.\n\n";
|
||||||
|
|
||||||
|
public const string EmoteDetail =
|
||||||
|
"The @emote command causes your character to emote some text, by performing an action in the third person. For example, if you typed the following while logged in as a character named Arville:\n @emote looks around the town curiously.\nthen the chat windows of everyone around you would display:\n Arville looks around the town curiously.\nYou can use any of these shorter forms of the command as well:\n @e <text>\n @em <text>\n ; <text>\n : <text>\n\nYou can also use a variety of standard emotes. These emotes come with special animations as well as text. Type @emotes to see a list.\n\n";
|
||||||
|
|
||||||
|
public const string EmoteListDetail =
|
||||||
|
"Standard Emotes:\nNote: These commands should be bound on either side by asterisks. (Example: *wave*)\nShakeFist; Beckon; BeSeeingYou; BlowKiss; BowDeep; ClapHands; Cry; Laugh; Nod; Point; Shrug; Wave; Akimbo; HeartyLaugh; Salute; TapFoot; WaveHigh; WaveLow; Yawn; Stretch; Cringe; Kneel; Plead; Shiver; Shoo; Slouch; Spit; Surrender; Woah; Winded; YMCA; Eat; Drink; Teapot; Pray; Mock; Cheer; Helper; Warm Hands; Scratch Head; Shake Head\n\n";
|
||||||
|
|
||||||
|
public const string FriendsDetail =
|
||||||
|
"Every time someone on your friends list logs in or out, you will receive notification. In addition, you can query the online status of your friends list at any time. Your friends list can contain up to 50 characters.\n@friends - Shows all your current friends and indicates if any of them are online.\n@friends online - Shows your current online friends.\n@friends add <name> - Adds a character to your friends list.\n@friends remove <name> - Removes a character from your friends list.\n@friends remove -all - Clears your friends list.\n@friends old - Shows the characters who were on your old-style friends list prior to the January 2006 update, so you can move them to your new-style friends list if necessary.\n";
|
||||||
|
|
||||||
|
public const string SquelchDetail =
|
||||||
|
"The @squelch commands let you block out messages from specific characters or players. The @unsquelch commands lets squelched messages reach you again. Use the options on these commands to squelch all message types or just some types of messages; one character or an entire account. You may have up to 32 players squelched at once. Note that NPCs cannot be permanently squelched.\n\n@squelch - Shows the current list of squelched characters.\n@squelch [-account] <name> - Squelches all messages from a character. With the account flag, this command also stops everything except normal chat coming from the target's other characters.\n@squelch [-message_type] <character> - This will filter out all text messages of a certain type from a specific character. For example, the following will filter out all tell messages from Oswald:\n Example: @squelch -tell Oswald.\n@squelch -reply [-account] [-message_type] - This filters out all text messages from whoever last tell'd you. You may also use the -account flag and/or limit the squelch by indicating specific message types. For example, this will filter out all tell messages from the account of Oswald, assuming that Oswald was the last person who sent you an @tell:\n Example: @squelch -reply -account -tell\n\n@unsquelch - Shows the current list of squelched characters.\n@unsquelch <name> - Removes all squelches from a character, including account squelch.\n@unsquelch [-message_type] <character> : This allows text messages of type message_type to come from a squelched character. For example the following allows assessment messages from a character name Oswald:\n Example: @unsquelch -assessment Oswald\n@unsquelch -reply [-account] [-message_type] : This allows text messages of type message_type from whoever last sent you an @tell. For example, the following will allow any character on Oswald's account to once again send you @tells, assuming that Oswald was the last person who sent you an @tell:\n Example: @squelch -reply -account -tell\n\nType @messagetypes for a complete list of message types.\n";
|
||||||
|
|
||||||
|
public const string FilterDetail =
|
||||||
|
"The @filter commands filter out all incoming messages of a certain type. Type @messagetypes to see a list of the message types that you can filter.\n@filter - List all the filters currently in place.\n@filter <-message_type> - Filters out all incoming messages of a specific type. For example, the following will filter out all spellcasting text: \n Example: @filter -spellcasting\n@filter -all - Filters out all incoming messages of all types.\n\n";
|
||||||
|
|
||||||
|
public const string UnfilterDetail =
|
||||||
|
"The @unfilter commands remove specific filters from your incoming messages. For a complete list of message types that you can filter, type @help messagetypes.\n@unfilter <-message_type> - Removes filters on incoming messages of a specific type. For example, the following allows spellcasting text to resume:\n Example: @unfilter -spellcasting\n@unfilter -all - Removes all filters on incoming messages of all types.\n";
|
||||||
|
|
||||||
|
public const string FillCompsDetail =
|
||||||
|
"The @fillcomps command assists in the bulk purchase of spell components. It is the sole interface for filling the buy list, which is the column of red zeros to the right in your components panel. To designate which components you would like to buy, change the zeros to the number of each component you would like to buy. The types of components you can buy are scarabs, herbs, powders, potions, and talismans.\n\nThis is the proper syntax: @fillcomps <component type> <pyreal value>\n\n@fillcomps - Fills the buy list with all of the components that are desired.\n@fillcomps <component type> - Fills the buy list with all of the components of the given type.\n@fillcomps <pyreal value> - Fills the buy list with all of the components until the total price of the components exceeds the given value.\n@fillcomps <component type> <pyreal value> - Fills the buy list with all of the components of the given type until the total price of components exceeds the given value.\n@fillcomps clear - Sets the requested amount for all components to zero.\n";
|
||||||
|
|
||||||
|
public const string EnduranceDetail =
|
||||||
|
"The endurance attribute has a number of abilities tied to it.\nFirst, some combination of strength and endurance (with endurance being more important) now allows one to regenerate hit points at a faster rate the higher one's endurance is. This bonus is in addition to any regeneration spells one may have placed upon themselves. This endurance regeneration bonus caps at around 110%.\nSecond, the higher a player's Endurance, the less stamina one uses while attacking. This benefit is tied to Endurance only, and it caps out at around 50% less stamina used per attack. The minimum stamina used per attack remains one.\nThird, the higher a player's Endurance, the more likely they are not to use a point of stamina to successfully evade a missile or melee attack. A player is required to have Melee Defense for melee attacks or Missile Defense for missile attacks trained or specialized in order for this specific ability to work. This benefit is tied to Endurance only, and it caps out at around a 75% chance to avoid losing a point of stamina per successful evasion.\nFourth, some combination of strength and endurance (the two are roughly of equivalent importance) now allows one to partially resist drain and harm attacks, up to a maximum of roughly 50%.\nFifth, some combination of strength and endurance (the two are roughly of equivalent importance) now allows one to have a level of \"natural resistances\" to the 7 damage types, the same as a certain level of life protections. This caps out at a 50% resistance (the equivalent to level 5 life prots) to these damage types. This resistance is not additive to life protections: higher level life protections will overwrite these natural resistances, although life vulns will take these natural resistances into account, if the player does not have a higher level life protection cast upon him.\nThe natural resistances, drain resistances, and regeneration rate info are now visible on the Character Information Panel, in what was once the Burden panel. This panel now displays the above three Endurance benefits, the burden info, as well as information about your age, birth date, and number of deaths.\nThe 5 categories for the endurance benefits are, in order from lowest benefit to highest: Poor, Mediocre, Hardy, Resilient, and Indomitable, with each range of benefits divided up equally amongst the 5 (e.g. Poor describes having anywhere from 1-10% resistance against drain health attacks, etc.).\n";
|
||||||
|
|
||||||
|
public const string SpeakerDetail =
|
||||||
|
"@speaker - No longer used, see @allegiance officer for a similar command.\n";
|
||||||
|
|
||||||
|
public const string TitleDetail =
|
||||||
|
"@title <new title> - Sets the title of the popup chat window.\n";
|
||||||
|
|
||||||
|
public const string ChatToggleDetail =
|
||||||
|
"@chat <on/off> - Sets whether or not you receive normal chat. When set to \"off\", you will no longer receive any spoken speech (normal chat). However, you will still receive tells.\n";
|
||||||
|
|
||||||
|
public const string NoTellDetail =
|
||||||
|
"@notell <on/off> - Sets whether or not you receive @tells. When set to \"on\", you will not receive any tells.\n";
|
||||||
|
|
||||||
|
public const string JoinChatDetail =
|
||||||
|
"@join <channel tag> - Allows you to hear and speak on the given channel.\n";
|
||||||
|
|
||||||
|
public const string LeaveChatDetail =
|
||||||
|
"@leave <channel tag> - Prevents you from hearing or speaking on the given channel.\n";
|
||||||
|
|
||||||
|
public const string PermitDetail =
|
||||||
|
"The @permit command gives or revokes corpse-looting permissions to other players. You can permit other players to loot any one of your corpses. You may not @permit a player again until he or she has looted your corpse. Permissions expire either after one hour or when the permitted player logs off. If you were killed by a player killer, no one can loot your corpse except you or your killer, even if you give someone else permission.\n@permit add <name> - Allows another player to loot your corpse.\n@permit remove <name> - Removes permission to access your corpse from the named character.\nType @help consent for more details on corpse looting.\n";
|
||||||
|
|
||||||
|
public const string HslistDetail =
|
||||||
|
"@hslist <house type> - Lists the number and, if appropriate, positions of houses currently available for purchase. Types include: Apartment, Cottage, Villa, Mansion\n";
|
||||||
|
|
||||||
|
private static readonly FrozenDictionary<string, string> CatalogVerbDetailByVerb =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["lifestone"] = LifestoneDetail,
|
||||||
|
["lif"] = LifestoneDetail,
|
||||||
|
["ls"] = LifestoneDetail,
|
||||||
|
["marketplace"] = MarketplaceDetail,
|
||||||
|
["mar"] = MarketplaceDetail,
|
||||||
|
["mp"] = MarketplaceDetail,
|
||||||
|
["pkarena"] = PkArenaDetail,
|
||||||
|
["pka"] = PkArenaDetail,
|
||||||
|
["pklarena"] = PkLiteArenaDetail,
|
||||||
|
["pla"] = PkLiteArenaDetail,
|
||||||
|
["pklite"] = PkLiteDetail,
|
||||||
|
["pkl"] = PkLiteDetail,
|
||||||
|
// hor/hr/hom/hoa share HelpHouse with "house"/"hou" — see the
|
||||||
|
// CmdHashData note in the class remarks above.
|
||||||
|
["hor"] = HouseOverview,
|
||||||
|
["hr"] = HouseOverview,
|
||||||
|
["hom"] = HouseOverview,
|
||||||
|
["hoa"] = HouseOverview,
|
||||||
|
["age"] = AgeDetail,
|
||||||
|
["birth"] = BirthDetail,
|
||||||
|
["framerate"] = FrameRateDetail,
|
||||||
|
["lockui"] = LockUiDetail,
|
||||||
|
["version"] = VersionDetail,
|
||||||
|
["loc"] = LocDetail,
|
||||||
|
["corpse"] = CorpseDetail,
|
||||||
|
["cor"] = CorpseDetail,
|
||||||
|
["die"] = DieDetail,
|
||||||
|
["clear"] = ClearDetail,
|
||||||
|
["saveui"] = SaveUiDetail,
|
||||||
|
["loadui"] = LoadUiDetail,
|
||||||
|
["saveautoui"] = SaveAutoUiDetail,
|
||||||
|
["loadautoui"] = LoadAutoUiDetail,
|
||||||
|
["afk"] = AfkDetail,
|
||||||
|
["consent"] = ConsentDetail,
|
||||||
|
["e"] = EmoteDetail,
|
||||||
|
["em"] = EmoteDetail,
|
||||||
|
["emote"] = EmoteDetail,
|
||||||
|
["me"] = EmoteDetail,
|
||||||
|
["emotes"] = EmoteListDetail,
|
||||||
|
// friends/friends_add/friends_remove share HelpFriends — see the
|
||||||
|
// CmdHashData note in the class remarks above.
|
||||||
|
["friends"] = FriendsDetail,
|
||||||
|
["friends_add"] = FriendsDetail,
|
||||||
|
["friends_remove"] = FriendsDetail,
|
||||||
|
// squelch/unsquelch share HelpSquelch (and its own concatenated
|
||||||
|
// HelpAdvancedSquelch/HelpAdvancedUnSquelch delegation) — see
|
||||||
|
// the CmdHashData note in the class remarks above.
|
||||||
|
["squelch"] = SquelchDetail,
|
||||||
|
["unsquelch"] = SquelchDetail,
|
||||||
|
["filter"] = FilterDetail,
|
||||||
|
["unfilter"] = UnfilterDetail,
|
||||||
|
// messagetypes/message_types/msgtypes/msg_types intentionally
|
||||||
|
// absent — HelpMessageTypes builds its text from a live enum
|
||||||
|
// table at runtime (GetListofSquelchChannels), not a static
|
||||||
|
// string; see the class remarks above.
|
||||||
|
["fillcomps"] = FillCompsDetail,
|
||||||
|
["endurance"] = EnduranceDetail,
|
||||||
|
["speaker"] = SpeakerDetail,
|
||||||
|
["title"] = TitleDetail,
|
||||||
|
["chat"] = ChatToggleDetail,
|
||||||
|
["notell"] = NoTellDetail,
|
||||||
|
["join"] = JoinChatDetail,
|
||||||
|
["leave"] = LeaveChatDetail,
|
||||||
|
["permit"] = PermitDetail,
|
||||||
|
["hslist"] = HslistDetail,
|
||||||
|
// alh/ah share HelpAllegiance with "allegiance"/"all" — see the
|
||||||
|
// CmdHashData note in the class remarks above.
|
||||||
|
["alh"] = AllegianceOverview,
|
||||||
|
["ah"] = AllegianceOverview,
|
||||||
|
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail-registered catalog leaf verbs confirmed (via the CmdHashData
|
||||||
|
/// help-pointer note in the class remarks above) to register with a
|
||||||
|
/// NULL help function pointer. Retail's own <c>DoHelp</c> skips its
|
||||||
|
/// help-callback branch entirely for these and falls straight to the
|
||||||
|
/// generic <see cref="UnknownCommand"/> text — the SAME text an
|
||||||
|
/// unregistered verb gets — even though the verb dispatches normally
|
||||||
|
/// for ordinary (non-help) use. <c>ChatCommandRouter</c> must check
|
||||||
|
/// this BEFORE consulting either <see cref="CatalogVerbDetailByVerb"/>
|
||||||
|
/// or the catalog's own summary, or it would show acdream-invented
|
||||||
|
/// help text retail never displays.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly FrozenSet<string> CatalogVerbsWithNoRetailHelp =
|
||||||
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "index", "clist", "on", "off" }
|
||||||
|
.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retail Detail_HelpType(2) text for a <see cref="RetailClientCommandCatalog"/>
|
||||||
|
/// leaf verb — see the class remarks' "Catalog leaf-verb Detail
|
||||||
|
/// extraction" section for the recovery method and coverage count.
|
||||||
|
/// Callers should check <see cref="CatalogVerbsWithNoRetailHelp"/>
|
||||||
|
/// FIRST (a confirmed-null verb must never reach this lookup, since a
|
||||||
|
/// stale entry here would silently show text retail never displays for
|
||||||
|
/// it), then this method, then fall back to
|
||||||
|
/// <see cref="RetailClientCommandCatalog.TryGetHelpText"/> for the
|
||||||
|
/// verbs not yet extracted.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryGetCatalogVerbDetailText(string verb, out string detailText) =>
|
||||||
|
CatalogVerbDetailByVerb.TryGetValue(verb.TrimEnd(','), out detailText!);
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, string> ByVerb =
|
private static readonly FrozenDictionary<string, string> ByVerb =
|
||||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Composition;
|
namespace AcDream.App.Tests.Composition;
|
||||||
|
|
||||||
|
|
@ -149,6 +150,43 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
], fixture.Factory.Releases);
|
], fixture.Factory.Releases);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Consolidated-review round (2026-08-10), SHOULD-FIX 2: the a5a7eb4f
|
||||||
|
/// defect class — a composed <see cref="ChatVM.OnInterfaceText"/> hook
|
||||||
|
/// wired but never transferred to production — had no test, because
|
||||||
|
/// <see cref="FakeFactory.CreateRetainedUi"/> above substitutes a
|
||||||
|
/// <see cref="Stub{T}"/> <see cref="ChatVM"/> for the composition-order
|
||||||
|
/// tests and never exercises the REAL wiring in
|
||||||
|
/// <see cref="RetailInteractionRetainedUiCompositionFactory.CreateChatViewModel"/>.
|
||||||
|
/// This test calls that production method directly (it only touches
|
||||||
|
/// <see cref="InteractionRetainedUiDependencies.Communication"/>, no
|
||||||
|
/// GPU/dat/UiHost dependencies, so the existing null-heavy
|
||||||
|
/// <see cref="Fixture"/> dependencies are sufficient without a full
|
||||||
|
/// <see cref="Fixture.Compose"/>) and proves the hook both EXISTS and
|
||||||
|
/// actually reaches <see cref="RuntimeCommunicationState.SpewBox"/>,
|
||||||
|
/// not merely that it was assigned.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ComposedChatViewModelWiresOnInterfaceTextToSpewBox()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(retailUi: true);
|
||||||
|
|
||||||
|
ChatVM chat = RetailInteractionRetainedUiCompositionFactory
|
||||||
|
.CreateChatViewModel(fixture.Dependencies);
|
||||||
|
|
||||||
|
Assert.NotNull(chat.OnInterfaceText);
|
||||||
|
|
||||||
|
const string probeText = "consolidated-review SHOULD-FIX 2 probe";
|
||||||
|
chat.ShowInterfaceText(probeText);
|
||||||
|
// ShowInterfaceText only enqueues; SpewBoxState decouples enqueue
|
||||||
|
// from visibility (see its own class remarks) -- Tick drains it.
|
||||||
|
fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Tick(0);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Snapshot(),
|
||||||
|
entry => entry.Text == probeText);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody()
|
public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1558,14 +1558,32 @@ public sealed class HeadlessSessionHostTests
|
||||||
/// a DIFFERENT landblock, the same shape as one of the OTHER eight
|
/// a DIFFERENT landblock, the same shape as one of the OTHER eight
|
||||||
/// landblocks in production's real 3x3 publication plan — proving Step
|
/// landblocks in production's real 3x3 publication plan — proving Step
|
||||||
/// 3a's gate covers "an admission is open ANYWHERE in the plan", not
|
/// 3a's gate covers "an admission is open ANYWHERE in the plan", not
|
||||||
/// just the center. Must FAIL on the pre-Step-3a tree — verified by
|
/// just the center.
|
||||||
/// temporarily reverting the three <c>IsQuiescent</c> gates in
|
|
||||||
/// <c>HeadlessSessionWorldProjection.ProjectSpawn</c>/
|
|
||||||
/// <c>ProjectPosition</c>/<c>PumpFirstEntry</c> and confirming this test
|
|
||||||
/// fails (the conductor reaches <c>PublicationCommitted</c> — a non-null
|
|
||||||
/// dormant controller — while the unrelated admission is still held
|
|
||||||
/// open, since driving is unconditional pre-fix).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Consolidated-review round (2026-08-10), NIT (c): what THIS bounded
|
||||||
|
/// xunit test proves is the GATE, not the #365 hydration stall's
|
||||||
|
/// closure — that broader claim's evidence is the separate live-ACE
|
||||||
|
/// end-to-end run recorded in
|
||||||
|
/// <c>docs/research/2026-08-10-365-headless-hydration-diagnosis.md</c>
|
||||||
|
/// §8 ("hydration now succeeds ... entityCount reaches 136"). This
|
||||||
|
/// test's own scope is narrower and fully mechanical: the conductor
|
||||||
|
/// stays undriven (<c>runtime.MovementOwner.Controller</c> stays null)
|
||||||
|
/// for as long as the unrelated admission is held open, and drives and
|
||||||
|
/// reaches <c>IsRuntimePublished</c> within a bounded tick budget once
|
||||||
|
/// it releases. Temporarily reverting the three <c>IsQuiescent</c>
|
||||||
|
/// gates in <c>HeadlessSessionWorldProjection.ProjectSpawn</c>/
|
||||||
|
/// <c>ProjectPosition</c>/<c>PumpFirstEntry</c> (never committed — see
|
||||||
|
/// §8's "Verification that the new test... actually discriminates")
|
||||||
|
/// makes this test fail the SAME way §8 itself observed: the
|
||||||
|
/// <c>Assert.Null(runtime.MovementOwner.Controller)</c> checks below
|
||||||
|
/// fail because the controller is ALREADY built AND published
|
||||||
|
/// (<c>CanExecuteLiveMovement = True</c>) while the unrelated admission
|
||||||
|
/// is still open — not merely "reached <c>PublicationCommitted</c>,
|
||||||
|
/// still dormant" as an earlier draft of this comment claimed; that
|
||||||
|
/// wording described a weaker intermediate state than what reverting
|
||||||
|
/// the gate actually produces.
|
||||||
|
/// </remarks>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased()
|
public void RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Headless.Configuration;
|
using AcDream.Headless.Configuration;
|
||||||
|
using AcDream.Headless.Diagnostics;
|
||||||
using AcDream.Headless.Hosting;
|
using AcDream.Headless.Hosting;
|
||||||
|
|
||||||
namespace AcDream.Headless.Tests;
|
namespace AcDream.Headless.Tests;
|
||||||
|
|
@ -8,9 +9,13 @@ namespace AcDream.Headless.Tests;
|
||||||
/// #365 Step 1: the audit's refusal rationale is multi-root attribution
|
/// #365 Step 1: the audit's refusal rationale is multi-root attribution
|
||||||
/// ambiguity, which does not hold for a process that owns exactly one
|
/// ambiguity, which does not hold for a process that owns exactly one
|
||||||
/// session. These tests mutate <see cref="PhysicsDiagnostics"/> process-
|
/// session. These tests mutate <see cref="PhysicsDiagnostics"/> process-
|
||||||
/// global probe flags and swap <see cref="Console.Out"/>, so they run in
|
/// global probe flags, so they run in their own non-parallel collection —
|
||||||
/// their own non-parallel collection — see
|
/// see <see cref="HeadlessStaticStateAuditCollection"/>. Consolidated-
|
||||||
/// <see cref="HeadlessStaticStateAuditCollection"/>.
|
/// review round (2026-08-10), NIT (a): the single-session log line now
|
||||||
|
/// routes through the injected <see cref="HeadlessDiagnosticWriter"/>
|
||||||
|
/// instead of <see cref="Console.Out"/>, so these tests capture its
|
||||||
|
/// backing <see cref="TextWriter"/> directly rather than swapping the
|
||||||
|
/// process-global console.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CollectionDefinition(
|
[CollectionDefinition(
|
||||||
HeadlessStaticStateAuditCollection.Name,
|
HeadlessStaticStateAuditCollection.Name,
|
||||||
|
|
@ -31,17 +36,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
||||||
public void SingleSessionWithProbeEnabledIsAllowedAndLoggedLoudly()
|
public void SingleSessionWithProbeEnabledIsAllowedAndLoggedLoudly()
|
||||||
{
|
{
|
||||||
PhysicsDiagnostics.ProbeParkEnabled = true;
|
PhysicsDiagnostics.ProbeParkEnabled = true;
|
||||||
var originalOut = Console.Out;
|
|
||||||
using var captured = new StringWriter();
|
using var captured = new StringWriter();
|
||||||
Console.SetOut(captured);
|
var diagnostics = new HeadlessDiagnosticWriter(captured);
|
||||||
try
|
|
||||||
{
|
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||||
HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1);
|
sessionCount: 1, diagnostics);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Console.SetOut(originalOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
||||||
|
|
@ -52,17 +51,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SingleSessionWithNoProbesEnabledIsSilent()
|
public void SingleSessionWithNoProbesEnabledIsSilent()
|
||||||
{
|
{
|
||||||
var originalOut = Console.Out;
|
|
||||||
using var captured = new StringWriter();
|
using var captured = new StringWriter();
|
||||||
Console.SetOut(captured);
|
var diagnostics = new HeadlessDiagnosticWriter(captured);
|
||||||
try
|
|
||||||
{
|
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||||
HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1);
|
sessionCount: 1, diagnostics);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Console.SetOut(originalOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Equal(string.Empty, captured.ToString());
|
Assert.Equal(string.Empty, captured.ToString());
|
||||||
}
|
}
|
||||||
|
|
@ -71,11 +64,12 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
||||||
public void MultiSessionWithProbeEnabledStillThrowsNamingTheProbe()
|
public void MultiSessionWithProbeEnabledStillThrowsNamingTheProbe()
|
||||||
{
|
{
|
||||||
PhysicsDiagnostics.ProbeParkEnabled = true;
|
PhysicsDiagnostics.ProbeParkEnabled = true;
|
||||||
|
var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null);
|
||||||
|
|
||||||
HeadlessConfigurationException exception = Assert.Throws<
|
HeadlessConfigurationException exception = Assert.Throws<
|
||||||
HeadlessConfigurationException>(
|
HeadlessConfigurationException>(
|
||||||
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||||
sessionCount: 2));
|
sessionCount: 2, diagnostics));
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
||||||
|
|
@ -86,9 +80,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MultiSessionWithNoProbesEnabledIsAllowed()
|
public void MultiSessionWithNoProbesEnabledIsAllowed()
|
||||||
{
|
{
|
||||||
|
var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null);
|
||||||
|
|
||||||
Exception? exception = Record.Exception(
|
Exception? exception = Record.Exception(
|
||||||
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||||
sessionCount: 3));
|
sessionCount: 3, diagnostics));
|
||||||
|
|
||||||
Assert.Null(exception);
|
Assert.Null(exception);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,20 +93,23 @@ public class ChatCommandRouterTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LifestoneWithArguments_ShowsRetailBadArgsRefusal_ViaInterfaceTextSeam()
|
public void LifestoneWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam()
|
||||||
{
|
{
|
||||||
// #363 / register row AP-183: a Definition with no bespoke
|
// Consolidated-review round (2026-08-10), SHOULD-FIX 3: retires
|
||||||
// InvalidArgumentsText falls to retail's own generic bad-args
|
// register row AP-113. DoLifestone prints ITS OWN 0x1A string for
|
||||||
// fallback (HandleFailureEvent(0x26), "That is not a valid
|
// bad args and returns 1 -- retail never reaches the generic
|
||||||
// command.") at 0x1A ClientLocal — never the acdream-invented
|
// HandleFailureEvent(0x26) fallback for it. Byte-recovered from the
|
||||||
// "Usage: /lifestone" line this used to synthesize.
|
// PDB-paired acclient.exe (Binary Ninja mis-attributes the literal
|
||||||
|
// to an unrelated vtable-slot symbol).
|
||||||
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||||
|
|
||||||
var outcome = ChatCommandRouter.Submit("/ls now", vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit("/ls now", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts));
|
Assert.Equal(
|
||||||
|
"Please see @help lifestone for more information on how to use this command.",
|
||||||
|
Assert.Single(interfaceTexts));
|
||||||
Assert.Empty(log.Snapshot());
|
Assert.Empty(log.Snapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,10 +125,51 @@ public class ChatCommandRouterTests
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
var entry = Assert.Single(log.Snapshot());
|
var entry = Assert.Single(log.Snapshot());
|
||||||
Assert.Equal("That is not a valid command.", entry.Text);
|
Assert.Equal(
|
||||||
|
"Please see @help lifestone for more information on how to use this command.",
|
||||||
|
entry.Text);
|
||||||
Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
|
Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PkArenaWithArguments_ShowsRetailsGenericBadArgsFallback_ViaInterfaceTextSeam()
|
||||||
|
{
|
||||||
|
// #363 / register row AP-183: a Definition with no bespoke
|
||||||
|
// InvalidArgumentsText (PkArena never had one, unlike Lifestone/
|
||||||
|
// Marketplace above) falls to retail's own generic bad-args
|
||||||
|
// fallback (HandleFailureEvent(0x26), "That is not a valid
|
||||||
|
// command.") at 0x1A ClientLocal -- never the acdream-invented
|
||||||
|
// "Usage: /pkarena" line this used to synthesize. Carries the
|
||||||
|
// generic-fallback coverage the two Lifestone tests above used to
|
||||||
|
// own before Lifestone got its own bespoke text.
|
||||||
|
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit("/pka now", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
Assert.Empty(bus.Published);
|
||||||
|
Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts));
|
||||||
|
Assert.Empty(log.Snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MarketplaceWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam()
|
||||||
|
{
|
||||||
|
// Same methodology and same vtable-mislabeling artifact as
|
||||||
|
// Lifestone above -- see RetailClientCommandCatalog.Marketplace's
|
||||||
|
// own citation.
|
||||||
|
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit("/mar now", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
Assert.Empty(bus.Published);
|
||||||
|
Assert.Equal(
|
||||||
|
"Please see @help marketplace for more information on how to use this command.",
|
||||||
|
Assert.Single(interfaceTexts));
|
||||||
|
Assert.Empty(log.Snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PkLiteAlias_ResolvesAsClientHandled_NotTheServerTextPath()
|
public void PkLiteAlias_ResolvesAsClientHandled_NotTheServerTextPath()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,182 @@ public sealed class RetailCommandHelpTableTests
|
||||||
entries[1].Text);
|
entries[1].Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Consolidated-review round (2026-08-10), SHOULD-FIX 1 ────────────
|
||||||
|
// Catalog leaf-verb Detail extraction: /help die and /help lifestone
|
||||||
|
// pinned byte-exact through the router (the actual user-visible
|
||||||
|
// outcome, not just the table's stored string — same pattern as
|
||||||
|
// DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape above),
|
||||||
|
// plus a coverage-count assertion so a future extraction pass (or an
|
||||||
|
// accidental regression that silently drops an override) is caught.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpDie_ThroughRouter_PrintsRetailsExactDetailText()
|
||||||
|
{
|
||||||
|
// HelpDie @0x005784a0: Summary @0x005784ad ("Kills your character
|
||||||
|
// and leaves a corpse..."), Detail @0x005784b4 (the long paragraph
|
||||||
|
// below) -- DoHelp always calls Detail_HelpType(2) for a resolved
|
||||||
|
// verb, never Summary. Byte-swept against the PDB-paired
|
||||||
|
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH).
|
||||||
|
var log = new AcDream.Core.Chat.ChatLog();
|
||||||
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
|
var bus = new RecordingCommandBus();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit(
|
||||||
|
"/help die", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
var entries = log.Snapshot();
|
||||||
|
Assert.Equal(2, entries.Length);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
RetailCommandHelpTable.ForMoreInformationPrefix
|
||||||
|
+ RetailCommandHelpTable.DieDetail,
|
||||||
|
entries[1].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
"@die - If you wish to kill your character and leave a corpse, "
|
||||||
|
+ "you may use the @die command. This will result in your "
|
||||||
|
+ "character's death, you will leave behind a corpse with some "
|
||||||
|
+ "of your items, and you will appear at your lifestone. If "
|
||||||
|
+ "you wish to travel to your lifestone without leaving behind "
|
||||||
|
+ "a corpse, you may use the @lifestone command.\n",
|
||||||
|
RetailCommandHelpTable.DieDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpLifestone_ThroughRouter_PrintsRetailsExactDetailText()
|
||||||
|
{
|
||||||
|
// HelpLifestone @0x00578500 is UNCONDITIONAL (no Summary/Detail
|
||||||
|
// branch at all) -- the same string prints for every HelpType,
|
||||||
|
// confirmed by reading the decompiled body directly (no if/else).
|
||||||
|
var log = new AcDream.Core.Chat.ChatLog();
|
||||||
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
|
var bus = new RecordingCommandBus();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit(
|
||||||
|
"/help lifestone", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
var entries = log.Snapshot();
|
||||||
|
Assert.Equal(2, entries.Length);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
RetailCommandHelpTable.ForMoreInformationPrefix
|
||||||
|
+ RetailCommandHelpTable.LifestoneDetail,
|
||||||
|
entries[1].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
"@lifestone - Returns you to the last lifestone you used without killing you.\n",
|
||||||
|
RetailCommandHelpTable.LifestoneDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpAlh_RoutesToTheSameHelpAllegianceTextAsHelpAllegiance()
|
||||||
|
{
|
||||||
|
// alh/ah are registered with HelpAllegiance as their OWN help
|
||||||
|
// pointer (confirmed via InitializeCommands's field-store
|
||||||
|
// registration pattern, not the CmdHashData-constructor pattern) --
|
||||||
|
// the exact same function "allegiance"/"all" use, so /help alh must
|
||||||
|
// show byte-identical text to /help allegiance, not a separate
|
||||||
|
// (possibly stale) copy.
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText(
|
||||||
|
"alh", out string alhText));
|
||||||
|
Assert.Equal(RetailCommandHelpTable.AllegianceOverview, alhText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("index")]
|
||||||
|
[InlineData("clist")]
|
||||||
|
[InlineData("on")]
|
||||||
|
[InlineData("off")]
|
||||||
|
public void ConfirmedNullHelpVerb_ThroughRouter_ShowsUnknownCommandNotCatalogSummary(
|
||||||
|
string verb)
|
||||||
|
{
|
||||||
|
// index/clist/on/off are registered in InitializeCommands with a
|
||||||
|
// plain `int32_t var_3c_NN = 0;` local -- no preceding Help-pointer
|
||||||
|
// assignment, unlike every extracted verb above -- confirming a
|
||||||
|
// genuinely NULL help function pointer. Retail's own DoHelp skips
|
||||||
|
// its help-callback branch entirely for these and falls to the
|
||||||
|
// SAME "Unknown command" 0x1A text an unregistered verb gets, even
|
||||||
|
// though the verb dispatches fine for ordinary (non-help) use.
|
||||||
|
// Showing the catalog's own invented summary here would be
|
||||||
|
// retail-inaccurate.
|
||||||
|
var log = new AcDream.Core.Chat.ChatLog();
|
||||||
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
|
var bus = new RecordingCommandBus();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit(
|
||||||
|
$"/help {verb}", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
var entries = log.Snapshot();
|
||||||
|
Assert.Single(entries);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
(uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
|
||||||
|
entries[0].LogTextType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CatalogLeafVerbCoverage_ExtractedVsConfirmedNullVsUnverified_MatchesConsolidatedReviewCount()
|
||||||
|
{
|
||||||
|
// Coverage-count assertion (SHOULD-FIX 1's explicit acceptance
|
||||||
|
// criterion): dedupe RetailClientCommandCatalog.KnownVerbs down to
|
||||||
|
// one entry per distinct Definition via its (unique-per-Definition)
|
||||||
|
// catalog summary text, excluding the two meta-dispatchers
|
||||||
|
// ("house"/"hou" and "allegiance"/"all" resolve subcommand-by-
|
||||||
|
// subcommand, not as a single leaf Definition), then classify each
|
||||||
|
// distinct Definition into exactly one bucket. A future extraction
|
||||||
|
// pass should bump ExtractedCount down UnverifiedCount and update
|
||||||
|
// this pin in the same commit; an accidental regression that drops
|
||||||
|
// an override without updating CatalogVerbsWithNoRetailHelp would
|
||||||
|
// fail this test instead of silently reverting to a summary.
|
||||||
|
var seenSummaries = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
int extractedCount = 0;
|
||||||
|
int confirmedNullCount = 0;
|
||||||
|
int unverifiedCount = 0;
|
||||||
|
|
||||||
|
foreach (string verb in RetailClientCommandCatalog.KnownVerbs)
|
||||||
|
{
|
||||||
|
if (verb is "house" or "hou" or "allegiance" or "all")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
RetailClientCommandCatalog.TryGetHelpText(verb, out string summary),
|
||||||
|
$"catalog verb '{verb}' has no summary text");
|
||||||
|
if (!seenSummaries.Add(summary))
|
||||||
|
continue; // an alias of a Definition already counted
|
||||||
|
|
||||||
|
bool confirmedNull = RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp
|
||||||
|
.Contains(verb);
|
||||||
|
bool extracted = RetailCommandHelpTable.TryGetCatalogVerbDetailText(
|
||||||
|
verb, out _);
|
||||||
|
|
||||||
|
// A verb must never be BOTH confirmed-null AND carry an
|
||||||
|
// extracted override -- that would mean ChatCommandRouter's
|
||||||
|
// ordering (confirmed-null checked first) is hiding a real
|
||||||
|
// Detail text, or the two tables disagree about the same verb.
|
||||||
|
Assert.False(
|
||||||
|
confirmedNull && extracted,
|
||||||
|
$"'{verb}' is both confirmed-null and has an extracted Detail override");
|
||||||
|
|
||||||
|
if (confirmedNull)
|
||||||
|
confirmedNullCount++;
|
||||||
|
else if (extracted)
|
||||||
|
extractedCount++;
|
||||||
|
else
|
||||||
|
unverifiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measured, not hand-counted -- 47 distinct leaf Definitions total
|
||||||
|
// (squelch/unsquelch and friends/friends_add/friends_remove each
|
||||||
|
// count separately here since they are DISTINCT Definitions with
|
||||||
|
// their own summary text, even though several share ONE extracted
|
||||||
|
// Detail text -- see the CmdHashData note in RetailCommandHelpTable's
|
||||||
|
// class remarks).
|
||||||
|
Assert.Equal(42, extractedCount);
|
||||||
|
Assert.Equal(4, confirmedNullCount);
|
||||||
|
Assert.Equal(1, unverifiedCount);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingCommandBus : ICommandBus
|
private sealed class RecordingCommandBus : ICommandBus
|
||||||
{
|
{
|
||||||
public List<object> Published { get; } = new();
|
public List<object> Published { get; } = new();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue