fix(chat): Campaign CH user-gate round 2 -- portal notice rerouted to SpewBox, verbatim /help extraction, jump-in-air evidence
Item 2: retail's portal-space "In Portal Space..." notice is the SpewBox (ECM_UI::SendNotice_DisplayStringInfo(0x1A,...) -> AddTextToScroll(str, 0x1A, 1, 0), hardcoded to the SpewBox per the decomp), not a dedicated centered overlay. PortalWaitNoticeController and its lease are deleted; PortalTunnelPresentation's per-rotation-segment cadence now writes straight into RuntimeCommunicationState.AddText(ClientLocal) -- the SpewBox's own dedupe-at-index-0 handles the repetition exactly as retail's does. Register row AP-184 records the surface fix and the AP-178 scope extension. Items 4+5: /help text was partially fabricated -- the user caught the "/help death" meta-message. Generalized tools/pdb-extract/sweep_weenie_strings.py to decode narrow PStringBase<char> literals (the ClientCommunicationSystem::Help* family's shape) alongside its original UTF-16LE support, then swept every HelpXxxGroup function's exact byte extent against the PDB-paired acclient.exe. 4 of 7 group topics (death/status/text/allegiances) are now complete verbatim listings; the other 3 (channels/chatting/commands) keep an honest UNVERIFIED note citing HelpStupidChannelHack @0x0056f290 (a genuinely undecodable BN-mislabeled-fragment mechanism) instead of the old fabricated sentinel. 7 of ~35 channel one-liners are also now verbatim. ISSUES.md #364 tracks the remainder; RetailCommandHelpTableTests.cs pins every result byte-exact. Item 1: jump-in-air refusal still silent live is NOT reproduced and NOT speculatively fixed. Exhaustive static re-audit found the mechanism correct by construction (single-writer OnWalkable, exactly-once-per-frame Update()/Capture(), no interfering edge-history resets). A live headless repro (new jump-probe bot policy, real ACE connect) was blocked -- probeaccount2 has no character, and the graphical client already owned testaccount this session so the task's own fallback rule forbade using it. Two temporary probes are left behind ACDREAM_PROBE_JUMP=1 (blocked entirely in Headless by the existing multi-session static-state guard -- graphical-only for the next round). Item 3 confirmed fixed, no regression. Item 6 (resize: no diagonal cursors, cannot grow Y from bottom-right) folded into CH6a's existing scope. Full Release suite: 12,267 passed / 4 skipped / 0 failed (up from 12,221/4/0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a485425743
commit
c1f1582576
14 changed files with 788 additions and 254 deletions
|
|
@ -121,6 +121,38 @@ than a REJECT-review fix batch; CH5-or-later.
|
||||||
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
|
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
|
||||||
CH4 REJECT-review).
|
CH4 REJECT-review).
|
||||||
|
|
||||||
|
## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved
|
||||||
|
|
||||||
|
**Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3.
|
||||||
|
The user caught `/help death` printing an acdream meta-message instead of
|
||||||
|
retail's real listing; all 7 `ClientCommunicationSystem::HelpXxxGroup`
|
||||||
|
nodes were re-extracted verbatim from the PDB-paired binary via a
|
||||||
|
generalized `tools/pdb-extract/sweep_weenie_strings.py --ascii-only`.
|
||||||
|
4 of 7 (death/status/text/allegiances) are now COMPLETE verbatim listings
|
||||||
|
(`RetailCommandHelpTable.DeathGroupDetail` etc.). 3 remain PARTIAL
|
||||||
|
(`ChannelsGroupDetail`, `ChattingGroupDetail`, `CommandsGroupDetail`):
|
||||||
|
their detail text is built (in full or in part) by
|
||||||
|
`ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which
|
||||||
|
constructs its output from three string fragments Binary Ninja
|
||||||
|
mis-attributes to unrelated vtable slots (the classic BN pooled/
|
||||||
|
mislabeled-data artifact — `&ClientCommunicationSystem::\`vftable'.
|
||||||
|
RecvNotice_StartBarberNotice` etc. are NOT real vtable dispatch)
|
||||||
|
concatenated around a live `ChannelSystem::GetChannelName` call —
|
||||||
|
genuinely not decodable from a static string sweep. Each partial group
|
||||||
|
keeps its own summary line (fully verbatim) and an explicit UNVERIFIED
|
||||||
|
note citing the address; `chatting` additionally resolves 7 of its 8
|
||||||
|
detail entries (only the 5 HelpStupidChannelHack-delegated channel-alias
|
||||||
|
lines are missing). The same mechanism also blocks 5 of ~35 channel
|
||||||
|
one-liners in `ByVerb` (fellowship/monarch/patron/vassals/covassal
|
||||||
|
family), which stay acdream-authored summaries. Resolving
|
||||||
|
HelpStupidChannelHack requires reading the three mislabeled string
|
||||||
|
fragments directly out of `.rdata` by address (not by BN symbol name) and
|
||||||
|
confirming the channel-name substring logic — a focused follow-up, not
|
||||||
|
guessed.
|
||||||
|
|
||||||
|
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
|
||||||
|
user gate round 2).
|
||||||
|
|
||||||
## Note — six invented chat verbs removed for registry parity (2026-08-09)
|
## Note — six invented chat verbs removed for registry parity (2026-08-09)
|
||||||
|
|
||||||
Campaign CH slice CH4 deleted `/gen`, `/cv`, `/lookingforgroup`, `/tr`,
|
Campaign CH slice CH4 deleted `/gen`, `/cv`, `/lookingforgroup`, `/tr`,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -18,11 +18,21 @@ rows TS-68/TS-69/TS-70), and 9 are retail's own null-handler help-only
|
||||||
nodes. CH5 (this closeout sweep) is COMPLETE — plan/register/ISSUES/
|
nodes. CH5 (this closeout sweep) is COMPLETE — plan/register/ISSUES/
|
||||||
CLAUDE.md/roadmap ledger flip plus the chat memory digest. **User gate
|
CLAUDE.md/roadmap ledger flip plus the chat memory digest. **User gate
|
||||||
round 1 ran 2026-08-09 and found ten live defects; see "User gate —
|
round 1 ran 2026-08-09 and found ten live defects; see "User gate —
|
||||||
round 1" below.** Items A–G are fixed in this round's commit; the three
|
round 1" below.** Items A–G are fixed in round 1's commit; the three
|
||||||
remaining findings (extra chat windows on 1/2/3/4, resize only working in
|
remaining findings (extra chat windows on 1/2/3/4, resize only working in
|
||||||
one corner, transparency/artifacts) are out of this round's scope and
|
one corner, transparency/artifacts) are out of round 1's scope and filed
|
||||||
filed as a new slice, CH6. Status stays CODE-COMPLETE pending the next
|
as a new slice, CH6. **User gate round 2 ran 2026-08-09 and found six
|
||||||
user gate round.
|
more findings; see "User gate — round 2" below.** Items 2 ("In Portal
|
||||||
|
Space…" on the wrong surface) and 4+5 (`/help` text fabricated/misaligned)
|
||||||
|
are fixed in this round's commit; item 3 is confirmed-fixed with no
|
||||||
|
regression; item 6 is folded into CH6a's existing scope. Item 1
|
||||||
|
(jump-in-air still silent live) is NOT reproduced and NOT speculatively
|
||||||
|
fixed — round 1's press-edge logic is provably correct by construction
|
||||||
|
from source, a live headless repro was attempted and blocked (no
|
||||||
|
character on the probe account), and two temporary graphical-only probes
|
||||||
|
are left behind for the next round. Status stays CODE-COMPLETE pending
|
||||||
|
the next user gate round (still needed for item 1, CH6, and a final
|
||||||
|
in-client visual pass on everything fixed so far).
|
||||||
|
|
||||||
**Why now:** first track of the alpha-release program (chat is the most
|
**Why now:** first track of the alpha-release program (chat is the most
|
||||||
visible daily surface for the friend-alpha). User-directed 2026-08-09.
|
visible daily surface for the friend-alpha). User-directed 2026-08-09.
|
||||||
|
|
@ -117,7 +127,14 @@ implementer per slice against a pinned contract (per
|
||||||
teach `LayoutImporter` element type 9 (`UIElement_Resizebar`, 8
|
teach `LayoutImporter` element type 9 (`UIElement_Resizebar`, 8
|
||||||
authored grips: 4 edges + 4 corners via bools 0x2A–0x2D); fix the
|
authored grips: 4 edges + 4 corners via bools 0x2A–0x2D); fix the
|
||||||
resize mask that excludes Top. Expected to also clear the round-1
|
resize mask that excludes Top. Expected to also clear the round-1
|
||||||
artifact report.
|
artifact report. **User gate round 2 (2026-08-09) added two more
|
||||||
|
symptoms to fold into this same slice:** resize has no diagonal
|
||||||
|
(corner) cursor feedback — only edge cursors show, the 4 corner grips
|
||||||
|
from the 8-grip authored set above have no matching cursor affordance
|
||||||
|
yet; and the window cannot grow in the Y axis when dragging from the
|
||||||
|
bottom-right corner (a corner-grip axis-composition bug, likely the
|
||||||
|
same resize-mask gap already scoped for the Top edge above, now
|
||||||
|
confirmed to also affect corner grips specifically).
|
||||||
- **CH6b — floating windows 1–4.** Mount `0x2100005B` ×4 as
|
- **CH6b — floating windows 1–4.** Mount `0x2100005B` ×4 as
|
||||||
always-resident children per `gmGamePlayUI::SetupChildren
|
always-resident children per `gmGamePlayUI::SetupChildren
|
||||||
@0x004E9EC0` (ids 0x10000505/0x1000050E/0x1000050F/0x10000510);
|
@0x004E9EC0` (ids 0x10000505/0x1000050E/0x1000050F/0x10000510);
|
||||||
|
|
@ -158,6 +175,7 @@ implementer per slice against a pinned contract (per
|
||||||
| CH5 closeout | (this commit) | — (docs/memory only, no build) | — | pending (connected gate — see test script) |
|
| CH5 closeout | (this commit) | — (docs/memory only, no build) | — | pending (connected gate — see test script) |
|
||||||
| User gate round 1 | (this commit) | 12,221 passed / 4 skipped / 0 failed (baseline; items A–G fixed this commit) | — | items A–G user-gate round 1 fixed; ten findings total, see "User gate — round 1" below |
|
| User gate round 1 | (this commit) | 12,221 passed / 4 skipped / 0 failed (baseline; items A–G fixed this commit) | — | items A–G user-gate round 1 fixed; ten findings total, see "User gate — round 1" below |
|
||||||
| CH6 chat-window shell parity | not started | — | — | filed 2026-08-09 at user gate round 1; research first |
|
| CH6 chat-window shell parity | not started | — | — | filed 2026-08-09 at user gate round 1; research first |
|
||||||
|
| User gate round 2 | (this commit) | 12,267 passed / 4 skipped / 0 failed | — | items 2/4/5 fixed this commit, item 3 confirmed-fixed, item 6 folded into CH6a's spec, item 1 NOT reproduced (see "User gate — round 2" below) |
|
||||||
|
|
||||||
### CH4 closeout (2026-08-09)
|
### CH4 closeout (2026-08-09)
|
||||||
|
|
||||||
|
|
@ -512,3 +530,26 @@ Release suite green (see the commit message for the exact count). Items
|
||||||
H/I/J need the next visual round once CH6 lands; A–G still want a final
|
H/I/J need the next visual round once CH6 lands; A–G still want a final
|
||||||
in-client eyes-on pass to confirm the fix reads correctly on screen (build
|
in-client eyes-on pass to confirm the fix reads correctly on screen (build
|
||||||
+ test green is necessary, not sufficient, for a presentation change).
|
+ test green is necessary, not sufficient, for a presentation change).
|
||||||
|
|
||||||
|
## User gate — round 2 (2026-08-09)
|
||||||
|
|
||||||
|
The user tested round 1's fixes live and reported six more findings.
|
||||||
|
|
||||||
|
| # | User finding (condensed) | Disposition |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Jumping while already airborne is STILL silent live — round 1's press-edge branch (`PlayerMovementController._prevJumpHeld`) has a passing unit test but never visibly fires in the running client. | **NOT REPRODUCED; NO SPECULATIVE FIX SHIPPED.** Exhaustive static re-audit of the whole live path (branch logic, `_body.OnWalkable`'s single writer — the quantum-loop resolve, driven only by real physics results, never anything else — the exact-once-per-frame `Update()`/`Capture()` call site, `TakeControlFromServer`'s edge-history reset scope, mouse-look's extra `Capture()` calls) found no bug: the mechanism is provably correct BY CONSTRUCTION from source. Attempted a live headless repro (new `jump-probe` bot policy exercising the SAME typed `commands.Movement.SetIntent` surface, real ACE connect at 127.0.0.1:9000) — blocked: `probeaccount2` connects but has NO CHARACTER ("no available characters on account"), and per the task's own constraint the fallback to `testaccount` is refused because the graphical client (PID confirmed running this session) owns that account. Left TWO temporary probes behind a `PhysicsDiagnostics`-family flag (`ACDREAM_PROBE_JUMP=1`, blocked entirely in Headless by the existing multi-session static-state guard, so this is a GRAPHICAL-client-only diagnostic for the next round): `[jump]` in `ReportJumpRefusal` (prints unconditionally, even when `OnInterfaceText` is null, to separate "branch never evaluated true" from "callback dropped it"), and `[jump-tick]` (per-tick trace bracketing every frame where Jump is/was held). Next round: launch with `ACDREAM_PROBE_JUMP=1`, reproduce, and read the console trace — it will show exactly which of the three jump branches fires and what `OnWalkable`/`_prevJumpHeld` were at that instant. |
|
||||||
|
| 2 | Retail shows "In Portal Space…" at the TOP of the screen, SMALL font, tell-yellow (the SpewBox) — acdream renders a big centered white/yellow overlay instead. | **FIXED this SHA.** Verified in the decomp: `gmSmartBoxUI::UseTime`'s notice emits via `ECM_UI::SendNotice_DisplayStringInfo(0x1A, ...)`, which forwards to `AddTextToScroll(str, 0x1A, 1, 0)` — type `0x1A` is HARDCODED to the SpewBox (`docs/research/2026-08-09-chat-retail-interface-text.md` §1.1/§4.2), the same surface every other `ClientLocal` refusal (jump-in-air, etc.) already uses. `PortalWaitNoticeController` (the dedicated centered-overlay presentation) and its lease are DELETED outright; `PortalTunnelPresentation`'s round-1 per-rotation-segment emission cadence is unchanged, now writing straight into `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)` — the SpewBox's own dedupe-at-index-0 (`SpewBoxState.Tick`) collapses the per-segment repetition exactly as retail's `gmSpewBoxUI::Update` does. No conflict with the decomp — the fix and the user's report agree exactly. |
|
||||||
|
| 3 | (Round 1 item B, `[System] ` prefix) | **CONFIRMED FIXED** — no regression, no further action. |
|
||||||
|
| 4+5 | `/help` output differs from retail; `/help death` prints an acdream META-MESSAGE ("This is a retail help-topic group; acdream has not yet extracted its exact retail listing text…"); spacing/alignment is off. | **FIXED this SHA via verbatim extraction, not authorship.** `tools/pdb-extract/sweep_weenie_strings.py` generalized to decode narrow `PStringBase<char>` literals (the Help* family's shape) alongside its original UTF-16LE support, then swept every `ClientCommunicationSystem::HelpXxxGroup` function's exact byte extent (read from the pseudo-C's own function-header addresses) against the PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH). 4 of 7 groups (death/status/text/allegiances) are now COMPLETE verbatim listings — `/help death` now prints retail's real 8-line text, byte-exact including the retail-authentic trailing space on the `@day` line. The other 3 (channels/chatting/commands) delegate part or all of their detail text to `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which builds its output from BN-mislabeled data fragments around a live channel-name lookup — genuinely not decodable with confidence; each keeps its own verbatim summary line plus an explicit UNVERIFIED note citing the address, never a fabricated meta-message. 7 of the ~35 channel one-liners are also now verbatim (a/guild/gu, general/cg, trade/ct, lfg/clfg, roleplay/crp, society/soc, olthoi/o), each including retail's own "Also: @alias" text — this is also the alignment/spacing fix, since the fabricated summaries never matched retail's exact wording. Register row AP-184 filed; ISSUES.md #364 tracks the remaining 3-group gap. `RetailCommandHelpTableTests.cs` (new) pins every complete listing and the partial/UNVERIFIED shape byte-exact. |
|
||||||
|
| 6 | Resize is still buggy: missing diagonal (corner) cursor feedback, and the window cannot grow in the Y axis when dragging from the bottom-right corner. | **FOLDED into CH6a's spec** (not fixed this SHA — CH6a itself has not started; see the slice list above, now carrying these two symptoms alongside the pre-existing resize-mask/LayoutDesc-import scope). |
|
||||||
|
|
||||||
|
Evidence for items 2, 4, and 5: this commit's diff + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs` (new) + the deletion of
|
||||||
|
`tests/AcDream.App.Tests/UI/PortalWaitNoticeControllerTests.cs` (its subject
|
||||||
|
class no longer exists) + `PortalTunnelAssetTests.cs` (unchanged; verified by
|
||||||
|
inspection — its `CreateRequired` call never passed the removed
|
||||||
|
`displayNoticeLifetime` parameter, so it needed no update). Item 1's evidence
|
||||||
|
is negative: no code changed in the jump branch logic itself, only
|
||||||
|
diagnostics; the two temporary probes and the `jump-probe` headless bot
|
||||||
|
policy are the round's deliverable for item 1, pending either a successful
|
||||||
|
next-round repro or a character created on `probeaccount2`. Full Release
|
||||||
|
suite green (see the commit message for the exact count).
|
||||||
|
|
|
||||||
|
|
@ -1061,25 +1061,30 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"The graphics backend must publish a world pass scope.")),
|
"The graphics backend must publish a world pass scope.")),
|
||||||
static value => value.Dispose());
|
static value => value.Dispose());
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
// Campaign CH user-gate round 2, item 2: the user reported retail's
|
||||||
PortalWaitNoticeController>? portalWaitNoticeLease = null;
|
// portal-space notice at the TOP of the screen in SMALL tell-yellow
|
||||||
if (interaction.RetainedUi is { } portalRetainedUi)
|
// text — the SpewBox — while the former PortalWaitNoticeController
|
||||||
{
|
// rendered a big centered white/yellow overlay of its own. The
|
||||||
portalWaitNoticeLease = scope.Acquire(
|
// decomp confirms the user's report: gmSmartBoxUI::UseTime
|
||||||
"portal wait notice",
|
// @0x004D6E30 emits via ECM_UI::SendNotice_DisplayStringInfo(0x1A,
|
||||||
() => new PortalWaitNoticeController(
|
// ...), which forwards to AddTextToScroll(str, 0x1A, 1, 0) — type
|
||||||
portalRetainedUi.Host.Root),
|
// 0x1A is HARDCODED to the SpewBox
|
||||||
static value => value.Dispose());
|
// (docs/research/2026-08-09-chat-retail-interface-text.md
|
||||||
}
|
// §1.1/§4.2), the SAME surface every other ClientLocal refusal uses
|
||||||
Action<string?>? displayPortalWaitNotice =
|
// (PlayerMovementController.ReportJumpRefusal, etc.), not a
|
||||||
portalWaitNoticeLease is { } waitNoticeLease
|
// dedicated overlay. PortalWaitNoticeController is deleted along
|
||||||
? waitNoticeLease.Resource.Set
|
// with its lease; this delegate now writes straight into the
|
||||||
: null;
|
// canonical AddText router every other on-screen interface-text
|
||||||
|
// site already uses. Unlike the deleted controller, this has no
|
||||||
|
// retained-UI dependency (Runtime state, not a UI element), so it
|
||||||
|
// is always wired, not gated on interaction.RetainedUi.
|
||||||
|
Action<string> displayPortalWaitNotice = text =>
|
||||||
|
d.Runtime.CommunicationOwner.AddText(
|
||||||
|
text,
|
||||||
|
AcDream.Core.Chat.RetailLogTextType.ClientLocal);
|
||||||
// Campaign CH slice CH2: the SpewBox is retail's OTHER on-screen
|
// Campaign CH slice CH2: the SpewBox is retail's OTHER on-screen
|
||||||
// interface-text surface (research doc §1.1/§7.3/§7.4) — modeled
|
// interface-text surface (research doc §1.1/§7.3/§7.4), wired into
|
||||||
// directly on the PortalWaitNoticeController lease immediately
|
// the retained-UI host. The transferred controller is reclaimed by
|
||||||
// above, wired into the same retained-UI host. Like the wait notice
|
|
||||||
// in its no-tunnel arm, the transferred controller is reclaimed by
|
|
||||||
// the retained-UI root's own teardown (its Dispose only detaches
|
// the retained-UI root's own teardown (its Dispose only detaches
|
||||||
// children from that root).
|
// children from that root).
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
|
@ -1120,8 +1125,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
portalDispatcher,
|
portalDispatcher,
|
||||||
foundation.SceneLighting!,
|
foundation.SceneLighting!,
|
||||||
foundation.MeshAdapter!,
|
foundation.MeshAdapter!,
|
||||||
displayPortalWaitNotice,
|
displayPortalWaitNotice),
|
||||||
portalWaitNoticeLease?.Resource),
|
|
||||||
static tunnel => tunnel.PrepareResources());
|
static tunnel => tunnel.PrepareResources());
|
||||||
}
|
}
|
||||||
catch (Exception acquisitionFailure)
|
catch (Exception acquisitionFailure)
|
||||||
|
|
@ -1304,7 +1308,6 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
clipFrameLease.Transfer();
|
clipFrameLease.Transfer();
|
||||||
portalDepthLease.Transfer();
|
portalDepthLease.Transfer();
|
||||||
portalTunnelLease?.Transfer();
|
portalTunnelLease?.Transfer();
|
||||||
portalWaitNoticeLease?.Transfer();
|
|
||||||
spewBoxLease?.Transfer();
|
spewBoxLease?.Transfer();
|
||||||
skyLease.Transfer();
|
skyLease.Transfer();
|
||||||
particleLease.Transfer();
|
particleLease.Transfer();
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
private readonly WorldEntity _entity;
|
private readonly WorldEntity _entity;
|
||||||
private readonly PortalTunnelCamera _camera = new();
|
private readonly PortalTunnelCamera _camera = new();
|
||||||
private readonly Random _random;
|
private readonly Random _random;
|
||||||
private readonly Action<string?>? _displayNotice;
|
private readonly Action<string>? _displayNotice;
|
||||||
private IDisposable? _displayNoticeLifetime;
|
|
||||||
private readonly SyntheticEntityMeshReferenceOwner _meshReferences;
|
private readonly SyntheticEntityMeshReferenceOwner _meshReferences;
|
||||||
|
|
||||||
private bool _visible;
|
private bool _visible;
|
||||||
|
|
@ -113,8 +112,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
IAnimationLoader animationLoader,
|
IAnimationLoader animationLoader,
|
||||||
IAnimationHookSink hookSink,
|
IAnimationHookSink hookSink,
|
||||||
Random random,
|
Random random,
|
||||||
Action<string?>? displayNotice,
|
Action<string>? displayNotice)
|
||||||
IDisposable? displayNoticeLifetime)
|
|
||||||
{
|
{
|
||||||
_scope = scope;
|
_scope = scope;
|
||||||
_frames = frames;
|
_frames = frames;
|
||||||
|
|
@ -128,7 +126,6 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
_sequence.HookObj = _animationHooks;
|
_sequence.HookObj = _animationHooks;
|
||||||
_random = random;
|
_random = random;
|
||||||
_displayNotice = displayNotice;
|
_displayNotice = displayNotice;
|
||||||
_displayNoticeLifetime = displayNoticeLifetime;
|
|
||||||
|
|
||||||
_entity = new WorldEntity
|
_entity = new WorldEntity
|
||||||
{
|
{
|
||||||
|
|
@ -164,8 +161,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
WbDrawDispatcher dispatcher,
|
WbDrawDispatcher dispatcher,
|
||||||
SceneLightingUboBinding lightUbo,
|
SceneLightingUboBinding lightUbo,
|
||||||
IWbMeshAdapter meshAdapter,
|
IWbMeshAdapter meshAdapter,
|
||||||
Action<string?>? displayNotice = null,
|
Action<string>? displayNotice = null,
|
||||||
IDisposable? displayNoticeLifetime = null,
|
|
||||||
Random? random = null)
|
Random? random = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(scope);
|
ArgumentNullException.ThrowIfNull(scope);
|
||||||
|
|
@ -202,8 +198,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
animationLoader,
|
animationLoader,
|
||||||
hookSink,
|
hookSink,
|
||||||
random ?? Random.Shared,
|
random ?? Random.Shared,
|
||||||
displayNotice,
|
displayNotice);
|
||||||
displayNoticeLifetime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void EnsureRequiredAssets(
|
internal static void EnsureRequiredAssets(
|
||||||
|
|
@ -260,7 +255,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
_rotationEndAngle = 0f;
|
_rotationEndAngle = 0f;
|
||||||
_rotationCurrentAngle = 0f;
|
_rotationCurrentAngle = 0f;
|
||||||
_camera.DirectionDegrees = 0f;
|
_camera.DirectionDegrees = 0f;
|
||||||
ClearWaitCueNotice();
|
_waitCueVisible = false;
|
||||||
_visible = true;
|
_visible = true;
|
||||||
RebuildPose();
|
RebuildPose();
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +266,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_visible = false;
|
_visible = false;
|
||||||
ClearWaitCueNotice();
|
_waitCueVisible = false;
|
||||||
_animationHooks.Clear();
|
_animationHooks.Clear();
|
||||||
_sequence.ClearAnimations();
|
_sequence.ClearAnimations();
|
||||||
}
|
}
|
||||||
|
|
@ -291,38 +286,19 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
/// The hold-delay-gated arm/disarm <c>LocalPlayerTeleportController</c>
|
/// The hold-delay-gated arm/disarm <c>LocalPlayerTeleportController</c>
|
||||||
/// still drives every frame from <c>RuntimeWorldTransitState.ObserveWait</c>
|
/// still drives every frame from <c>RuntimeWorldTransitState.ObserveWait</c>
|
||||||
/// (own telemetry: <c>RuntimePortalSnapshot.WaitCueShown</c>). This is
|
/// (own telemetry: <c>RuntimePortalSnapshot.WaitCueShown</c>). This is
|
||||||
/// deliberately NOT the retail cue-emission path any more — see
|
/// deliberately NOT the retail cue-emission path — see
|
||||||
/// <see cref="TickRotation"/>'s unconditional per-segment write (item D,
|
/// <see cref="TickRotation"/>'s unconditional per-segment write (item D,
|
||||||
/// #329). Kept only so the controller's own hold bookkeeping still has
|
/// #329). Kept only so the controller's own hold bookkeeping still has
|
||||||
/// somewhere to land; because <c>visible</c> is false for the entire
|
/// somewhere to land; it is pure bookkeeping now (Campaign CH user-gate
|
||||||
/// common case (a transit that never crosses the invented 5-second
|
/// round 2, item 2). The former text-clear invoke made sense only for
|
||||||
/// hold), this is a same-value no-op there and never contends with the
|
/// the deleted <c>PortalWaitNoticeController</c>'s overwrite-only slot —
|
||||||
/// per-segment write above.
|
/// the SpewBox <see cref="_displayNotice"/> now targets has no "hide"
|
||||||
|
/// concept; a line disappears when its own timeout elapses
|
||||||
|
/// (<c>SpewBoxState.DefaultLifetime</c>), exactly like retail's
|
||||||
|
/// <c>gmSpewBoxUI</c>. <see cref="Enter"/>/<see cref="Exit"/> reset this
|
||||||
|
/// same bookkeeping flag directly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetWaitCue(bool visible)
|
public void SetWaitCue(bool visible) => _waitCueVisible = visible;
|
||||||
{
|
|
||||||
if (_waitCueVisible == visible)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_waitCueVisible = visible;
|
|
||||||
_displayNotice?.Invoke(
|
|
||||||
visible ? "In Portal Space - Please Wait..." : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unconditionally hides any wait-cue notice text and resets the
|
|
||||||
/// hold-delay dedup state, independent of <see cref="_waitCueVisible"/>'s
|
|
||||||
/// current value. <see cref="TickRotation"/> now writes the notice text
|
|
||||||
/// directly (bypassing <see cref="SetWaitCue"/>'s dedup), so the old
|
|
||||||
/// `SetWaitCue(false)` calls at Enter/Exit/Dispose could no-op and leave
|
|
||||||
/// a stale "In Portal Space..." line on screen after the presentation
|
|
||||||
/// went invisible — this always clears it.
|
|
||||||
/// </summary>
|
|
||||||
private void ClearWaitCueNotice()
|
|
||||||
{
|
|
||||||
_waitCueVisible = false;
|
|
||||||
_displayNotice?.Invoke(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw retail portal space into the active viewport. The caller suppresses
|
/// Draw retail portal space into the active viewport. The caller suppresses
|
||||||
|
|
@ -420,12 +396,21 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
// independent of `_waitCueVisible`/SetWaitCue (see that
|
// independent of `_waitCueVisible`/SetWaitCue (see that
|
||||||
// method's own doc comment): LocalPlayerTeleportController
|
// method's own doc comment): LocalPlayerTeleportController
|
||||||
// still drives SetWaitCue every frame from its own hold-delay
|
// still drives SetWaitCue every frame from its own hold-delay
|
||||||
// bookkeeping, but because that call is a same-value no-op for
|
// bookkeeping, but that call is pure bookkeeping now, so it
|
||||||
// the entire common case (a transit that never crosses the 5s
|
// never fights this unconditional per-segment write.
|
||||||
// hold), it never fights this unconditional per-segment write.
|
// Campaign CH user-gate round 2, item 2: this now targets the
|
||||||
// Enter/Exit/Dispose clear the notice directly (not through
|
// SpewBox (RuntimeCommunicationState.AddText, ClientLocal),
|
||||||
// SetWaitCue's dedup) so a stale line can never survive past
|
// retail's real destination for this notice
|
||||||
// this presentation going invisible.
|
// (ECM_UI::SendNotice_DisplayStringInfo(0x1A, ...) ->
|
||||||
|
// AddTextToScroll(str, 0x1A, 1, 0), hardcoded to the SpewBox —
|
||||||
|
// docs/research/2026-08-09-chat-retail-interface-text.md
|
||||||
|
// §1.1/§4.2), not the former dedicated centered-overlay
|
||||||
|
// controller. Neither Enter/Exit/Dispose nor SetWaitCue clears
|
||||||
|
// anything any more — the SpewBox has no "hide" concept, a line
|
||||||
|
// simply times out (SpewBoxState.DefaultLifetime) exactly like
|
||||||
|
// retail's gmSpewBoxUI, and its own dedupe-at-index-0
|
||||||
|
// (SpewBoxState.Tick) collapses this call's per-segment
|
||||||
|
// repetition into one refreshed line, same as retail.
|
||||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -517,14 +502,12 @@ public sealed class PortalTunnelPresentation : IDisposable
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_visible = false;
|
_visible = false;
|
||||||
ClearWaitCueNotice();
|
_waitCueVisible = false;
|
||||||
_animationHooks.Clear();
|
_animationHooks.Clear();
|
||||||
_sequence.ClearAnimations();
|
_sequence.ClearAnimations();
|
||||||
_meshReferences.Dispose();
|
_meshReferences.Dispose();
|
||||||
if (_meshReferences.IsDisposed)
|
if (_meshReferences.IsDisposed)
|
||||||
{
|
{
|
||||||
_displayNoticeLifetime?.Dispose();
|
|
||||||
_displayNoticeLifetime = null;
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retained presentation of retail's ECM_UI display-string notice emitted by
|
|
||||||
/// <c>gmSmartBoxUI::UseTime @ 0x004D6E30</c> while portal-space cell blocking
|
|
||||||
/// continues. It is a centered overlay, not a chat message.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class PortalWaitNoticeController : IDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Register row AP-150/AP-178: CH user-gate round 1 (2026-08-09) PINNED
|
|
||||||
/// this — the user confirmed live, side-by-side against retail, that
|
|
||||||
/// this notice renders in the same bright yellow as an incoming Tell
|
|
||||||
/// (<c>0x81C4C8</c>, <c>RetailChatColorTable.Yellow</c> =
|
|
||||||
/// <c>(1, 1, 0.247, 1)</c>), not white. Same exact value as the
|
|
||||||
/// SpewBox's pinned colour (<see cref="AcDream.App.UI.SpewBoxController"/>).
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Vector4 RetailWaitCueColor = new(1f, 1f, 0.247f, 1f);
|
|
||||||
|
|
||||||
private readonly UiRoot _root;
|
|
||||||
private readonly UiText _text;
|
|
||||||
private UiText.Line[] _lines = [];
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public PortalWaitNoticeController(UiRoot root)
|
|
||||||
{
|
|
||||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
|
||||||
_text = new UiText
|
|
||||||
{
|
|
||||||
Name = "PortalSpaceWaitNotice",
|
|
||||||
Left = 0f,
|
|
||||||
Top = 0f,
|
|
||||||
Width = root.Width,
|
|
||||||
Height = root.Height,
|
|
||||||
Anchors = AnchorEdges.Left
|
|
||||||
| AnchorEdges.Top
|
|
||||||
| AnchorEdges.Right
|
|
||||||
| AnchorEdges.Bottom,
|
|
||||||
Centered = true,
|
|
||||||
OneLine = true,
|
|
||||||
ClickThrough = true,
|
|
||||||
ZOrder = int.MaxValue,
|
|
||||||
DefaultColor = RetailWaitCueColor,
|
|
||||||
Visible = false,
|
|
||||||
};
|
|
||||||
_text.LinesProvider = () => _lines;
|
|
||||||
_root.AddChild(_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(string? message)
|
|
||||||
{
|
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
||||||
if (string.IsNullOrEmpty(message))
|
|
||||||
{
|
|
||||||
_text.Visible = false;
|
|
||||||
_lines = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lines = [new UiText.Line(message, RetailWaitCueColor)];
|
|
||||||
_text.Visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_root.RemoveChild(_text);
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,11 +7,15 @@ namespace AcDream.App.UI;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retained presentation of retail's <c>gmSpewBoxUI</c> (research doc
|
/// Retained presentation of retail's <c>gmSpewBoxUI</c> (research doc
|
||||||
/// §1.1/§7.3/§7.4) — the transient top-of-viewport interface-text queue.
|
/// §1.1/§7.3/§7.4) — the transient top-of-viewport interface-text queue.
|
||||||
/// Modeled directly on <see cref="PortalWaitNoticeController"/>: a single
|
/// A single <c>ClickThrough</c> <see cref="UiText"/> block at a high
|
||||||
/// <c>ClickThrough</c> <see cref="UiText"/> block at a high
|
/// <see cref="UiElement.ZOrder"/>. Campaign CH user-gate round 2, item 2:
|
||||||
/// <see cref="UiElement.ZOrder"/>. Unlike that controller's single
|
/// this is now the ONLY on-screen interface-text presentation surface —
|
||||||
/// overwrite-only slot, this reads <see cref="SpewBoxVM"/>'s bounded,
|
/// the former <c>PortalWaitNoticeController</c> (a dedicated centered
|
||||||
/// newest-on-top, per-entry-expiring queue every frame.
|
/// overlay with a single overwrite-only slot) is deleted; the portal-space
|
||||||
|
/// wait notice routes here too, through the same
|
||||||
|
/// <c>RuntimeCommunicationState.AddText</c> chokepoint every other
|
||||||
|
/// <c>ClientLocal</c> refusal uses. This reads <see cref="SpewBoxVM"/>'s
|
||||||
|
/// bounded, newest-on-top, per-entry-expiring queue every frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <b>CH2 REJECT-review rework, BLOCKER 1
|
/// <b>CH2 REJECT-review rework, BLOCKER 1
|
||||||
|
|
|
||||||
|
|
@ -1434,6 +1434,19 @@ public static class PhysicsDiagnostics
|
||||||
public static bool ProbeSupportEnabled { get; set; } =
|
public static bool ProbeSupportEnabled { get; set; } =
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SUPPORT") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SUPPORT") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_JUMP=1</c>. Campaign CH user-gate
|
||||||
|
/// round 2, item 1 (jump-in-air refusal reported STILL silent live after
|
||||||
|
/// round 1's press-edge branch landed with a passing unit test). Enables
|
||||||
|
/// the <c>[jump]</c> line in <c>PlayerMovementController.ReportJumpRefusal</c>
|
||||||
|
/// — printed UNCONDITIONALLY, even when <c>OnInterfaceText</c> is null, so
|
||||||
|
/// the probe can distinguish "the branch never evaluated true" from "the
|
||||||
|
/// branch fired but the callback dropped it." TEMPORARY — strip once the
|
||||||
|
/// live mechanism is confirmed and fixed.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeJumpEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_JUMP") == "1";
|
||||||
|
|
||||||
/// <summary>Vertical agreement window, in metres, inside which the contact
|
/// <summary>Vertical agreement window, in metres, inside which the contact
|
||||||
/// plane's height and the terrain's height at the same XY are called the
|
/// plane's height and the terrain's height at the same XY are called the
|
||||||
/// same surface.</summary>
|
/// same surface.</summary>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ internal static class HeadlessBotPolicyFactory
|
||||||
"lifecycle-smoke" => new LifecycleSmokeHeadlessBotPolicy(),
|
"lifecycle-smoke" => new LifecycleSmokeHeadlessBotPolicy(),
|
||||||
"observer-movement" => new ObserverMovementHeadlessBotPolicy(),
|
"observer-movement" => new ObserverMovementHeadlessBotPolicy(),
|
||||||
"portal-route-smoke" => new PortalRouteSmokeHeadlessBotPolicy(),
|
"portal-route-smoke" => new PortalRouteSmokeHeadlessBotPolicy(),
|
||||||
|
"jump-probe" => new JumpProbeHeadlessBotPolicy(),
|
||||||
_ => throw new HeadlessConfigurationException(
|
_ => throw new HeadlessConfigurationException(
|
||||||
$"Unknown headless bot policy '{id}'."),
|
$"Unknown headless bot policy '{id}'."),
|
||||||
};
|
};
|
||||||
|
|
@ -573,3 +574,167 @@ internal sealed class PortalRouteSmokeHeadlessBotPolicy
|
||||||
ForwardTwo,
|
ForwardTwo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign CH user-gate round 2, item 1 (TEMPORARY, evidence-gathering
|
||||||
|
/// only — not a shipped bot behavior). Round 1 added a press-edge branch in
|
||||||
|
/// <c>PlayerMovementController.Update</c>
|
||||||
|
/// (<c>input.Jump && !_prevJumpHeld && !_body.OnWalkable</c>
|
||||||
|
/// → <c>ReportJumpRefusal(NotGrounded)</c>) with a passing unit test, but the
|
||||||
|
/// user reported it STILL silent live. This policy drives the SAME typed
|
||||||
|
/// <c>commands.Movement.SetIntent</c> surface a headless bot uses (the
|
||||||
|
/// <c>RuntimeLocalPlayerMovementState.HasCommandInput</c> route — distinct
|
||||||
|
/// from the graphical dispatcher's held-key polling path) to charge and fire
|
||||||
|
/// a jump, wait for <see cref="RuntimeMovementSnapshot.IsAirborne"/> to go
|
||||||
|
/// true under REAL physics against a live ACE connection, then press jump
|
||||||
|
/// again mid-air. Pair with <c>ACDREAM_PROBE_JUMP=1</c> (prints
|
||||||
|
/// <c>[jump]</c>/<c>[jump-tick]</c> lines from <c>PlayerMovementController</c>
|
||||||
|
/// itself) to see whether OnWalkable actually clears while genuinely
|
||||||
|
/// airborne and whether <c>ReportJumpRefusal</c> is reached.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class JumpProbeHeadlessBotPolicy : IHeadlessBotPolicy
|
||||||
|
{
|
||||||
|
private enum Stage
|
||||||
|
{
|
||||||
|
WaitForPlayer,
|
||||||
|
Charging,
|
||||||
|
WaitReleaseAirborne,
|
||||||
|
WaitMidAirPress,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stage _stage = Stage.WaitForPlayer;
|
||||||
|
private double _stageDeadline;
|
||||||
|
private bool _wasAirborne;
|
||||||
|
|
||||||
|
public bool IsComplete => _stage == Stage.Done;
|
||||||
|
|
||||||
|
public void Tick(IGameRuntimeView view, IGameRuntimeCommands commands)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(view);
|
||||||
|
ArgumentNullException.ThrowIfNull(commands);
|
||||||
|
|
||||||
|
RuntimeMovementSnapshot movement = view.Movement.Snapshot;
|
||||||
|
if (movement.IsAirborne != _wasAirborne)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[jump-probe] airborne-transition {_wasAirborne} -> "
|
||||||
|
+ $"{movement.IsAirborne} stage={_stage} "
|
||||||
|
+ $"simTime={view.Clock.SimulationTimeSeconds:F3} "
|
||||||
|
+ $"hasCommandInput={movement.HasCommandInput} "
|
||||||
|
+ $"commandInput.Jump={movement.CommandInput.Jump}");
|
||||||
|
_wasAirborne = movement.IsAirborne;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_stage)
|
||||||
|
{
|
||||||
|
case Stage.WaitForPlayer:
|
||||||
|
if (!HasLocalPlayer(view))
|
||||||
|
return;
|
||||||
|
Console.WriteLine("[jump-probe] local player present; charging jump");
|
||||||
|
Require(commands.Movement.SetIntent(
|
||||||
|
view.Generation,
|
||||||
|
new MovementInput(Jump: true)));
|
||||||
|
_stageDeadline = view.Clock.SimulationTimeSeconds + 0.6;
|
||||||
|
_stage = Stage.Charging;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Stage.Charging:
|
||||||
|
if (view.Clock.SimulationTimeSeconds < _stageDeadline)
|
||||||
|
return;
|
||||||
|
Console.WriteLine("[jump-probe] releasing jump (fire)");
|
||||||
|
Require(commands.Movement.SetIntent(
|
||||||
|
view.Generation,
|
||||||
|
new MovementInput(Jump: false)));
|
||||||
|
_stageDeadline = view.Clock.SimulationTimeSeconds + 3.0;
|
||||||
|
_stage = Stage.WaitReleaseAirborne;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Stage.WaitReleaseAirborne:
|
||||||
|
if (movement.IsAirborne)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"[jump-probe] airborne confirmed; pressing jump mid-air "
|
||||||
|
+ $"simTime={view.Clock.SimulationTimeSeconds:F3}");
|
||||||
|
Require(commands.Movement.SetIntent(
|
||||||
|
view.Generation,
|
||||||
|
new MovementInput(Jump: true)));
|
||||||
|
_stageDeadline = view.Clock.SimulationTimeSeconds + 1.0;
|
||||||
|
_stage = Stage.WaitMidAirPress;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (view.Clock.SimulationTimeSeconds >= _stageDeadline)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"[jump-probe] TIMEOUT waiting for airborne after jump "
|
||||||
|
+ "fire -- the released jump never registered as "
|
||||||
|
+ "airborne.");
|
||||||
|
Require(commands.Movement.ClearIntent(view.Generation));
|
||||||
|
_stage = Stage.Done;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case Stage.WaitMidAirPress:
|
||||||
|
if (view.Clock.SimulationTimeSeconds < _stageDeadline)
|
||||||
|
return;
|
||||||
|
Console.WriteLine(
|
||||||
|
"[jump-probe] probe complete; final airborne="
|
||||||
|
+ $"{movement.IsAirborne}");
|
||||||
|
Require(commands.Movement.ClearIntent(view.Generation));
|
||||||
|
_stage = Stage.Done;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnLifecycle(in RuntimeLifecycleDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCommand(in RuntimeCommandDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnEntity(in RuntimeEntityDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnInventory(in RuntimeInventoryDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnChat(in RuntimeChatDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnMovement(in RuntimeMovementDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPortal(in RuntimePortalDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasLocalPlayer(IGameRuntimeView view) =>
|
||||||
|
view.Lifecycle.State is RuntimeLifecycleState.InWorld
|
||||||
|
&& view.Lifecycle.PlayerGuid != 0u
|
||||||
|
&& view.Entities.TryGet(
|
||||||
|
view.Lifecycle.PlayerGuid,
|
||||||
|
out _);
|
||||||
|
|
||||||
|
private static void Require(in RuntimeCommandResult result)
|
||||||
|
{
|
||||||
|
if (!result.Accepted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Jump probe command was rejected with {result.Status}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -967,6 +967,20 @@ public sealed class PlayerMovementController
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ReportJumpRefusal(WeenieError result)
|
private void ReportJumpRefusal(WeenieError result)
|
||||||
{
|
{
|
||||||
|
// Campaign CH user-gate round 2, item 1 (TEMPORARY): print
|
||||||
|
// UNCONDITIONALLY, before the OnInterfaceText null-check, so the
|
||||||
|
// probe distinguishes "ReportJumpRefusal was never called with a
|
||||||
|
// reportable result" from "it was called but OnInterfaceText was
|
||||||
|
// null" from "the callback fired but downstream dropped the line."
|
||||||
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeJumpEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[jump] ReportJumpRefusal result={result} "
|
||||||
|
+ $"hasCallback={OnInterfaceText is not null} "
|
||||||
|
+ $"onWalkable={_body.OnWalkable} "
|
||||||
|
+ $"prevJumpHeld={_prevJumpHeld}");
|
||||||
|
}
|
||||||
|
|
||||||
if (OnInterfaceText is null)
|
if (OnInterfaceText is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -2595,6 +2609,19 @@ public sealed class PlayerMovementController
|
||||||
// in-air raises exactly one report, not one per frame.
|
// in-air raises exactly one report, not one per frame.
|
||||||
ReportJumpRefusal(WeenieError.NotGrounded);
|
ReportJumpRefusal(WeenieError.NotGrounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign CH user-gate round 2, item 1 (TEMPORARY): per-tick trace
|
||||||
|
// bracketing every frame where jump is (or was) held, so the probe
|
||||||
|
// can see the OnWalkable transition around a live jump attempt
|
||||||
|
// without spamming every ordinary frame.
|
||||||
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeJumpEnabled
|
||||||
|
&& (input.Jump || _prevJumpHeld))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[jump-tick] input.Jump={input.Jump} prevJumpHeld={_prevJumpHeld} "
|
||||||
|
+ $"onWalkable={_body.OnWalkable} jumpCharging={_jumpCharging} "
|
||||||
|
+ $"jumpExtent={_jumpExtent:F2}");
|
||||||
|
}
|
||||||
_prevJumpHeld = input.Jump;
|
_prevJumpHeld = input.Jump;
|
||||||
|
|
||||||
// ── 2. Run admitted complete-object quanta ────────────────────────────
|
// ── 2. Run admitted complete-object quanta ────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,9 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||||
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c> by the
|
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c> by the
|
||||||
/// recipe in the command-registry doc §5 (scan each <c>Help*</c>
|
/// recipe in the command-registry doc §5 (scan each <c>Help*</c>
|
||||||
/// function's byte extent for <c>push imm32</c> into <c>.rdata</c>). Entries
|
/// function's byte extent for <c>push imm32</c> into <c>.rdata</c>). Entries
|
||||||
/// that could not be recovered this way (the 7 group-index headers'
|
/// that could not be recovered this way (@render's option list) are NOT
|
||||||
/// summary text, @day's confirmation lines, @render's option list) are
|
/// fabricated — they are simply absent from this table; the lookup falls
|
||||||
/// NOT fabricated — they are simply absent from this table; the lookup
|
/// through to a generic "no detailed help" line rather than guess.
|
||||||
/// falls through to a generic "no detailed help" line rather than guess.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -36,9 +35,28 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||||
/// one-liners in <see cref="ByVerb"/> ("Sends text to your Fellowship
|
/// one-liners in <see cref="ByVerb"/> ("Sends text to your Fellowship
|
||||||
/// channel.", etc.) are acdream-authored SUMMARIES, not individually
|
/// channel.", etc.) are acdream-authored SUMMARIES, not individually
|
||||||
/// hand-extracted retail strings — retail's own per-channel help text was
|
/// hand-extracted retail strings — retail's own per-channel help text was
|
||||||
/// not recovered this slice. Recovering them (or deleting the class-doc
|
/// not recovered this slice.
|
||||||
/// overclaim) is future work; this comment now says so honestly instead
|
/// </para>
|
||||||
/// of leaving the contradiction standing.
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Campaign CH user-gate round 2, item 3 (2026-08-09):</b> the user
|
||||||
|
/// caught that <c>/help death</c> printed an acdream META-MESSAGE ("This is
|
||||||
|
/// a retail help-topic group; acdream has not yet extracted its exact
|
||||||
|
/// retail listing text…") instead of retail's real listing — this was the
|
||||||
|
/// class-doc overclaim's SHARP end. All 7 <c>HelpXxxGroup</c> nodes are now
|
||||||
|
/// resolved: <see cref="DeathGroupDetail"/>, <see cref="StatusGroupDetail"/>,
|
||||||
|
/// <see cref="TextGroupDetail"/>, and <see cref="AllegiancesGroupDetail"/>
|
||||||
|
/// are COMPLETE verbatim listings; <see cref="ChannelsGroupDetail"/>,
|
||||||
|
/// <see cref="ChattingGroupDetail"/>, and <see cref="CommandsGroupDetail"/>
|
||||||
|
/// are PARTIAL (their own summary line, plus — for chatting — 7 of 8
|
||||||
|
/// entries) with an explicit UNVERIFIED note citing
|
||||||
|
/// <c>ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290</c> for
|
||||||
|
/// the un-decodable remainder, never a guess. 7 of the ~35 channel
|
||||||
|
/// one-liners (a/guild/gu, general/cg, trade/ct, lfg/clfg, roleplay/crp,
|
||||||
|
/// society/soc, olthoi/o) are now verbatim too; the rest (fellowship/
|
||||||
|
/// monarch/patron/vassals/covassal family) route through the same
|
||||||
|
/// unresolved mechanism and remain acdream summaries. See the remarks on
|
||||||
|
/// <see cref="ChannelsGroupSummary"/> for the full extraction method.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RetailCommandHelpTable
|
public static class RetailCommandHelpTable
|
||||||
|
|
@ -154,11 +172,138 @@ public static class RetailCommandHelpTable
|
||||||
|
|
||||||
// The 7 retail-registered "group index" nodes — retail registers them
|
// The 7 retail-registered "group index" nodes — retail registers them
|
||||||
// with a NULL func (like @mr/@pr); typing one bare reaches the server.
|
// with a NULL func (like @mr/@pr); typing one bare reaches the server.
|
||||||
// Only @help <group> shows anything, and only this generic pointer —
|
// Only @help <group> shows anything.
|
||||||
// the exact per-group summary/listing text (retail's HelpXxxGroup
|
//
|
||||||
// functions) was not extracted this slice; see ISSUES.md.
|
// Campaign CH user-gate round 2, item 3 (2026-08-09): the user caught
|
||||||
private const string GroupNodeNotExtracted =
|
// that "/help death" printed an acdream META-MESSAGE instead of
|
||||||
"This is a retail help-topic group; acdream has not yet extracted its exact retail listing text. Typing this verb alone (without @help) reaches the server as literal text, matching retail's null-handler registration.";
|
// retail's real listing. Extracted verbatim via
|
||||||
|
// tools/pdb-extract/sweep_weenie_strings.py (generalized this round for
|
||||||
|
// narrow PStringBase<char> literals — the Help* family uses 8-bit ASCII,
|
||||||
|
// not the UTF-16LE ECM_UI notice shape the script originally targeted)
|
||||||
|
// against the PDB-paired C:\Users\erikn\Downloads\acclient.exe. Method:
|
||||||
|
// each HelpXxxGroup function (ClientCommunicationSystem::HelpXxxGroup,
|
||||||
|
// decomp §5) is a single `if (arg2 != Summary_HelpType) { <DETAIL:
|
||||||
|
// straight-line concatenation of subcommand summaries> } else { <the
|
||||||
|
// group's own one-line summary> }`; the exact byte extent (this
|
||||||
|
// function's start VA to the next function's start VA) was read from
|
||||||
|
// the pseudo-C's own function-header addresses, and every extent was
|
||||||
|
// swept with --ascii-only. Four groups decode COMPLETELY (every
|
||||||
|
// subcommand summary is either an inline literal in the group function
|
||||||
|
// itself or a direct `HelpXxx(this, Summary_HelpType, "@tag", arg4)`
|
||||||
|
// call whose OWN summary text was independently swept from HelpXxx's
|
||||||
|
// own extent): death, status, text, allegiances. Three groups
|
||||||
|
// (commands, channels, chatting) delegate part or all of their detail
|
||||||
|
// text to ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290,
|
||||||
|
// which builds its output from THREE string fragments Binary Ninja
|
||||||
|
// mis-attributes to unrelated vtable slots
|
||||||
|
// (`ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice`
|
||||||
|
// etc. — the classic BN pooled/mislabeled-data artifact, not real
|
||||||
|
// vtable dispatch) concatenated around a live channel-name lookup
|
||||||
|
// (`ChannelSystem::GetChannelName`) — genuinely not decodable via this
|
||||||
|
// sweep with confidence. Those three keep an honest UNVERIFIED fallback
|
||||||
|
// (still real retail text where available — each group's own one-line
|
||||||
|
// summary IS fully decoded and included) rather than inventing the
|
||||||
|
// missing portion. Tracked as ISSUES.md #364.
|
||||||
|
private const string ChannelsGroupSummary =
|
||||||
|
"@help channels - How to communicate with people in your allegiance or fellowship.";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:391002 (0x0057b1b0). HelpChattingGroup's
|
||||||
|
// own one-liner — not used standalone below (its Detail branch does
|
||||||
|
// NOT prepend it, matching every other group), kept for documentation
|
||||||
|
// parity with the other two Summary consts above/below.
|
||||||
|
private const string ChattingGroupSummaryVerbatim =
|
||||||
|
"@help chatting - How to chat publically and privately.";
|
||||||
|
|
||||||
|
private const string CommandsGroupSummary =
|
||||||
|
"@help commands - Lists all commands.";
|
||||||
|
|
||||||
|
private const string GroupDetailUnverifiedSuffix =
|
||||||
|
" UNVERIFIED: the rest of this group's retail listing routes through ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290, which builds its text from BN-mislabeled data fragments around a live channel-name lookup — not decodable with confidence from a static string sweep. See docs/research/2026-08-09-chat-retail-command-registry.md §5 and ISSUES.md #364.";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:391365-391436 (HelpDeathGroup, decomp
|
||||||
|
// §5 sweep, addresses 0x57b913-0x57ba56). The Detail branch is a
|
||||||
|
// straight-line concatenation with NO leading group-summary line (that
|
||||||
|
// string is exclusive to the Summary_HelpType branch, i.e. what a
|
||||||
|
// PARENT listing shows about "death" as one entry — @help death itself
|
||||||
|
// never shows it).
|
||||||
|
public const string DeathGroupDetail =
|
||||||
|
"@permit - Commands to give or revoke permission for others to loot your corpse.\n"
|
||||||
|
+ "@consent - Commands to help you manage the corpse-looting permissions that others give you.\n"
|
||||||
|
+ "@corpse - Displays the location of your last outdoor death.\n"
|
||||||
|
+ "@die - Kills your character and leaves a corpse, returning you to your lifestone.\n"
|
||||||
|
+ "@lifestone - Returns you to the last lifestone you used without killing you.\n"
|
||||||
|
+ "@marketplace - Teleports you to the Marketplace of Dereth.\n"
|
||||||
|
+ "@pkarena - Teleports you to the PK Arena. You must be PK to use this command.\n"
|
||||||
|
+ "@pklarena - Teleports you to the PKL Arena. You must be PKL to use this command.\n";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:392030-392093 (HelpStatusGroup). Same
|
||||||
|
// shape as HelpDeathGroup — Detail branch, no leading group-summary
|
||||||
|
// line. The trailing space before "\n" on the @day line is retail's
|
||||||
|
// own byte content (data_0x7de280), not an acdream typo.
|
||||||
|
public const string StatusGroupDetail =
|
||||||
|
"@age - Displays your total gameplay time.\n"
|
||||||
|
+ "@birth - Displays when your character was created.\n"
|
||||||
|
+ "@day - A toggle that lightens the outdoor landscape. Note that this command may take several seconds to take effect. \n"
|
||||||
|
+ "@endurance - Explains how endurance affects your character.\n"
|
||||||
|
+ "@framerate - Toggles the framerate display.\n"
|
||||||
|
+ "@loc - Displays your current position.\n"
|
||||||
|
+ "@pklite - Sets your status to Player Killer Lite. Type @help pklite for more details.\n"
|
||||||
|
+ "@version - Tells you what version of the software you are using.\n";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:392209-392279 (HelpTextGroup).
|
||||||
|
public const string TextGroupDetail =
|
||||||
|
"@clear - Clears the chat box of all text.\n"
|
||||||
|
+ "@filter - Commands to filter out incoming messages.\n"
|
||||||
|
+ "@unfilter - Commands to remove filters from incoming messages.\n"
|
||||||
|
+ "@loadfile - Reads in the given text file and executes each line in the chat entry field.\n"
|
||||||
|
+ "@log - Commands to echo chat text to a logfile.\n"
|
||||||
|
+ "@title <new title> - Sets the title of the popup chat window.\n";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:393033-393059 (HelpAllegiancesGroup) —
|
||||||
|
// the shortest group, only 2 lines.
|
||||||
|
public const string AllegiancesGroupDetail =
|
||||||
|
"@allegiance - Commands to help manage your allegiance.\n"
|
||||||
|
+ "@allegiance motd - Displays or sets the message of the day for your allegiance, see @help motd for more information.\n";
|
||||||
|
|
||||||
|
// Partial: own summary (fully verbatim, swept) + the honest UNVERIFIED
|
||||||
|
// note for the HelpStupidChannelHack-delegated remainder. HelpChannelsGroup's
|
||||||
|
// Detail branch (acclient_2013_pseudo_c.txt:387166-387194) is ENTIRELY
|
||||||
|
// 6 HelpStupidChannelHack calls -- none of its own text is decodable.
|
||||||
|
public const string ChannelsGroupDetail =
|
||||||
|
ChannelsGroupSummary + GroupDetailUnverifiedSuffix;
|
||||||
|
|
||||||
|
// Partial: HelpChattingGroup's Detail branch
|
||||||
|
// (acclient_2013_pseudo_c.txt:390909-390984) resolves 7 of 8 entries
|
||||||
|
// verbatim (chat/notell/reply/retell/say/tell/afk, each an inline
|
||||||
|
// literal or a direct HelpXxx(Summary_HelpType,...) call whose own text
|
||||||
|
// was independently swept) -- only the 5 HelpStupidChannelHack calls in
|
||||||
|
// the middle (channel-alias entries) are unresolved.
|
||||||
|
public const string ChattingGroupDetail =
|
||||||
|
"@chat - Sets whether or not you receive normal chat.\n"
|
||||||
|
+ "@notell - Sets whether or not you receive @tell's.\n"
|
||||||
|
+ "@reply - Sends some text to the last person who @tell'd you.\n"
|
||||||
|
+ "@retell - Sends some text to the last person you @tell'd.\n"
|
||||||
|
+ "@say - Says some text to everyone around you.\n"
|
||||||
|
+ "@tell - Sends a private message to another character.\n"
|
||||||
|
+ "[5 more retail channel-alias one-liners not yet extracted verbatim -- ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290, UNVERIFIED; see the class remarks above.]\n"
|
||||||
|
+ "@afk - Set your away-from-keyboard status.\n";
|
||||||
|
|
||||||
|
// HelpAllGroup's Detail branch (acclient_2013_pseudo_c.txt:394006-394176)
|
||||||
|
// recursively calls HelpAllegiancesGroup AND HelpChattingGroup in Detail
|
||||||
|
// mode (not Summary), plus the same 5 HelpStupidChannelHack calls
|
||||||
|
// directly, plus HelpDeathGroup in Detail mode, plus several more direct
|
||||||
|
// entries, plus a CONFIRMED retail duplicate (the saveui/loadui pair
|
||||||
|
// appears twice in the straight-line decompiled body with no
|
||||||
|
// conditional between the two occurrences -- verified at
|
||||||
|
// acclient_2013_pseudo_c.txt:394089-394137, not an acdream artifact).
|
||||||
|
// The nesting compounds two independent HelpStupidChannelHack gaps
|
||||||
|
// (its own 5 calls + HelpChattingGroup's 5) with enough structural
|
||||||
|
// uncertainty (unconfirmed HelpType arg on the trailing @status/@text
|
||||||
|
// calls) that a full reconstruction risks presenting an inaccurate
|
||||||
|
// listing as verbatim. Kept UNVERIFIED with its own summary (fully
|
||||||
|
// decoded) rather than risk that.
|
||||||
|
public const string CommandsGroupDetail =
|
||||||
|
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -181,16 +326,24 @@ public static class RetailCommandHelpTable
|
||||||
["log"] = Log,
|
["log"] = Log,
|
||||||
["render"] = Render,
|
["render"] = Render,
|
||||||
["motd"] = Motd,
|
["motd"] = Motd,
|
||||||
["commands"] = GroupNodeNotExtracted,
|
["commands"] = CommandsGroupDetail,
|
||||||
["allegiances"] = GroupNodeNotExtracted,
|
["allegiances"] = AllegiancesGroupDetail,
|
||||||
["channels"] = GroupNodeNotExtracted,
|
["channels"] = ChannelsGroupDetail,
|
||||||
["chatting"] = GroupNodeNotExtracted,
|
["chatting"] = ChattingGroupDetail,
|
||||||
["death"] = GroupNodeNotExtracted,
|
["death"] = DeathGroupDetail,
|
||||||
["status"] = GroupNodeNotExtracted,
|
["status"] = StatusGroupDetail,
|
||||||
["text"] = GroupNodeNotExtracted,
|
["text"] = TextGroupDetail,
|
||||||
// Channel verbs — one line each, generated rather than
|
// Channel verbs. Campaign CH user-gate round 2, item 3: 7 of
|
||||||
// hand-extracted (retail's per-channel help strings were not
|
// these are now VERBATIM retail text (each its own
|
||||||
// individually recovered this slice).
|
// HelpTurbineChat_Xxx function, decomp §5 sweep,
|
||||||
|
// acclient_2013_pseudo_c.txt:387294-387411/387410 area,
|
||||||
|
// addresses 0x577620-0x577850) — the "Also: @alias" suffix is
|
||||||
|
// retail's own text, not an acdream addition. The remaining
|
||||||
|
// ones (f/fellow(s)/fellowship/g/group/party, m/monarch,
|
||||||
|
// p/patron, v/vassal(s), c/covassal(s)/co-vassals) still route
|
||||||
|
// through the unresolved HelpStupidChannelHack pooled-string
|
||||||
|
// mechanism (see the class remarks above) and stay
|
||||||
|
// acdream-authored summaries — an honest gap, not a silent one.
|
||||||
["f"] = "Sends text to your Fellowship channel.",
|
["f"] = "Sends text to your Fellowship channel.",
|
||||||
["fellow"] = "Sends text to your Fellowship channel.",
|
["fellow"] = "Sends text to your Fellowship channel.",
|
||||||
["fellows"] = "Sends text to your Fellowship channel.",
|
["fellows"] = "Sends text to your Fellowship channel.",
|
||||||
|
|
@ -198,22 +351,22 @@ public static class RetailCommandHelpTable
|
||||||
["g"] = "Sends text to your Fellowship channel.",
|
["g"] = "Sends text to your Fellowship channel.",
|
||||||
["group"] = "Sends text to your Fellowship channel.",
|
["group"] = "Sends text to your Fellowship channel.",
|
||||||
["party"] = "Sends text to your Fellowship channel.",
|
["party"] = "Sends text to your Fellowship channel.",
|
||||||
["a"] = "Sends text to your Allegiance chat room.",
|
["a"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
|
||||||
["guild"] = "Sends text to your Allegiance chat room.",
|
["guild"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
|
||||||
["gu"] = "Sends text to your Allegiance chat room.",
|
["gu"] = "@a - Sends a message to your Allegiance. Also: @guild, @gu",
|
||||||
["ab"] = "Broadcasts text to your entire allegiance (monarch/speaker permission). Also @allegiance broadcast.",
|
["ab"] = "Broadcasts text to your entire allegiance (monarch/speaker permission). Also @allegiance broadcast.",
|
||||||
["general"] = "Sends text to the General chat room.",
|
["general"] = "@general - Sends a message to the global General chat channel. Also: @cg",
|
||||||
["cg"] = "Sends text to the General chat room.",
|
["cg"] = "@general - Sends a message to the global General chat channel. Also: @cg",
|
||||||
["trade"] = "Sends text to the Trade chat room.",
|
["trade"] = "@trade - Sends a message to the global Trade chat channel. Also: @ct",
|
||||||
["ct"] = "Sends text to the Trade chat room.",
|
["ct"] = "@trade - Sends a message to the global Trade chat channel. Also: @ct",
|
||||||
["lfg"] = "Sends text to the Looking-For-Group chat room.",
|
["lfg"] = "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg",
|
||||||
["clfg"] = "Sends text to the Looking-For-Group chat room.",
|
["clfg"] = "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg",
|
||||||
["roleplay"] = "Sends text to the Roleplay chat room.",
|
["roleplay"] = "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp",
|
||||||
["crp"] = "Sends text to the Roleplay chat room.",
|
["crp"] = "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp",
|
||||||
["society"] = "Sends text to your Society chat room.",
|
["society"] = "@society - Sends a message to the your Society chat channel. Also: @soc",
|
||||||
["soc"] = "Sends text to your Society chat room.",
|
["soc"] = "@society - Sends a message to the your Society chat channel. Also: @soc",
|
||||||
["olthoi"] = "Sends text to the Olthoi Player Killer chat room.",
|
["olthoi"] = "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o",
|
||||||
["o"] = "Sends text to the Olthoi Player Killer chat room.",
|
["o"] = "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o",
|
||||||
["m"] = "Sends text to your Monarch.",
|
["m"] = "Sends text to your Monarch.",
|
||||||
["monarch"] = "Sends text to your Monarch.",
|
["monarch"] = "Sends text to your Monarch.",
|
||||||
["p"] = "Sends text to your Patron.",
|
["p"] = "Sends text to your Patron.",
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
using AcDream.App.UI;
|
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI;
|
|
||||||
|
|
||||||
public sealed class PortalWaitNoticeControllerTests
|
|
||||||
{
|
|
||||||
// Campaign CH user-gate round 1 (item D): the user confirmed live,
|
|
||||||
// side-by-side against retail, that this notice renders in the same
|
|
||||||
// bright yellow as an incoming Tell (RetailChatColorTable.Yellow).
|
|
||||||
private static readonly Vector4 RetailWaitCueColor = new(1f, 1f, 0.247f, 1f);
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Notice_IsCenteredOverlayAndDisposesFromRetainedRoot()
|
|
||||||
{
|
|
||||||
var root = new UiRoot
|
|
||||||
{
|
|
||||||
Width = 1280f,
|
|
||||||
Height = 720f,
|
|
||||||
};
|
|
||||||
|
|
||||||
using (var controller = new PortalWaitNoticeController(root))
|
|
||||||
{
|
|
||||||
UiText text = Assert.IsType<UiText>(Assert.Single(root.Children));
|
|
||||||
Assert.False(text.Visible);
|
|
||||||
Assert.True(text.Centered);
|
|
||||||
Assert.True(text.ClickThrough);
|
|
||||||
Assert.Equal(root.Width, text.Width);
|
|
||||||
Assert.Equal(root.Height, text.Height);
|
|
||||||
Assert.Equal(RetailWaitCueColor, text.DefaultColor);
|
|
||||||
|
|
||||||
controller.Set("In Portal Space - Please Wait...");
|
|
||||||
|
|
||||||
Assert.True(text.Visible);
|
|
||||||
UiText.Line line = Assert.Single(text.LinesProvider!());
|
|
||||||
Assert.Equal("In Portal Space - Please Wait...", line.Text);
|
|
||||||
Assert.Equal(RetailWaitCueColor, line.Color);
|
|
||||||
|
|
||||||
controller.Set(null);
|
|
||||||
Assert.False(text.Visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Empty(root.Children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign CH user-gate round 2, item 3 (2026-08-09): pins a sample of
|
||||||
|
/// <see cref="RetailCommandHelpTable"/>'s extracted strings byte-exact
|
||||||
|
/// against the values recovered from the PDB-paired
|
||||||
|
/// <c>C:\Users\erikn\Downloads\acclient.exe</c> via
|
||||||
|
/// <c>tools/pdb-extract/sweep_weenie_strings.py --ascii-only</c> (generalized
|
||||||
|
/// this round for narrow <c>PStringBase<char></c> literals — see the
|
||||||
|
/// script's own docstring). Covers all four COMPLETE group listings
|
||||||
|
/// (death/status/text/allegiances — <c>/help death</c> was the user's own
|
||||||
|
/// example of the fabricated meta-message this round replaces), the three
|
||||||
|
/// PARTIAL groups' honest UNVERIFIED fallback, and a sample of the newly
|
||||||
|
/// verbatim channel one-liners.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RetailCommandHelpTableTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DeathGroup_IsCompleteVerbatimListing_NoLeadingSummaryLine()
|
||||||
|
{
|
||||||
|
// acclient_2013_pseudo_c.txt:391365-391436 (HelpDeathGroup Detail
|
||||||
|
// branch) -- straight-line concatenation, no "@help death - ..."
|
||||||
|
// prefix (that string is exclusive to the Summary_HelpType branch).
|
||||||
|
Assert.Equal(
|
||||||
|
"@permit - Commands to give or revoke permission for others to loot your corpse.\n"
|
||||||
|
+ "@consent - Commands to help you manage the corpse-looting permissions that others give you.\n"
|
||||||
|
+ "@corpse - Displays the location of your last outdoor death.\n"
|
||||||
|
+ "@die - Kills your character and leaves a corpse, returning you to your lifestone.\n"
|
||||||
|
+ "@lifestone - Returns you to the last lifestone you used without killing you.\n"
|
||||||
|
+ "@marketplace - Teleports you to the Marketplace of Dereth.\n"
|
||||||
|
+ "@pkarena - Teleports you to the PK Arena. You must be PK to use this command.\n"
|
||||||
|
+ "@pklarena - Teleports you to the PKL Arena. You must be PKL to use this command.\n",
|
||||||
|
RetailCommandHelpTable.DeathGroupDetail);
|
||||||
|
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText("death", out string viaLookup));
|
||||||
|
Assert.Equal(RetailCommandHelpTable.DeathGroupDetail, viaLookup);
|
||||||
|
Assert.DoesNotContain("has not yet extracted", viaLookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StatusGroup_IsCompleteVerbatimListing_PreservesRetailTrailingSpace()
|
||||||
|
{
|
||||||
|
// The trailing space before "\n" on the @day line is retail's own
|
||||||
|
// byte content (data_0x7de280) -- not an acdream typo.
|
||||||
|
Assert.Equal(
|
||||||
|
"@age - Displays your total gameplay time.\n"
|
||||||
|
+ "@birth - Displays when your character was created.\n"
|
||||||
|
+ "@day - A toggle that lightens the outdoor landscape. Note that this command may take several seconds to take effect. \n"
|
||||||
|
+ "@endurance - Explains how endurance affects your character.\n"
|
||||||
|
+ "@framerate - Toggles the framerate display.\n"
|
||||||
|
+ "@loc - Displays your current position.\n"
|
||||||
|
+ "@pklite - Sets your status to Player Killer Lite. Type @help pklite for more details.\n"
|
||||||
|
+ "@version - Tells you what version of the software you are using.\n",
|
||||||
|
RetailCommandHelpTable.StatusGroupDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TextGroup_IsCompleteVerbatimListing()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
"@clear - Clears the chat box of all text.\n"
|
||||||
|
+ "@filter - Commands to filter out incoming messages.\n"
|
||||||
|
+ "@unfilter - Commands to remove filters from incoming messages.\n"
|
||||||
|
+ "@loadfile - Reads in the given text file and executes each line in the chat entry field.\n"
|
||||||
|
+ "@log - Commands to echo chat text to a logfile.\n"
|
||||||
|
+ "@title <new title> - Sets the title of the popup chat window.\n",
|
||||||
|
RetailCommandHelpTable.TextGroupDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllegiancesGroup_IsCompleteVerbatimListing_TwoLines()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
"@allegiance - Commands to help manage your allegiance.\n"
|
||||||
|
+ "@allegiance motd - Displays or sets the message of the day for your allegiance, see @help motd for more information.\n",
|
||||||
|
RetailCommandHelpTable.AllegiancesGroupDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("channels")]
|
||||||
|
[InlineData("commands")]
|
||||||
|
public void UnresolvedGroups_ContainVerbatimSummaryPlusHonestUnverifiedNote_NoMetaFabrication(
|
||||||
|
string verb)
|
||||||
|
{
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
|
||||||
|
// The old sentinel this round retired -- must never come back.
|
||||||
|
Assert.DoesNotContain("has not yet extracted", text);
|
||||||
|
Assert.Contains("UNVERIFIED", text);
|
||||||
|
Assert.Contains("HelpStupidChannelHack @0x0056f290", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChannelsGroup_SummaryLineIsVerbatim()
|
||||||
|
{
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText("channels", out string text));
|
||||||
|
Assert.StartsWith(
|
||||||
|
"@help channels - How to communicate with people in your allegiance or fellowship.",
|
||||||
|
text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChattingGroup_SevenOfEightEntriesAreVerbatim_FifthEntryMarkedUnverified()
|
||||||
|
{
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText("chatting", out string text));
|
||||||
|
Assert.Contains(
|
||||||
|
"@chat - Sets whether or not you receive normal chat.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@notell - Sets whether or not you receive @tell's.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@reply - Sends some text to the last person who @tell'd you.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@retell - Sends some text to the last person you @tell'd.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@say - Says some text to everyone around you.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@tell - Sends a private message to another character.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains(
|
||||||
|
"@afk - Set your away-from-keyboard status.\n",
|
||||||
|
text);
|
||||||
|
Assert.Contains("HelpStupidChannelHack @0x0056f290", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Campaign CH user-gate round 2, item 3: 7 of the ~35 channel
|
||||||
|
// one-liners are now verbatim retail text (HelpTurbineChat_Xxx,
|
||||||
|
// acclient_2013_pseudo_c.txt:387294-387411, addresses
|
||||||
|
// 0x577620-0x577850) -- the "Also: @alias" suffix is retail's own text.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("a", "@a - Sends a message to your Allegiance. Also: @guild, @gu")]
|
||||||
|
[InlineData("guild", "@a - Sends a message to your Allegiance. Also: @guild, @gu")]
|
||||||
|
[InlineData("general", "@general - Sends a message to the global General chat channel. Also: @cg")]
|
||||||
|
[InlineData("trade", "@trade - Sends a message to the global Trade chat channel. Also: @ct")]
|
||||||
|
[InlineData("lfg", "@lfg - Sends a message to the global Looking For Group (LFG) chat channel. Also: @clfg")]
|
||||||
|
[InlineData("roleplay", "@roleplay - Sends a message to the global Roleplay chat channel. Also: @crp")]
|
||||||
|
[InlineData("society", "@society - Sends a message to the your Society chat channel. Also: @soc")]
|
||||||
|
[InlineData("olthoi", "@olthoi - If you are an Olthoi, sends a message to the global Olthoi chat channel. Also: @o")]
|
||||||
|
public void ChannelVerb_VerbatimOneLiner_MatchesExactly(string verb, string expected)
|
||||||
|
{
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
|
||||||
|
Assert.Equal(expected, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fellowship/monarch/patron/vassals/covassal family still routes
|
||||||
|
// through the unresolved HelpStupidChannelHack pooled-string mechanism
|
||||||
|
// (the "fvpca" BN artifact) and remains an honest acdream summary, not
|
||||||
|
// a claimed-verbatim string. Pinned so a future accidental "upgrade" to
|
||||||
|
// a fabricated retail-looking string doesn't slip in unnoticed.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("f", "Sends text to your Fellowship channel.")]
|
||||||
|
[InlineData("monarch", "Sends text to your Monarch.")]
|
||||||
|
[InlineData("patron", "Sends text to your Patron.")]
|
||||||
|
[InlineData("vassal", "Sends text to your Vassals.")]
|
||||||
|
[InlineData("covassal", "Sends text to your Co-vassals.")]
|
||||||
|
public void ChannelVerb_StillAcdreamSummary_UnresolvedChannelHackFamily(
|
||||||
|
string verb, string expected)
|
||||||
|
{
|
||||||
|
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
|
||||||
|
Assert.Equal(expected, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Sweep `push imm32` (0x68) operands inside a VA range of a PE binary,
|
"""Sweep `push imm32` (0x68) operands inside a VA range of a PE binary,
|
||||||
dereference each into a data section (.rdata/.data), and decode any that
|
dereference each into a data section (.rdata/.data), and decode any that
|
||||||
resolve to a printable UTF-16LE literal.
|
resolve to a printable string literal -- UTF-16LE (PStringBase<unsigned
|
||||||
|
short>, e.g. ECM_UI notice text) or narrow ASCII (PStringBase<char>, e.g.
|
||||||
|
the ClientCommunicationSystem::Help* command-help family) alike.
|
||||||
|
|
||||||
Built for the CH2 REJECT-review rework (BLOCKER 2,
|
Built for the CH2 REJECT-review rework (BLOCKER 2,
|
||||||
docs/research/2026-08-09-ch2-review-findings.md) to re-derive
|
docs/research/2026-08-09-ch2-review-findings.md) to re-derive
|
||||||
|
|
@ -8,9 +10,22 @@ ClientCommunicationSystem::HandleFailureEvent (@0x00571990)'s 344-row
|
||||||
display-string table from ground truth instead of the Binary Ninja
|
display-string table from ground truth instead of the Binary Ninja
|
||||||
pseudo-C's ~33-char inline preview -- the same class of problem
|
pseudo-C's ~33-char inline preview -- the same class of problem
|
||||||
check_exe_pdb.py and dump_pdb_info.py solve for PDB metadata, applied to
|
check_exe_pdb.py and dump_pdb_info.py solve for PDB metadata, applied to
|
||||||
wide-string literal recovery. Not tied to WeenieError specifically: any VA
|
string literal recovery. Not tied to WeenieError specifically: any VA
|
||||||
range in any PDB-paired PE binary works.
|
range in any PDB-paired PE binary works.
|
||||||
|
|
||||||
|
Generalized for Campaign CH user-gate round 2, item 3 (2026-08-09): the
|
||||||
|
retail help command family (ClientCommunicationSystem::Help*, e.g.
|
||||||
|
HelpAllGroup/HelpAllegiancesGroup/HelpChannelsGroup/HelpChattingGroup/
|
||||||
|
HelpDeathGroup/HelpStatusGroup/HelpTextGroup) constructs its strings via
|
||||||
|
PStringBase<char> (narrow 8-bit ASCII), NOT PStringBase<unsigned short>
|
||||||
|
(wide UTF-16LE) like the WeenieError table or ECM_UI notices. Both
|
||||||
|
encodings now share one sweep: each push-imm32 hit is decoded as UTF-16LE
|
||||||
|
first (a real wide string reads back false as ASCII almost immediately --
|
||||||
|
every other byte is 0x00, which read_ascii_cstr rejects as a control
|
||||||
|
character), then as ASCII if that fails. The reported tuple carries which
|
||||||
|
encoding matched so callers can tell narrow help text apart from wide
|
||||||
|
notice text at a glance.
|
||||||
|
|
||||||
ALWAYS run check_exe_pdb.py first to confirm the candidate .exe pairs with
|
ALWAYS run check_exe_pdb.py first to confirm the candidate .exe pairs with
|
||||||
the PDB you're cross-referencing addresses against -- a mismatched binary
|
the PDB you're cross-referencing addresses against -- a mismatched binary
|
||||||
will produce confident-looking garbage.
|
will produce confident-looking garbage.
|
||||||
|
|
@ -30,6 +45,8 @@ Usage:
|
||||||
directly encodes its own VA in hex)
|
directly encodes its own VA in hex)
|
||||||
--min-len N minimum decoded string length to report (default 3);
|
--min-len N minimum decoded string length to report (default 3);
|
||||||
raise this to cut noise from short accidental hits
|
raise this to cut noise from short accidental hits
|
||||||
|
--ascii-only skip the UTF-16LE attempt entirely (narrow-string ranges
|
||||||
|
run faster and cannot false-positive against wide data)
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import struct
|
import struct
|
||||||
|
|
@ -109,14 +126,44 @@ class PeImage:
|
||||||
out.append(chr(code))
|
out.append(chr(code))
|
||||||
return None # ran off the end without a NUL -- not a bounded literal
|
return None # ran off the end without a NUL -- not a bounded literal
|
||||||
|
|
||||||
|
def read_ascii_cstr(self, va, max_chars=800):
|
||||||
|
"""Decode a narrow (8-bit) NUL-terminated C string -- the
|
||||||
|
PStringBase<char> literal shape the Help* command family uses,
|
||||||
|
distinct from read_utf16_cstr's PStringBase<unsigned short>
|
||||||
|
shape."""
|
||||||
|
off = self.va_to_off(va)
|
||||||
|
if off is None:
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for i in range(max_chars):
|
||||||
|
chunk = self.data[off + i: off + i + 1]
|
||||||
|
if len(chunk) < 1:
|
||||||
|
break
|
||||||
|
code = chunk[0]
|
||||||
|
if code == 0:
|
||||||
|
return "".join(out)
|
||||||
|
# Same control-char allowance as read_utf16_cstr (\n, \t only);
|
||||||
|
# anything else (including high bytes outside printable ASCII)
|
||||||
|
# means this isn't a real narrow literal.
|
||||||
|
if code < 0x20 and code not in (0x0A, 0x09):
|
||||||
|
return None
|
||||||
|
if code > 0x7E:
|
||||||
|
return None
|
||||||
|
out.append(chr(code))
|
||||||
|
return None # ran off the end without a NUL -- not a bounded literal
|
||||||
|
|
||||||
def is_data_section(self, va):
|
def is_data_section(self, va):
|
||||||
s = self.section_for_va(va)
|
s = self.section_for_va(va)
|
||||||
return s is not None and s["name"] in (".rdata", ".data")
|
return s is not None and s["name"] in (".rdata", ".data")
|
||||||
|
|
||||||
def sweep_push_imm32(self, lo, hi, min_len=3):
|
def sweep_push_imm32(self, lo, hi, min_len=3, ascii_only=False):
|
||||||
"""Scan [lo, hi) for `push imm32` (opcode 0x68) whose operand VA
|
"""Scan [lo, hi) for `push imm32` (opcode 0x68) whose operand VA
|
||||||
dereferences to a UTF-16LE string in .rdata/.data. Returns a list
|
dereferences to a printable string literal in .rdata/.data --
|
||||||
of (instr_va, target_va, text)."""
|
UTF-16LE tried first (unless ascii_only), narrow ASCII as the
|
||||||
|
fallback (a real wide string's alternating 0x00 bytes make the
|
||||||
|
ASCII decode reject it as a control character almost immediately,
|
||||||
|
so the two encodings do not cross-contaminate each other's hits).
|
||||||
|
Returns a list of (instr_va, target_va, text, encoding)."""
|
||||||
hits = []
|
hits = []
|
||||||
off_lo = self.va_to_off(lo)
|
off_lo = self.va_to_off(lo)
|
||||||
off_hi = self.va_to_off(hi)
|
off_hi = self.va_to_off(hi)
|
||||||
|
|
@ -127,19 +174,27 @@ class PeImage:
|
||||||
if self.data[i] == 0x68:
|
if self.data[i] == 0x68:
|
||||||
operand = struct.unpack_from("<I", self.data, i + 1)[0]
|
operand = struct.unpack_from("<I", self.data, i + 1)[0]
|
||||||
if self.is_data_section(operand):
|
if self.is_data_section(operand):
|
||||||
text = self.read_utf16_cstr(operand)
|
text = None
|
||||||
|
encoding = None
|
||||||
|
if not ascii_only:
|
||||||
|
text = self.read_utf16_cstr(operand)
|
||||||
|
encoding = "utf16" if text is not None else None
|
||||||
|
if text is None:
|
||||||
|
text = self.read_ascii_cstr(operand)
|
||||||
|
encoding = "ascii" if text is not None else None
|
||||||
if text is not None and len(text) >= min_len:
|
if text is not None and len(text) >= min_len:
|
||||||
instr_va = lo + (i - off_lo)
|
instr_va = lo + (i - off_lo)
|
||||||
hits.append((instr_va, operand, text))
|
hits.append((instr_va, operand, text, encoding))
|
||||||
i += 1
|
i += 1
|
||||||
return hits
|
return hits
|
||||||
|
|
||||||
def find_push_before(self, anchor_va, window=64, min_len=3):
|
def find_push_before(self, anchor_va, window=64, min_len=3, ascii_only=False):
|
||||||
"""Search backward from anchor_va (a call-site VA taken from the
|
"""Search backward from anchor_va (a call-site VA taken from the
|
||||||
pseudo-C) for the nearest preceding `push imm32` whose operand
|
pseudo-C) for the nearest preceding `push imm32` whose operand
|
||||||
dereferences to a printable UTF-16LE string."""
|
dereferences to a printable string literal."""
|
||||||
lo = anchor_va - window
|
lo = anchor_va - window
|
||||||
return self.sweep_push_imm32(lo, anchor_va + 2, min_len=min_len)
|
return self.sweep_push_imm32(
|
||||||
|
lo, anchor_va + 2, min_len=min_len, ascii_only=ascii_only)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -150,6 +205,7 @@ def main():
|
||||||
ap.add_argument("--window", type=int, default=64)
|
ap.add_argument("--window", type=int, default=64)
|
||||||
ap.add_argument("--deref", action="append", default=[])
|
ap.add_argument("--deref", action="append", default=[])
|
||||||
ap.add_argument("--min-len", type=int, default=3)
|
ap.add_argument("--min-len", type=int, default=3)
|
||||||
|
ap.add_argument("--ascii-only", action="store_true")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
pe = PeImage(args.exe)
|
pe = PeImage(args.exe)
|
||||||
|
|
@ -162,22 +218,30 @@ def main():
|
||||||
if args.range:
|
if args.range:
|
||||||
lo = int(args.range[0], 16)
|
lo = int(args.range[0], 16)
|
||||||
hi = int(args.range[1], 16)
|
hi = int(args.range[1], 16)
|
||||||
hits = pe.sweep_push_imm32(lo, hi, min_len=args.min_len)
|
hits = pe.sweep_push_imm32(
|
||||||
|
lo, hi, min_len=args.min_len, ascii_only=args.ascii_only)
|
||||||
print(f"# sweep 0x{lo:08x}-0x{hi:08x}: {len(hits)} string-valued push imm32 sites")
|
print(f"# sweep 0x{lo:08x}-0x{hi:08x}: {len(hits)} string-valued push imm32 sites")
|
||||||
for instr_va, target_va, text in hits:
|
for instr_va, target_va, text, encoding in hits:
|
||||||
print(f"0x{instr_va:08x} -> data_0x{target_va:08x} {text!r}")
|
print(f"0x{instr_va:08x} -> data_0x{target_va:08x} [{encoding}] {text!r}")
|
||||||
|
|
||||||
for a in args.anchor:
|
for a in args.anchor:
|
||||||
anchor = int(a, 16)
|
anchor = int(a, 16)
|
||||||
hits = pe.find_push_before(anchor, window=args.window, min_len=args.min_len)
|
hits = pe.find_push_before(
|
||||||
|
anchor, window=args.window, min_len=args.min_len,
|
||||||
|
ascii_only=args.ascii_only)
|
||||||
print(f"\n# anchor 0x{anchor:08x} (window={args.window}): {len(hits)} hits")
|
print(f"\n# anchor 0x{anchor:08x} (window={args.window}): {len(hits)} hits")
|
||||||
for instr_va, target_va, text in hits:
|
for instr_va, target_va, text, encoding in hits:
|
||||||
print(f"0x{instr_va:08x} -> data_0x{target_va:08x} {text!r}")
|
print(f"0x{instr_va:08x} -> data_0x{target_va:08x} [{encoding}] {text!r}")
|
||||||
|
|
||||||
for d in args.deref:
|
for d in args.deref:
|
||||||
target = int(d, 16)
|
target = int(d, 16)
|
||||||
text = pe.read_utf16_cstr(target)
|
text = pe.read_utf16_cstr(target)
|
||||||
print(f"\n# deref 0x{target:08x}: {text!r}")
|
if text is None:
|
||||||
|
text = pe.read_ascii_cstr(target)
|
||||||
|
kind = "ascii"
|
||||||
|
else:
|
||||||
|
kind = "utf16"
|
||||||
|
print(f"\n# deref 0x{target:08x}: [{kind}] {text!r}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue