acdream/src/AcDream.App/Interaction/PlayerInteractionMovementSink.cs
Erik c1e6e3da44 fix #435 (part 2, closes it): attribute the unowned probes — delete 7, reclassify 8, restore 1
Part 1 deleted probes whose owning issues were closed. These 14 named no
issue at all, so each was traced to its introducing commit
(git log -S) instead of guessed at. Attribution split them three ways:

DELETED (7, investigations closed): ACDREAM_A8_DUMP_PV and
ACDREAM_DUMP_LIVE_SPAWNS (Phase A8), ACDREAM_DUMP_CLOTHING (#37),
ACDREAM_DUMP_EDGE_SLIDE (#32), ACDREAM_DUMP_STEPUP (L.2.3d-f),
ACDREAM_DUMP_VENDOR (the vendor campaign, 25 call sites across 8 files),
ACDREAM_DUMP_VITALS (#5, four independent read sites). VendorDiagnostics.cs
went entirely.

RECLASSIFIED (8, tools misfiled as probes): the DUMP_CELLS/DUMP_GFXOBJS
fixture-extraction family (replay-harness tooling with a roundtrip test),
PROBE_CELL (standing cell-transit tracer, pair of the permanent
PROBE_RESOLVE), DUMP_SKY and HIDE_PART (generic isolation tools), and
DUMP_STEEP_ROOF — which looked like an L.4 relic but observes LIVE
divergence-register row AD-56; deleting it would have removed the only
runtime lens on an active divergence. All moved to Permanent diagnostics
with their attribution recorded.

RESTORED (1): ACDREAM_DUMP_MOVE_TRUTH was deleted and un-deleted the same
day. It is not a probe — the canonical nine-stop soak
(run-connected-r6-soak.ps1) hard-fails every destination without its
'move-truth OUT' records, with a message that would misdirect the next
operator. Under the no-workarounds rule the gate's mechanism is restored,
not left broken with an IOU (#437, closed). Process lesson recorded on
both issues: a closed owning issue is NOT sufficient to delete a probe —
grep tools/ and the contract tests for consumers first.

Also lands the owner-requested default-off invariant: every diagnostic in
the codebase is inert until its env var is explicitly set. Exactly four
flags default ON and none is a diagnostic — RETAIL_CHASE, CAMERA_COLLIDE,
CAMERA_ALIGN_SLOPE, RETAIL_CLOSE_DEGRADES are retail behaviors wearing an
A/B off-switch. That set is now FROZEN by
LaunchOptionsDocumentationTests.OnlyTheFourRetailBehaviorFlagsDefaultOn;
docs/launch-options.md's Conventions and CLAUDE.md state the rule, and
CLAUDE.md now binds future probes to a documented row in the same commit.

The client reads 137 environment variables (161 at audit start); 40
temporary probes remain, every one attributed. Full hermetic suite 15,322
passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 12:32:42 +02:00

78 lines
3.1 KiB
C#

using AcDream.App.Input;
using AcDream.Core.Items;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
namespace AcDream.App.Interaction;
internal interface IPlayerInteractionMovementSink
{
/// <summary>
/// Cancels any preceding MoveTo, invokes <paramref name="armAfterCancel"/>,
/// then installs the new movement. The callback boundary lets the intent
/// owner arm completion state before an already-facing TurnTo can finish
/// synchronously, without letting the preceding cancellation clear it.
/// </summary>
bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? armAfterCancel = null);
}
/// <summary>
/// Installs retail's client-side TurnToObject/MoveToObject prediction through
/// the same MovementManager used by authoritative movement packets.
/// </summary>
internal sealed class PlayerInteractionMovementSink(
Func<PlayerMovementController?> player,
IPlayerApproachTokenSource approachTokens)
: IPlayerInteractionMovementSink
{
private readonly Func<PlayerMovementController?> _player = player
?? throw new ArgumentNullException(nameof(player));
private readonly IPlayerApproachTokenSource _approachTokens = approachTokens
?? throw new ArgumentNullException(nameof(approachTokens));
public bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? armAfterCancel = null)
{
PlayerMovementController? controller = _player();
if (controller?.MoveTo is null)
return false;
var parameters = new MovementParameters
{
DistanceToObject = approach.UseRadius,
CanCharge = approach.CanCharge,
};
var movement = new MovementStruct
{
ObjectId = approach.Target.ServerGuid,
TopLevelId = approach.Target.ServerGuid,
Pos = new Position(
approach.Player.CellId,
approach.Target.Entity.Position,
System.Numerics.Quaternion.Identity),
Params = parameters,
Type = approach.IsCloseRange
? MovementType.TurnToObject
: MovementType.MoveToObject,
Radius = approach.TargetRadius,
Height = approach.TargetHeight,
};
// PerformMovement cancels at its head. Do it explicitly before the
// intent is armed so cancellation of the preceding move cannot clear
// the new request; the internal second call is then a retail no-op.
controller.Movement.CancelMoveTo(WeenieError.ActionCancelled);
if (!_approachTokens.TryBeginApproach(out PlayerApproachToken token))
return false;
armAfterCancel?.Invoke(token);
// P1's wire MoveToObject store marks the command non-autonomous.
// The speculative local install must do the same or the next raw-input
// pump overwrites it before ACE's authoritative movement arrives.
controller.SetLastMoveWasAutonomous(false);
return controller.Movement.PerformMovement(movement) == WeenieError.None;
}
}