feat(headless): complete deterministic bot command parity
This commit is contained in:
parent
7e8acb74dd
commit
38e83640d9
37 changed files with 2805 additions and 295 deletions
|
|
@ -411,6 +411,7 @@ public sealed class PlayerMovementController
|
|||
_animationRootMotionScratch = new();
|
||||
private readonly AcDream.Core.Physics.Motion.MotionDeltaFrame
|
||||
_positionManagerDeltaScratch = new();
|
||||
private bool _externalMovementEventPending;
|
||||
|
||||
// ── R4-V5: the verbatim retail MoveToManager replaces B.6 auto-walk ──
|
||||
// The B.6 DriveServerAutoWalk overlay (synthesized turn-first phase,
|
||||
|
|
@ -856,6 +857,35 @@ public sealed class PlayerMovementController
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a command-originated retail posture through the same
|
||||
/// CPhysicsObj movement boundary used by keyboard input. The next admitted
|
||||
/// object turn emits the resulting raw movement state exactly once.
|
||||
/// </summary>
|
||||
public bool RequestPosture(uint motion)
|
||||
{
|
||||
if (motion is not (
|
||||
MotionCommand.Ready
|
||||
or MotionCommand.Crouch
|
||||
or MotionCommand.Sitting
|
||||
or MotionCommand.Sleeping))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TakeControlFromServer();
|
||||
var parameters =
|
||||
new AcDream.Core.Physics.Motion.MovementParameters();
|
||||
if (DoMotionAtPhysicsObjectBoundary(motion, parameters)
|
||||
!= WeenieError.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_externalMovementEventPending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetCharacterSkills(int runSkill, int jumpSkill)
|
||||
{
|
||||
_weenie.SetSkills(runSkill, jumpSkill);
|
||||
|
|
@ -1455,8 +1485,12 @@ public sealed class PlayerMovementController
|
|||
_motion.set_hold_run(input.Run, interrupt: false);
|
||||
}
|
||||
|
||||
bool externallyRequestedMovementEvent =
|
||||
_externalMovementEventPending;
|
||||
_externalMovementEventPending = false;
|
||||
bool motionEdgeFired = false;
|
||||
bool movementEventRequested = false;
|
||||
bool movementEventRequested =
|
||||
externallyRequestedMovementEvent;
|
||||
{
|
||||
bool userInputEdge = input.Run != _prevRunHeld
|
||||
|| input.Forward != _prevForwardHeld
|
||||
|
|
@ -1511,7 +1545,7 @@ public sealed class PlayerMovementController
|
|||
// an immediate SendMovementEvent. Mouse-origin turn updates below
|
||||
// deliberately do not: CameraSet reports them on its separate
|
||||
// half-second cadence.
|
||||
movementEventRequested = motionEdgeFired;
|
||||
movementEventRequested |= motionEdgeFired;
|
||||
|
||||
// Turn channel. Retail CameraSet::Rotate (0x00458310) feeds mouse
|
||||
// input through CommandInterpreter::MovePlayer as TurnLeft /
|
||||
|
|
@ -1937,6 +1971,14 @@ public sealed class PlayerMovementController
|
|||
outForwardCmd = MotionCommand.WalkBackward;
|
||||
outForwardSpeed = 1.0f;
|
||||
}
|
||||
else if (_motion.RawState.ForwardCommand is (
|
||||
MotionCommand.Crouch
|
||||
or MotionCommand.Sitting
|
||||
or MotionCommand.Sleeping))
|
||||
{
|
||||
outForwardCmd = _motion.RawState.ForwardCommand;
|
||||
outForwardSpeed = _motion.RawState.ForwardSpeed;
|
||||
}
|
||||
|
||||
// Source the outbound axis from the canonical input-owned raw channel.
|
||||
// CameraInstantMouseLook maps keyboard TurnLeft/TurnRight into this
|
||||
|
|
@ -1979,7 +2021,8 @@ public sealed class PlayerMovementController
|
|||
|| outTurnCmd != _prevTurnCmd
|
||||
|| !FloatsEqual(outForwardSpeed, _prevForwardSpeed)
|
||||
|| runHold != _prevRunHold
|
||||
|| motionEdgeFired;
|
||||
|| motionEdgeFired
|
||||
|| externallyRequestedMovementEvent;
|
||||
|
||||
bool mouseMovementEventDue = _mouseMovementEventPending
|
||||
|| (_mouseMovementEventCandidate
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ public sealed class RuntimeActionState : IDisposable
|
|||
public IRuntimeActionView View { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
internal event Action? CombatChanged;
|
||||
|
||||
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
|
||||
_disposed,
|
||||
_internalSubscriptionsAttached,
|
||||
|
|
@ -193,17 +195,26 @@ public sealed class RuntimeActionState : IDisposable
|
|||
private void OnSelectionChanged(SelectionTransition _) =>
|
||||
Interlocked.Increment(ref _selectionRevision);
|
||||
|
||||
private void OnCombatModeChanged(CombatMode _) =>
|
||||
private void OnCombatModeChanged(CombatMode _)
|
||||
{
|
||||
Interlocked.Increment(ref _combatRevision);
|
||||
CombatChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnHealthChanged(uint _, float __) =>
|
||||
private void OnHealthChanged(uint _, float __)
|
||||
{
|
||||
Interlocked.Increment(ref _combatRevision);
|
||||
CombatChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnInteractionChanged(InteractionModeTransition _) =>
|
||||
Interlocked.Increment(ref _interactionRevision);
|
||||
|
||||
private void OnCombatAttackChanged() =>
|
||||
private void OnCombatAttackChanged()
|
||||
{
|
||||
Interlocked.Increment(ref _combatIntentRevision);
|
||||
CombatChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnSpellCastChanged() =>
|
||||
Interlocked.Increment(ref _magicIntentRevision);
|
||||
|
|
|
|||
115
src/AcDream.Runtime/Gameplay/RuntimeHostileTargetQuery.cs
Normal file
115
src/AcDream.Runtime/Gameplay/RuntimeHostileTargetQuery.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-independent hostile-target query over the canonical Runtime
|
||||
/// directory and object table. Graphical hosts may retain their render-aware
|
||||
/// query, while direct hosts use this exact live state without constructing a
|
||||
/// second world model.
|
||||
/// </summary>
|
||||
public static class RuntimeHostileTargetQuery
|
||||
{
|
||||
public static uint? FindClosest(GameRuntime runtime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
uint playerGuid = runtime.PlayerIdentity.ServerGuid;
|
||||
if (playerGuid == 0u
|
||||
|| !runtime.EntityObjects.Entities.TryGetActive(
|
||||
playerGuid,
|
||||
out RuntimeEntityRecord playerRecord)
|
||||
|| playerRecord.Snapshot.Position is not { } playerPosition)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Vector3 playerWorld = AbsolutePosition(playerPosition);
|
||||
ClientObjectTable objects = runtime.InventoryOwner.Objects;
|
||||
ClientObject? player = objects.Get(playerGuid);
|
||||
uint? closest = null;
|
||||
float closestDistanceSquared = float.PositiveInfinity;
|
||||
foreach (RuntimeEntityRecord record
|
||||
in runtime.EntityObjects.Entities.ActiveRecords)
|
||||
{
|
||||
if (record.ServerGuid == playerGuid
|
||||
|| record.Snapshot.Position is not { } position
|
||||
|| (record.FinalPhysicsState
|
||||
& (PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.NoDraw)) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ClientObject? candidate = objects.Get(record.ServerGuid);
|
||||
if (!CombatTargetPolicy.IsHostileMonster(
|
||||
playerGuid,
|
||||
player,
|
||||
candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (runtime.ActionOwner.Combat.HasHealth(record.ServerGuid)
|
||||
&& runtime.ActionOwner.Combat.GetHealthPercent(
|
||||
record.ServerGuid) <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distanceSquared = Vector3.DistanceSquared(
|
||||
playerWorld,
|
||||
AbsolutePosition(position));
|
||||
if (distanceSquared >= closestDistanceSquared)
|
||||
continue;
|
||||
closestDistanceSquared = distanceSquared;
|
||||
closest = record.ServerGuid;
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
public static bool IsHostile(
|
||||
GameRuntime runtime,
|
||||
uint objectId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
uint playerGuid = runtime.PlayerIdentity.ServerGuid;
|
||||
if (objectId == 0u
|
||||
|| playerGuid == 0u
|
||||
|| !runtime.EntityObjects.Entities.TryGetActive(
|
||||
objectId,
|
||||
out RuntimeEntityRecord record)
|
||||
|| (record.FinalPhysicsState
|
||||
& (PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.NoDraw)) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (runtime.ActionOwner.Combat.HasHealth(objectId)
|
||||
&& runtime.ActionOwner.Combat.GetHealthPercent(objectId) <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientObjectTable objects = runtime.InventoryOwner.Objects;
|
||||
return CombatTargetPolicy.IsHostileMonster(
|
||||
playerGuid,
|
||||
objects.Get(playerGuid),
|
||||
objects.Get(objectId));
|
||||
}
|
||||
|
||||
private static Vector3 AbsolutePosition(
|
||||
CreateObject.ServerPosition position)
|
||||
{
|
||||
int landblockX =
|
||||
(int)((position.LandblockId >> 24) & 0xFFu);
|
||||
int landblockY =
|
||||
(int)((position.LandblockId >> 16) & 0xFFu);
|
||||
return new Vector3(
|
||||
position.PositionX + landblockX * 192f,
|
||||
position.PositionY + landblockY * 192f,
|
||||
position.PositionZ);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public interface IRuntimeMovementInputSource
|
||||
{
|
||||
MovementInput Capture();
|
||||
}
|
||||
|
||||
public interface IRuntimeLocalPlayerFrameHost
|
||||
{
|
||||
bool CanAdvancePlayer { get; }
|
||||
PlayerMovementController? Controller { get; }
|
||||
uint ResolveLocalEntityId();
|
||||
void HandleTargeting();
|
||||
bool IsHidden { get; }
|
||||
RetailObjectClockDisposition ObjectClockDisposition { get; }
|
||||
void Project(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden);
|
||||
void SendPreNetwork(
|
||||
PlayerMovementController controller,
|
||||
MovementResult movement,
|
||||
bool hidden);
|
||||
void SendPostNetwork(
|
||||
PlayerMovementController controller,
|
||||
bool hidden);
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeLocalPlayerPresentationFrame(
|
||||
MovementResult Movement,
|
||||
bool Hidden,
|
||||
bool AdvancedBeforeNetwork);
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-independent owner of retail's local-player object phase,
|
||||
/// inbound-network barrier, and post-network position phase. Graphical and
|
||||
/// direct hosts supply projections and input but execute this exact ordering.
|
||||
/// </summary>
|
||||
public sealed class RuntimeLocalPlayerFrameController
|
||||
{
|
||||
private readonly IRuntimeLocalPlayerFrameHost _host;
|
||||
private readonly IRuntimeMovementInputSource _input;
|
||||
private readonly Action? _publishMovement;
|
||||
private AdvancedFrame? _advancedFrame;
|
||||
|
||||
private readonly record struct AdvancedFrame(
|
||||
PlayerMovementController Controller,
|
||||
MovementResult Movement,
|
||||
bool ObjectAdvanced,
|
||||
bool Hidden,
|
||||
bool ObjectQuantumAdvanced);
|
||||
|
||||
public RuntimeLocalPlayerFrameController(
|
||||
IRuntimeLocalPlayerFrameHost host,
|
||||
IRuntimeMovementInputSource input,
|
||||
Action? publishMovement = null)
|
||||
{
|
||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||
_publishMovement = publishMovement;
|
||||
}
|
||||
|
||||
public bool HiddenPartPoseDirty => _advancedFrame is
|
||||
{
|
||||
Hidden: true,
|
||||
ObjectQuantumAdvanced: true,
|
||||
};
|
||||
|
||||
public void AdvanceBeforeNetwork(float deltaSeconds)
|
||||
{
|
||||
_advancedFrame = null;
|
||||
PlayerMovementController? controller = _host.Controller;
|
||||
if (!_host.CanAdvancePlayer || controller is null)
|
||||
return;
|
||||
|
||||
if (!float.IsFinite(deltaSeconds) || deltaSeconds <= 0f)
|
||||
{
|
||||
bool rejectedHidden = _host.IsHidden;
|
||||
MovementResult rejected =
|
||||
controller.CapturePresentationResult();
|
||||
_host.Project(controller, rejected, rejectedHidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
rejected,
|
||||
ObjectAdvanced: false,
|
||||
Hidden: rejectedHidden,
|
||||
ObjectQuantumAdvanced: false);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.LocalEntityId = _host.ResolveLocalEntityId();
|
||||
|
||||
bool hidden = _host.IsHidden;
|
||||
if (_host.ObjectClockDisposition
|
||||
is RetailObjectClockDisposition.Suspend)
|
||||
{
|
||||
controller.SuspendObjectUpdate(deltaSeconds);
|
||||
MovementResult paused =
|
||||
controller.CapturePresentationResult();
|
||||
_host.Project(controller, paused, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
paused,
|
||||
ObjectAdvanced: false,
|
||||
Hidden: hidden,
|
||||
ObjectQuantumAdvanced: false);
|
||||
return;
|
||||
}
|
||||
|
||||
MovementResult movement = hidden
|
||||
? controller.TickHidden(
|
||||
deltaSeconds,
|
||||
_host.HandleTargeting)
|
||||
: controller.Update(
|
||||
deltaSeconds,
|
||||
_input.Capture(),
|
||||
_host.HandleTargeting);
|
||||
|
||||
_host.Project(controller, movement, hidden);
|
||||
_host.SendPreNetwork(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
movement,
|
||||
ObjectAdvanced: true,
|
||||
Hidden: hidden,
|
||||
ObjectQuantumAdvanced:
|
||||
controller.AdvancedObjectQuantumLastTick);
|
||||
}
|
||||
|
||||
public void RunPostNetworkCommandPhase()
|
||||
{
|
||||
PlayerMovementController? controller = _host.Controller;
|
||||
if (!_host.CanAdvancePlayer || controller is null)
|
||||
return;
|
||||
|
||||
bool hidden = _host.IsHidden;
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
advanced.Movement,
|
||||
controller);
|
||||
_host.Project(controller, movement, hidden);
|
||||
_advancedFrame = new AdvancedFrame(
|
||||
controller,
|
||||
movement,
|
||||
advanced.ObjectAdvanced,
|
||||
advanced.Hidden,
|
||||
advanced.ObjectQuantumAdvanced);
|
||||
|
||||
if (!advanced.ObjectAdvanced)
|
||||
return;
|
||||
}
|
||||
|
||||
_host.SendPostNetwork(controller, hidden);
|
||||
_publishMovement?.Invoke();
|
||||
}
|
||||
|
||||
public bool TryGetPresentationAfterNetwork(
|
||||
out RuntimeLocalPlayerPresentationFrame frame)
|
||||
{
|
||||
frame = default;
|
||||
PlayerMovementController? controller = _host.Controller;
|
||||
if (!_host.CanAdvancePlayer || controller is null)
|
||||
return false;
|
||||
|
||||
bool hidden = _host.IsHidden;
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
advanced.Movement,
|
||||
controller);
|
||||
frame = new RuntimeLocalPlayerPresentationFrame(
|
||||
movement,
|
||||
hidden,
|
||||
AdvancedBeforeNetwork: advanced.ObjectAdvanced);
|
||||
return true;
|
||||
}
|
||||
|
||||
MovementResult initial =
|
||||
controller.CapturePresentationResult();
|
||||
_host.Project(controller, initial, hidden);
|
||||
frame = new RuntimeLocalPlayerPresentationFrame(
|
||||
initial,
|
||||
hidden,
|
||||
AdvancedBeforeNetwork: false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static MovementResult RefreshSpatialFields(
|
||||
MovementResult movement,
|
||||
PlayerMovementController controller) =>
|
||||
movement with
|
||||
{
|
||||
Position = controller.Position,
|
||||
RenderPosition = controller.RenderPosition,
|
||||
CellId = controller.CellId,
|
||||
IsOnGround = controller.CanSendPositionEvent,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
|
@ -26,6 +28,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
private PlayerMovementController? _controller;
|
||||
private PlayerMovementController? _preparingMotionOwner;
|
||||
private bool _autoRunActive;
|
||||
private bool _hasCommandInput;
|
||||
private MovementInput _commandInput;
|
||||
private bool _disposed;
|
||||
private long _revision;
|
||||
|
||||
|
|
@ -43,6 +47,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
}
|
||||
|
||||
public bool AutoRunActive => _autoRunActive;
|
||||
public bool HasCommandInput => _hasCommandInput;
|
||||
public MovementInput CommandInput => _commandInput;
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
public IRuntimeMovementView View => this;
|
||||
|
||||
|
|
@ -63,7 +69,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
false,
|
||||
0d,
|
||||
Revision,
|
||||
_autoRunActive)
|
||||
_autoRunActive,
|
||||
_hasCommandInput,
|
||||
_commandInput)
|
||||
: new RuntimeMovementSnapshot(
|
||||
true,
|
||||
controller.LocalEntityId,
|
||||
|
|
@ -72,7 +80,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
controller.IsAirborne,
|
||||
controller.SimTimeSeconds,
|
||||
Revision,
|
||||
_autoRunActive);
|
||||
_autoRunActive,
|
||||
_hasCommandInput,
|
||||
_commandInput);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +119,27 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
return true;
|
||||
case RuntimeMovementCommand.Stop:
|
||||
CancelAutoRun();
|
||||
ClearCommandInput();
|
||||
return true;
|
||||
case RuntimeMovementCommand.Ready:
|
||||
case RuntimeMovementCommand.Sit:
|
||||
case RuntimeMovementCommand.Crouch:
|
||||
case RuntimeMovementCommand.Sleep:
|
||||
CancelAutoRun();
|
||||
ClearCommandInput();
|
||||
uint motion = command switch
|
||||
{
|
||||
RuntimeMovementCommand.Ready =>
|
||||
MotionCommand.Ready,
|
||||
RuntimeMovementCommand.Sit =>
|
||||
MotionCommand.Sitting,
|
||||
RuntimeMovementCommand.Crouch =>
|
||||
MotionCommand.Crouch,
|
||||
RuntimeMovementCommand.Sleep =>
|
||||
MotionCommand.Sleeping,
|
||||
_ => throw new UnreachableException(),
|
||||
};
|
||||
return _controller?.RequestPosture(motion) == true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
@ -125,12 +155,75 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the persistent semantic movement level sampled by either host.
|
||||
/// The graphical input adapter and direct host both consume this exact
|
||||
/// owner; no bot-only movement latch exists.
|
||||
/// </summary>
|
||||
public void SetCommandInput(in MovementInput input)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_hasCommandInput && _commandInput == input)
|
||||
return;
|
||||
_commandInput = input;
|
||||
_hasCommandInput = true;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public bool ClearCommandInput()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_hasCommandInput)
|
||||
return false;
|
||||
_commandInput = default;
|
||||
_hasCommandInput = false;
|
||||
Interlocked.Increment(ref _revision);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct-host projection of the same combat readiness query used by the
|
||||
/// graphical attack adapter. A host without a constructed local movement
|
||||
/// owner is not ready; Slice K's content host must hydrate that owner
|
||||
/// before combat packets can leave.
|
||||
/// </summary>
|
||||
public bool IsReadyForAttack(CombatMode mode)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_controller is not { } controller)
|
||||
return false;
|
||||
var motion = controller.Motion.InterpretedState;
|
||||
return CombatInputPlanner.PlayerInReadyPositionForAttack(
|
||||
mode,
|
||||
motion.CurrentStyle,
|
||||
motion.ForwardCommand);
|
||||
}
|
||||
|
||||
public bool IsDualWield
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _controller?.Motion.InterpretedState.CurrentStyle
|
||||
== CombatInputPlanner.DualWieldCombatStyle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool PrepareForAttackRequest()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
CancelAutoRun();
|
||||
return _controller?.PrepareForAttackRequest() == true;
|
||||
}
|
||||
|
||||
public void ResetInputIntent()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_autoRunActive)
|
||||
if (!_autoRunActive && !_hasCommandInput)
|
||||
return;
|
||||
_autoRunActive = false;
|
||||
_hasCommandInput = false;
|
||||
_commandInput = default;
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
|
|
@ -145,9 +238,12 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
bool changed =
|
||||
_autoRunActive
|
||||
|| _hasCommandInput
|
||||
|| _controller is not null
|
||||
|| _preparingMotionOwner is not null;
|
||||
_autoRunActive = false;
|
||||
_hasCommandInput = false;
|
||||
_commandInput = default;
|
||||
_controller = null;
|
||||
_preparingMotionOwner = null;
|
||||
if (changed)
|
||||
|
|
@ -160,6 +256,7 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
_controller is not null,
|
||||
_preparingMotionOwner is not null,
|
||||
_autoRunActive,
|
||||
_hasCommandInput,
|
||||
Revision);
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -167,6 +264,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
|||
if (_disposed)
|
||||
return;
|
||||
_autoRunActive = false;
|
||||
_hasCommandInput = false;
|
||||
_commandInput = default;
|
||||
_controller = null;
|
||||
_preparingMotionOwner = null;
|
||||
Interlocked.Increment(ref _revision);
|
||||
|
|
@ -210,11 +309,13 @@ public readonly record struct RuntimeLocalMovementOwnershipSnapshot(
|
|||
bool HasController,
|
||||
bool HasPreparingMotionOwner,
|
||||
bool AutoRunActive,
|
||||
bool HasCommandInput,
|
||||
long Revision)
|
||||
{
|
||||
public bool IsConverged =>
|
||||
IsDisposed
|
||||
&& !HasController
|
||||
&& !HasPreparingMotionOwner
|
||||
&& !AutoRunActive;
|
||||
&& !AutoRunActive
|
||||
&& !HasCommandInput;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue