Pass explicit grounded/airborne contact bytes from MovementResult into MoveToState and AutonomousPosition, and add ACDREAM_DUMP_MOVE_TRUTH logging for outbound movement plus player UpdatePosition echoes. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
d4c3f947d2
commit
3be0c8b7c7
5 changed files with 166 additions and 56 deletions
|
|
@ -177,33 +177,6 @@ missing is the plugin-API surface.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #30 — AutonomousPosition contact byte is too often grounded
|
|
||||||
|
|
||||||
**Status:** OPEN
|
|
||||||
**Severity:** HIGH
|
|
||||||
**Filed:** 2026-04-29
|
|
||||||
**Component:** physics / net / movement
|
|
||||||
|
|
||||||
**Description:** Outbound movement can claim grounded contact even when the
|
|
||||||
local resolver result is uncertain or airborne. `AutonomousPosition.Build`
|
|
||||||
defaults `lastContact` to 1, and the app path needs an audit to ensure
|
|
||||||
`ResolveResult.IsOnGround` is what reaches the wire.
|
|
||||||
|
|
||||||
**Root cause / status:** Tracked under Phase L.2b. This can make ACE accept
|
|
||||||
movement that is not actually retail-valid and can hide edge/step-down bugs.
|
|
||||||
|
|
||||||
**Files:** `src/AcDream.Core.Net/Messages/AutonomousPosition.cs`,
|
|
||||||
`src/AcDream.Core.Net/Messages/MoveToState.cs`,
|
|
||||||
`src/AcDream.App/Input/PlayerMovementController.cs`.
|
|
||||||
|
|
||||||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
|
||||||
|
|
||||||
**Acceptance:** Ground contact byte is derived from the current resolved
|
|
||||||
movement result for both autonomous heartbeat and movement-state sends. Tests
|
|
||||||
cover grounded, airborne, and failed-transition cases.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## #31 — Low outdoor cell id can go stale after transition movement
|
## #31 — Low outdoor cell id can go stale after transition movement
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** OPEN
|
||||||
|
|
@ -285,33 +258,6 @@ one live creature case no longer use the single-cylinder fallback.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #34 — Missing routine local/server correction diagnostic
|
|
||||||
|
|
||||||
**Status:** OPEN
|
|
||||||
**Severity:** MEDIUM
|
|
||||||
**Filed:** 2026-04-29
|
|
||||||
**Component:** physics / net / diagnostics
|
|
||||||
|
|
||||||
**Description:** The client needs an opt-in diagnostic that logs local predicted
|
|
||||||
position/contact/cell, outbound movement fields, server `UpdatePosition` echo,
|
|
||||||
and correction delta. Current correction visibility is too focused on portal
|
|
||||||
arrival and not enough on normal walking.
|
|
||||||
|
|
||||||
**Root cause / status:** Tracked under Phase L.2a/L.2b. Without this probe,
|
|
||||||
ACE's tolerance can hide local collision divergence.
|
|
||||||
|
|
||||||
**Files:** `src/AcDream.Core.Net/WorldSession.cs`,
|
|
||||||
`src/AcDream.App/Input/PlayerMovementController.cs`,
|
|
||||||
`src/AcDream.App/Rendering/GameWindow.cs`.
|
|
||||||
|
|
||||||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
|
||||||
|
|
||||||
**Acceptance:** With the diagnostic enabled, a walking session logs local
|
|
||||||
resolved placement, outbound cell/contact fields, server echo placement, and
|
|
||||||
correction delta in a grep-friendly format.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
## #2 — Lightning visual mismatch (sky PES path disproved)
|
## #2 — Lightning visual mismatch (sky PES path disproved)
|
||||||
|
|
||||||
|
|
@ -494,6 +440,28 @@ If hypothesis (a) is correct, this issue effectively rolls into **#28** — the
|
||||||
|
|
||||||
# Recently closed
|
# Recently closed
|
||||||
|
|
||||||
|
## #34 — [DONE 2026-04-29] Missing routine local/server correction diagnostic
|
||||||
|
|
||||||
|
**Closed:** 2026-04-29
|
||||||
|
**Commit:** `(this commit)`
|
||||||
|
**Resolution:** Added `ACDREAM_DUMP_MOVE_TRUTH=1`, which logs local resolved
|
||||||
|
position/contact/cell, outbound movement fields, server `UpdatePosition` echo,
|
||||||
|
and local/server correction delta for the player in grep-friendly
|
||||||
|
`move-truth OUT` / `move-truth ECHO` lines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #30 — [DONE 2026-04-29] AutonomousPosition contact byte is too often grounded
|
||||||
|
|
||||||
|
**Closed:** 2026-04-29
|
||||||
|
**Commit:** `(this commit)`
|
||||||
|
**Resolution:** `GameWindow` now derives the movement contact byte from
|
||||||
|
`MovementResult.IsOnGround` and passes it explicitly to both `MoveToState.Build`
|
||||||
|
and `AutonomousPosition.Build`. Added packet tests proving both builders encode
|
||||||
|
an explicit airborne contact byte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## #27 — [DONE 2026-04-26] Cloud meshes appeared missing or faint vs retail
|
## #27 — [DONE 2026-04-26] Cloud meshes appeared missing or faint vs retail
|
||||||
|
|
||||||
**Closed:** 2026-04-26
|
**Closed:** 2026-04-26
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,11 @@ InputDispatcher / PlayerMovementController
|
||||||
ownership.
|
ownership.
|
||||||
- L.1 animation work must coordinate with L.2 when root motion or observer
|
- L.1 animation work must coordinate with L.2 when root motion or observer
|
||||||
movement changes the predicted body path.
|
movement changes the predicted body path.
|
||||||
|
|
||||||
|
## Shipped Slices
|
||||||
|
|
||||||
|
- 2026-04-29: L.2a/L.2b first diagnostic slice. `ACDREAM_DUMP_MOVE_TRUTH=1`
|
||||||
|
logs `move-truth OUT` for outbound `MoveToState` / `AutonomousPosition` and
|
||||||
|
`move-truth ECHO` for player `UpdatePosition` echoes, including local/server
|
||||||
|
delta. `GameWindow` now passes explicit grounded/airborne contact bytes from
|
||||||
|
`MovementResult.IsOnGround` to both movement packet builders.
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,8 @@ public sealed class GameWindow : IDisposable
|
||||||
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
|
private AcDream.UI.Abstractions.Panels.Debug.DebugVM? _debugVm;
|
||||||
private static readonly bool DevToolsEnabled =
|
private static readonly bool DevToolsEnabled =
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1";
|
||||||
|
private static readonly bool DumpMoveTruthEnabled =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOVE_TRUTH") == "1";
|
||||||
|
|
||||||
// Phase I.3 — real ICommandBus for live sessions. Constructed when
|
// Phase I.3 — real ICommandBus for live sessions. Constructed when
|
||||||
// the live session spins up (so SendChatCmd handlers can close over
|
// the live session spins up (so SendChatCmd handlers can close over
|
||||||
|
|
@ -464,6 +466,19 @@ public sealed class GameWindow : IDisposable
|
||||||
private uint? _playerCurrentAnimCommand;
|
private uint? _playerCurrentAnimCommand;
|
||||||
private float _playerCurrentAnimSpeed = 1f;
|
private float _playerCurrentAnimSpeed = 1f;
|
||||||
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
|
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
|
||||||
|
private MovementTruthOutbound? _lastMovementTruthOutbound;
|
||||||
|
|
||||||
|
private readonly record struct MovementTruthOutbound(
|
||||||
|
string Kind,
|
||||||
|
uint Sequence,
|
||||||
|
System.DateTime TimeUtc,
|
||||||
|
System.Numerics.Vector3 LocalWorldPosition,
|
||||||
|
uint LocalCellId,
|
||||||
|
System.Numerics.Vector3 WirePosition,
|
||||||
|
uint WireCellId,
|
||||||
|
bool IsOnGround,
|
||||||
|
byte ContactByte,
|
||||||
|
System.Numerics.Vector3 Velocity);
|
||||||
|
|
||||||
// K-fix7 (2026-04-26): server-authoritative Run + Jump skill values
|
// K-fix7 (2026-04-26): server-authoritative Run + Jump skill values
|
||||||
// received from PlayerDescription. -1 = "not yet received, fall back
|
// received from PlayerDescription. -1 = "not yet received, fall back
|
||||||
|
|
@ -3076,6 +3091,7 @@ public sealed class GameWindow : IDisposable
|
||||||
0f);
|
0f);
|
||||||
var worldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ) + origin;
|
var worldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ) + origin;
|
||||||
var rot = new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
|
var rot = new System.Numerics.Quaternion(p.RotationX, p.RotationY, p.RotationZ, p.RotationW);
|
||||||
|
DumpMovementTruthServerEcho(update, worldPos);
|
||||||
|
|
||||||
// Capture the pre-update render position for the soft-snap residual
|
// Capture the pre-update render position for the soft-snap residual
|
||||||
// calculation below. Assign entity.Position to the server truth up
|
// calculation below. Assign entity.Position to the server truth up
|
||||||
|
|
@ -4853,6 +4869,7 @@ public sealed class GameWindow : IDisposable
|
||||||
uint wireCellId = ((uint)lbX << 24) | ((uint)lbY << 16) | (result.CellId & 0xFFFFu);
|
uint wireCellId = ((uint)lbX << 24) | ((uint)lbY << 16) | (result.CellId & 0xFFFFu);
|
||||||
var wirePos = new System.Numerics.Vector3(localX, localY, result.Position.Z);
|
var wirePos = new System.Numerics.Vector3(localX, localY, result.Position.Z);
|
||||||
var wireRot = YawToAcQuaternion(_playerController.Yaw);
|
var wireRot = YawToAcQuaternion(_playerController.Yaw);
|
||||||
|
byte contactByte = result.IsOnGround ? (byte)1 : (byte)0;
|
||||||
|
|
||||||
if (result.MotionStateChanged)
|
if (result.MotionStateChanged)
|
||||||
{
|
{
|
||||||
|
|
@ -4885,7 +4902,10 @@ public sealed class GameWindow : IDisposable
|
||||||
instanceSequence: _liveSession.InstanceSequence,
|
instanceSequence: _liveSession.InstanceSequence,
|
||||||
serverControlSequence: _liveSession.ServerControlSequence,
|
serverControlSequence: _liveSession.ServerControlSequence,
|
||||||
teleportSequence: _liveSession.TeleportSequence,
|
teleportSequence: _liveSession.TeleportSequence,
|
||||||
forcePositionSequence: _liveSession.ForcePositionSequence);
|
forcePositionSequence: _liveSession.ForcePositionSequence,
|
||||||
|
contactLongJump: contactByte);
|
||||||
|
DumpMovementTruthOutbound(
|
||||||
|
"MTS", seq, result, wirePos, wireCellId, contactByte);
|
||||||
_liveSession.SendGameAction(body);
|
_liveSession.SendGameAction(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4900,7 +4920,10 @@ public sealed class GameWindow : IDisposable
|
||||||
instanceSequence: _liveSession.InstanceSequence,
|
instanceSequence: _liveSession.InstanceSequence,
|
||||||
serverControlSequence: _liveSession.ServerControlSequence,
|
serverControlSequence: _liveSession.ServerControlSequence,
|
||||||
teleportSequence: _liveSession.TeleportSequence,
|
teleportSequence: _liveSession.TeleportSequence,
|
||||||
forcePositionSequence: _liveSession.ForcePositionSequence);
|
forcePositionSequence: _liveSession.ForcePositionSequence,
|
||||||
|
lastContact: contactByte);
|
||||||
|
DumpMovementTruthOutbound(
|
||||||
|
"AP", seq, result, wirePos, wireCellId, contactByte);
|
||||||
_liveSession.SendGameAction(body);
|
_liveSession.SendGameAction(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4924,6 +4947,76 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DumpMovementTruthOutbound(
|
||||||
|
string kind,
|
||||||
|
uint sequence,
|
||||||
|
AcDream.App.Input.MovementResult result,
|
||||||
|
System.Numerics.Vector3 wirePosition,
|
||||||
|
uint wireCellId,
|
||||||
|
byte contactByte)
|
||||||
|
{
|
||||||
|
if (!DumpMoveTruthEnabled) return;
|
||||||
|
|
||||||
|
var velocity = _playerController?.BodyVelocity ?? System.Numerics.Vector3.Zero;
|
||||||
|
_lastMovementTruthOutbound = new MovementTruthOutbound(
|
||||||
|
kind,
|
||||||
|
sequence,
|
||||||
|
System.DateTime.UtcNow,
|
||||||
|
result.Position,
|
||||||
|
result.CellId,
|
||||||
|
wirePosition,
|
||||||
|
wireCellId,
|
||||||
|
result.IsOnGround,
|
||||||
|
contactByte,
|
||||||
|
velocity);
|
||||||
|
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant($"move-truth OUT kind={kind} seq={sequence} local={Fmt(result.Position)} localCell=0x{result.CellId:X8} wire={Fmt(wirePosition)} wireCell=0x{wireCellId:X8} grounded={result.IsOnGround} contact={contactByte} vel={Fmt(velocity)} f={FmtCmd(result.ForwardCommand)} s={FmtCmd(result.SidestepCommand)} t={FmtCmd(result.TurnCommand)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpMovementTruthServerEcho(
|
||||||
|
AcDream.Core.Net.WorldSession.EntityPositionUpdate update,
|
||||||
|
System.Numerics.Vector3 serverWorldPosition)
|
||||||
|
{
|
||||||
|
if (!DumpMoveTruthEnabled || update.Guid != _playerServerGuid) return;
|
||||||
|
|
||||||
|
var now = System.DateTime.UtcNow;
|
||||||
|
var localPosition = _playerController?.Position;
|
||||||
|
var localCellId = _playerController?.CellId;
|
||||||
|
var deltaLocal = localPosition.HasValue
|
||||||
|
? serverWorldPosition - localPosition.Value
|
||||||
|
: (System.Numerics.Vector3?)null;
|
||||||
|
|
||||||
|
string localText = localPosition.HasValue ? Fmt(localPosition.Value) : "-";
|
||||||
|
string localCellText = localCellId.HasValue
|
||||||
|
? System.FormattableString.Invariant($"0x{localCellId.Value:X8}")
|
||||||
|
: "-";
|
||||||
|
string deltaLocalText = deltaLocal.HasValue ? Fmt(deltaLocal.Value) : "-";
|
||||||
|
string deltaLocalLen = deltaLocal.HasValue
|
||||||
|
? System.FormattableString.Invariant($"{deltaLocal.Value.Length():F3}")
|
||||||
|
: "-";
|
||||||
|
|
||||||
|
string lastText = "-";
|
||||||
|
if (_lastMovementTruthOutbound is { } last)
|
||||||
|
{
|
||||||
|
var deltaOut = serverWorldPosition - last.LocalWorldPosition;
|
||||||
|
var ageMs = (now - last.TimeUtc).TotalMilliseconds;
|
||||||
|
lastText = System.FormattableString.Invariant($"{last.Kind}:{last.Sequence} ageMs={ageMs:F0} outGrounded={last.IsOnGround} outContact={last.ContactByte} outCell=0x{last.WireCellId:X8} deltaOut={Fmt(deltaOut)} distOut={deltaOut.Length():F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
string state = _playerController?.State.ToString() ?? "-";
|
||||||
|
string velocityText = update.Velocity.HasValue ? Fmt(update.Velocity.Value) : "-";
|
||||||
|
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant($"move-truth ECHO guid=0x{update.Guid:X8} server={Fmt(serverWorldPosition)} serverCell=0x{update.Position.LandblockId:X8} local={localText} localCell={localCellText} deltaLocal={deltaLocalText} distLocal={deltaLocalLen} serverVel={velocityText} state={state} lastOut={lastText}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Fmt(System.Numerics.Vector3 v) =>
|
||||||
|
System.FormattableString.Invariant($"({v.X:F3},{v.Y:F3},{v.Z:F3})");
|
||||||
|
|
||||||
|
private static string FmtCmd(uint? command) =>
|
||||||
|
command.HasValue
|
||||||
|
? System.FormattableString.Invariant($"0x{command.Value:X8}")
|
||||||
|
: "-";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert our internal yaw (math convention: 0=+X East, PI/2=+Y North)
|
/// Convert our internal yaw (math convention: 0=+X East, PI/2=+Y North)
|
||||||
/// to AC's quaternion heading convention.
|
/// to AC's quaternion heading convention.
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,23 @@ public class AutonomousPositionTests
|
||||||
Assert.Equal(56, body.Length);
|
Assert.Equal(56, body.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_UsesExplicitAirborneContactByte()
|
||||||
|
{
|
||||||
|
var body = AutonomousPosition.Build(
|
||||||
|
gameActionSequence: 7,
|
||||||
|
cellId: 0xA9B40001u,
|
||||||
|
position: Vector3.Zero,
|
||||||
|
rotation: Quaternion.Identity,
|
||||||
|
instanceSequence: 0,
|
||||||
|
serverControlSequence: 0,
|
||||||
|
teleportSequence: 0,
|
||||||
|
forcePositionSequence: 0,
|
||||||
|
lastContact: 0);
|
||||||
|
|
||||||
|
Assert.Equal(0, body[52]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_ContainsIdentityRotation_AfterPosition()
|
public void Build_ContainsIdentityRotation_AfterPosition()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,30 @@ public class MoveToStateTests
|
||||||
Assert.Equal(0, body.Length % 4);
|
Assert.Equal(0, body.Length % 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_UsesExplicitAirborneContactByte()
|
||||||
|
{
|
||||||
|
var body = MoveToState.Build(
|
||||||
|
gameActionSequence: 7,
|
||||||
|
forwardCommand: null,
|
||||||
|
forwardSpeed: null,
|
||||||
|
sidestepCommand: null,
|
||||||
|
sidestepSpeed: null,
|
||||||
|
turnCommand: null,
|
||||||
|
turnSpeed: null,
|
||||||
|
holdKey: null,
|
||||||
|
cellId: 0xA9B40001u,
|
||||||
|
position: Vector3.Zero,
|
||||||
|
rotation: Quaternion.Identity,
|
||||||
|
instanceSequence: 0,
|
||||||
|
serverControlSequence: 0,
|
||||||
|
teleportSequence: 0,
|
||||||
|
forcePositionSequence: 0,
|
||||||
|
contactLongJump: 0);
|
||||||
|
|
||||||
|
Assert.Equal(0, body[56]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_WithHoldKey_IncludesHoldKeyFlag()
|
public void Build_WithHoldKey_IncludesHoldKeyFlag()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue