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
|
|
@ -686,6 +686,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.PlayerOutbound,
|
d.PlayerOutbound,
|
||||||
liveSessionSource);
|
liveSessionSource);
|
||||||
var localPlayerFrame = new RetailLocalPlayerFrameController(
|
var localPlayerFrame = new RetailLocalPlayerFrameController(
|
||||||
|
d.Runtime,
|
||||||
localPlayerFrameRuntime,
|
localPlayerFrameRuntime,
|
||||||
d.MovementInput);
|
d.MovementInput);
|
||||||
var liveObjectFrame = new LiveObjectFrameController(
|
var liveObjectFrame = new LiveObjectFrameController(
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ using AcDream.UI.Abstractions.Input;
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
internal interface IMovementInputSource
|
internal interface IMovementInputSource
|
||||||
|
: IRuntimeMovementInputSource
|
||||||
{
|
{
|
||||||
MovementInput Capture();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -53,6 +53,9 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
||||||
if (_capture?.DevToolsWantCaptureKeyboard == true)
|
if (_capture?.DevToolsWantCaptureKeyboard == true)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
if (_movement.HasCommandInput)
|
||||||
|
return _movement.CommandInput;
|
||||||
|
|
||||||
if (_dispatcher is not { } dispatcher)
|
if (_dispatcher is not { } dispatcher)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,12 @@ internal interface ILocalPlayerPresentationRuntime
|
||||||
PlayerMovementController? Controller { get; }
|
PlayerMovementController? Controller { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface ILocalPlayerFrameRuntime : ILocalPlayerPresentationRuntime
|
internal interface ILocalPlayerFrameRuntime :
|
||||||
|
ILocalPlayerPresentationRuntime,
|
||||||
|
IRuntimeLocalPlayerFrameHost
|
||||||
{
|
{
|
||||||
uint ResolveLocalEntityId();
|
bool IRuntimeLocalPlayerFrameHost.CanAdvancePlayer =>
|
||||||
void HandleTargeting();
|
CanPresentPlayer;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
|
using AcDream.Runtime;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
|
|
@ -14,17 +15,7 @@ namespace AcDream.App.Input;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase
|
internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase
|
||||||
{
|
{
|
||||||
private readonly ILocalPlayerFrameRuntime _runtime;
|
private readonly RuntimeLocalPlayerFrameController _runtime;
|
||||||
private readonly IMovementInputSource _movementInput;
|
|
||||||
|
|
||||||
private AdvancedFrame? _advancedFrame;
|
|
||||||
|
|
||||||
private readonly record struct AdvancedFrame(
|
|
||||||
PlayerMovementController Controller,
|
|
||||||
MovementResult Movement,
|
|
||||||
bool ObjectAdvanced,
|
|
||||||
bool Hidden,
|
|
||||||
bool ObjectQuantumAdvanced);
|
|
||||||
|
|
||||||
public readonly record struct PresentationFrame(
|
public readonly record struct PresentationFrame(
|
||||||
MovementResult Movement,
|
MovementResult Movement,
|
||||||
|
|
@ -35,19 +26,29 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
||||||
/// Whether the pre-network Hidden player update admitted a complete object
|
/// Whether the pre-network Hidden player update admitted a complete object
|
||||||
/// quantum whose retained CPartArray pose must be sampled this frame.
|
/// quantum whose retained CPartArray pose must be sampled this frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool HiddenPartPoseDirty => _advancedFrame is
|
internal bool HiddenPartPoseDirty =>
|
||||||
{
|
_runtime.HiddenPartPoseDirty;
|
||||||
Hidden: true,
|
|
||||||
ObjectQuantumAdvanced: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
public RetailLocalPlayerFrameController(
|
public RetailLocalPlayerFrameController(
|
||||||
ILocalPlayerFrameRuntime runtime,
|
ILocalPlayerFrameRuntime runtime,
|
||||||
IMovementInputSource movementInput)
|
IMovementInputSource movementInput)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = new RuntimeLocalPlayerFrameController(
|
||||||
_movementInput = movementInput
|
runtime ?? throw new ArgumentNullException(nameof(runtime)),
|
||||||
?? throw new ArgumentNullException(nameof(movementInput));
|
movementInput
|
||||||
|
?? throw new ArgumentNullException(nameof(movementInput)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RetailLocalPlayerFrameController(
|
||||||
|
GameRuntime gameRuntime,
|
||||||
|
ILocalPlayerFrameRuntime runtime,
|
||||||
|
IMovementInputSource movementInput)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gameRuntime);
|
||||||
|
_runtime = gameRuntime.CreateLocalPlayerFrameController(
|
||||||
|
runtime ?? throw new ArgumentNullException(nameof(runtime)),
|
||||||
|
movementInput
|
||||||
|
?? throw new ArgumentNullException(nameof(movementInput)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -55,64 +56,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
||||||
/// the inbound-network barrier.
|
/// the inbound-network barrier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AdvanceBeforeNetwork(float deltaSeconds)
|
public void AdvanceBeforeNetwork(float deltaSeconds)
|
||||||
{
|
=> _runtime.AdvanceBeforeNetwork(deltaSeconds);
|
||||||
_advancedFrame = null;
|
|
||||||
PlayerMovementController? controller = _runtime.Controller;
|
|
||||||
if (!_runtime.CanPresentPlayer || controller is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!float.IsFinite(deltaSeconds) || deltaSeconds <= 0f)
|
|
||||||
{
|
|
||||||
bool rejectedHidden = _runtime.IsHidden;
|
|
||||||
MovementResult rejected = controller.CapturePresentationResult();
|
|
||||||
_runtime.Project(controller, rejected, rejectedHidden);
|
|
||||||
_advancedFrame = new AdvancedFrame(
|
|
||||||
controller,
|
|
||||||
rejected,
|
|
||||||
ObjectAdvanced: false,
|
|
||||||
Hidden: rejectedHidden,
|
|
||||||
ObjectQuantumAdvanced: false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.LocalEntityId = _runtime.ResolveLocalEntityId();
|
|
||||||
|
|
||||||
bool hidden = _runtime.IsHidden;
|
|
||||||
if (_runtime.ObjectClockDisposition
|
|
||||||
is AcDream.Core.Physics.RetailObjectClockDisposition.Suspend)
|
|
||||||
{
|
|
||||||
// CPhysicsObj::update_object returns before advancing time for a
|
|
||||||
// Frozen or cell-less object. Keep the retained projection live,
|
|
||||||
// but emit neither input actions nor AutonomousPosition from a
|
|
||||||
// physics tick that retail did not run.
|
|
||||||
controller.SuspendObjectUpdate(deltaSeconds);
|
|
||||||
MovementResult paused = controller.CapturePresentationResult();
|
|
||||||
_runtime.Project(controller, paused, hidden);
|
|
||||||
_advancedFrame = new AdvancedFrame(
|
|
||||||
controller,
|
|
||||||
paused,
|
|
||||||
ObjectAdvanced: false,
|
|
||||||
Hidden: hidden,
|
|
||||||
ObjectQuantumAdvanced: false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MovementResult movement = hidden
|
|
||||||
? controller.TickHidden(deltaSeconds, _runtime.HandleTargeting)
|
|
||||||
: controller.Update(
|
|
||||||
deltaSeconds,
|
|
||||||
_movementInput.Capture(),
|
|
||||||
_runtime.HandleTargeting);
|
|
||||||
|
|
||||||
_runtime.Project(controller, movement, hidden);
|
|
||||||
_runtime.SendPreNetwork(controller, movement, hidden);
|
|
||||||
_advancedFrame = new AdvancedFrame(
|
|
||||||
controller,
|
|
||||||
movement,
|
|
||||||
ObjectAdvanced: true,
|
|
||||||
Hidden: hidden,
|
|
||||||
ObjectQuantumAdvanced: controller.AdvancedObjectQuantumLastTick);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs retail's post-inbound command-interpreter phase. The split
|
/// Runs retail's post-inbound command-interpreter phase. The split
|
||||||
|
|
@ -120,32 +64,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
||||||
/// then AutonomousPosition is evaluated from that current state.
|
/// then AutonomousPosition is evaluated from that current state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RunPostNetworkCommandPhase()
|
public void RunPostNetworkCommandPhase()
|
||||||
{
|
=> _runtime.RunPostNetworkCommandPhase();
|
||||||
PlayerMovementController? controller = _runtime.Controller;
|
|
||||||
if (!_runtime.CanPresentPlayer || controller is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool hidden = _runtime.IsHidden;
|
|
||||||
if (_advancedFrame is { } advanced
|
|
||||||
&& ReferenceEquals(advanced.Controller, controller))
|
|
||||||
{
|
|
||||||
MovementResult movement = RefreshSpatialFields(
|
|
||||||
advanced.Movement,
|
|
||||||
controller);
|
|
||||||
_runtime.Project(controller, movement, hidden);
|
|
||||||
_advancedFrame = new AdvancedFrame(
|
|
||||||
controller,
|
|
||||||
movement,
|
|
||||||
advanced.ObjectAdvanced,
|
|
||||||
advanced.Hidden,
|
|
||||||
advanced.ObjectQuantumAdvanced);
|
|
||||||
|
|
||||||
if (!advanced.ObjectAdvanced)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_runtime.SendPostNetwork(controller, hidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Produces the post-network presentation sample without advancing
|
/// Produces the post-network presentation sample without advancing
|
||||||
|
|
@ -154,39 +73,15 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryGetPresentationAfterNetwork(out PresentationFrame frame)
|
public bool TryGetPresentationAfterNetwork(out PresentationFrame frame)
|
||||||
{
|
{
|
||||||
frame = default;
|
bool available = _runtime.TryGetPresentationAfterNetwork(
|
||||||
PlayerMovementController? controller = _runtime.Controller;
|
out AcDream.Runtime.Gameplay.RuntimeLocalPlayerPresentationFrame
|
||||||
if (!_runtime.CanPresentPlayer || controller is null)
|
shared);
|
||||||
return false;
|
frame = available
|
||||||
|
? new PresentationFrame(
|
||||||
bool hidden = _runtime.IsHidden;
|
shared.Movement,
|
||||||
if (_advancedFrame is { } advanced
|
shared.Hidden,
|
||||||
&& ReferenceEquals(advanced.Controller, controller))
|
shared.AdvancedBeforeNetwork)
|
||||||
{
|
: default;
|
||||||
MovementResult movement = RefreshSpatialFields(
|
return available;
|
||||||
advanced.Movement,
|
|
||||||
controller);
|
|
||||||
frame = new PresentationFrame(
|
|
||||||
movement,
|
|
||||||
hidden,
|
|
||||||
AdvancedBeforeNetwork: advanced.ObjectAdvanced);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
MovementResult initial = controller.CapturePresentationResult();
|
|
||||||
_runtime.Project(controller, initial, hidden);
|
|
||||||
frame = new PresentationFrame(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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
|
@ -164,6 +165,51 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
return Result(status, selected);
|
return Result(status, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult SelectObject(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint objectId)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate = Validate(
|
||||||
|
expectedGeneration,
|
||||||
|
requireWorld: true);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
objectId != 0u
|
||||||
|
&& _inventory.Objects.Get(objectId) is not null
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
if (status == RuntimeCommandStatus.Accepted)
|
||||||
|
{
|
||||||
|
_actions.Selection.Select(
|
||||||
|
objectId,
|
||||||
|
SelectionChangeSource.Plugin);
|
||||||
|
}
|
||||||
|
_events.EmitCommand(
|
||||||
|
RuntimeCommandDomain.Selection,
|
||||||
|
operation: 0x100,
|
||||||
|
status,
|
||||||
|
objectId);
|
||||||
|
return Result(status, objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult Clear(
|
||||||
|
RuntimeGenerationToken expectedGeneration)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate = Validate(
|
||||||
|
expectedGeneration,
|
||||||
|
requireWorld: true);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_actions.Selection.Clear(SelectionChangeSource.Plugin);
|
||||||
|
_events.EmitCommand(
|
||||||
|
RuntimeCommandDomain.Selection,
|
||||||
|
operation: 0x101,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
return Result(RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeCombatCommand command)
|
RuntimeCombatCommand command)
|
||||||
|
|
@ -254,6 +300,39 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
return Result(status);
|
return Result(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult SetIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in MovementInput input)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate = Validate(
|
||||||
|
expectedGeneration,
|
||||||
|
requireWorld: true);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_movement.SetCommandInput(input);
|
||||||
|
_events.EmitCommand(
|
||||||
|
RuntimeCommandDomain.Movement,
|
||||||
|
operation: 0x100,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
return Result(RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult ClearIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate = Validate(
|
||||||
|
expectedGeneration,
|
||||||
|
requireWorld: true);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_movement.ClearCommandInput();
|
||||||
|
_events.EmitCommand(
|
||||||
|
RuntimeCommandDomain.Movement,
|
||||||
|
operation: 0x101,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
return Result(RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
in RuntimeChatCommand command)
|
in RuntimeChatCommand command)
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,6 @@
|
||||||
<InternalsVisibleTo Include="AcDream.Core.Net.Tests" />
|
<InternalsVisibleTo Include="AcDream.Core.Net.Tests" />
|
||||||
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
<InternalsVisibleTo Include="AcDream.App.Tests" />
|
||||||
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
<InternalsVisibleTo Include="AcDream.Runtime.Tests" />
|
||||||
|
<InternalsVisibleTo Include="AcDream.Headless.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
|
||||||
|
|
||||||
namespace AcDream.App.Spells;
|
namespace AcDream.Core.Spells;
|
||||||
|
|
||||||
public readonly record struct SpellTargetPolicyResult(bool Allowed, string? Message)
|
public readonly record struct SpellTargetPolicyResult(
|
||||||
|
bool Allowed,
|
||||||
|
string? Message)
|
||||||
{
|
{
|
||||||
public static SpellTargetPolicyResult Accept { get; } = new(true, null);
|
public static SpellTargetPolicyResult Accept { get; } =
|
||||||
|
new(true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure projection of ClientMagicSystem::ObjectCompatibleWithSpellTargetType
|
/// Pure projection of
|
||||||
|
/// <c>ClientMagicSystem::ObjectCompatibleWithSpellTargetType</c>
|
||||||
/// (0x00567230). The caller owns target lookup and message presentation.
|
/// (0x00567230). The caller owns target lookup and message presentation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RetailSpellTargetPolicy
|
public static class RetailSpellTargetPolicy
|
||||||
|
|
@ -34,9 +37,11 @@ public static class RetailSpellTargetPolicy
|
||||||
// Retail joins the ordinary type match and the 0x8107 special-mask
|
// Retail joins the ordinary type match and the 0x8107 special-mask
|
||||||
// bypass before these two gates. IsPlayer is PWD bit 3; otherwise the
|
// bypass before these two gates. IsPlayer is PWD bit 3; otherwise the
|
||||||
// object must carry BF_ATTACKABLE. Pets are never legal spell targets.
|
// object must carry BF_ATTACKABLE. Pets are never legal spell targets.
|
||||||
var flags = (PublicWeenieFlags)target.PublicWeenieBitfield.GetValueOrDefault();
|
var flags = (PublicWeenieFlags)
|
||||||
|
target.PublicWeenieBitfield.GetValueOrDefault();
|
||||||
bool playerOrAttackable = (flags
|
bool playerOrAttackable = (flags
|
||||||
& (PublicWeenieFlags.Player | PublicWeenieFlags.Attackable)) != 0;
|
& (PublicWeenieFlags.Player
|
||||||
|
| PublicWeenieFlags.Attackable)) != 0;
|
||||||
if (!playerOrAttackable || target.PetOwnerId != 0u)
|
if (!playerOrAttackable || target.PetOwnerId != 0u)
|
||||||
return new(false, $"This spell cannot be cast on {target.Name}.");
|
return new(false, $"This spell cannot be cast on {target.Name}.");
|
||||||
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
namespace AcDream.Headless.Hosting;
|
namespace AcDream.Headless.Hosting;
|
||||||
|
|
||||||
|
|
@ -17,64 +19,251 @@ internal sealed class HeadlessGameplayOperations
|
||||||
IRuntimeCombatModeOperations,
|
IRuntimeCombatModeOperations,
|
||||||
IRuntimeSpellCastOperations
|
IRuntimeSpellCastOperations
|
||||||
{
|
{
|
||||||
|
private sealed class SessionRoute(
|
||||||
|
HeadlessGameplayOperations owner,
|
||||||
|
WorldSession session)
|
||||||
|
: ILiveSessionCommandRouting
|
||||||
|
{
|
||||||
|
private bool _active;
|
||||||
|
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
if (_active)
|
||||||
|
return;
|
||||||
|
owner.Activate(session, this);
|
||||||
|
_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
owner.Deactivate(this);
|
||||||
|
_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _gate = new();
|
||||||
private GameRuntime? _runtime;
|
private GameRuntime? _runtime;
|
||||||
|
private WorldSession? _session;
|
||||||
|
private SessionRoute? _route;
|
||||||
|
|
||||||
internal void Bind(GameRuntime runtime) =>
|
internal void Bind(GameRuntime runtime) =>
|
||||||
_runtime = runtime
|
_runtime = runtime
|
||||||
?? throw new ArgumentNullException(nameof(runtime));
|
?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
|
||||||
public bool CanStartAttack() => false;
|
internal ILiveSessionCommandRouting CreateRoute(
|
||||||
|
WorldSession session)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(session);
|
||||||
|
return new SessionRoute(this, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanStartAttack()
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
if (!IsInWorld
|
||||||
|
|| !CombatInputPlanner.SupportsTargetedAttack(
|
||||||
|
runtime.ActionOwner.Combat.CurrentMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return GetSelectedOrClosestTarget(runtime) is not null;
|
||||||
|
}
|
||||||
|
|
||||||
public void PrepareAttackRequest()
|
public void PrepareAttackRequest()
|
||||||
{
|
{
|
||||||
|
_ = RequireRuntime().MovementOwner.PrepareForAttackRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendAttack(AttackHeight height, float power)
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
uint? target = GetSelectedOrClosestTarget(runtime);
|
||||||
|
if (target is null || !TryGetSession(out WorldSession? session))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
power = Math.Clamp(power, 0f, 1f);
|
||||||
|
if (runtime.ActionOwner.Combat.CurrentMode == CombatMode.Missile)
|
||||||
|
session!.SendMissileAttack(target.Value, height, power);
|
||||||
|
else
|
||||||
|
session!.SendMeleeAttack(target.Value, height, power);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SendAttack(AttackHeight height, float power) => false;
|
|
||||||
public void SendCancelAttack()
|
public void SendCancelAttack()
|
||||||
{
|
{
|
||||||
|
if (TryGetSession(out WorldSession? session))
|
||||||
|
session!.SendCancelAttack();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDualWield => false;
|
public bool IsDualWield =>
|
||||||
public bool PlayerReadyForAttack => false;
|
RequireRuntime().MovementOwner.IsDualWield;
|
||||||
|
public bool PlayerReadyForAttack
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
return runtime.MovementOwner.IsReadyForAttack(
|
||||||
|
runtime.ActionOwner.Combat.CurrentMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
public bool AutoRepeatAttack => false;
|
public bool AutoRepeatAttack => false;
|
||||||
public bool AutoTarget => false;
|
public bool AutoTarget => true;
|
||||||
public uint? SelectClosestTarget() => null;
|
|
||||||
|
public uint? SelectClosestTarget()
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
uint? closest = RuntimeHostileTargetQuery.FindClosest(runtime);
|
||||||
|
if (closest is { } target)
|
||||||
|
{
|
||||||
|
runtime.ActionOwner.Selection.Select(
|
||||||
|
target,
|
||||||
|
AcDream.Core.Selection.SelectionChangeSource.Keyboard);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runtime.ActionOwner.Selection.Clear(
|
||||||
|
AcDream.Core.Selection.SelectionChangeSource.Keyboard);
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
|
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
|
||||||
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
|
public IReadOnlyList<ClientObject> GetOrderedEquipment()
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
return runtime.InventoryOwner.Objects.GetEquippedBy(
|
||||||
|
runtime.PlayerIdentity.ServerGuid);
|
||||||
|
}
|
||||||
|
|
||||||
public void NotifyExplicitCombatModeRequest()
|
public void NotifyExplicitCombatModeRequest()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendChangeCombatMode(CombatMode mode)
|
public void SendChangeCombatMode(CombatMode mode)
|
||||||
{
|
{
|
||||||
|
if (TryGetSession(out WorldSession? session))
|
||||||
|
session!.SendChangeCombatMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint LocalPlayerId =>
|
public uint LocalPlayerId =>
|
||||||
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
|
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
|
||||||
public bool CanSend => false;
|
public bool CanSend => TryGetSession(out _);
|
||||||
public bool HasRequiredComponents(uint spellId) => false;
|
|
||||||
|
public bool HasRequiredComponents(uint spellId)
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
ClientObject? player = runtime.InventoryOwner.Objects.Get(
|
||||||
|
runtime.PlayerIdentity.ServerGuid);
|
||||||
|
return player is not null
|
||||||
|
&& !player.Properties.GetBool(
|
||||||
|
SpellComponentsRequiredProperty,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsTargetCompatible(
|
public bool IsTargetCompatible(
|
||||||
uint targetId,
|
uint targetId,
|
||||||
SpellMetadata spell,
|
SpellMetadata spell,
|
||||||
bool showMessage) => false;
|
bool showMessage)
|
||||||
|
{
|
||||||
|
GameRuntime runtime = RequireRuntime();
|
||||||
|
ClientObject? target =
|
||||||
|
runtime.InventoryOwner.Objects.Get(targetId);
|
||||||
|
if (target is null)
|
||||||
|
return false;
|
||||||
|
SpellTargetPolicyResult result =
|
||||||
|
RetailSpellTargetPolicy.Evaluate(
|
||||||
|
runtime.PlayerIdentity.ServerGuid,
|
||||||
|
target,
|
||||||
|
spell);
|
||||||
|
if (showMessage
|
||||||
|
&& !result.Allowed
|
||||||
|
&& result.Message is { } message)
|
||||||
|
{
|
||||||
|
DisplayMessage(message);
|
||||||
|
}
|
||||||
|
return result.Allowed;
|
||||||
|
}
|
||||||
|
|
||||||
public void StopCompletely()
|
public void StopCompletely()
|
||||||
{
|
{
|
||||||
|
_ = RequireRuntime().MovementOwner.PrepareForAttackRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendUntargeted(uint spellId)
|
public void SendUntargeted(uint spellId)
|
||||||
{
|
{
|
||||||
|
if (TryGetSession(out WorldSession? session))
|
||||||
|
session!.SendCastUntargetedSpell(spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SendTargeted(uint targetId, uint spellId)
|
public void SendTargeted(uint targetId, uint spellId)
|
||||||
{
|
{
|
||||||
|
if (TryGetSession(out WorldSession? session))
|
||||||
|
session!.SendCastTargetedSpell(targetId, spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisplayMessage(string message)
|
public void DisplayMessage(string message) =>
|
||||||
|
RequireRuntime().CommunicationOwner.Chat.OnSystemMessage(
|
||||||
|
message,
|
||||||
|
chatType: 0u);
|
||||||
|
|
||||||
|
public void IncrementBusy() =>
|
||||||
|
RequireRuntime().ActionOwner.Transactions
|
||||||
|
.IncrementBusyCount();
|
||||||
|
|
||||||
|
private uint? GetSelectedOrClosestTarget(GameRuntime runtime)
|
||||||
{
|
{
|
||||||
|
uint? selected =
|
||||||
|
runtime.ActionOwner.Selection.SelectedObjectId;
|
||||||
|
if (selected is { } target
|
||||||
|
&& RuntimeHostileTargetQuery.IsHostile(runtime, target))
|
||||||
|
{
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
return AutoTarget ? SelectClosestTarget() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void IncrementBusy()
|
private GameRuntime RequireRuntime() =>
|
||||||
|
_runtime
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Headless gameplay operations are not bound.");
|
||||||
|
|
||||||
|
private bool TryGetSession(out WorldSession? session)
|
||||||
{
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
session = _session;
|
||||||
|
return _route is not null
|
||||||
|
&& session is not null
|
||||||
|
&& IsInWorld;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Activate(
|
||||||
|
WorldSession session,
|
||||||
|
SessionRoute route)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_route is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Headless gameplay operations already have an active session.");
|
||||||
|
}
|
||||||
|
_session = session;
|
||||||
|
_route = route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Deactivate(SessionRoute route)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(_route, route))
|
||||||
|
return;
|
||||||
|
_session = null;
|
||||||
|
_route = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const uint SpellComponentsRequiredProperty = 68u;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
src/AcDream.Headless/Hosting/HeadlessLocalPlayerFrameHost.cs
Normal file
124
src/AcDream.Headless/Hosting/HeadlessLocalPlayerFrameHost.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
|
namespace AcDream.Headless.Hosting;
|
||||||
|
|
||||||
|
internal sealed class HeadlessMovementInputSource(
|
||||||
|
RuntimeLocalPlayerMovementState movement)
|
||||||
|
: IRuntimeMovementInputSource
|
||||||
|
{
|
||||||
|
private readonly RuntimeLocalPlayerMovementState _movement =
|
||||||
|
movement ?? throw new ArgumentNullException(nameof(movement));
|
||||||
|
|
||||||
|
public MovementInput Capture()
|
||||||
|
{
|
||||||
|
if (_movement.HasCommandInput)
|
||||||
|
return _movement.CommandInput;
|
||||||
|
return new MovementInput(
|
||||||
|
Forward: _movement.AutoRunActive,
|
||||||
|
Run: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class HeadlessLocalPlayerFrameHost
|
||||||
|
: IRuntimeLocalPlayerFrameHost
|
||||||
|
{
|
||||||
|
private readonly GameRuntime _runtime;
|
||||||
|
private readonly LiveSessionHost _session;
|
||||||
|
private readonly LocalPlayerOutboundController _outbound;
|
||||||
|
|
||||||
|
internal HeadlessLocalPlayerFrameHost(
|
||||||
|
GameRuntime runtime,
|
||||||
|
LiveSessionHost session)
|
||||||
|
{
|
||||||
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
|
_outbound = new LocalPlayerOutboundController(
|
||||||
|
static (_, _, _, _, _, _) => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanAdvancePlayer =>
|
||||||
|
_runtime.Session.IsInWorld
|
||||||
|
&& _runtime.MovementOwner.Controller is not null;
|
||||||
|
|
||||||
|
public PlayerMovementController? Controller =>
|
||||||
|
_runtime.MovementOwner.Controller;
|
||||||
|
|
||||||
|
public uint ResolveLocalEntityId()
|
||||||
|
{
|
||||||
|
uint player = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
return player != 0u
|
||||||
|
&& _runtime.EntityObjects.Entities.TryGetActive(
|
||||||
|
player,
|
||||||
|
out var record)
|
||||||
|
? record.LocalEntityId ?? 0u
|
||||||
|
: 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleTargeting()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsHidden
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
uint player = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
return player != 0u
|
||||||
|
&& _runtime.EntityObjects.Entities.TryGetActive(
|
||||||
|
player,
|
||||||
|
out var record)
|
||||||
|
&& (record.FinalPhysicsState
|
||||||
|
& PhysicsStateFlags.Hidden) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RetailObjectClockDisposition ObjectClockDisposition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
uint player = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
if (player == 0u
|
||||||
|
|| !_runtime.EntityObjects.Entities.TryGetActive(
|
||||||
|
player,
|
||||||
|
out var record)
|
||||||
|
|| record.FullCellId == 0u
|
||||||
|
|| (record.FinalPhysicsState
|
||||||
|
& (PhysicsStateFlags.Frozen
|
||||||
|
| PhysicsStateFlags.Static)) != 0)
|
||||||
|
{
|
||||||
|
return RetailObjectClockDisposition.Suspend;
|
||||||
|
}
|
||||||
|
return RetailObjectClockDisposition.Advance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Project(
|
||||||
|
PlayerMovementController controller,
|
||||||
|
MovementResult movement,
|
||||||
|
bool hidden)
|
||||||
|
{
|
||||||
|
// The controller and Runtime physics body are the direct host's
|
||||||
|
// canonical projection. There is no render bucket or shadow mirror.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendPreNetwork(
|
||||||
|
PlayerMovementController controller,
|
||||||
|
MovementResult movement,
|
||||||
|
bool hidden) =>
|
||||||
|
_outbound.SendPreNetworkActions(
|
||||||
|
_session.CurrentSession,
|
||||||
|
controller,
|
||||||
|
movement,
|
||||||
|
hidden);
|
||||||
|
|
||||||
|
public void SendPostNetwork(
|
||||||
|
PlayerMovementController controller,
|
||||||
|
bool hidden) =>
|
||||||
|
_outbound.SendPostNetworkPosition(
|
||||||
|
_session.CurrentSession,
|
||||||
|
controller,
|
||||||
|
hidden);
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,76 @@ using AcDream.Headless.Credentials;
|
||||||
using AcDream.Headless.Diagnostics;
|
using AcDream.Headless.Diagnostics;
|
||||||
using AcDream.Headless.Policies;
|
using AcDream.Headless.Policies;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
namespace AcDream.Headless.Hosting;
|
namespace AcDream.Headless.Hosting;
|
||||||
|
|
||||||
internal sealed class HeadlessSessionHost : IDisposable
|
internal sealed class HeadlessSessionHost : IDisposable
|
||||||
{
|
{
|
||||||
|
private sealed class SessionCommandRoute(
|
||||||
|
ILiveSessionCommandRouting gameplay,
|
||||||
|
ILiveSessionCommandRouting commands)
|
||||||
|
: ILiveSessionCommandRouting
|
||||||
|
{
|
||||||
|
private bool _gameplayActive;
|
||||||
|
private bool _commandsActive;
|
||||||
|
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
if (_gameplayActive || _commandsActive)
|
||||||
|
return;
|
||||||
|
gameplay.Activate();
|
||||||
|
_gameplayActive = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commands.Activate();
|
||||||
|
_commandsActive = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
gameplay.Dispose();
|
||||||
|
_gameplayActive = false;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
List<Exception>? failures = null;
|
||||||
|
if (_commandsActive)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commands.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
(failures ??= []).Add(error);
|
||||||
|
}
|
||||||
|
_commandsActive = false;
|
||||||
|
}
|
||||||
|
if (_gameplayActive)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
gameplay.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
(failures ??= []).Add(error);
|
||||||
|
}
|
||||||
|
_gameplayActive = false;
|
||||||
|
}
|
||||||
|
if (failures is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Headless command routes did not detach cleanly.",
|
||||||
|
failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SessionCommandBridge : IRuntimeSessionCommands
|
private sealed class SessionCommandBridge : IRuntimeSessionCommands
|
||||||
{
|
{
|
||||||
private HeadlessSessionHost? _owner;
|
private HeadlessSessionHost? _owner;
|
||||||
|
|
@ -52,6 +116,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
private readonly IHeadlessBotPolicy _policy;
|
private readonly IHeadlessBotPolicy _policy;
|
||||||
private readonly IDisposable _policySubscription;
|
private readonly IDisposable _policySubscription;
|
||||||
private readonly LiveSessionHost _liveSession;
|
private readonly LiveSessionHost _liveSession;
|
||||||
|
private readonly RuntimeLocalPlayerFrameController _localPlayerFrame;
|
||||||
private int _disposeStage;
|
private int _disposeStage;
|
||||||
private long _reconnectDeadline;
|
private long _reconnectDeadline;
|
||||||
private bool _reconnectPending;
|
private bool _reconnectPending;
|
||||||
|
|
@ -114,7 +179,9 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
new LiveSessionHostBindings(
|
new LiveSessionHostBindings(
|
||||||
new LiveSessionRoutingFactories(
|
new LiveSessionRoutingFactories(
|
||||||
CreateEventRoute,
|
CreateEventRoute,
|
||||||
commands.CreateRoute),
|
session => new SessionCommandRoute(
|
||||||
|
gameplay.CreateRoute(session),
|
||||||
|
commands.CreateRoute(session))),
|
||||||
generation =>
|
generation =>
|
||||||
runtime.ResetGeneration(generation, _resetHost),
|
runtime.ResetGeneration(generation, _resetHost),
|
||||||
new LiveSessionSelectionBindings(
|
new LiveSessionSelectionBindings(
|
||||||
|
|
@ -143,6 +210,13 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
Runtime = runtime;
|
Runtime = runtime;
|
||||||
Commands = commands;
|
Commands = commands;
|
||||||
_liveSession = liveSession;
|
_liveSession = liveSession;
|
||||||
|
_localPlayerFrame =
|
||||||
|
runtime.CreateLocalPlayerFrameController(
|
||||||
|
new HeadlessLocalPlayerFrameHost(
|
||||||
|
runtime,
|
||||||
|
liveSession),
|
||||||
|
new HeadlessMovementInputSource(
|
||||||
|
runtime.MovementOwner));
|
||||||
bridge.Bind(this);
|
bridge.Bind(this);
|
||||||
|
|
||||||
hostLease = runtime.AcquireHostLease(
|
hostLease = runtime.AcquireHostLease(
|
||||||
|
|
@ -194,7 +268,10 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
if (_reconnectPending)
|
if (_reconnectPending)
|
||||||
return;
|
return;
|
||||||
_ = Runtime.Clock.Advance(deltaSeconds);
|
_ = Runtime.Clock.Advance(deltaSeconds);
|
||||||
|
_localPlayerFrame.AdvanceBeforeNetwork(
|
||||||
|
checked((float)deltaSeconds));
|
||||||
Runtime.Session.Tick();
|
Runtime.Session.Tick();
|
||||||
|
_localPlayerFrame.RunPostNetworkCommandPhase();
|
||||||
Runtime.ActionOwner.CombatAttack.Tick();
|
Runtime.ActionOwner.CombatAttack.Tick();
|
||||||
_policy.Tick(Runtime, Commands);
|
_policy.Tick(Runtime, Commands);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ internal sealed class IdleHeadlessBotPolicy : IHeadlessBotPolicy
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -161,6 +165,10 @@ internal sealed class LifecycleSmokeHeadlessBotPolicy
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,11 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public RuntimeEntityObjectLifetime(
|
public RuntimeEntityObjectLifetime(
|
||||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||||
Physics = new RuntimePhysicsState(Entities);
|
Physics = new RuntimePhysicsState(Entities, timeProvider: timeProvider);
|
||||||
Objects = new ClientObjectTable();
|
Objects = new ClientObjectTable();
|
||||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||||
EntityView = views.Entities;
|
EntityView = views.Entities;
|
||||||
|
|
@ -107,11 +108,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
internal RuntimeEntityObjectLifetime(
|
internal RuntimeEntityObjectLifetime(
|
||||||
PhysicsDataCache physicsDataCache,
|
PhysicsDataCache physicsDataCache,
|
||||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(physicsDataCache);
|
ArgumentNullException.ThrowIfNull(physicsDataCache);
|
||||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||||
Physics = new RuntimePhysicsState(Entities, physicsDataCache);
|
Physics = new RuntimePhysicsState(
|
||||||
|
Entities,
|
||||||
|
physicsDataCache,
|
||||||
|
timeProvider);
|
||||||
Objects = new ClientObjectTable();
|
Objects = new ClientObjectTable();
|
||||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||||
EntityView = views.Entities;
|
EntityView = views.Entities;
|
||||||
|
|
@ -121,11 +126,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
|
|
||||||
internal RuntimeEntityObjectLifetime(
|
internal RuntimeEntityObjectLifetime(
|
||||||
PhysicsEngine physicsEngine,
|
PhysicsEngine physicsEngine,
|
||||||
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
ArgumentNullException.ThrowIfNull(physicsEngine);
|
||||||
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
|
||||||
Physics = new RuntimePhysicsState(Entities, physicsEngine);
|
Physics = new RuntimePhysicsState(
|
||||||
|
Entities,
|
||||||
|
physicsEngine,
|
||||||
|
timeProvider);
|
||||||
Objects = new ClientObjectTable();
|
Objects = new ClientObjectTable();
|
||||||
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
var views = new RuntimeEntityObjectViews(Entities, Objects);
|
||||||
EntityView = views.Entities;
|
EntityView = views.Entities;
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,8 @@ public sealed class GameRuntime
|
||||||
faultInjection);
|
faultInjection);
|
||||||
|
|
||||||
context.EntityObjects = new RuntimeEntityObjectLifetime(
|
context.EntityObjects = new RuntimeEntityObjectLifetime(
|
||||||
dependencies.FirstLocalEntityId);
|
dependencies.FirstLocalEntityId,
|
||||||
|
dependencies.TimeProvider);
|
||||||
construction.Own(context.EntityObjects);
|
construction.Own(context.EntityObjects);
|
||||||
Fault(
|
Fault(
|
||||||
GameRuntimeConstructionPoint.EntityObjectsCreated,
|
GameRuntimeConstructionPoint.EntityObjectsCreated,
|
||||||
|
|
@ -259,7 +260,8 @@ public sealed class GameRuntime
|
||||||
() => clock.FrameNumber);
|
() => clock.FrameNumber);
|
||||||
context.Events = new GameRuntimeEventHub(
|
context.Events = new GameRuntimeEventHub(
|
||||||
context.EntityObjects,
|
context.EntityObjects,
|
||||||
context.Communication);
|
context.Communication,
|
||||||
|
context.Actions);
|
||||||
construction.Own(context.Events);
|
construction.Own(context.Events);
|
||||||
Fault(
|
Fault(
|
||||||
GameRuntimeConstructionPoint.EventsCreated,
|
GameRuntimeConstructionPoint.EventsCreated,
|
||||||
|
|
@ -362,6 +364,17 @@ public sealed class GameRuntime
|
||||||
Portal.Snapshot,
|
Portal.Snapshot,
|
||||||
Portal.Ownership);
|
Portal.Ownership);
|
||||||
|
|
||||||
|
public RuntimeLocalPlayerFrameController CreateLocalPlayerFrameController(
|
||||||
|
IRuntimeLocalPlayerFrameHost host,
|
||||||
|
IRuntimeMovementInputSource input)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
|
||||||
|
return new RuntimeLocalPlayerFrameController(
|
||||||
|
host,
|
||||||
|
input,
|
||||||
|
() => _events.EmitMovement(MovementOwner.Snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetGeneration(
|
public void ResetGeneration(
|
||||||
RuntimeGenerationToken retiringGeneration,
|
RuntimeGenerationToken retiringGeneration,
|
||||||
IRuntimeGenerationResetHost host) =>
|
IRuntimeGenerationResetHost host) =>
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,13 @@ public interface IRuntimeSelectionCommands
|
||||||
RuntimeCommandResult Execute(
|
RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeSelectionCommand command);
|
RuntimeSelectionCommand command);
|
||||||
|
|
||||||
|
RuntimeCommandResult SelectObject(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint objectId);
|
||||||
|
|
||||||
|
RuntimeCommandResult Clear(
|
||||||
|
RuntimeGenerationToken expectedGeneration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IRuntimeCombatCommands
|
public interface IRuntimeCombatCommands
|
||||||
|
|
@ -132,6 +139,13 @@ public interface IRuntimeMovementCommands
|
||||||
RuntimeCommandResult Execute(
|
RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeMovementCommand command);
|
RuntimeMovementCommand command);
|
||||||
|
|
||||||
|
RuntimeCommandResult SetIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in Gameplay.MovementInput input);
|
||||||
|
|
||||||
|
RuntimeCommandResult ClearIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IRuntimeChatCommands
|
public interface IRuntimeChatCommands
|
||||||
|
|
|
||||||
|
|
@ -48,21 +48,26 @@ internal sealed class GameRuntimeEventHub
|
||||||
{
|
{
|
||||||
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
private readonly RuntimeEntityObjectLifetime _entityObjects;
|
||||||
private readonly RuntimeCommunicationState _communication;
|
private readonly RuntimeCommunicationState _communication;
|
||||||
|
private readonly RuntimeActionState _actions;
|
||||||
private readonly object _observerGate = new();
|
private readonly object _observerGate = new();
|
||||||
private IRuntimeEventObserver[] _observers = [];
|
private IRuntimeEventObserver[] _observers = [];
|
||||||
private IDisposable? _entityObjectSubscription;
|
private IDisposable? _entityObjectSubscription;
|
||||||
private IDisposable? _communicationSubscription;
|
private IDisposable? _communicationSubscription;
|
||||||
|
private bool _actionEventsAttached;
|
||||||
private bool _disposeRequested;
|
private bool _disposeRequested;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public GameRuntimeEventHub(
|
public GameRuntimeEventHub(
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
RuntimeCommunicationState communication)
|
RuntimeCommunicationState communication,
|
||||||
|
RuntimeActionState actions)
|
||||||
{
|
{
|
||||||
_entityObjects = entityObjects
|
_entityObjects = entityObjects
|
||||||
?? throw new ArgumentNullException(nameof(entityObjects));
|
?? throw new ArgumentNullException(nameof(entityObjects));
|
||||||
_communication = communication
|
_communication = communication
|
||||||
?? throw new ArgumentNullException(nameof(communication));
|
?? throw new ArgumentNullException(nameof(communication));
|
||||||
|
_actions = actions
|
||||||
|
?? throw new ArgumentNullException(nameof(actions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public long DispatchFailureCount { get; private set; }
|
public long DispatchFailureCount { get; private set; }
|
||||||
|
|
@ -127,6 +132,7 @@ internal sealed class GameRuntimeEventHub
|
||||||
ref _entityObjectSubscription,
|
ref _entityObjectSubscription,
|
||||||
"entity/object event subscription",
|
"entity/object event subscription",
|
||||||
ref failures);
|
ref failures);
|
||||||
|
DetachActionEvents();
|
||||||
_disposed = !OwnerEventsAttached;
|
_disposed = !OwnerEventsAttached;
|
||||||
|
|
||||||
if (failures is not null)
|
if (failures is not null)
|
||||||
|
|
@ -284,7 +290,8 @@ internal sealed class GameRuntimeEventHub
|
||||||
|
|
||||||
private bool OwnerEventsAttached =>
|
private bool OwnerEventsAttached =>
|
||||||
_entityObjectSubscription is not null
|
_entityObjectSubscription is not null
|
||||||
|| _communicationSubscription is not null;
|
|| _communicationSubscription is not null
|
||||||
|
|| _actionEventsAttached;
|
||||||
|
|
||||||
private void AttachOwnerEvents()
|
private void AttachOwnerEvents()
|
||||||
{
|
{
|
||||||
|
|
@ -294,11 +301,14 @@ internal sealed class GameRuntimeEventHub
|
||||||
{
|
{
|
||||||
entity = _entityObjects.Events.Subscribe(this);
|
entity = _entityObjects.Events.Subscribe(this);
|
||||||
communication = _communication.Events.Subscribe(this);
|
communication = _communication.Events.Subscribe(this);
|
||||||
|
_actions.CombatChanged += OnCombatChanged;
|
||||||
|
_actionEventsAttached = true;
|
||||||
_entityObjectSubscription = entity;
|
_entityObjectSubscription = entity;
|
||||||
_communicationSubscription = communication;
|
_communicationSubscription = communication;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
DetachActionEvents();
|
||||||
communication?.Dispose();
|
communication?.Dispose();
|
||||||
entity?.Dispose();
|
entity?.Dispose();
|
||||||
throw;
|
throw;
|
||||||
|
|
@ -347,6 +357,7 @@ internal sealed class GameRuntimeEventHub
|
||||||
ref _entityObjectSubscription,
|
ref _entityObjectSubscription,
|
||||||
"entity/object event subscription",
|
"entity/object event subscription",
|
||||||
ref failures);
|
ref failures);
|
||||||
|
DetachActionEvents();
|
||||||
if (failures is not null)
|
if (failures is not null)
|
||||||
{
|
{
|
||||||
throw new AggregateException(
|
throw new AggregateException(
|
||||||
|
|
@ -378,6 +389,37 @@ internal sealed class GameRuntimeEventHub
|
||||||
private RuntimeEventStamp NextStamp() =>
|
private RuntimeEventStamp NextStamp() =>
|
||||||
_entityObjects.Events.NextStamp();
|
_entityObjects.Events.NextStamp();
|
||||||
|
|
||||||
|
private void OnCombatChanged()
|
||||||
|
{
|
||||||
|
if (!TryObservers(out IRuntimeEventObserver[] observers))
|
||||||
|
return;
|
||||||
|
RuntimeActionSnapshot actions = _actions.View.Snapshot;
|
||||||
|
var delta = new RuntimeCombatDelta(
|
||||||
|
NextStamp(),
|
||||||
|
actions.CombatMode,
|
||||||
|
actions.TrackedTargetHealthCount,
|
||||||
|
actions.CombatAttack);
|
||||||
|
foreach (IRuntimeEventObserver observer in observers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
observer.OnCombat(in delta);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
RecordDispatchFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachActionEvents()
|
||||||
|
{
|
||||||
|
if (!_actionEventsAttached)
|
||||||
|
return;
|
||||||
|
_actions.CombatChanged -= OnCombatChanged;
|
||||||
|
_actionEventsAttached = false;
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryObservers(out IRuntimeEventObserver[] observers)
|
private bool TryObservers(out IRuntimeEventObserver[] observers)
|
||||||
{
|
{
|
||||||
observers = Volatile.Read(ref _observers);
|
observers = Volatile.Read(ref _observers);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AcDream.Core.Combat;
|
||||||
|
|
||||||
namespace AcDream.Runtime;
|
namespace AcDream.Runtime;
|
||||||
|
|
||||||
public readonly record struct RuntimeEventStamp(
|
public readonly record struct RuntimeEventStamp(
|
||||||
|
|
@ -82,6 +84,12 @@ public readonly record struct RuntimePortalDelta(
|
||||||
RuntimeEventStamp Stamp,
|
RuntimeEventStamp Stamp,
|
||||||
RuntimePortalSnapshot Portal);
|
RuntimePortalSnapshot Portal);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCombatDelta(
|
||||||
|
RuntimeEventStamp Stamp,
|
||||||
|
CombatMode Mode,
|
||||||
|
int TrackedTargetCount,
|
||||||
|
RuntimeCombatAttackSnapshot Attack);
|
||||||
|
|
||||||
public interface IRuntimeEventObserver
|
public interface IRuntimeEventObserver
|
||||||
{
|
{
|
||||||
void OnLifecycle(in RuntimeLifecycleDelta delta);
|
void OnLifecycle(in RuntimeLifecycleDelta delta);
|
||||||
|
|
@ -97,6 +105,8 @@ public interface IRuntimeEventObserver
|
||||||
void OnMovement(in RuntimeMovementDelta delta);
|
void OnMovement(in RuntimeMovementDelta delta);
|
||||||
|
|
||||||
void OnPortal(in RuntimePortalDelta delta);
|
void OnPortal(in RuntimePortalDelta delta);
|
||||||
|
|
||||||
|
void OnCombat(in RuntimeCombatDelta delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IRuntimeEventSource
|
public interface IRuntimeEventSource
|
||||||
|
|
@ -113,6 +123,7 @@ public enum RuntimeTraceKind
|
||||||
Chat,
|
Chat,
|
||||||
Movement,
|
Movement,
|
||||||
Portal,
|
Portal,
|
||||||
|
Combat,
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,6 +289,20 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
||||||
$"{delta.Portal.IsReady}:{delta.Portal.IsMaterialized}:" +
|
$"{delta.Portal.IsReady}:{delta.Portal.IsMaterialized}:" +
|
||||||
$"{delta.Portal.IsCompleted}:{delta.Portal.IsCancelled}:" +
|
$"{delta.Portal.IsCompleted}:{delta.Portal.IsCancelled}:" +
|
||||||
$"{delta.Portal.IsWorldVisible}"));
|
$"{delta.Portal.IsWorldVisible}"));
|
||||||
|
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta) =>
|
||||||
|
_entries.Add(new RuntimeTraceEntry(
|
||||||
|
delta.Stamp,
|
||||||
|
RuntimeTraceKind.Combat,
|
||||||
|
(int)delta.Mode,
|
||||||
|
delta.Attack.Revision,
|
||||||
|
(uint)delta.TrackedTargetCount,
|
||||||
|
0u,
|
||||||
|
$"{(int)delta.Attack.RequestedHeight}:" +
|
||||||
|
$"{BitConverter.SingleToInt32Bits(delta.Attack.DesiredPower):X8}:" +
|
||||||
|
$"{BitConverter.SingleToInt32Bits(delta.Attack.PowerBarLevel):X8}:" +
|
||||||
|
$"{delta.Attack.BuildInProgress}:" +
|
||||||
|
$"{delta.Attack.RequestInProgress}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Monotonic sequence owner for one runtime instance.</summary>
|
/// <summary>Monotonic sequence owner for one runtime instance.</summary>
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@ public readonly record struct RuntimeMovementSnapshot(
|
||||||
bool IsAirborne,
|
bool IsAirborne,
|
||||||
double SimulationTimeSeconds,
|
double SimulationTimeSeconds,
|
||||||
long Revision = 0,
|
long Revision = 0,
|
||||||
bool AutoRunActive = false);
|
bool AutoRunActive = false,
|
||||||
|
bool HasCommandInput = false,
|
||||||
|
Gameplay.MovementInput CommandInput = default);
|
||||||
|
|
||||||
public interface IRuntimeMovementView
|
public interface IRuntimeMovementView
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,7 @@ public sealed class PlayerMovementController
|
||||||
_animationRootMotionScratch = new();
|
_animationRootMotionScratch = new();
|
||||||
private readonly AcDream.Core.Physics.Motion.MotionDeltaFrame
|
private readonly AcDream.Core.Physics.Motion.MotionDeltaFrame
|
||||||
_positionManagerDeltaScratch = new();
|
_positionManagerDeltaScratch = new();
|
||||||
|
private bool _externalMovementEventPending;
|
||||||
|
|
||||||
// ── R4-V5: the verbatim retail MoveToManager replaces B.6 auto-walk ──
|
// ── R4-V5: the verbatim retail MoveToManager replaces B.6 auto-walk ──
|
||||||
// The B.6 DriveServerAutoWalk overlay (synthesized turn-first phase,
|
// The B.6 DriveServerAutoWalk overlay (synthesized turn-first phase,
|
||||||
|
|
@ -856,6 +857,35 @@ public sealed class PlayerMovementController
|
||||||
return true;
|
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)
|
public void SetCharacterSkills(int runSkill, int jumpSkill)
|
||||||
{
|
{
|
||||||
_weenie.SetSkills(runSkill, jumpSkill);
|
_weenie.SetSkills(runSkill, jumpSkill);
|
||||||
|
|
@ -1455,8 +1485,12 @@ public sealed class PlayerMovementController
|
||||||
_motion.set_hold_run(input.Run, interrupt: false);
|
_motion.set_hold_run(input.Run, interrupt: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool externallyRequestedMovementEvent =
|
||||||
|
_externalMovementEventPending;
|
||||||
|
_externalMovementEventPending = false;
|
||||||
bool motionEdgeFired = false;
|
bool motionEdgeFired = false;
|
||||||
bool movementEventRequested = false;
|
bool movementEventRequested =
|
||||||
|
externallyRequestedMovementEvent;
|
||||||
{
|
{
|
||||||
bool userInputEdge = input.Run != _prevRunHeld
|
bool userInputEdge = input.Run != _prevRunHeld
|
||||||
|| input.Forward != _prevForwardHeld
|
|| input.Forward != _prevForwardHeld
|
||||||
|
|
@ -1511,7 +1545,7 @@ public sealed class PlayerMovementController
|
||||||
// an immediate SendMovementEvent. Mouse-origin turn updates below
|
// an immediate SendMovementEvent. Mouse-origin turn updates below
|
||||||
// deliberately do not: CameraSet reports them on its separate
|
// deliberately do not: CameraSet reports them on its separate
|
||||||
// half-second cadence.
|
// half-second cadence.
|
||||||
movementEventRequested = motionEdgeFired;
|
movementEventRequested |= motionEdgeFired;
|
||||||
|
|
||||||
// Turn channel. Retail CameraSet::Rotate (0x00458310) feeds mouse
|
// Turn channel. Retail CameraSet::Rotate (0x00458310) feeds mouse
|
||||||
// input through CommandInterpreter::MovePlayer as TurnLeft /
|
// input through CommandInterpreter::MovePlayer as TurnLeft /
|
||||||
|
|
@ -1937,6 +1971,14 @@ public sealed class PlayerMovementController
|
||||||
outForwardCmd = MotionCommand.WalkBackward;
|
outForwardCmd = MotionCommand.WalkBackward;
|
||||||
outForwardSpeed = 1.0f;
|
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.
|
// Source the outbound axis from the canonical input-owned raw channel.
|
||||||
// CameraInstantMouseLook maps keyboard TurnLeft/TurnRight into this
|
// CameraInstantMouseLook maps keyboard TurnLeft/TurnRight into this
|
||||||
|
|
@ -1979,7 +2021,8 @@ public sealed class PlayerMovementController
|
||||||
|| outTurnCmd != _prevTurnCmd
|
|| outTurnCmd != _prevTurnCmd
|
||||||
|| !FloatsEqual(outForwardSpeed, _prevForwardSpeed)
|
|| !FloatsEqual(outForwardSpeed, _prevForwardSpeed)
|
||||||
|| runHold != _prevRunHold
|
|| runHold != _prevRunHold
|
||||||
|| motionEdgeFired;
|
|| motionEdgeFired
|
||||||
|
|| externallyRequestedMovementEvent;
|
||||||
|
|
||||||
bool mouseMovementEventDue = _mouseMovementEventPending
|
bool mouseMovementEventDue = _mouseMovementEventPending
|
||||||
|| (_mouseMovementEventCandidate
|
|| (_mouseMovementEventCandidate
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ public sealed class RuntimeActionState : IDisposable
|
||||||
public IRuntimeActionView View { get; }
|
public IRuntimeActionView View { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
internal event Action? CombatChanged;
|
||||||
|
|
||||||
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
|
public RuntimeActionOwnershipSnapshot CaptureOwnership() => new(
|
||||||
_disposed,
|
_disposed,
|
||||||
_internalSubscriptionsAttached,
|
_internalSubscriptionsAttached,
|
||||||
|
|
@ -193,17 +195,26 @@ public sealed class RuntimeActionState : IDisposable
|
||||||
private void OnSelectionChanged(SelectionTransition _) =>
|
private void OnSelectionChanged(SelectionTransition _) =>
|
||||||
Interlocked.Increment(ref _selectionRevision);
|
Interlocked.Increment(ref _selectionRevision);
|
||||||
|
|
||||||
private void OnCombatModeChanged(CombatMode _) =>
|
private void OnCombatModeChanged(CombatMode _)
|
||||||
|
{
|
||||||
Interlocked.Increment(ref _combatRevision);
|
Interlocked.Increment(ref _combatRevision);
|
||||||
|
CombatChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnHealthChanged(uint _, float __) =>
|
private void OnHealthChanged(uint _, float __)
|
||||||
|
{
|
||||||
Interlocked.Increment(ref _combatRevision);
|
Interlocked.Increment(ref _combatRevision);
|
||||||
|
CombatChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnInteractionChanged(InteractionModeTransition _) =>
|
private void OnInteractionChanged(InteractionModeTransition _) =>
|
||||||
Interlocked.Increment(ref _interactionRevision);
|
Interlocked.Increment(ref _interactionRevision);
|
||||||
|
|
||||||
private void OnCombatAttackChanged() =>
|
private void OnCombatAttackChanged()
|
||||||
|
{
|
||||||
Interlocked.Increment(ref _combatIntentRevision);
|
Interlocked.Increment(ref _combatIntentRevision);
|
||||||
|
CombatChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSpellCastChanged() =>
|
private void OnSpellCastChanged() =>
|
||||||
Interlocked.Increment(ref _magicIntentRevision);
|
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;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Gameplay;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
@ -26,6 +28,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
private PlayerMovementController? _controller;
|
private PlayerMovementController? _controller;
|
||||||
private PlayerMovementController? _preparingMotionOwner;
|
private PlayerMovementController? _preparingMotionOwner;
|
||||||
private bool _autoRunActive;
|
private bool _autoRunActive;
|
||||||
|
private bool _hasCommandInput;
|
||||||
|
private MovementInput _commandInput;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private long _revision;
|
private long _revision;
|
||||||
|
|
||||||
|
|
@ -43,6 +47,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AutoRunActive => _autoRunActive;
|
public bool AutoRunActive => _autoRunActive;
|
||||||
|
public bool HasCommandInput => _hasCommandInput;
|
||||||
|
public MovementInput CommandInput => _commandInput;
|
||||||
public long Revision => Interlocked.Read(ref _revision);
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
public IRuntimeMovementView View => this;
|
public IRuntimeMovementView View => this;
|
||||||
|
|
||||||
|
|
@ -63,7 +69,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
false,
|
false,
|
||||||
0d,
|
0d,
|
||||||
Revision,
|
Revision,
|
||||||
_autoRunActive)
|
_autoRunActive,
|
||||||
|
_hasCommandInput,
|
||||||
|
_commandInput)
|
||||||
: new RuntimeMovementSnapshot(
|
: new RuntimeMovementSnapshot(
|
||||||
true,
|
true,
|
||||||
controller.LocalEntityId,
|
controller.LocalEntityId,
|
||||||
|
|
@ -72,7 +80,9 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
controller.IsAirborne,
|
controller.IsAirborne,
|
||||||
controller.SimTimeSeconds,
|
controller.SimTimeSeconds,
|
||||||
Revision,
|
Revision,
|
||||||
_autoRunActive);
|
_autoRunActive,
|
||||||
|
_hasCommandInput,
|
||||||
|
_commandInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +119,27 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
return true;
|
return true;
|
||||||
case RuntimeMovementCommand.Stop:
|
case RuntimeMovementCommand.Stop:
|
||||||
CancelAutoRun();
|
CancelAutoRun();
|
||||||
|
ClearCommandInput();
|
||||||
return true;
|
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:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -125,12 +155,75 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
return true;
|
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()
|
public void ResetInputIntent()
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
if (!_autoRunActive)
|
if (!_autoRunActive && !_hasCommandInput)
|
||||||
return;
|
return;
|
||||||
_autoRunActive = false;
|
_autoRunActive = false;
|
||||||
|
_hasCommandInput = false;
|
||||||
|
_commandInput = default;
|
||||||
Interlocked.Increment(ref _revision);
|
Interlocked.Increment(ref _revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,9 +238,12 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
bool changed =
|
bool changed =
|
||||||
_autoRunActive
|
_autoRunActive
|
||||||
|
|| _hasCommandInput
|
||||||
|| _controller is not null
|
|| _controller is not null
|
||||||
|| _preparingMotionOwner is not null;
|
|| _preparingMotionOwner is not null;
|
||||||
_autoRunActive = false;
|
_autoRunActive = false;
|
||||||
|
_hasCommandInput = false;
|
||||||
|
_commandInput = default;
|
||||||
_controller = null;
|
_controller = null;
|
||||||
_preparingMotionOwner = null;
|
_preparingMotionOwner = null;
|
||||||
if (changed)
|
if (changed)
|
||||||
|
|
@ -160,6 +256,7 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
_controller is not null,
|
_controller is not null,
|
||||||
_preparingMotionOwner is not null,
|
_preparingMotionOwner is not null,
|
||||||
_autoRunActive,
|
_autoRunActive,
|
||||||
|
_hasCommandInput,
|
||||||
Revision);
|
Revision);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -167,6 +264,8 @@ public sealed class RuntimeLocalPlayerMovementState
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_autoRunActive = false;
|
_autoRunActive = false;
|
||||||
|
_hasCommandInput = false;
|
||||||
|
_commandInput = default;
|
||||||
_controller = null;
|
_controller = null;
|
||||||
_preparingMotionOwner = null;
|
_preparingMotionOwner = null;
|
||||||
Interlocked.Increment(ref _revision);
|
Interlocked.Increment(ref _revision);
|
||||||
|
|
@ -210,11 +309,13 @@ public readonly record struct RuntimeLocalMovementOwnershipSnapshot(
|
||||||
bool HasController,
|
bool HasController,
|
||||||
bool HasPreparingMotionOwner,
|
bool HasPreparingMotionOwner,
|
||||||
bool AutoRunActive,
|
bool AutoRunActive,
|
||||||
|
bool HasCommandInput,
|
||||||
long Revision)
|
long Revision)
|
||||||
{
|
{
|
||||||
public bool IsConverged =>
|
public bool IsConverged =>
|
||||||
IsDisposed
|
IsDisposed
|
||||||
&& !HasController
|
&& !HasController
|
||||||
&& !HasPreparingMotionOwner
|
&& !HasPreparingMotionOwner
|
||||||
&& !AutoRunActive;
|
&& !AutoRunActive
|
||||||
|
&& !HasCommandInput;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ public readonly record struct RuntimeCollisionAcknowledgement(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RuntimePhysicsState : IDisposable
|
public sealed class RuntimePhysicsState : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
|
private readonly Dictionary<RuntimeEntityKey, RuntimeEntityRecord>
|
||||||
_spatialRoots = new();
|
_spatialRoots = new();
|
||||||
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
||||||
|
|
@ -88,9 +89,11 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
|
|
||||||
internal RuntimePhysicsState(
|
internal RuntimePhysicsState(
|
||||||
RuntimeEntityDirectory entities,
|
RuntimeEntityDirectory entities,
|
||||||
PhysicsDataCache? dataCache = null)
|
PhysicsDataCache? dataCache = null,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
|
DataCache = dataCache ?? PhysicsDataCache.CreateProduction();
|
||||||
Engine = new PhysicsEngine
|
Engine = new PhysicsEngine
|
||||||
{
|
{
|
||||||
|
|
@ -100,9 +103,11 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
|
|
||||||
internal RuntimePhysicsState(
|
internal RuntimePhysicsState(
|
||||||
RuntimeEntityDirectory entities,
|
RuntimeEntityDirectory entities,
|
||||||
PhysicsEngine engine)
|
PhysicsEngine engine,
|
||||||
|
TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||||
DataCache = engine.DataCache ?? PhysicsDataCache.CreateProduction();
|
DataCache = engine.DataCache ?? PhysicsDataCache.CreateProduction();
|
||||||
Engine.DataCache = DataCache;
|
Engine.DataCache = DataCache;
|
||||||
|
|
@ -114,6 +119,9 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
public int SpatialRootCount => _spatialRoots.Count;
|
public int SpatialRootCount => _spatialRoots.Count;
|
||||||
public int SpatialRemoteCount => _spatialRemotes.Count;
|
public int SpatialRemoteCount => _spatialRemotes.Count;
|
||||||
public int SpatialProjectileCount => _spatialProjectiles.Count;
|
public int SpatialProjectileCount => _spatialProjectiles.Count;
|
||||||
|
internal double UtcNowSeconds =>
|
||||||
|
(_timeProvider.GetUtcNow() - DateTimeOffset.UnixEpoch)
|
||||||
|
.TotalSeconds;
|
||||||
|
|
||||||
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
||||||
new(
|
new(
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ internal sealed class RuntimeRemotePhysicsUpdater
|
||||||
// -> UpdatePhysicsInternal (FUN_00515020 / FUN_00513730 / FUN_005111D0).
|
// -> UpdatePhysicsInternal (FUN_00515020 / FUN_00513730 / FUN_005111D0).
|
||||||
// The bare block scopes this update's locals (formerly the else body).
|
// The bare block scopes this update's locals (formerly the else body).
|
||||||
{
|
{
|
||||||
double nowSec = (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
double nowSec = _physics.UtcNowSeconds;
|
||||||
|
|
||||||
// Retail CPhysicsObj::UpdatePositionInternal @ 0x00512C30 scales
|
// Retail CPhysicsObj::UpdatePositionInternal @ 0x00512C30 scales
|
||||||
// the CSequence root displacement by m_scale only while the body
|
// the CSequence root displacement by m_scale only while the body
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Session;
|
namespace AcDream.Runtime.Session;
|
||||||
|
|
@ -20,7 +27,8 @@ public sealed class DirectGameRuntimeCommandAdapter
|
||||||
IRuntimeInventoryStateCommands,
|
IRuntimeInventoryStateCommands,
|
||||||
IRuntimeSpellbookCommands,
|
IRuntimeSpellbookCommands,
|
||||||
IRuntimeCharacterCommands,
|
IRuntimeCharacterCommands,
|
||||||
IRuntimeSocialCommands
|
IRuntimeSocialCommands,
|
||||||
|
IRuntimeInteractionTransport
|
||||||
{
|
{
|
||||||
private sealed class CommandRoute(
|
private sealed class CommandRoute(
|
||||||
DirectGameRuntimeCommandAdapter owner,
|
DirectGameRuntimeCommandAdapter owner,
|
||||||
|
|
@ -139,9 +147,13 @@ public sealed class DirectGameRuntimeCommandAdapter
|
||||||
session!.SendTell(command.TargetName, command.Text);
|
session!.SendTell(command.TargetName, command.Text);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return EmitUnsupported(
|
if (!TrySendChannel(session!, command.Channel, command.Text))
|
||||||
RuntimeCommandDomain.Chat,
|
{
|
||||||
(int)command.Channel);
|
return EmitUnsupported(
|
||||||
|
RuntimeCommandDomain.Chat,
|
||||||
|
(int)command.Channel);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_runtime.EventSink.EmitCommand(
|
_runtime.EventSink.EmitCommand(
|
||||||
|
|
@ -190,167 +202,829 @@ public sealed class DirectGameRuntimeCommandAdapter
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeSelectionCommand command) =>
|
RuntimeSelectionCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
|
||||||
|
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||||
|
SelectionState selection = _runtime.ActionOwner.Selection;
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case RuntimeSelectionCommand.SelectClosestHostile:
|
||||||
|
{
|
||||||
|
uint? closest =
|
||||||
|
RuntimeHostileTargetQuery.FindClosest(_runtime);
|
||||||
|
if (closest is { } objectId)
|
||||||
|
{
|
||||||
|
selection.Select(
|
||||||
|
objectId,
|
||||||
|
SelectionChangeSource.Keyboard);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selection.Clear(
|
||||||
|
SelectionChangeSource.Keyboard);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RuntimeSelectionCommand.SelectPrevious:
|
||||||
|
selection.SelectPrevious();
|
||||||
|
break;
|
||||||
|
case RuntimeSelectionCommand.ExamineSelected:
|
||||||
|
if (selection.SelectedObjectId is { } appraisalId)
|
||||||
|
{
|
||||||
|
_runtime.ActionOwner.Transactions.TryRequestAppraisal(
|
||||||
|
appraisalId,
|
||||||
|
session!.SendAppraise);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_runtime.ActionOwner.Interaction.EnterExamine();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RuntimeSelectionCommand.UseSelected:
|
||||||
|
status = UseSelected(session!);
|
||||||
|
break;
|
||||||
|
case RuntimeSelectionCommand.PickUpSelected:
|
||||||
|
status = PickUpSelected();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = RuntimeCommandStatus.Unsupported;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint selected = selection.SelectedObjectId ?? 0u;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Selection,
|
RuntimeCommandDomain.Selection,
|
||||||
(int)command);
|
(int)command,
|
||||||
|
status,
|
||||||
|
selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult SelectObject(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint objectId)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
objectId != 0u
|
||||||
|
&& _runtime.InventoryOwner.Objects.Get(objectId) is not null
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
if (status == RuntimeCommandStatus.Accepted)
|
||||||
|
{
|
||||||
|
_runtime.ActionOwner.Selection.Select(
|
||||||
|
objectId,
|
||||||
|
SelectionChangeSource.Plugin);
|
||||||
|
}
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Selection,
|
||||||
|
operation: 0x100,
|
||||||
|
status,
|
||||||
|
objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult Clear(
|
||||||
|
RuntimeGenerationToken expectedGeneration)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_runtime.ActionOwner.Selection.Clear(
|
||||||
|
SelectionChangeSource.Plugin);
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Selection,
|
||||||
|
operation: 0x101,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeCombatCommand command) =>
|
RuntimeCombatCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
|
||||||
|
RuntimeCommandStatus status;
|
||||||
|
if (command == RuntimeCombatCommand.ToggleMode)
|
||||||
|
{
|
||||||
|
RuntimeCombatModeRequestResult result =
|
||||||
|
_runtime.ActionOwner.CombatMode.Toggle();
|
||||||
|
status = result.Status switch
|
||||||
|
{
|
||||||
|
RuntimeCombatModeRequestStatus.Sent =>
|
||||||
|
RuntimeCommandStatus.Accepted,
|
||||||
|
RuntimeCombatModeRequestStatus.Inactive =>
|
||||||
|
RuntimeCommandStatus.Inactive,
|
||||||
|
_ => RuntimeCommandStatus.Rejected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = RuntimeCommandStatus.Unsupported;
|
||||||
|
}
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Combat,
|
RuntimeCommandDomain.Combat,
|
||||||
(int)command);
|
(int)command,
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult ExecuteAttack(
|
public RuntimeCommandResult ExecuteAttack(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
in RuntimeCombatAttackInput command) =>
|
in RuntimeCombatAttackInput command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.ActionOwner.CombatAttack.HandleCommand(command)
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Unsupported;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Combat,
|
RuntimeCommandDomain.Combat,
|
||||||
(int)command.Command);
|
0x100 + (int)command.Command,
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
in RuntimeMagicCommand command) =>
|
in RuntimeMagicCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
CastRequestResult cast =
|
||||||
|
_runtime.ActionOwner.SpellCast.Cast(command.SpellId);
|
||||||
|
RuntimeCommandStatus status = cast == CastRequestResult.Sent
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Magic,
|
RuntimeCommandDomain.Magic,
|
||||||
operation: 0,
|
(int)cast,
|
||||||
|
status,
|
||||||
command.SpellId);
|
command.SpellId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
public RuntimeCommandResult Execute(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
RuntimeMovementCommand command) =>
|
RuntimeMovementCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.MovementOwner.Execute(command)
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Unsupported;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Movement,
|
RuntimeCommandDomain.Movement,
|
||||||
(int)command);
|
(int)command,
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult SetIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in MovementInput input)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_runtime.MovementOwner.SetCommandInput(input);
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Movement,
|
||||||
|
operation: 0x100,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult ClearIntent(
|
||||||
|
RuntimeGenerationToken expectedGeneration)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out _);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_runtime.MovementOwner.ClearCommandInput();
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Movement,
|
||||||
|
operation: 0x101,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult AddShortcut(
|
public RuntimeCommandResult AddShortcut(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
in RuntimeShortcutCommand command) =>
|
in RuntimeShortcutCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
var entry = new ShortcutEntry(
|
||||||
|
command.Index,
|
||||||
|
command.ObjectId,
|
||||||
|
command.SpellId);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.InventoryOwner.TryAddShortcut(
|
||||||
|
entry,
|
||||||
|
() => session!.SendAddShortcut(entry))
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
operation: 0,
|
operation: 0,
|
||||||
|
status,
|
||||||
command.ObjectId);
|
command.ObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult RemoveShortcut(
|
public RuntimeCommandResult RemoveShortcut(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
int index) =>
|
int index)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.InventoryOwner.TryRemoveShortcut(
|
||||||
|
index,
|
||||||
|
() => session!.SendRemoveShortcut((uint)index))
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.InventoryState,
|
RuntimeCommandDomain.InventoryState,
|
||||||
operation: 1);
|
operation: 1,
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult AddFavorite(
|
public RuntimeCommandResult AddFavorite(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
int tabIndex,
|
int tabIndex,
|
||||||
int position,
|
int position,
|
||||||
uint spellId) =>
|
uint spellId)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.CharacterOwner.TryAddFavorite(
|
||||||
|
tabIndex,
|
||||||
|
position,
|
||||||
|
spellId,
|
||||||
|
() => session!.SendAddSpellFavorite(
|
||||||
|
spellId,
|
||||||
|
position,
|
||||||
|
tabIndex))
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 0,
|
operation: 0,
|
||||||
|
status,
|
||||||
spellId);
|
spellId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult RemoveFavorite(
|
public RuntimeCommandResult RemoveFavorite(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
int tabIndex,
|
int tabIndex,
|
||||||
uint spellId) =>
|
uint spellId)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.CharacterOwner.TryRemoveFavorite(
|
||||||
|
tabIndex,
|
||||||
|
spellId,
|
||||||
|
() => session!.SendRemoveSpellFavorite(
|
||||||
|
spellId,
|
||||||
|
tabIndex))
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 1,
|
operation: 1,
|
||||||
|
status,
|
||||||
spellId);
|
spellId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult SetFilter(
|
public RuntimeCommandResult SetFilter(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
uint filters) =>
|
uint filters)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_runtime.CharacterOwner.SetSpellbookFilter(
|
||||||
|
filters,
|
||||||
|
() => session!.SendSpellbookFilter(filters));
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 2);
|
operation: 2,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult ForgetSpell(
|
public RuntimeCommandResult ForgetSpell(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
uint spellId) =>
|
uint spellId)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status = spellId == 0u
|
||||||
|
? RuntimeCommandStatus.Rejected
|
||||||
|
: RuntimeCommandStatus.Accepted;
|
||||||
|
if (status == RuntimeCommandStatus.Accepted)
|
||||||
|
session!.SendRemoveSpell(spellId);
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 3,
|
operation: 3,
|
||||||
|
status,
|
||||||
spellId);
|
spellId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult SetDesiredComponent(
|
public RuntimeCommandResult SetDesiredComponent(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
uint componentId,
|
uint componentId,
|
||||||
uint amount) =>
|
uint amount)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status =
|
||||||
|
_runtime.CharacterOwner.TrySetDesiredComponent(
|
||||||
|
componentId,
|
||||||
|
amount,
|
||||||
|
() => session!.SendSetDesiredComponentLevel(
|
||||||
|
componentId,
|
||||||
|
amount))
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 4,
|
operation: 4,
|
||||||
|
status,
|
||||||
componentId);
|
componentId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult ClearDesiredComponents(
|
public RuntimeCommandResult ClearDesiredComponents(
|
||||||
RuntimeGenerationToken expectedGeneration) =>
|
RuntimeGenerationToken expectedGeneration)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
_runtime.CharacterOwner.ClearDesiredComponents(
|
||||||
|
session!.SendClearDesiredComponents);
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Spellbook,
|
RuntimeCommandDomain.Spellbook,
|
||||||
operation: 5);
|
operation: 5,
|
||||||
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult Advance(
|
public RuntimeCommandResult Advance(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
in RuntimeAdvancementCommand command) =>
|
in RuntimeAdvancementCommand command)
|
||||||
Unsupported(
|
{
|
||||||
expectedGeneration,
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||||
|
if (command.StatId == 0u || command.Cost == 0u)
|
||||||
|
{
|
||||||
|
status = RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (command.Kind)
|
||||||
|
{
|
||||||
|
case RuntimeAdvancementKind.Attribute:
|
||||||
|
session!.SendRaiseAttribute(
|
||||||
|
command.StatId,
|
||||||
|
command.Cost);
|
||||||
|
break;
|
||||||
|
case RuntimeAdvancementKind.Vital:
|
||||||
|
session!.SendRaiseVital(
|
||||||
|
command.StatId,
|
||||||
|
command.Cost);
|
||||||
|
break;
|
||||||
|
case RuntimeAdvancementKind.Skill:
|
||||||
|
session!.SendRaiseSkill(
|
||||||
|
command.StatId,
|
||||||
|
command.Cost);
|
||||||
|
break;
|
||||||
|
case RuntimeAdvancementKind.TrainSkill
|
||||||
|
when command.Cost <= uint.MaxValue:
|
||||||
|
session!.SendTrainSkill(
|
||||||
|
command.StatId,
|
||||||
|
(uint)command.Cost);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = RuntimeCommandStatus.Rejected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EmitResult(
|
||||||
RuntimeCommandDomain.Character,
|
RuntimeCommandDomain.Character,
|
||||||
(int)command.Kind,
|
(int)command.Kind,
|
||||||
|
status,
|
||||||
command.StatId);
|
command.StatId);
|
||||||
|
}
|
||||||
|
|
||||||
public RuntimeCommandResult SetOptions1(
|
public RuntimeCommandResult SetOptions1(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
uint options) =>
|
uint options)
|
||||||
Unsupported(
|
|
||||||
expectedGeneration,
|
|
||||||
RuntimeCommandDomain.Character,
|
|
||||||
operation: 4);
|
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
|
||||||
RuntimeGenerationToken expectedGeneration,
|
|
||||||
in RuntimeFriendCommand command) =>
|
|
||||||
Unsupported(
|
|
||||||
expectedGeneration,
|
|
||||||
RuntimeCommandDomain.Social,
|
|
||||||
(int)command.Kind,
|
|
||||||
command.CharacterId);
|
|
||||||
|
|
||||||
public RuntimeCommandResult Execute(
|
|
||||||
RuntimeGenerationToken expectedGeneration,
|
|
||||||
in RuntimeSquelchCommand command) =>
|
|
||||||
Unsupported(
|
|
||||||
expectedGeneration,
|
|
||||||
RuntimeCommandDomain.Social,
|
|
||||||
(int)command.Scope,
|
|
||||||
command.CharacterId);
|
|
||||||
|
|
||||||
private RuntimeCommandResult Unsupported(
|
|
||||||
RuntimeGenerationToken expectedGeneration,
|
|
||||||
RuntimeCommandDomain domain,
|
|
||||||
int operation,
|
|
||||||
uint primaryObjectId = 0u)
|
|
||||||
{
|
{
|
||||||
RuntimeCommandStatus gate =
|
RuntimeCommandStatus gate =
|
||||||
Validate(expectedGeneration, out _);
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
return gate == RuntimeCommandStatus.Accepted
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
? EmitUnsupported(
|
return Result(gate);
|
||||||
domain,
|
session!.SendSetCharacterOptions(options);
|
||||||
operation,
|
return EmitResult(
|
||||||
RuntimeCommandStatus.Unsupported,
|
RuntimeCommandDomain.Character,
|
||||||
primaryObjectId)
|
operation: 4,
|
||||||
: Result(gate);
|
RuntimeCommandStatus.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult Execute(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeFriendCommand command)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||||
|
switch (command.Kind)
|
||||||
|
{
|
||||||
|
case RuntimeFriendCommandKind.Add
|
||||||
|
when !string.IsNullOrWhiteSpace(command.Name):
|
||||||
|
session!.SendAddFriend(command.Name);
|
||||||
|
break;
|
||||||
|
case RuntimeFriendCommandKind.Remove
|
||||||
|
when command.CharacterId != 0u:
|
||||||
|
session!.SendRemoveFriend(command.CharacterId);
|
||||||
|
break;
|
||||||
|
case RuntimeFriendCommandKind.Clear:
|
||||||
|
session!.SendClearFriends();
|
||||||
|
break;
|
||||||
|
case RuntimeFriendCommandKind.RequestLegacyList:
|
||||||
|
session!.SendLegacyFriendsListRequest();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = RuntimeCommandStatus.Rejected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Social,
|
||||||
|
(int)command.Kind,
|
||||||
|
status,
|
||||||
|
command.CharacterId,
|
||||||
|
command.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeCommandResult Execute(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeSquelchCommand command)
|
||||||
|
{
|
||||||
|
RuntimeCommandStatus gate =
|
||||||
|
Validate(expectedGeneration, out WorldSession? session);
|
||||||
|
if (gate != RuntimeCommandStatus.Accepted)
|
||||||
|
return Result(gate);
|
||||||
|
RuntimeCommandStatus status = RuntimeCommandStatus.Accepted;
|
||||||
|
switch (command.Scope)
|
||||||
|
{
|
||||||
|
case RuntimeSquelchScope.Character
|
||||||
|
when command.CharacterId != 0u
|
||||||
|
&& !string.IsNullOrWhiteSpace(command.Name):
|
||||||
|
session!.SendModifyCharacterSquelch(
|
||||||
|
command.Add,
|
||||||
|
command.CharacterId,
|
||||||
|
command.Name,
|
||||||
|
command.MessageType);
|
||||||
|
break;
|
||||||
|
case RuntimeSquelchScope.Account
|
||||||
|
when !string.IsNullOrWhiteSpace(command.Name):
|
||||||
|
session!.SendModifyAccountSquelch(
|
||||||
|
command.Add,
|
||||||
|
command.Name);
|
||||||
|
break;
|
||||||
|
case RuntimeSquelchScope.Global:
|
||||||
|
session!.SendModifyGlobalSquelch(
|
||||||
|
command.Add,
|
||||||
|
command.MessageType);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = RuntimeCommandStatus.Rejected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return EmitResult(
|
||||||
|
RuntimeCommandDomain.Social,
|
||||||
|
4 + (int)command.Scope,
|
||||||
|
status,
|
||||||
|
command.CharacterId,
|
||||||
|
command.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IRuntimeInteractionTransport.IsInWorld
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _route is not null
|
||||||
|
&& _session is not null
|
||||||
|
&& _routeGeneration == _runtime.Generation
|
||||||
|
&& _runtime.Session.IsInWorld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IRuntimeInteractionTransport.TrySendUse(
|
||||||
|
uint serverGuid,
|
||||||
|
out uint sequence)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_route is null
|
||||||
|
|| _session is null
|
||||||
|
|| _routeGeneration != _runtime.Generation
|
||||||
|
|| !_runtime.Session.IsInWorld)
|
||||||
|
{
|
||||||
|
sequence = 0u;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence = _session.NextGameActionSequence();
|
||||||
|
_session.SendGameAction(
|
||||||
|
InteractRequests.BuildUse(sequence, serverGuid));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IRuntimeInteractionTransport.TrySendPickup(
|
||||||
|
uint itemGuid,
|
||||||
|
uint destinationContainerId,
|
||||||
|
int placement,
|
||||||
|
out uint sequence)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_route is null
|
||||||
|
|| _session is null
|
||||||
|
|| _routeGeneration != _runtime.Generation
|
||||||
|
|| !_runtime.Session.IsInWorld)
|
||||||
|
{
|
||||||
|
sequence = 0u;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence = _session.NextGameActionSequence();
|
||||||
|
_session.SendGameAction(
|
||||||
|
InteractRequests.BuildPickUp(
|
||||||
|
sequence,
|
||||||
|
itemGuid,
|
||||||
|
destinationContainerId,
|
||||||
|
placement));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeCommandStatus UseSelected(WorldSession session)
|
||||||
|
{
|
||||||
|
if (_runtime.ActionOwner.Selection.SelectedObjectId
|
||||||
|
is not uint selected)
|
||||||
|
{
|
||||||
|
_runtime.ActionOwner.Interaction.EnterUse();
|
||||||
|
return RuntimeCommandStatus.Accepted;
|
||||||
|
}
|
||||||
|
if (_runtime.InventoryOwner.Objects.Get(selected)
|
||||||
|
is not { } item)
|
||||||
|
{
|
||||||
|
return RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowMs = checked((long)Math.Floor(
|
||||||
|
_runtime.Clock.SimulationTimeSeconds * 1000d));
|
||||||
|
if (!_runtime.ActionOwner.Transactions
|
||||||
|
.TryConsumeUseThrottle(nowMs))
|
||||||
|
{
|
||||||
|
return RuntimeCommandStatus.Accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint useability = item.Useability ?? ItemUseability.Undef;
|
||||||
|
if (ItemUseability.IsTargeted(useability))
|
||||||
|
{
|
||||||
|
uint targetId;
|
||||||
|
if (ItemUseability.AllowsSelfTarget(useability))
|
||||||
|
targetId = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
else if (ItemUseability.AllowsObjectSelfTarget(useability))
|
||||||
|
targetId = selected;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_runtime.ActionOwner.Interaction
|
||||||
|
.EnterUseItemOnTarget(selected);
|
||||||
|
return RuntimeCommandStatus.Accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sent = _runtime.ActionOwner.Transactions
|
||||||
|
.TryDispatchTargetedUse(
|
||||||
|
selected,
|
||||||
|
targetId,
|
||||||
|
session.SendUseWithTarget,
|
||||||
|
incrementBusy: true);
|
||||||
|
return sent
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ownedByPlayer = IsOwnedByPlayer(item);
|
||||||
|
ItemUseRequestReservation reservation;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reservation = _runtime.ActionOwner.Transactions
|
||||||
|
.BeginUseRequestReservation();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeInteractionDispatchResult dispatched =
|
||||||
|
_runtime.ActionOwner.Transactions.TryDispatchUse(
|
||||||
|
selected,
|
||||||
|
ownedByPlayer,
|
||||||
|
ownedByPlayer || ItemUseability.IsUseable(useability),
|
||||||
|
reservation,
|
||||||
|
this,
|
||||||
|
out _);
|
||||||
|
return dispatched == RuntimeInteractionDispatchResult.Dispatched
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeCommandStatus PickUpSelected()
|
||||||
|
{
|
||||||
|
if (_runtime.ActionOwner.Selection.SelectedObjectId
|
||||||
|
is not uint selected)
|
||||||
|
{
|
||||||
|
return RuntimeCommandStatus.Accepted;
|
||||||
|
}
|
||||||
|
ClientObject? item =
|
||||||
|
_runtime.InventoryOwner.Objects.Get(selected);
|
||||||
|
if (item is null
|
||||||
|
|| (item.Type & ItemType.Creature) != 0)
|
||||||
|
{
|
||||||
|
return RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint playerGuid = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
if (playerGuid == 0u)
|
||||||
|
return RuntimeCommandStatus.Inactive;
|
||||||
|
uint localEntityId =
|
||||||
|
_runtime.EntityObjects.Entities.TryGetActive(
|
||||||
|
selected,
|
||||||
|
out RuntimeEntityRecord record)
|
||||||
|
? record.LocalEntityId ?? 0u
|
||||||
|
: 0u;
|
||||||
|
var pickup = new RuntimePendingPickup(
|
||||||
|
Token: 0u,
|
||||||
|
selected,
|
||||||
|
localEntityId,
|
||||||
|
playerGuid,
|
||||||
|
Placement: 0,
|
||||||
|
PendingPlacementToken: 0u,
|
||||||
|
ApproachToken: default);
|
||||||
|
return _runtime.ActionOwner.Transactions.TryDispatchPickup(
|
||||||
|
pickup,
|
||||||
|
this,
|
||||||
|
out _)
|
||||||
|
? RuntimeCommandStatus.Accepted
|
||||||
|
: RuntimeCommandStatus.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOwnedByPlayer(ClientObject item)
|
||||||
|
{
|
||||||
|
uint playerGuid = _runtime.PlayerIdentity.ServerGuid;
|
||||||
|
if (playerGuid == 0u)
|
||||||
|
return false;
|
||||||
|
ClientObject current = item;
|
||||||
|
for (int depth = 0; depth < 16; depth++)
|
||||||
|
{
|
||||||
|
if (current.ObjectId == playerGuid
|
||||||
|
|| current.WielderId == playerGuid
|
||||||
|
|| current.ContainerId == playerGuid)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.ContainerId == 0u
|
||||||
|
|| _runtime.InventoryOwner.Objects.Get(
|
||||||
|
current.ContainerId) is not { } parent)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TrySendChannel(
|
||||||
|
WorldSession session,
|
||||||
|
RuntimeChatChannel channel,
|
||||||
|
string text)
|
||||||
|
{
|
||||||
|
if (TryMapTurbine(
|
||||||
|
channel,
|
||||||
|
out ChatChannelKindLite turbineKind,
|
||||||
|
out TurbineChat.ChatType chatType))
|
||||||
|
{
|
||||||
|
TurbineChatState state =
|
||||||
|
_runtime.CommunicationOwner.TurbineChat;
|
||||||
|
uint roomId = state.RoomFor(turbineKind);
|
||||||
|
if (state.Enabled && roomId != 0u)
|
||||||
|
{
|
||||||
|
session.SendTurbineChatTo(
|
||||||
|
roomId,
|
||||||
|
(uint)chatType,
|
||||||
|
(uint)TurbineChat.DispatchType.SendToRoomById,
|
||||||
|
_runtime.PlayerIdentity.ServerGuid,
|
||||||
|
text,
|
||||||
|
state.NextContextId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint? legacyChannel = channel switch
|
||||||
|
{
|
||||||
|
RuntimeChatChannel.Fellowship => 0x00000800u,
|
||||||
|
RuntimeChatChannel.Allegiance => 0x02000000u,
|
||||||
|
RuntimeChatChannel.Vassals => 0x00001000u,
|
||||||
|
RuntimeChatChannel.Patron => 0x00002000u,
|
||||||
|
RuntimeChatChannel.Monarch => 0x00004000u,
|
||||||
|
RuntimeChatChannel.CoVassals => 0x01000000u,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (legacyChannel is not { } channelId)
|
||||||
|
return false;
|
||||||
|
session.SendChannel(channelId, text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryMapTurbine(
|
||||||
|
RuntimeChatChannel channel,
|
||||||
|
out ChatChannelKindLite kind,
|
||||||
|
out TurbineChat.ChatType chatType)
|
||||||
|
{
|
||||||
|
(kind, chatType) = channel switch
|
||||||
|
{
|
||||||
|
RuntimeChatChannel.Allegiance => (
|
||||||
|
ChatChannelKindLite.Allegiance,
|
||||||
|
TurbineChat.ChatType.Allegiance),
|
||||||
|
RuntimeChatChannel.General => (
|
||||||
|
ChatChannelKindLite.General,
|
||||||
|
TurbineChat.ChatType.General),
|
||||||
|
RuntimeChatChannel.Trade => (
|
||||||
|
ChatChannelKindLite.Trade,
|
||||||
|
TurbineChat.ChatType.Trade),
|
||||||
|
RuntimeChatChannel.LookingForGroup => (
|
||||||
|
ChatChannelKindLite.Lfg,
|
||||||
|
TurbineChat.ChatType.Lfg),
|
||||||
|
RuntimeChatChannel.Roleplay => (
|
||||||
|
ChatChannelKindLite.Roleplay,
|
||||||
|
TurbineChat.ChatType.Roleplay),
|
||||||
|
RuntimeChatChannel.Society => (
|
||||||
|
ChatChannelKindLite.Society,
|
||||||
|
TurbineChat.ChatType.Society),
|
||||||
|
RuntimeChatChannel.Olthoi => (
|
||||||
|
ChatChannelKindLite.Olthoi,
|
||||||
|
TurbineChat.ChatType.Olthoi),
|
||||||
|
_ => default,
|
||||||
|
};
|
||||||
|
return channel is RuntimeChatChannel.Allegiance
|
||||||
|
or RuntimeChatChannel.General
|
||||||
|
or RuntimeChatChannel.Trade
|
||||||
|
or RuntimeChatChannel.LookingForGroup
|
||||||
|
or RuntimeChatChannel.Roleplay
|
||||||
|
or RuntimeChatChannel.Society
|
||||||
|
or RuntimeChatChannel.Olthoi;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuntimeCommandResult EmitUnsupported(
|
private RuntimeCommandResult EmitUnsupported(
|
||||||
|
|
@ -367,6 +1041,22 @@ public sealed class DirectGameRuntimeCommandAdapter
|
||||||
return Result(status, primaryObjectId);
|
return Result(status, primaryObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RuntimeCommandResult EmitResult(
|
||||||
|
RuntimeCommandDomain domain,
|
||||||
|
int operation,
|
||||||
|
RuntimeCommandStatus status,
|
||||||
|
uint primaryObjectId = 0u,
|
||||||
|
string? text = null)
|
||||||
|
{
|
||||||
|
_runtime.EventSink.EmitCommand(
|
||||||
|
domain,
|
||||||
|
operation,
|
||||||
|
status,
|
||||||
|
primaryObjectId,
|
||||||
|
text);
|
||||||
|
return Result(status, primaryObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
private RuntimeCommandStatus Validate(
|
private RuntimeCommandStatus Validate(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
out WorldSession? session)
|
out WorldSession? session)
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,60 @@ public sealed class RetailLocalPlayerFrameControllerTests
|
||||||
Assert.Equal(10, projections);
|
Assert.Equal(10, projections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphicalWrapperAndDirectRuntimeFrameProduceIdenticalTrace()
|
||||||
|
{
|
||||||
|
List<string> Run(bool graphical)
|
||||||
|
{
|
||||||
|
PlayerMovementController controller = CreateController();
|
||||||
|
var trace = new List<string>();
|
||||||
|
var host = new TestLocalPlayerFrameRuntime(
|
||||||
|
() => true,
|
||||||
|
() => controller,
|
||||||
|
() => 7u,
|
||||||
|
() => trace.Add("target"),
|
||||||
|
() => false,
|
||||||
|
(_, movement, hidden) =>
|
||||||
|
trace.Add(
|
||||||
|
$"project:{movement.CellId:X8}:"
|
||||||
|
+ $"{movement.ForwardCommand:X8}:"
|
||||||
|
+ $"{movement.ShouldSendMovementEvent}:"
|
||||||
|
+ $"{hidden}"),
|
||||||
|
(_, movement, hidden) =>
|
||||||
|
trace.Add(
|
||||||
|
$"pre:{movement.ForwardCommand:X8}:"
|
||||||
|
+ $"{movement.ShouldSendMovementEvent}:"
|
||||||
|
+ $"{hidden}"),
|
||||||
|
(_, hidden) => trace.Add($"post:{hidden}"),
|
||||||
|
() => RetailObjectClockDisposition.Advance);
|
||||||
|
IMovementInputSource input = Input(
|
||||||
|
() => new MovementInput(
|
||||||
|
Forward: true,
|
||||||
|
Run: true));
|
||||||
|
|
||||||
|
if (graphical)
|
||||||
|
{
|
||||||
|
var frame = new RetailLocalPlayerFrameController(
|
||||||
|
host,
|
||||||
|
input);
|
||||||
|
frame.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
||||||
|
frame.RunPostNetworkCommandPhase();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var frame = new RuntimeLocalPlayerFrameController(
|
||||||
|
host,
|
||||||
|
input);
|
||||||
|
frame.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
|
||||||
|
frame.RunPostNetworkCommandPhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(Run(graphical: true), Run(graphical: false));
|
||||||
|
}
|
||||||
|
|
||||||
private static PlayerMovementController CreateController()
|
private static PlayerMovementController CreateController()
|
||||||
{
|
{
|
||||||
var engine = new PhysicsEngine();
|
var engine = new PhysicsEngine();
|
||||||
|
|
|
||||||
|
|
@ -1261,6 +1261,10 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public void OnPortal(in RuntimePortalDelta delta)
|
public void OnPortal(in RuntimePortalDelta delta)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SelectionQuery(uint target) : IWorldSelectionQuery
|
private sealed class SelectionQuery(uint target) : IWorldSelectionQuery
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using AcDream.App.Spells;
|
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,33 @@ public sealed class HeadlessProcessSchedulerTests
|
||||||
Assert.True(session.Runtime.Session.IsInWorld);
|
Assert.True(session.Runtime.Session.IsInWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IdleDispatchBeforeDeadlineAllocatesNothingAndDoesNotTick()
|
||||||
|
{
|
||||||
|
var time = new ManualTimeProvider();
|
||||||
|
var operations = new FixtureSessionOperations();
|
||||||
|
using HeadlessSessionHost session =
|
||||||
|
CreateSession("idle", time, operations);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSessionStartStatus.Connected,
|
||||||
|
session.Start().Status);
|
||||||
|
var scheduler = new HeadlessProcessScheduler([session], time);
|
||||||
|
|
||||||
|
for (int index = 0; index < 32; index++)
|
||||||
|
Assert.False(scheduler.DispatchDue(time.GetTimestamp()));
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int index = 0; index < 100_000; index++)
|
||||||
|
{
|
||||||
|
if (scheduler.DispatchDue(time.GetTimestamp()))
|
||||||
|
throw new InvalidOperationException("Idle session ticked.");
|
||||||
|
}
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0L, allocated);
|
||||||
|
Assert.Equal(0UL, session.Runtime.Clock.FrameNumber);
|
||||||
|
Assert.Equal(0L, scheduler.CaptureSnapshot().TurnCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessHostRunsMultipleSessionsOnOneScheduler()
|
public async Task ProcessHostRunsMultipleSessionsOnOneScheduler()
|
||||||
{
|
{
|
||||||
|
|
@ -134,7 +161,7 @@ public sealed class HeadlessProcessSchedulerTests
|
||||||
using var host = new HeadlessProcessHost(
|
using var host = new HeadlessProcessHost(
|
||||||
configuration,
|
configuration,
|
||||||
HeadlessPathSet.Resolve(new HeadlessPathOverrides()),
|
HeadlessPathSet.Resolve(new HeadlessPathOverrides()),
|
||||||
new StringReader(
|
new System.IO.StringReader(
|
||||||
"first-password"
|
"first-password"
|
||||||
+ Environment.NewLine
|
+ Environment.NewLine
|
||||||
+ "second-password"
|
+ "second-password"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Headless.Configuration;
|
using AcDream.Headless.Configuration;
|
||||||
using AcDream.Headless.Credentials;
|
using AcDream.Headless.Credentials;
|
||||||
using AcDream.Headless.Diagnostics;
|
using AcDream.Headless.Diagnostics;
|
||||||
using AcDream.Headless.Hosting;
|
using AcDream.Headless.Hosting;
|
||||||
using AcDream.Headless.Platform;
|
using AcDream.Headless.Platform;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
|
||||||
namespace AcDream.Headless.Tests;
|
namespace AcDream.Headless.Tests;
|
||||||
|
|
@ -72,7 +77,8 @@ public sealed class HeadlessSessionHostTests
|
||||||
using var host = new HeadlessProcessHost(
|
using var host = new HeadlessProcessHost(
|
||||||
configuration,
|
configuration,
|
||||||
paths,
|
paths,
|
||||||
new StringReader("process-password" + Environment.NewLine),
|
new System.IO.StringReader(
|
||||||
|
"process-password" + Environment.NewLine),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
operations);
|
operations);
|
||||||
using var cancellation = new CancellationTokenSource();
|
using var cancellation = new CancellationTokenSource();
|
||||||
|
|
@ -88,6 +94,71 @@ public sealed class HeadlessSessionHostTests
|
||||||
diagnostics.ToString());
|
diagnostics.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectFrameUsesSharedRetailOrderAndMovementCadence()
|
||||||
|
{
|
||||||
|
var operations = new FixtureSessionOperations();
|
||||||
|
using var credential = new HeadlessCredentialSecret(
|
||||||
|
"fixture",
|
||||||
|
"password");
|
||||||
|
using var host = new HeadlessSessionHost(
|
||||||
|
Descriptor(),
|
||||||
|
credential,
|
||||||
|
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||||
|
operations);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSessionStartStatus.Connected,
|
||||||
|
host.Start().Status);
|
||||||
|
HydrateGroundedPlayer(host.Runtime);
|
||||||
|
var sent = new List<(byte[] Body, double Time)>();
|
||||||
|
var trace = new RuntimeTraceRecorder();
|
||||||
|
using IDisposable subscription =
|
||||||
|
host.Runtime.Subscribe(trace);
|
||||||
|
operations.Sessions[^1].GameActionCapture =
|
||||||
|
body => sent.Add((
|
||||||
|
body,
|
||||||
|
host.Runtime.Clock.SimulationTimeSeconds));
|
||||||
|
|
||||||
|
RuntimeCommandResult intent = host.Commands.Movement.SetIntent(
|
||||||
|
host.Runtime.Generation,
|
||||||
|
new MovementInput(Forward: true, Run: true));
|
||||||
|
host.Tick(0.015d);
|
||||||
|
|
||||||
|
Assert.True(intent.Accepted);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
static entry =>
|
||||||
|
entry.Kind == RuntimeTraceKind.Movement);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
MoveToState.MoveToStateAction,
|
||||||
|
AutonomousPosition.AutonomousPositionAction,
|
||||||
|
],
|
||||||
|
sent.Select(static entry =>
|
||||||
|
ActionOpcode(entry.Body)).ToArray());
|
||||||
|
|
||||||
|
sent.Clear();
|
||||||
|
for (int index = 0; index < 70; index++)
|
||||||
|
host.Tick(0.015d);
|
||||||
|
|
||||||
|
(byte[] Body, double Time)[] positions = sent
|
||||||
|
.Where(static entry =>
|
||||||
|
ActionOpcode(entry.Body)
|
||||||
|
== AutonomousPosition.AutonomousPositionAction)
|
||||||
|
.ToArray();
|
||||||
|
Assert.InRange(positions.Length, 1, 2);
|
||||||
|
Assert.True(positions[^1].Time >= 1d);
|
||||||
|
if (positions.Length == 2)
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
positions[1].Time - positions[0].Time >= 0.99d);
|
||||||
|
}
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
sent,
|
||||||
|
entry => ActionOpcode(entry.Body)
|
||||||
|
== MoveToState.MoveToStateAction);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TeardownRetriesOnlyTheUnfinishedSuffix()
|
public void TeardownRetriesOnlyTheUnfinishedSuffix()
|
||||||
{
|
{
|
||||||
|
|
@ -142,8 +213,109 @@ public sealed class HeadlessSessionHostTests
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static void HydrateGroundedPlayer(GameRuntime runtime)
|
||||||
|
{
|
||||||
|
const uint player = 0x50000002u;
|
||||||
|
PhysicsEngine engine = runtime.EntityObjects.Physics.Engine;
|
||||||
|
var heights = new byte[81];
|
||||||
|
Array.Fill(heights, (byte)50);
|
||||||
|
var heightTable = new float[256];
|
||||||
|
for (int index = 0; index < heightTable.Length; index++)
|
||||||
|
heightTable[index] = index;
|
||||||
|
engine.AddLandblock(
|
||||||
|
0xA9B4FFFFu,
|
||||||
|
new TerrainSurface(heights, heightTable),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
|
||||||
|
RuntimeEntityRecord record = runtime.EntityObjects
|
||||||
|
.RegisterEntity(Spawn(player))
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||||
|
record,
|
||||||
|
record.CreateIntegrationVersion,
|
||||||
|
record.Snapshot,
|
||||||
|
replaceGeneration: false));
|
||||||
|
var controller = new PlayerMovementController(engine);
|
||||||
|
controller.SetPosition(
|
||||||
|
new Vector3(96f, 97f, 50f),
|
||||||
|
0xA9B40001u,
|
||||||
|
new Vector3(96f, 97f, 50f));
|
||||||
|
runtime.MovementOwner.Controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldSession.EntitySpawn Spawn(uint guid)
|
||||||
|
{
|
||||||
|
var position = new CreateObject.ServerPosition(
|
||||||
|
0xA9B40001u,
|
||||||
|
96f,
|
||||||
|
97f,
|
||||||
|
50f,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
var timestamps = new PhysicsTimestamps(
|
||||||
|
Position: 1,
|
||||||
|
Movement: 1,
|
||||||
|
State: 1,
|
||||||
|
Vector: 1,
|
||||||
|
Teleport: 0,
|
||||||
|
ServerControlledMove: 1,
|
||||||
|
ForcePosition: 0,
|
||||||
|
ObjDesc: 1,
|
||||||
|
Instance: 1);
|
||||||
|
var physics = new PhysicsSpawnData(
|
||||||
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||||
|
Position: position,
|
||||||
|
Movement: null,
|
||||||
|
AnimationFrame: null,
|
||||||
|
SetupTableId: 0x02000001u,
|
||||||
|
MotionTableId: null,
|
||||||
|
SoundTableId: null,
|
||||||
|
PhysicsScriptTableId: null,
|
||||||
|
Parent: null,
|
||||||
|
Children: null,
|
||||||
|
Scale: null,
|
||||||
|
Friction: null,
|
||||||
|
Elasticity: null,
|
||||||
|
Translucency: null,
|
||||||
|
Velocity: null,
|
||||||
|
Acceleration: null,
|
||||||
|
AngularVelocity: null,
|
||||||
|
DefaultScriptType: null,
|
||||||
|
DefaultScriptIntensity: null,
|
||||||
|
Timestamps: timestamps);
|
||||||
|
return new WorldSession.EntitySpawn(
|
||||||
|
guid,
|
||||||
|
position,
|
||||||
|
0x02000001u,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Headless",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
PhysicsState: physics.RawState,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
MovementSequence: 1,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
Physics: physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ActionOpcode(byte[] body) =>
|
||||||
|
BinaryPrimitives.ReadUInt32LittleEndian(
|
||||||
|
body.AsSpan(8, sizeof(uint)));
|
||||||
|
|
||||||
private sealed class FixtureSessionOperations : ILiveSessionOperations
|
private sealed class FixtureSessionOperations : ILiveSessionOperations
|
||||||
{
|
{
|
||||||
|
public List<WorldSession> Sessions { get; } = [];
|
||||||
public int CreatedSessionCount { get; private set; }
|
public int CreatedSessionCount { get; private set; }
|
||||||
public int DisposedSessionCount { get; private set; }
|
public int DisposedSessionCount { get; private set; }
|
||||||
|
|
||||||
|
|
@ -153,7 +325,9 @@ public sealed class HeadlessSessionHostTests
|
||||||
public WorldSession CreateSession(IPEndPoint endpoint)
|
public WorldSession CreateSession(IPEndPoint endpoint)
|
||||||
{
|
{
|
||||||
CreatedSessionCount++;
|
CreatedSessionCount++;
|
||||||
return new WorldSession(endpoint);
|
var session = new WorldSession(endpoint);
|
||||||
|
Sessions.Add(session);
|
||||||
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Connect(
|
public void Connect(
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,7 @@ public sealed class GameRuntimeTests
|
||||||
public void OnChat(in RuntimeChatDelta delta) => Chat.Add(delta);
|
public void OnChat(in RuntimeChatDelta delta) => Chat.Add(delta);
|
||||||
public void OnMovement(in RuntimeMovementDelta delta) { }
|
public void OnMovement(in RuntimeMovementDelta delta) { }
|
||||||
public void OnPortal(in RuntimePortalDelta delta) { }
|
public void OnPortal(in RuntimePortalDelta delta) { }
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class InjectedConstructionFailure : Exception
|
private sealed class InjectedConstructionFailure : Exception
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Net;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
|
public sealed class RuntimeHostileTargetQueryTests
|
||||||
|
{
|
||||||
|
private const uint Player = 0x50000001u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindClosest_UsesAbsoluteDerethCoordinatesAcrossLandblocks()
|
||||||
|
{
|
||||||
|
using GameRuntime runtime = Create();
|
||||||
|
runtime.PlayerIdentity.ServerGuid = Player;
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
Player,
|
||||||
|
landblock: 0x01010001u,
|
||||||
|
x: 191f,
|
||||||
|
y: 100f,
|
||||||
|
PlayerObject(Player));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000010u,
|
||||||
|
landblock: 0x02010001u,
|
||||||
|
x: 1f,
|
||||||
|
y: 100f,
|
||||||
|
Hostile(0x50000010u));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000011u,
|
||||||
|
landblock: 0x01010001u,
|
||||||
|
x: 180f,
|
||||||
|
y: 100f,
|
||||||
|
Hostile(0x50000011u));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
0x50000010u,
|
||||||
|
RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||||
|
Assert.True(
|
||||||
|
RuntimeHostileTargetQuery.IsHostile(
|
||||||
|
runtime,
|
||||||
|
0x50000010u));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Query_RejectsHiddenNoDrawDeadPlayersPetsNpcsAndNonCreatures()
|
||||||
|
{
|
||||||
|
using GameRuntime runtime = Create();
|
||||||
|
runtime.PlayerIdentity.ServerGuid = Player;
|
||||||
|
Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000010u,
|
||||||
|
0x01010001u,
|
||||||
|
11f,
|
||||||
|
10f,
|
||||||
|
Hostile(0x50000010u),
|
||||||
|
PhysicsStateFlags.Hidden);
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000011u,
|
||||||
|
0x01010001u,
|
||||||
|
12f,
|
||||||
|
10f,
|
||||||
|
Hostile(0x50000011u),
|
||||||
|
PhysicsStateFlags.NoDraw);
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000012u,
|
||||||
|
0x01010001u,
|
||||||
|
13f,
|
||||||
|
10f,
|
||||||
|
Hostile(0x50000012u));
|
||||||
|
runtime.ActionOwner.Combat.OnUpdateHealth(0x50000012u, 0f);
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000013u,
|
||||||
|
0x01010001u,
|
||||||
|
14f,
|
||||||
|
10f,
|
||||||
|
PlayerObject(0x50000013u));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000014u,
|
||||||
|
0x01010001u,
|
||||||
|
15f,
|
||||||
|
10f,
|
||||||
|
Hostile(0x50000014u, petOwner: Player));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000015u,
|
||||||
|
0x01010001u,
|
||||||
|
16f,
|
||||||
|
10f,
|
||||||
|
new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x50000015u,
|
||||||
|
Type = ItemType.Misc,
|
||||||
|
PublicWeenieBitfield =
|
||||||
|
SelectedObjectHealthPolicy.BfAttackable,
|
||||||
|
});
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000016u,
|
||||||
|
0x01010001u,
|
||||||
|
17f,
|
||||||
|
10f,
|
||||||
|
new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x50000016u,
|
||||||
|
Type = ItemType.Creature,
|
||||||
|
});
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000017u,
|
||||||
|
0x01010001u,
|
||||||
|
18f,
|
||||||
|
10f,
|
||||||
|
Hostile(0x50000017u));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
0x50000017u,
|
||||||
|
RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||||
|
Assert.False(RuntimeHostileTargetQuery.IsHostile(runtime, 0u));
|
||||||
|
Assert.False(
|
||||||
|
RuntimeHostileTargetQuery.IsHostile(
|
||||||
|
runtime,
|
||||||
|
0x50000010u));
|
||||||
|
Assert.False(
|
||||||
|
RuntimeHostileTargetQuery.IsHostile(
|
||||||
|
runtime,
|
||||||
|
0x50000012u));
|
||||||
|
Assert.False(
|
||||||
|
RuntimeHostileTargetQuery.IsHostile(
|
||||||
|
runtime,
|
||||||
|
0x50000014u));
|
||||||
|
Assert.True(
|
||||||
|
RuntimeHostileTargetQuery.IsHostile(
|
||||||
|
runtime,
|
||||||
|
0x50000017u));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindClosest_ReturnsNullWithoutPlayerPositionOrHostile()
|
||||||
|
{
|
||||||
|
using GameRuntime runtime = Create();
|
||||||
|
runtime.PlayerIdentity.ServerGuid = Player;
|
||||||
|
runtime.InventoryOwner.Objects.AddOrUpdate(PlayerObject(Player));
|
||||||
|
|
||||||
|
Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||||
|
|
||||||
|
Add(runtime, Player, 0x01010001u, 10f, 10f, PlayerObject(Player));
|
||||||
|
Add(
|
||||||
|
runtime,
|
||||||
|
0x50000010u,
|
||||||
|
0x01010001u,
|
||||||
|
11f,
|
||||||
|
10f,
|
||||||
|
new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = 0x50000010u,
|
||||||
|
Type = ItemType.Creature,
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Null(RuntimeHostileTargetQuery.FindClosest(runtime));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameRuntime Create()
|
||||||
|
{
|
||||||
|
var operations = new Operations();
|
||||||
|
return new GameRuntime(new GameRuntimeDependencies(
|
||||||
|
operations,
|
||||||
|
operations,
|
||||||
|
operations,
|
||||||
|
operations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientObject PlayerObject(uint id) => new()
|
||||||
|
{
|
||||||
|
ObjectId = id,
|
||||||
|
Type = ItemType.Creature,
|
||||||
|
PublicWeenieBitfield =
|
||||||
|
SelectedObjectHealthPolicy.BfPlayer,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ClientObject Hostile(
|
||||||
|
uint id,
|
||||||
|
uint petOwner = 0u) => new()
|
||||||
|
{
|
||||||
|
ObjectId = id,
|
||||||
|
Type = ItemType.Creature,
|
||||||
|
PublicWeenieBitfield =
|
||||||
|
SelectedObjectHealthPolicy.BfAttackable,
|
||||||
|
PetOwnerId = petOwner,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void Add(
|
||||||
|
GameRuntime runtime,
|
||||||
|
uint guid,
|
||||||
|
uint landblock,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
ClientObject item,
|
||||||
|
PhysicsStateFlags state = 0)
|
||||||
|
{
|
||||||
|
RuntimeEntityRecord record = runtime.EntityObjects
|
||||||
|
.RegisterEntity(
|
||||||
|
Spawn(guid, landblock, x, y, state))
|
||||||
|
.Canonical!;
|
||||||
|
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||||
|
record,
|
||||||
|
record.CreateIntegrationVersion,
|
||||||
|
record.Snapshot,
|
||||||
|
replaceGeneration: false));
|
||||||
|
runtime.InventoryOwner.Objects.AddOrUpdate(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldSession.EntitySpawn Spawn(
|
||||||
|
uint guid,
|
||||||
|
uint landblock,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
PhysicsStateFlags state)
|
||||||
|
{
|
||||||
|
var position = new CreateObject.ServerPosition(
|
||||||
|
landblock,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
5f,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
var timestamps = new PhysicsTimestamps(
|
||||||
|
Position: 1,
|
||||||
|
Movement: 1,
|
||||||
|
State: 1,
|
||||||
|
Vector: 1,
|
||||||
|
Teleport: 0,
|
||||||
|
ServerControlledMove: 1,
|
||||||
|
ForcePosition: 0,
|
||||||
|
ObjDesc: 1,
|
||||||
|
Instance: 1);
|
||||||
|
var physics = new PhysicsSpawnData(
|
||||||
|
RawState: (uint)state,
|
||||||
|
Position: position,
|
||||||
|
Movement: null,
|
||||||
|
AnimationFrame: null,
|
||||||
|
SetupTableId: 0x02000001u,
|
||||||
|
MotionTableId: null,
|
||||||
|
SoundTableId: null,
|
||||||
|
PhysicsScriptTableId: null,
|
||||||
|
Parent: null,
|
||||||
|
Children: null,
|
||||||
|
Scale: null,
|
||||||
|
Friction: null,
|
||||||
|
Elasticity: null,
|
||||||
|
Translucency: null,
|
||||||
|
Velocity: null,
|
||||||
|
Acceleration: null,
|
||||||
|
AngularVelocity: null,
|
||||||
|
DefaultScriptType: null,
|
||||||
|
DefaultScriptIntensity: null,
|
||||||
|
Timestamps: timestamps);
|
||||||
|
return new WorldSession.EntitySpawn(
|
||||||
|
guid,
|
||||||
|
position,
|
||||||
|
0x02000001u,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
guid.ToString("X8"),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
PhysicsState: physics.RawState,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
MovementSequence: 1,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
Physics: physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Operations :
|
||||||
|
IRuntimeCombatAttackOperations,
|
||||||
|
IRuntimeCombatTargetOperations,
|
||||||
|
IRuntimeCombatModeOperations,
|
||||||
|
IRuntimeSpellCastOperations
|
||||||
|
{
|
||||||
|
public bool CanStartAttack() => false;
|
||||||
|
public void PrepareAttackRequest() { }
|
||||||
|
public bool SendAttack(AttackHeight height, float power) => false;
|
||||||
|
public void SendCancelAttack() { }
|
||||||
|
public bool IsDualWield => false;
|
||||||
|
public bool PlayerReadyForAttack => false;
|
||||||
|
public bool AutoRepeatAttack => false;
|
||||||
|
public bool AutoTarget => false;
|
||||||
|
public uint? SelectClosestTarget() => null;
|
||||||
|
public bool IsInWorld => false;
|
||||||
|
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
|
||||||
|
public void NotifyExplicitCombatModeRequest() { }
|
||||||
|
public void SendChangeCombatMode(CombatMode mode) { }
|
||||||
|
public uint LocalPlayerId => 0u;
|
||||||
|
public bool CanSend => false;
|
||||||
|
public bool HasRequiredComponents(uint spellId) => false;
|
||||||
|
public bool IsTargetCompatible(
|
||||||
|
uint targetId,
|
||||||
|
SpellMetadata spell,
|
||||||
|
bool showMessage) => false;
|
||||||
|
public void StopCompletely() { }
|
||||||
|
public void SendUntargeted(uint spellId) { }
|
||||||
|
public void SendTargeted(uint targetId, uint spellId) { }
|
||||||
|
public void DisplayMessage(string message) { }
|
||||||
|
public void IncrementBusy() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,14 +40,87 @@ public sealed class RuntimeLocalPlayerMovementStateTests
|
||||||
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
|
public void GraphicalAndDirectCommandsMutateOneAutorunLatch()
|
||||||
{
|
{
|
||||||
using var movement = new RuntimeLocalPlayerMovementState();
|
using var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
var input = new MovementInput(
|
||||||
|
Forward: true,
|
||||||
|
Run: true);
|
||||||
|
|
||||||
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
Assert.True(movement.Execute(RuntimeMovementCommand.ToggleRunLock));
|
||||||
|
movement.SetCommandInput(input);
|
||||||
Assert.True(movement.AutoRunActive);
|
Assert.True(movement.AutoRunActive);
|
||||||
Assert.True(movement.View.Snapshot.AutoRunActive);
|
Assert.True(movement.View.Snapshot.AutoRunActive);
|
||||||
|
Assert.True(movement.HasCommandInput);
|
||||||
|
Assert.Equal(input, movement.CommandInput);
|
||||||
|
Assert.Equal(input, movement.View.Snapshot.CommandInput);
|
||||||
|
|
||||||
Assert.True(movement.Execute(RuntimeMovementCommand.Stop));
|
Assert.True(movement.Execute(RuntimeMovementCommand.Stop));
|
||||||
Assert.False(movement.AutoRunActive);
|
Assert.False(movement.AutoRunActive);
|
||||||
Assert.False(movement.View.Snapshot.AutoRunActive);
|
Assert.False(movement.View.Snapshot.AutoRunActive);
|
||||||
|
Assert.False(movement.HasCommandInput);
|
||||||
|
Assert.False(movement.View.Snapshot.HasCommandInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CommandInputIsDeduplicatedAndResetWithSessionIntent()
|
||||||
|
{
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState();
|
||||||
|
var input = new MovementInput(
|
||||||
|
StrafeLeft: true,
|
||||||
|
Run: true);
|
||||||
|
|
||||||
|
movement.SetCommandInput(input);
|
||||||
|
long revision = movement.Revision;
|
||||||
|
movement.SetCommandInput(input);
|
||||||
|
|
||||||
|
Assert.Equal(revision, movement.Revision);
|
||||||
|
Assert.True(movement.Snapshot.HasCommandInput);
|
||||||
|
|
||||||
|
movement.ResetInputIntent();
|
||||||
|
|
||||||
|
Assert.False(movement.HasCommandInput);
|
||||||
|
Assert.Equal(default, movement.CommandInput);
|
||||||
|
Assert.Equal(revision + 1, movement.Revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RuntimeMovementCommand.Ready, MotionCommand.Ready)]
|
||||||
|
[InlineData(RuntimeMovementCommand.Crouch, MotionCommand.Crouch)]
|
||||||
|
[InlineData(RuntimeMovementCommand.Sit, MotionCommand.Sitting)]
|
||||||
|
[InlineData(RuntimeMovementCommand.Sleep, MotionCommand.Sleeping)]
|
||||||
|
public void StanceCommandsUseCanonicalControllerAndEmitOneMovementEdge(
|
||||||
|
RuntimeMovementCommand command,
|
||||||
|
uint expectedMotion)
|
||||||
|
{
|
||||||
|
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||||
|
using var movement = new RuntimeLocalPlayerMovementState
|
||||||
|
{
|
||||||
|
Controller = controller,
|
||||||
|
};
|
||||||
|
movement.SetCommandInput(
|
||||||
|
new MovementInput(Forward: true, Run: true));
|
||||||
|
|
||||||
|
Assert.True(movement.Execute(command));
|
||||||
|
Assert.False(movement.HasCommandInput);
|
||||||
|
Assert.Equal(
|
||||||
|
expectedMotion,
|
||||||
|
controller.Motion.RawState.ForwardCommand);
|
||||||
|
|
||||||
|
MovementResult first = controller.Update(
|
||||||
|
1f / 60f,
|
||||||
|
default);
|
||||||
|
MovementResult second = controller.Update(
|
||||||
|
1f / 60f,
|
||||||
|
default);
|
||||||
|
|
||||||
|
Assert.True(first.ShouldSendMovementEvent);
|
||||||
|
Assert.False(second.ShouldSendMovementEvent);
|
||||||
|
if (command == RuntimeMovementCommand.Ready)
|
||||||
|
{
|
||||||
|
Assert.Null(first.ForwardCommand);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedMotion, first.ForwardCommand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -581,6 +581,8 @@ public sealed class NoWindowGameRuntimeHostTests
|
||||||
throw new InvalidOperationException("observer");
|
throw new InvalidOperationException("observer");
|
||||||
public void OnPortal(in RuntimePortalDelta delta) =>
|
public void OnPortal(in RuntimePortalDelta delta) =>
|
||||||
throw new InvalidOperationException("observer");
|
throw new InvalidOperationException("observer");
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta) =>
|
||||||
|
throw new InvalidOperationException("observer");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn Spawn(
|
private static WorldSession.EntitySpawn Spawn(
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,52 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.Equal(record.FullCellId, snapshot.FullCellId);
|
Assert.Equal(record.FullCellId, snapshot.FullCellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoteVelocityStalenessUsesInjectedRuntimeClock()
|
||||||
|
{
|
||||||
|
var time = new ManualTimeProvider(
|
||||||
|
DateTimeOffset.UnixEpoch.AddSeconds(100d));
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
||||||
|
timeProvider: time);
|
||||||
|
RuntimeEntityRecord record =
|
||||||
|
lifetime.Entities.AddActive(Spawn(0x70000025u, 1));
|
||||||
|
var remote = new RemoteMotion
|
||||||
|
{
|
||||||
|
HasServerVelocity = true,
|
||||||
|
LastServerPosTime = 99d,
|
||||||
|
ServerVelocity = Vector3.UnitX,
|
||||||
|
};
|
||||||
|
remote.Body.Position = new Vector3(10f, 20f, 5f);
|
||||||
|
remote.Body.TransientState = TransientStateFlags.Active
|
||||||
|
| TransientStateFlags.Contact
|
||||||
|
| TransientStateFlags.OnWalkable;
|
||||||
|
lifetime.Physics.SetRemoteMotion(record, remote);
|
||||||
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||||
|
var updater = new RuntimeRemotePhysicsUpdater(lifetime.Physics);
|
||||||
|
Vector3? cycle = null;
|
||||||
|
|
||||||
|
Assert.True(updater.Tick(
|
||||||
|
record,
|
||||||
|
remote,
|
||||||
|
objectScale: 1f,
|
||||||
|
sequencer: null,
|
||||||
|
dt: 0.1f,
|
||||||
|
objectClockEpoch: record.ObjectClockEpoch,
|
||||||
|
new MotionDeltaFrame
|
||||||
|
{
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
},
|
||||||
|
radius: 0.48f,
|
||||||
|
height: 1.835f,
|
||||||
|
liveCenterX: 1,
|
||||||
|
liveCenterY: 1,
|
||||||
|
applyStaleVelocityCycle: value => cycle = value));
|
||||||
|
|
||||||
|
Assert.False(remote.HasServerVelocity);
|
||||||
|
Assert.Equal(Vector3.Zero, remote.ServerVelocity);
|
||||||
|
Assert.Equal(Vector3.Zero, cycle);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PhysicsBodyAcquisitionIsCanonicalAndRejectsGuidReuse()
|
public void PhysicsBodyAcquisitionIsCanonicalAndRejectsGuidReuse()
|
||||||
{
|
{
|
||||||
|
|
@ -551,6 +597,12 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.False(stale.PhysicsBodyAcquisitionInProgress);
|
Assert.False(stale.PhysicsBodyAcquisitionInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ManualTimeProvider(DateTimeOffset utcNow)
|
||||||
|
: TimeProvider
|
||||||
|
{
|
||||||
|
public override DateTimeOffset GetUtcNow() => utcNow;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PhysicsHostIdentityAndLookupBelongToExactRuntimeIncarnation()
|
public void PhysicsHostIdentityAndLookupBelongToExactRuntimeIncarnation()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,14 @@ public sealed class RuntimeGenerationResetTests
|
||||||
Assert.Equal(1, host.DrainCalls);
|
Assert.Equal(1, host.DrainCalls);
|
||||||
Assert.Equal(1, host.CompleteCalls);
|
Assert.Equal(1, host.CompleteCalls);
|
||||||
Assert.All(
|
Assert.All(
|
||||||
observer.Entity.Concat(observer.Inventory),
|
observer.Combat
|
||||||
|
.Concat(observer.Entity)
|
||||||
|
.Concat(observer.Inventory),
|
||||||
stamp => Assert.Equal(retiring, stamp.Generation));
|
stamp => Assert.Equal(retiring, stamp.Generation));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[1UL, 2UL],
|
[1UL, 2UL, 3UL, 4UL, 5UL],
|
||||||
observer.Inventory
|
observer.Combat
|
||||||
|
.Concat(observer.Inventory)
|
||||||
.Concat(observer.Entity)
|
.Concat(observer.Entity)
|
||||||
.OrderBy(static stamp => stamp.Sequence)
|
.OrderBy(static stamp => stamp.Sequence)
|
||||||
.Select(static stamp => stamp.Sequence));
|
.Select(static stamp => stamp.Sequence));
|
||||||
|
|
@ -312,6 +315,7 @@ public sealed class RuntimeGenerationResetTests
|
||||||
{
|
{
|
||||||
public List<RuntimeEventStamp> Entity { get; } = [];
|
public List<RuntimeEventStamp> Entity { get; } = [];
|
||||||
public List<RuntimeEventStamp> Inventory { get; } = [];
|
public List<RuntimeEventStamp> Inventory { get; } = [];
|
||||||
|
public List<RuntimeEventStamp> Combat { get; } = [];
|
||||||
|
|
||||||
public void OnLifecycle(in RuntimeLifecycleDelta delta) { }
|
public void OnLifecycle(in RuntimeLifecycleDelta delta) { }
|
||||||
public void OnCommand(in RuntimeCommandDelta delta) { }
|
public void OnCommand(in RuntimeCommandDelta delta) { }
|
||||||
|
|
@ -322,6 +326,8 @@ public sealed class RuntimeGenerationResetTests
|
||||||
public void OnChat(in RuntimeChatDelta delta) { }
|
public void OnChat(in RuntimeChatDelta delta) { }
|
||||||
public void OnMovement(in RuntimeMovementDelta delta) { }
|
public void OnMovement(in RuntimeMovementDelta delta) { }
|
||||||
public void OnPortal(in RuntimePortalDelta delta) { }
|
public void OnPortal(in RuntimePortalDelta delta) { }
|
||||||
|
public void OnCombat(in RuntimeCombatDelta delta) =>
|
||||||
|
Combat.Add(delta.Stamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Operations :
|
private sealed class Operations :
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,12 @@ public sealed class DirectGameRuntimeCommandAdapterTests
|
||||||
var gameActions = new List<byte[]>();
|
var gameActions = new List<byte[]>();
|
||||||
operations.Sessions[^1].GameActionCapture =
|
operations.Sessions[^1].GameActionCapture =
|
||||||
body => gameActions.Add(body);
|
body => gameActions.Add(body);
|
||||||
|
const uint selectedObject = 0x70000001u;
|
||||||
|
runtime.InventoryOwner.Objects.AddOrUpdate(new ClientObject
|
||||||
|
{
|
||||||
|
ObjectId = selectedObject,
|
||||||
|
Type = ItemType.Misc,
|
||||||
|
});
|
||||||
|
|
||||||
RuntimeCommandResult chat = adapter.Chat.Execute(
|
RuntimeCommandResult chat = adapter.Chat.Execute(
|
||||||
runtime.Generation,
|
runtime.Generation,
|
||||||
|
|
@ -76,9 +82,128 @@ public sealed class DirectGameRuntimeCommandAdapterTests
|
||||||
RuntimeCommandResult portal = adapter.Portal.Execute(
|
RuntimeCommandResult portal = adapter.Portal.Execute(
|
||||||
runtime.Generation,
|
runtime.Generation,
|
||||||
RuntimePortalCommand.RecallLifestone);
|
RuntimePortalCommand.RecallLifestone);
|
||||||
RuntimeCommandResult unsupported = adapter.Movement.Execute(
|
runtime.CommunicationOwner.TurbineChat.OnChannelsReceived(
|
||||||
runtime.Generation,
|
allegianceRoom: 0x10u,
|
||||||
RuntimeMovementCommand.Stop);
|
generalRoom: 0x11u,
|
||||||
|
tradeRoom: 0x12u,
|
||||||
|
lfgRoom: 0x13u,
|
||||||
|
roleplayRoom: 0x14u,
|
||||||
|
olthoiRoom: 0x15u,
|
||||||
|
societyRoom: 0x16u,
|
||||||
|
societyCelestialHandRoom: 0u,
|
||||||
|
societyEldrytchWebRoom: 0u,
|
||||||
|
societyRadiantBloodRoom: 0u);
|
||||||
|
RuntimeCommandResult[] stateAndWireCommands =
|
||||||
|
[
|
||||||
|
adapter.Selection.SelectObject(
|
||||||
|
runtime.Generation,
|
||||||
|
selectedObject),
|
||||||
|
adapter.Selection.Clear(runtime.Generation),
|
||||||
|
adapter.Movement.SetIntent(
|
||||||
|
runtime.Generation,
|
||||||
|
new MovementInput(Forward: true, Run: true)),
|
||||||
|
adapter.Movement.ClearIntent(runtime.Generation),
|
||||||
|
adapter.Movement.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
RuntimeMovementCommand.Stop),
|
||||||
|
adapter.Chat.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeChatCommand(
|
||||||
|
RuntimeChatChannel.Fellowship,
|
||||||
|
"group")),
|
||||||
|
adapter.Chat.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeChatCommand(
|
||||||
|
RuntimeChatChannel.General,
|
||||||
|
"global")),
|
||||||
|
adapter.InventoryState.AddShortcut(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeShortcutCommand(0, 0x70000001u, 0u)),
|
||||||
|
adapter.InventoryState.RemoveShortcut(
|
||||||
|
runtime.Generation,
|
||||||
|
0),
|
||||||
|
adapter.Spellbook.AddFavorite(
|
||||||
|
runtime.Generation,
|
||||||
|
tabIndex: 0,
|
||||||
|
position: 0,
|
||||||
|
spellId: 7u),
|
||||||
|
adapter.Spellbook.RemoveFavorite(
|
||||||
|
runtime.Generation,
|
||||||
|
tabIndex: 0,
|
||||||
|
spellId: 7u),
|
||||||
|
adapter.Spellbook.SetFilter(
|
||||||
|
runtime.Generation,
|
||||||
|
filters: 3u),
|
||||||
|
adapter.Spellbook.ForgetSpell(
|
||||||
|
runtime.Generation,
|
||||||
|
spellId: 7u),
|
||||||
|
adapter.Spellbook.SetDesiredComponent(
|
||||||
|
runtime.Generation,
|
||||||
|
componentId: 11u,
|
||||||
|
amount: 3u),
|
||||||
|
adapter.Spellbook.ClearDesiredComponents(
|
||||||
|
runtime.Generation),
|
||||||
|
adapter.Character.Advance(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind.Attribute,
|
||||||
|
StatId: 1u,
|
||||||
|
Cost: 10u)),
|
||||||
|
adapter.Character.Advance(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind.Vital,
|
||||||
|
StatId: 2u,
|
||||||
|
Cost: 11u)),
|
||||||
|
adapter.Character.Advance(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind.Skill,
|
||||||
|
StatId: 3u,
|
||||||
|
Cost: 12u)),
|
||||||
|
adapter.Character.Advance(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind.TrainSkill,
|
||||||
|
StatId: 4u,
|
||||||
|
Cost: 1u)),
|
||||||
|
adapter.Character.SetOptions1(
|
||||||
|
runtime.Generation,
|
||||||
|
options: 0x1234u),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeFriendCommand(
|
||||||
|
RuntimeFriendCommandKind.Add,
|
||||||
|
Name: "Friend")),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeFriendCommand(
|
||||||
|
RuntimeFriendCommandKind.Remove,
|
||||||
|
CharacterId: 0x50000003u)),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeFriendCommand(
|
||||||
|
RuntimeFriendCommandKind.Clear)),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeSquelchCommand(
|
||||||
|
RuntimeSquelchScope.Character,
|
||||||
|
Add: true,
|
||||||
|
CharacterId: 0x50000004u,
|
||||||
|
Name: "Muted")),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeSquelchCommand(
|
||||||
|
RuntimeSquelchScope.Account,
|
||||||
|
Add: true,
|
||||||
|
Name: "AccountMuted")),
|
||||||
|
adapter.Social.Execute(
|
||||||
|
runtime.Generation,
|
||||||
|
new RuntimeSquelchCommand(
|
||||||
|
RuntimeSquelchScope.Global,
|
||||||
|
Add: true,
|
||||||
|
MessageType: 2u)),
|
||||||
|
];
|
||||||
|
|
||||||
RuntimeSessionStartResult reconnected =
|
RuntimeSessionStartResult reconnected =
|
||||||
adapter.Session.Reconnect(runtime.Generation);
|
adapter.Session.Reconnect(runtime.Generation);
|
||||||
|
|
@ -87,6 +212,10 @@ public sealed class DirectGameRuntimeCommandAdapterTests
|
||||||
new RuntimeChatCommand(
|
new RuntimeChatCommand(
|
||||||
RuntimeChatChannel.Say,
|
RuntimeChatChannel.Say,
|
||||||
"stale"));
|
"stale"));
|
||||||
|
RuntimeCommandResult staleMovement =
|
||||||
|
adapter.Movement.SetIntent(
|
||||||
|
firstGeneration,
|
||||||
|
new MovementInput(Forward: true));
|
||||||
|
|
||||||
Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status);
|
Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|
@ -94,13 +223,19 @@ public sealed class DirectGameRuntimeCommandAdapterTests
|
||||||
reconnected.Status);
|
reconnected.Status);
|
||||||
Assert.True(chat.Accepted);
|
Assert.True(chat.Accepted);
|
||||||
Assert.True(portal.Accepted);
|
Assert.True(portal.Accepted);
|
||||||
Assert.Equal(
|
Assert.All(
|
||||||
RuntimeCommandStatus.Unsupported,
|
stateAndWireCommands,
|
||||||
unsupported.Status);
|
result => Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Accepted,
|
||||||
|
result.Status));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
RuntimeCommandStatus.StaleGeneration,
|
RuntimeCommandStatus.StaleGeneration,
|
||||||
stale.Status);
|
stale.Status);
|
||||||
Assert.Equal(2, gameActions.Count);
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.StaleGeneration,
|
||||||
|
staleMovement.Status);
|
||||||
|
Assert.False(runtime.MovementOwner.HasCommandInput);
|
||||||
|
Assert.True(gameActions.Count >= 20);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
trace.Entries,
|
trace.Entries,
|
||||||
entry => entry.Kind == RuntimeTraceKind.Command
|
entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
|
|
@ -112,6 +247,9 @@ public sealed class DirectGameRuntimeCommandAdapterTests
|
||||||
entry => entry.Kind == RuntimeTraceKind.Command
|
entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
&& (entry.Code >> 16)
|
&& (entry.Code >> 16)
|
||||||
== (int)RuntimeCommandDomain.Portal);
|
== (int)RuntimeCommandDomain.Portal);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
entry => entry.Kind == RuntimeTraceKind.Combat);
|
||||||
|
|
||||||
RuntimeTeardownAcknowledgement stopped =
|
RuntimeTeardownAcknowledgement stopped =
|
||||||
adapter.Session.Stop(runtime.Generation);
|
adapter.Session.Stop(runtime.Generation);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue