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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue