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,
|
||||
liveSessionSource);
|
||||
var localPlayerFrame = new RetailLocalPlayerFrameController(
|
||||
d.Runtime,
|
||||
localPlayerFrameRuntime,
|
||||
d.MovementInput);
|
||||
var liveObjectFrame = new LiveObjectFrameController(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ using AcDream.UI.Abstractions.Input;
|
|||
namespace AcDream.App.Input;
|
||||
|
||||
internal interface IMovementInputSource
|
||||
: IRuntimeMovementInputSource
|
||||
{
|
||||
MovementInput Capture();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -53,6 +53,9 @@ internal sealed class DispatcherMovementInputSource : IMovementInputSource
|
|||
if (_capture?.DevToolsWantCaptureKeyboard == true)
|
||||
return default;
|
||||
|
||||
if (_movement.HasCommandInput)
|
||||
return _movement.CommandInput;
|
||||
|
||||
if (_dispatcher is not { } dispatcher)
|
||||
return default;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,12 @@ internal interface ILocalPlayerPresentationRuntime
|
|||
PlayerMovementController? Controller { get; }
|
||||
}
|
||||
|
||||
internal interface ILocalPlayerFrameRuntime : ILocalPlayerPresentationRuntime
|
||||
internal interface ILocalPlayerFrameRuntime :
|
||||
ILocalPlayerPresentationRuntime,
|
||||
IRuntimeLocalPlayerFrameHost
|
||||
{
|
||||
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);
|
||||
bool IRuntimeLocalPlayerFrameHost.CanAdvancePlayer =>
|
||||
CanPresentPlayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Update;
|
||||
using AcDream.Runtime;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
|
|
@ -14,17 +15,7 @@ namespace AcDream.App.Input;
|
|||
/// </remarks>
|
||||
internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFramePhase
|
||||
{
|
||||
private readonly ILocalPlayerFrameRuntime _runtime;
|
||||
private readonly IMovementInputSource _movementInput;
|
||||
|
||||
private AdvancedFrame? _advancedFrame;
|
||||
|
||||
private readonly record struct AdvancedFrame(
|
||||
PlayerMovementController Controller,
|
||||
MovementResult Movement,
|
||||
bool ObjectAdvanced,
|
||||
bool Hidden,
|
||||
bool ObjectQuantumAdvanced);
|
||||
private readonly RuntimeLocalPlayerFrameController _runtime;
|
||||
|
||||
public readonly record struct PresentationFrame(
|
||||
MovementResult Movement,
|
||||
|
|
@ -35,19 +26,29 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
/// Whether the pre-network Hidden player update admitted a complete object
|
||||
/// quantum whose retained CPartArray pose must be sampled this frame.
|
||||
/// </summary>
|
||||
internal bool HiddenPartPoseDirty => _advancedFrame is
|
||||
{
|
||||
Hidden: true,
|
||||
ObjectQuantumAdvanced: true,
|
||||
};
|
||||
internal bool HiddenPartPoseDirty =>
|
||||
_runtime.HiddenPartPoseDirty;
|
||||
|
||||
public RetailLocalPlayerFrameController(
|
||||
ILocalPlayerFrameRuntime runtime,
|
||||
IMovementInputSource movementInput)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_movementInput = movementInput
|
||||
?? throw new ArgumentNullException(nameof(movementInput));
|
||||
_runtime = new RuntimeLocalPlayerFrameController(
|
||||
runtime ?? throw new ArgumentNullException(nameof(runtime)),
|
||||
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>
|
||||
|
|
@ -55,64 +56,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
/// the inbound-network barrier.
|
||||
/// </summary>
|
||||
public void AdvanceBeforeNetwork(float 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);
|
||||
}
|
||||
=> _runtime.AdvanceBeforeNetwork(deltaSeconds);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void 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);
|
||||
}
|
||||
=> _runtime.RunPostNetworkCommandPhase();
|
||||
|
||||
/// <summary>
|
||||
/// Produces the post-network presentation sample without advancing
|
||||
|
|
@ -154,39 +73,15 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
|||
/// </summary>
|
||||
public bool TryGetPresentationAfterNetwork(out PresentationFrame frame)
|
||||
{
|
||||
frame = default;
|
||||
PlayerMovementController? controller = _runtime.Controller;
|
||||
if (!_runtime.CanPresentPlayer || controller is null)
|
||||
return false;
|
||||
|
||||
bool hidden = _runtime.IsHidden;
|
||||
if (_advancedFrame is { } advanced
|
||||
&& ReferenceEquals(advanced.Controller, controller))
|
||||
{
|
||||
MovementResult movement = RefreshSpatialFields(
|
||||
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;
|
||||
bool available = _runtime.TryGetPresentationAfterNetwork(
|
||||
out AcDream.Runtime.Gameplay.RuntimeLocalPlayerPresentationFrame
|
||||
shared);
|
||||
frame = available
|
||||
? new PresentationFrame(
|
||||
shared.Movement,
|
||||
shared.Hidden,
|
||||
shared.AdvancedBeforeNetwork)
|
||||
: default;
|
||||
return available;
|
||||
}
|
||||
|
||||
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.Net;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
|
@ -164,6 +165,51 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
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(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeCombatCommand command)
|
||||
|
|
@ -254,6 +300,39 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
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(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeChatCommand command)
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
public readonly record struct SpellTargetPolicyResult(bool Allowed, string? Message)
|
||||
{
|
||||
public static SpellTargetPolicyResult Accept { get; } = new(true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure projection of ClientMagicSystem::ObjectCompatibleWithSpellTargetType
|
||||
/// (0x00567230). The caller owns target lookup and message presentation.
|
||||
/// </summary>
|
||||
public static class RetailSpellTargetPolicy
|
||||
{
|
||||
private const uint SpecialTargetMask = 0x00008107u;
|
||||
|
||||
public static SpellTargetPolicyResult Evaluate(
|
||||
uint localPlayerId,
|
||||
ClientObject target,
|
||||
SpellMetadata spell)
|
||||
{
|
||||
uint mask = spell.TargetMask;
|
||||
uint special = mask & SpecialTargetMask;
|
||||
if (target.ObjectId == localPlayerId && special == 0u)
|
||||
return new(false, "You cannot cast this spell upon yourself.");
|
||||
if (target.StackSize > 1)
|
||||
return new(false, "Cannot cast spell on a stack of items.");
|
||||
|
||||
if (((uint)target.Type & mask) == 0u && special == 0u)
|
||||
return new(false, $"This spell cannot be cast on {target.Name}.");
|
||||
|
||||
// Retail joins the ordinary type match and the 0x8107 special-mask
|
||||
// bypass before these two gates. IsPlayer is PWD bit 3; otherwise the
|
||||
// object must carry BF_ATTACKABLE. Pets are never legal spell targets.
|
||||
var flags = (PublicWeenieFlags)target.PublicWeenieBitfield.GetValueOrDefault();
|
||||
bool playerOrAttackable = (flags
|
||||
& (PublicWeenieFlags.Player | PublicWeenieFlags.Attackable)) != 0;
|
||||
if (!playerOrAttackable || target.PetOwnerId != 0u)
|
||||
return new(false, $"This spell cannot be cast on {target.Name}.");
|
||||
|
||||
return SpellTargetPolicyResult.Accept;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue