fix(app): Phase B.2 — fix camera rotation offset + pass server MotionTableId to walk animation

Two fixes:

1. Camera side-view: AC character models face +Y in their default
   orientation, but our yaw convention has +X at yaw=0. Added a
   -PI/2 offset to the character's rotation so the model faces
   the actual walk direction. Camera was already correct — it was
   the model rotation that was 90 degrees off.

2. No walk animation: UpdatePlayerAnimation loaded the Setup
   directly from dats, but Setup.DefaultMotionTable is 0 for human
   characters — the real motion table comes from the server's
   PhysicsDescriptionFlag.MTable field in CreateObject. Without
   the override, GetIdleCycle returned null for every command.
   Now stores _playerMotionTableId from the spawn event and
   passes it as motionTableIdOverride to GetIdleCycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 15:17:55 +02:00
parent b341193cfe
commit 980e79dae9

View file

@ -99,6 +99,7 @@ public sealed class GameWindow : IDisposable
private bool _playerMode;
private uint _playerServerGuid;
private uint? _playerCurrentAnimCommand;
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
// Accumulated mouse X delta for player turning; written in mouse-move
// callback, consumed + reset in OnUpdate each frame.
private float _playerMouseDeltaX;
@ -776,6 +777,13 @@ public sealed class GameWindow : IDisposable
// UpdateMotion / UpdatePosition events can reseat this entity by guid.
_entitiesByServerGuid[spawn.Guid] = entity;
// Phase B.2: capture the server-sent MotionTableId for our own
// character so UpdatePlayerAnimation can pass it to GetIdleCycle.
// The Setup's DefaultMotionTable is often 0 for human characters;
// the real table comes from PhysicsDescriptionFlag.MTable.
if (spawn.Guid == _playerServerGuid && spawn.MotionTableId is not null)
_playerMotionTableId = spawn.MotionTableId;
// Phase 6.4: register for per-frame playback if we resolved a real
// cycle with a non-zero framerate and at least two frames in the
// cycle (single-frame poses are static and don't need ticking).
@ -1512,8 +1520,13 @@ public sealed class GameWindow : IDisposable
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
{
pe.Position = result.Position;
// AC character models face +Y in their default orientation.
// Our yaw convention has cos(yaw)=+X at yaw=0, so yaw=0
// means facing +X. Offset by -PI/2 so the model faces the
// actual walk direction (at yaw=0, model rotation = -PI/2
// = facing +X instead of the model's default +Y).
pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle(
System.Numerics.Vector3.UnitZ, _playerController.Yaw);
System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f);
}
// Update chase camera.
@ -1801,7 +1814,13 @@ public sealed class GameWindow : IDisposable
ushort cmdOverride = (ushort)(animCommand & 0xFFFFu);
var cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(
ae.Setup, _dats, commandOverride: cmdOverride);
ae.Setup, _dats,
motionTableIdOverride: _playerMotionTableId,
commandOverride: cmdOverride);
Console.WriteLine($"[PLAYER-ANIM] cmd=0x{animCommand:X8} cmdOverride=0x{cmdOverride:X4} " +
$"cycle={(cycle is null ? "NULL" : $"fr={cycle.Framerate:F1} low={cycle.LowFrame} high={cycle.HighFrame}")} " +
$"setup=0x{pe.SourceGfxObjOrSetupId:X8} mtable=0x{(uint)ae.Setup.DefaultMotionTable:X8}");
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;