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
|
|
@ -1061,25 +1061,30 @@ internal sealed class LivePresentationCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The graphics backend must publish a world pass scope.")),
|
||||
static value => value.Dispose());
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||
PortalWaitNoticeController>? portalWaitNoticeLease = null;
|
||||
if (interaction.RetainedUi is { } portalRetainedUi)
|
||||
{
|
||||
portalWaitNoticeLease = scope.Acquire(
|
||||
"portal wait notice",
|
||||
() => new PortalWaitNoticeController(
|
||||
portalRetainedUi.Host.Root),
|
||||
static value => value.Dispose());
|
||||
}
|
||||
Action<string?>? displayPortalWaitNotice =
|
||||
portalWaitNoticeLease is { } waitNoticeLease
|
||||
? waitNoticeLease.Resource.Set
|
||||
: null;
|
||||
// Campaign CH user-gate round 2, item 2: the user reported retail's
|
||||
// portal-space notice at the TOP of the screen in SMALL tell-yellow
|
||||
// text — the SpewBox — while the former PortalWaitNoticeController
|
||||
// rendered a big centered white/yellow overlay of its own. The
|
||||
// decomp confirms the user's report: gmSmartBoxUI::UseTime
|
||||
// @0x004D6E30 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 uses
|
||||
// (PlayerMovementController.ReportJumpRefusal, etc.), not a
|
||||
// dedicated overlay. PortalWaitNoticeController is deleted along
|
||||
// with its lease; this delegate now writes straight into the
|
||||
// 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
|
||||
// interface-text surface (research doc §1.1/§7.3/§7.4) — modeled
|
||||
// directly on the PortalWaitNoticeController lease immediately
|
||||
// above, wired into the same retained-UI host. Like the wait notice
|
||||
// in its no-tunnel arm, the transferred controller is reclaimed by
|
||||
// interface-text surface (research doc §1.1/§7.3/§7.4), wired into
|
||||
// the retained-UI host. The transferred controller is reclaimed by
|
||||
// the retained-UI root's own teardown (its Dispose only detaches
|
||||
// children from that root).
|
||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||
|
|
@ -1120,8 +1125,7 @@ internal sealed class LivePresentationCompositionPhase
|
|||
portalDispatcher,
|
||||
foundation.SceneLighting!,
|
||||
foundation.MeshAdapter!,
|
||||
displayPortalWaitNotice,
|
||||
portalWaitNoticeLease?.Resource),
|
||||
displayPortalWaitNotice),
|
||||
static tunnel => tunnel.PrepareResources());
|
||||
}
|
||||
catch (Exception acquisitionFailure)
|
||||
|
|
@ -1304,7 +1308,6 @@ internal sealed class LivePresentationCompositionPhase
|
|||
clipFrameLease.Transfer();
|
||||
portalDepthLease.Transfer();
|
||||
portalTunnelLease?.Transfer();
|
||||
portalWaitNoticeLease?.Transfer();
|
||||
spewBoxLease?.Transfer();
|
||||
skyLease.Transfer();
|
||||
particleLease.Transfer();
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private readonly WorldEntity _entity;
|
||||
private readonly PortalTunnelCamera _camera = new();
|
||||
private readonly Random _random;
|
||||
private readonly Action<string?>? _displayNotice;
|
||||
private IDisposable? _displayNoticeLifetime;
|
||||
private readonly Action<string>? _displayNotice;
|
||||
private readonly SyntheticEntityMeshReferenceOwner _meshReferences;
|
||||
|
||||
private bool _visible;
|
||||
|
|
@ -113,8 +112,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
IAnimationLoader animationLoader,
|
||||
IAnimationHookSink hookSink,
|
||||
Random random,
|
||||
Action<string?>? displayNotice,
|
||||
IDisposable? displayNoticeLifetime)
|
||||
Action<string>? displayNotice)
|
||||
{
|
||||
_scope = scope;
|
||||
_frames = frames;
|
||||
|
|
@ -128,7 +126,6 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_sequence.HookObj = _animationHooks;
|
||||
_random = random;
|
||||
_displayNotice = displayNotice;
|
||||
_displayNoticeLifetime = displayNoticeLifetime;
|
||||
|
||||
_entity = new WorldEntity
|
||||
{
|
||||
|
|
@ -164,8 +161,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
Action<string?>? displayNotice = null,
|
||||
IDisposable? displayNoticeLifetime = null,
|
||||
Action<string>? displayNotice = null,
|
||||
Random? random = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scope);
|
||||
|
|
@ -202,8 +198,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
animationLoader,
|
||||
hookSink,
|
||||
random ?? Random.Shared,
|
||||
displayNotice,
|
||||
displayNoticeLifetime);
|
||||
displayNotice);
|
||||
}
|
||||
|
||||
internal static void EnsureRequiredAssets(
|
||||
|
|
@ -260,7 +255,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
_rotationEndAngle = 0f;
|
||||
_rotationCurrentAngle = 0f;
|
||||
_camera.DirectionDegrees = 0f;
|
||||
ClearWaitCueNotice();
|
||||
_waitCueVisible = false;
|
||||
_visible = true;
|
||||
RebuildPose();
|
||||
}
|
||||
|
|
@ -271,7 +266,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
if (_disposed)
|
||||
return;
|
||||
_visible = false;
|
||||
ClearWaitCueNotice();
|
||||
_waitCueVisible = false;
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
}
|
||||
|
|
@ -291,38 +286,19 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
/// The hold-delay-gated arm/disarm <c>LocalPlayerTeleportController</c>
|
||||
/// still drives every frame from <c>RuntimeWorldTransitState.ObserveWait</c>
|
||||
/// (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,
|
||||
/// #329). Kept only so the controller's own hold bookkeeping still has
|
||||
/// somewhere to land; because <c>visible</c> is false for the entire
|
||||
/// common case (a transit that never crosses the invented 5-second
|
||||
/// hold), this is a same-value no-op there and never contends with the
|
||||
/// per-segment write above.
|
||||
/// somewhere to land; it is pure bookkeeping now (Campaign CH user-gate
|
||||
/// round 2, item 2). The former text-clear invoke made sense only for
|
||||
/// the deleted <c>PortalWaitNoticeController</c>'s overwrite-only slot —
|
||||
/// 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>
|
||||
public void SetWaitCue(bool 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);
|
||||
}
|
||||
public void SetWaitCue(bool visible) => _waitCueVisible = visible;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
// method's own doc comment): LocalPlayerTeleportController
|
||||
// still drives SetWaitCue every frame from its own hold-delay
|
||||
// bookkeeping, but because that call is a same-value no-op for
|
||||
// the entire common case (a transit that never crosses the 5s
|
||||
// hold), it never fights this unconditional per-segment write.
|
||||
// Enter/Exit/Dispose clear the notice directly (not through
|
||||
// SetWaitCue's dedup) so a stale line can never survive past
|
||||
// this presentation going invisible.
|
||||
// bookkeeping, but that call is pure bookkeeping now, so it
|
||||
// never fights this unconditional per-segment write.
|
||||
// Campaign CH user-gate round 2, item 2: this now targets the
|
||||
// SpewBox (RuntimeCommunicationState.AddText, ClientLocal),
|
||||
// retail's real destination for this notice
|
||||
// (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...");
|
||||
}
|
||||
else
|
||||
|
|
@ -517,14 +502,12 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
try
|
||||
{
|
||||
_visible = false;
|
||||
ClearWaitCueNotice();
|
||||
_waitCueVisible = false;
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
_meshReferences.Dispose();
|
||||
if (_meshReferences.IsDisposed)
|
||||
{
|
||||
_displayNoticeLifetime?.Dispose();
|
||||
_displayNoticeLifetime = null;
|
||||
_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>
|
||||
/// Retained presentation of retail's <c>gmSpewBoxUI</c> (research doc
|
||||
/// §1.1/§7.3/§7.4) — the transient top-of-viewport interface-text queue.
|
||||
/// Modeled directly on <see cref="PortalWaitNoticeController"/>: a single
|
||||
/// <c>ClickThrough</c> <see cref="UiText"/> block at a high
|
||||
/// <see cref="UiElement.ZOrder"/>. Unlike that controller's single
|
||||
/// overwrite-only slot, this reads <see cref="SpewBoxVM"/>'s bounded,
|
||||
/// newest-on-top, per-entry-expiring queue every frame.
|
||||
/// A single <c>ClickThrough</c> <see cref="UiText"/> block at a high
|
||||
/// <see cref="UiElement.ZOrder"/>. Campaign CH user-gate round 2, item 2:
|
||||
/// this is now the ONLY on-screen interface-text presentation surface —
|
||||
/// the former <c>PortalWaitNoticeController</c> (a dedicated centered
|
||||
/// 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>
|
||||
/// <remarks>
|
||||
/// <b>CH2 REJECT-review rework, BLOCKER 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue