Move fly/chase publication, combat target tracking, and local player projection behind typed runtime owners. Preserve the inbound-created projection/reconcile barrier while removing GameWindow callbacks and duplicate shadow helpers.
120 lines
4.5 KiB
C#
120 lines
4.5 KiB
C#
using AcDream.Core.World;
|
|
using AcDream.App.Physics;
|
|
using AcDream.App.World;
|
|
|
|
namespace AcDream.App.Input;
|
|
|
|
internal interface ILocalPlayerProjectionRuntime
|
|
{
|
|
WorldEntity? ResolveEntity();
|
|
int LiveCenterX { get; }
|
|
int LiveCenterY { get; }
|
|
void SyncShadow(WorldEntity entity, uint cellId);
|
|
void Rebucket(uint serverGuid, uint landblockId);
|
|
bool IsCurrentVisibleProjection(WorldEntity entity);
|
|
void SuspendShadow(WorldEntity entity);
|
|
}
|
|
|
|
internal sealed class LiveLocalPlayerProjectionRuntime
|
|
: ILocalPlayerProjectionRuntime
|
|
{
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly ILocalPlayerIdentitySource _identity;
|
|
private readonly LiveWorldOriginState _origin;
|
|
private readonly LocalPlayerShadowSynchronizer _shadow;
|
|
|
|
public LiveLocalPlayerProjectionRuntime(
|
|
LiveEntityRuntime liveEntities,
|
|
ILocalPlayerIdentitySource identity,
|
|
LiveWorldOriginState origin,
|
|
LocalPlayerShadowSynchronizer shadow)
|
|
{
|
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
|
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
|
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
|
}
|
|
|
|
public WorldEntity? ResolveEntity() =>
|
|
_liveEntities.TryGetWorldEntity(_identity.ServerGuid, out var entity)
|
|
? entity
|
|
: null;
|
|
|
|
public int LiveCenterX => _origin.CenterX;
|
|
public int LiveCenterY => _origin.CenterY;
|
|
|
|
public void SyncShadow(WorldEntity entity, uint cellId) =>
|
|
_shadow.Sync(entity, cellId);
|
|
|
|
public void Rebucket(uint serverGuid, uint landblockId) =>
|
|
_liveEntities.RebucketLiveEntity(serverGuid, landblockId);
|
|
|
|
public bool IsCurrentVisibleProjection(WorldEntity entity) =>
|
|
_shadow.IsCurrentVisibleProjection(entity);
|
|
|
|
public void SuspendShadow(WorldEntity entity) => _shadow.Suspend(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Projects the canonical local physics body into world rendering and spatial
|
|
/// buckets without advancing it.
|
|
/// </summary>
|
|
public sealed class LocalPlayerProjectionController
|
|
{
|
|
private readonly ILocalPlayerProjectionRuntime _runtime;
|
|
|
|
internal LocalPlayerProjectionController(ILocalPlayerProjectionRuntime runtime) =>
|
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
|
|
|
public void Project(
|
|
PlayerMovementController controller,
|
|
MovementResult movement,
|
|
bool hidden)
|
|
{
|
|
WorldEntity? entity = _runtime.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 = _runtime.LiveCenterX + (int)Math.Floor(position.X / 192f);
|
|
int landblockY = _runtime.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.
|
|
_runtime.Rebucket(entity.ServerGuid, currentLandblock);
|
|
if (hidden
|
|
|| movement.CellId == 0
|
|
|| !_runtime.IsCurrentVisibleProjection(entity))
|
|
{
|
|
_runtime.SuspendShadow(entity);
|
|
return;
|
|
}
|
|
|
|
_runtime.SyncShadow(entity, movement.CellId);
|
|
}
|
|
}
|