Merge main into claude/strange-ardinghelli-d810cd

Brings in #38 render-interpolation camera work before testing #48
diagnostic dump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-06 17:58:06 +02:00
commit c1bb43ab89
5 changed files with 218 additions and 16 deletions

View file

@ -41,6 +41,7 @@ public readonly record struct MovementInput(
/// </summary>
public readonly record struct MovementResult(
Vector3 Position,
Vector3 RenderPosition,
uint CellId,
bool IsOnGround,
bool MotionStateChanged,
@ -128,6 +129,7 @@ public sealed class PlayerMovementController
public float Yaw { get; set; }
public Vector3 Position => _body.Position;
public Vector3 RenderPosition => ComputeRenderPosition();
public uint CellId { get; private set; }
/// <summary>
@ -213,6 +215,8 @@ public sealed class PlayerMovementController
// ACE: PhysicsObj.UpdateObject (Physics.cs).
// Named-retail: CPhysicsObj::update_object (acclient_2013_pseudo_c.txt:283950).
private float _physicsAccum;
private Vector3 _prevPhysicsPos;
private Vector3 _currPhysicsPos;
public PlayerMovementController(PhysicsEngine physics)
{
@ -287,6 +291,8 @@ public sealed class PlayerMovementController
public void SetPosition(Vector3 pos, uint cellId)
{
_body.Position = pos;
_prevPhysicsPos = pos;
_currPhysicsPos = pos;
CellId = cellId;
// Treat as grounded after a server-side position snap.
@ -295,6 +301,13 @@ public sealed class PlayerMovementController
// Reset physics clock so any subsequent update_object calls start fresh.
_body.LastUpdateTime = 0.0;
_physicsAccum = 0f;
}
private Vector3 ComputeRenderPosition()
{
float alpha = Math.Clamp(_physicsAccum / PhysicsBody.MinQuantum, 0f, 1f);
return Vector3.Lerp(_prevPhysicsPos, _currPhysicsPos, alpha);
}
public MovementResult Update(float dt, MovementInput input)
@ -306,6 +319,7 @@ public sealed class PlayerMovementController
{
return new MovementResult(
Position: Position,
RenderPosition: RenderPosition,
CellId: CellId,
IsOnGround: _body.OnWalkable,
MotionStateChanged: false,
@ -524,12 +538,16 @@ public sealed class PlayerMovementController
// accumulated dt) when the threshold is reached. See _physicsAccum
// declaration for the full retail trace evidence.
var preIntegratePos = _body.Position;
bool physicsTickRan = false;
Vector3 oldTickEndPos = _currPhysicsPos;
_physicsAccum += dt;
if (_physicsAccum > PhysicsBody.HugeQuantum)
{
// Stale frame (debugger break, GC pause). Discard accumulated dt.
_physicsAccum = 0f;
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
}
else if (_physicsAccum >= PhysicsBody.MinQuantum)
{
@ -539,6 +557,7 @@ public sealed class PlayerMovementController
_body.calc_acceleration();
_body.UpdatePhysicsInternal(tickDt);
_physicsAccum -= tickDt;
physicsTickRan = true;
}
// Else: dt below MinQuantum threshold — skip integration. Position
// and velocity remain unchanged; Resolve below runs as a zero-distance
@ -591,6 +610,11 @@ public sealed class PlayerMovementController
// Apply resolved position.
_body.Position = resolveResult.Position;
if (physicsTickRan)
{
_prevPhysicsPos = oldTickEndPos;
_currPhysicsPos = _body.Position;
}
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
//
@ -874,6 +898,7 @@ public sealed class PlayerMovementController
return new MovementResult(
Position: Position,
RenderPosition: RenderPosition,
CellId: CellId,
IsOnGround: _body.OnWalkable,
MotionStateChanged: changed,

View file

@ -173,15 +173,15 @@ public sealed class GameWindow : IDisposable
private static readonly int s_hidePartIndex =
int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_HIDE_PART"), out var hp) ? hp : -1;
// Issue #47 — opt in to retail's close-detail GfxObj selection on
// Issue #47 — use retail's close-detail GfxObj selection on
// humanoid setups. When enabled, every per-part GfxObj id (after
// server AnimPartChanges are applied) is replaced with Degrades[0]
// from its DIDDegrade table when present. See GfxObjDegradeResolver
// for the full retail-decomp citation. Off by default while the fix
// bakes; flip to default-on once we've confirmed no scenery/setup
// regressions.
// for the full retail-decomp citation. Default-on after visual
// confirmation; set ACDREAM_RETAIL_CLOSE_DEGRADES=0 only for
// diagnostic before/after comparisons.
private static readonly bool s_retailCloseDegrades =
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "1", StringComparison.Ordinal);
!string.Equals(Environment.GetEnvironmentVariable("ACDREAM_RETAIL_CLOSE_DEGRADES"), "0", StringComparison.Ordinal);
// Issue #48 diagnostic — dump per-scenery-spawn placement evidence
// (rendered gfx id, sample source physics-vs-bilinear, ground/baseLoc/finalZ,
@ -5790,7 +5790,7 @@ public sealed class GameWindow : IDisposable
// the physics-resolved location each frame.
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
{
pe.Position = result.Position;
pe.Position = result.RenderPosition;
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
@ -5812,7 +5812,7 @@ public sealed class GameWindow : IDisposable
// position never changes. With the pin: player visibly
// rises above the camera, matching retail "you can see
// yourself jump" feedback.
_chaseCamera.Update(result.Position, _playerController.Yaw,
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
isOnGround: result.IsOnGround,
dt: (float)dt);