Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates. Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean. Co-authored-by: OpenAI Codex <codex@openai.com>
90 lines
3.8 KiB
C#
90 lines
3.8 KiB
C#
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Input;
|
|
|
|
/// <summary>
|
|
/// Projects the canonical local physics body into world rendering and spatial
|
|
/// buckets without advancing it.
|
|
/// </summary>
|
|
public sealed class LocalPlayerProjectionController
|
|
{
|
|
private readonly Func<WorldEntity?> _resolveEntity;
|
|
private readonly Func<int> _liveCenterX;
|
|
private readonly Func<int> _liveCenterY;
|
|
private readonly Action<WorldEntity, uint> _syncShadow;
|
|
private readonly Action<uint, uint> _rebucket;
|
|
private readonly Func<WorldEntity, bool> _isCurrentVisibleProjection;
|
|
private readonly Action<WorldEntity> _suspendShadow;
|
|
|
|
public LocalPlayerProjectionController(
|
|
Func<WorldEntity?> resolveEntity,
|
|
Func<int> liveCenterX,
|
|
Func<int> liveCenterY,
|
|
Action<WorldEntity, uint> syncShadow,
|
|
Action<uint, uint> rebucket,
|
|
Func<WorldEntity, bool> isCurrentVisibleProjection,
|
|
Action<WorldEntity> suspendShadow)
|
|
{
|
|
_resolveEntity = resolveEntity ?? throw new ArgumentNullException(nameof(resolveEntity));
|
|
_liveCenterX = liveCenterX ?? throw new ArgumentNullException(nameof(liveCenterX));
|
|
_liveCenterY = liveCenterY ?? throw new ArgumentNullException(nameof(liveCenterY));
|
|
_syncShadow = syncShadow ?? throw new ArgumentNullException(nameof(syncShadow));
|
|
_rebucket = rebucket ?? throw new ArgumentNullException(nameof(rebucket));
|
|
_isCurrentVisibleProjection = isCurrentVisibleProjection
|
|
?? throw new ArgumentNullException(nameof(isCurrentVisibleProjection));
|
|
_suspendShadow = suspendShadow
|
|
?? throw new ArgumentNullException(nameof(suspendShadow));
|
|
}
|
|
|
|
public void Project(
|
|
PlayerMovementController controller,
|
|
MovementResult movement,
|
|
bool hidden)
|
|
{
|
|
WorldEntity? entity = _resolveEntity();
|
|
if (entity is null)
|
|
return;
|
|
|
|
entity.SetPosition(movement.RenderPosition);
|
|
entity.ParentCellId = movement.CellId;
|
|
// Retail's root is a complete Frame. Project the authoritative body
|
|
// quaternion directly so pitch/roll from a server correction or DAT
|
|
// root frame cannot be flattened by the presentation layer.
|
|
entity.Rotation = controller.BodyOrientation;
|
|
|
|
uint currentLandblock;
|
|
if (movement.CellId != 0 && (movement.CellId & 0xFFFFu) >= 0x0100u)
|
|
{
|
|
// Indoor positions belong to the cell's landblock. Dungeon frame
|
|
// origins can be negative, so render XYZ is not a landblock key.
|
|
currentLandblock = (movement.CellId & 0xFFFF0000u) | 0xFFFFu;
|
|
}
|
|
else
|
|
{
|
|
System.Numerics.Vector3 position = controller.Position;
|
|
int landblockX = _liveCenterX() + (int)Math.Floor(position.X / 192f);
|
|
int landblockY = _liveCenterY() + (int)Math.Floor(position.Y / 192f);
|
|
currentLandblock = (uint)((landblockX << 24) | (landblockY << 16) | 0xFFFF);
|
|
}
|
|
|
|
// The teleport owner alone projects the destination while the local
|
|
// controller deliberately retains its frozen source cell.
|
|
if (controller.State == PlayerState.PortalSpace)
|
|
return;
|
|
|
|
// SetPositionInternal commits the complete root before changing cell
|
|
// membership, then replaces collision rows only while the object still
|
|
// owns a loaded cell. Rebucket is a synchronous callback boundary: it
|
|
// can move this incarnation to pending or replace the GUID entirely.
|
|
_rebucket(entity.ServerGuid, currentLandblock);
|
|
if (hidden
|
|
|| movement.CellId == 0
|
|
|| !_isCurrentVisibleProjection(entity))
|
|
{
|
|
_suspendShadow(entity);
|
|
return;
|
|
}
|
|
|
|
_syncShadow(entity, movement.CellId);
|
|
}
|
|
}
|