Preserve prepublication local motion completion, require the PartArray enter-world lifecycle port, and balance deferred Use busy ownership across dispatch and cancellation. Reconcile the completed GameWindow connected gates and add regression coverage. Co-authored-by: Codex <codex@openai.com>
184 lines
5.3 KiB
C#
184 lines
5.3 KiB
C#
using AcDream.Core.Physics;
|
|
using AcDream.App.Physics;
|
|
|
|
namespace AcDream.App.Input;
|
|
|
|
internal interface ILocalPlayerIdentitySource
|
|
{
|
|
uint ServerGuid { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session-scoped identity slot shared by focused update owners.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerIdentityState : ILocalPlayerIdentitySource
|
|
{
|
|
public uint ServerGuid { get; set; }
|
|
}
|
|
|
|
internal interface ILocalPlayerControllerSource
|
|
{
|
|
PlayerMovementController? Controller { get; }
|
|
}
|
|
|
|
internal interface ILocalPlayerMotionSource
|
|
{
|
|
MotionInterpreter? Motion { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The one mutable local movement-controller slot. Player-mode lifecycle owns
|
|
/// assignment; update and presentation owners receive the read-only seam.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerControllerSlot
|
|
: ILocalPlayerControllerSource,
|
|
ILocalPlayerMotionSource
|
|
{
|
|
private PlayerMovementController? _preparingMotionOwner;
|
|
|
|
public PlayerMovementController? Controller { get; set; }
|
|
|
|
/// <summary>
|
|
/// The motion owner visible to the local PartArray completion relay.
|
|
/// During player-mode construction this is the fully animation-bound
|
|
/// candidate controller; all other consumers continue to see only the
|
|
/// committed <see cref="Controller"/>.
|
|
/// </summary>
|
|
MotionInterpreter? ILocalPlayerMotionSource.Motion =>
|
|
_preparingMotionOwner?.Motion ?? Controller?.Motion;
|
|
|
|
/// <summary>
|
|
/// Opens the narrow construction-time ownership seam required by retail's
|
|
/// CPhysicsObj/PartArray lifetime. Initial placement synchronously queues
|
|
/// and completes StopCompletely, so its MotionDone relay must reach the
|
|
/// candidate interpreter before the complete controller is published.
|
|
/// </summary>
|
|
public IDisposable BeginMotionPreparation(PlayerMovementController controller)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(controller);
|
|
if (_preparingMotionOwner is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A local player motion owner is already being prepared.");
|
|
}
|
|
|
|
_preparingMotionOwner = controller;
|
|
return new MotionPreparation(this, controller);
|
|
}
|
|
|
|
private void EndMotionPreparation(PlayerMovementController controller)
|
|
{
|
|
if (!ReferenceEquals(_preparingMotionOwner, controller))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The local player motion preparation owner changed unexpectedly.");
|
|
}
|
|
|
|
_preparingMotionOwner = null;
|
|
}
|
|
|
|
private sealed class MotionPreparation(
|
|
LocalPlayerControllerSlot owner,
|
|
PlayerMovementController controller) : IDisposable
|
|
{
|
|
private LocalPlayerControllerSlot? _owner = owner;
|
|
|
|
public void Dispose()
|
|
{
|
|
LocalPlayerControllerSlot? current = Interlocked.Exchange(ref _owner, null);
|
|
current?.EndMotionPreparation(controller);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal interface ILocalPlayerPhysicsHostSource
|
|
{
|
|
EntityPhysicsHost? Host { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The one mutable local physics-host slot. Player-mode lifecycle owns
|
|
/// assignment; teleport and update owners receive the read-only seam.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerPhysicsHostSlot : ILocalPlayerPhysicsHostSource
|
|
{
|
|
public EntityPhysicsHost? Host { get; set; }
|
|
}
|
|
|
|
internal interface ILocalPlayerModeSource
|
|
{
|
|
bool IsPlayerMode { get; }
|
|
bool ChaseModeEverEntered { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The one mutable local presentation-mode slot. Checkpoint F transfers write
|
|
/// ownership to PlayerModeController; streaming and camera owners only read it.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerModeState : ILocalPlayerModeSource
|
|
{
|
|
public bool IsPlayerMode { get; set; }
|
|
public bool ChaseModeEverEntered { get; set; }
|
|
|
|
public void ResetSession()
|
|
{
|
|
IsPlayerMode = false;
|
|
ChaseModeEverEntered = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session-scoped server-authoritative movement skills. Player-description
|
|
/// delivery and player-mode construction share this owner so rebuilding the
|
|
/// local physics controller cannot fall back to stale defaults.
|
|
/// </summary>
|
|
internal sealed class LocalPlayerSkillState
|
|
{
|
|
public int RunSkill { get; private set; } = -1;
|
|
public int JumpSkill { get; private set; } = -1;
|
|
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
|
|
|
public void Update(
|
|
int runSkill,
|
|
int jumpSkill,
|
|
PlayerMovementController? controller)
|
|
{
|
|
if (runSkill >= 0)
|
|
RunSkill = runSkill;
|
|
if (jumpSkill >= 0)
|
|
JumpSkill = jumpSkill;
|
|
ApplyTo(controller);
|
|
}
|
|
|
|
public bool ApplyTo(PlayerMovementController? controller)
|
|
{
|
|
if (controller is null || !IsComplete)
|
|
return false;
|
|
|
|
controller.SetCharacterSkills(RunSkill, JumpSkill);
|
|
return true;
|
|
}
|
|
|
|
public void ResetSession()
|
|
{
|
|
RunSkill = -1;
|
|
JumpSkill = -1;
|
|
}
|
|
}
|
|
|
|
internal interface IViewportAspectSource
|
|
{
|
|
float Aspect { get; }
|
|
}
|
|
|
|
/// <summary>Framebuffer aspect published by the window host.</summary>
|
|
internal sealed class ViewportAspectState : IViewportAspectSource
|
|
{
|
|
public float Aspect { get; private set; } = 16f / 9f;
|
|
|
|
public void Update(int width, int height)
|
|
{
|
|
if (width > 0 && height > 0)
|
|
Aspect = width / (float)height;
|
|
}
|
|
}
|