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
|
|
@ -20,6 +20,7 @@ internal static class HeadlessBotPolicyFactory
|
|||
"lifecycle-smoke" => new LifecycleSmokeHeadlessBotPolicy(),
|
||||
"observer-movement" => new ObserverMovementHeadlessBotPolicy(),
|
||||
"portal-route-smoke" => new PortalRouteSmokeHeadlessBotPolicy(),
|
||||
"jump-probe" => new JumpProbeHeadlessBotPolicy(),
|
||||
_ => throw new HeadlessConfigurationException(
|
||||
$"Unknown headless bot policy '{id}'."),
|
||||
};
|
||||
|
|
@ -573,3 +574,167 @@ internal sealed class PortalRouteSmokeHeadlessBotPolicy
|
|||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue