fix(headless): complete connected movement gate
This commit is contained in:
parent
fd9559a063
commit
3f3401257c
11 changed files with 756 additions and 48 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Headless.Policies;
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ internal static class HeadlessBotPolicyFactory
|
|||
{
|
||||
"idle" => new IdleHeadlessBotPolicy(),
|
||||
"lifecycle-smoke" => new LifecycleSmokeHeadlessBotPolicy(),
|
||||
"observer-movement" => new ObserverMovementHeadlessBotPolicy(),
|
||||
"portal-route-smoke" => new PortalRouteSmokeHeadlessBotPolicy(),
|
||||
_ => throw new HeadlessConfigurationException(
|
||||
$"Unknown headless bot policy '{id}'."),
|
||||
};
|
||||
|
|
@ -189,3 +192,384 @@ internal sealed class LifecycleSmokeHeadlessBotPolicy
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Long-lived connected observer gate. It materializes at the canonical
|
||||
/// Rynthid checkpoint, announces itself, and alternates run/turn intents
|
||||
/// through the same typed Runtime command API used by graphical hosts. The
|
||||
/// process remains connected until externally cancelled so a second client
|
||||
/// has an unambiguous observation window.
|
||||
/// </summary>
|
||||
internal sealed class ObserverMovementHeadlessBotPolicy
|
||||
: IHeadlessBotPolicy
|
||||
{
|
||||
private const uint RynthidCell = 0xF6820033u;
|
||||
private int _stage;
|
||||
private long _issuedAfterPortalGeneration;
|
||||
private double _movementDeadline;
|
||||
private bool _turning;
|
||||
|
||||
public bool IsComplete => false;
|
||||
|
||||
public void Tick(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(view);
|
||||
ArgumentNullException.ThrowIfNull(commands);
|
||||
|
||||
if (!HasLocalPlayer(view))
|
||||
return;
|
||||
|
||||
if (_stage == 0)
|
||||
{
|
||||
_issuedAfterPortalGeneration =
|
||||
view.Portal.Snapshot.Generation;
|
||||
Require(commands.Chat.Execute(
|
||||
view.Generation,
|
||||
new RuntimeChatCommand(
|
||||
RuntimeChatChannel.Say,
|
||||
"@teleloc 0xF6820033 145.7 49.855 58.010 1 0 0 0")));
|
||||
_stage = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_stage == 1)
|
||||
{
|
||||
RuntimePortalSnapshot portal = view.Portal.Snapshot;
|
||||
if (portal.Generation <= _issuedAfterPortalGeneration
|
||||
|| portal.Kind is not RuntimePortalKind.Portal
|
||||
|| !portal.Completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (portal.DestinationCell != RynthidCell)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Observer gate completed at "
|
||||
+ $"0x{portal.DestinationCell:X8}; expected "
|
||||
+ $"0x{RynthidCell:X8}.");
|
||||
}
|
||||
|
||||
Require(commands.Chat.Execute(
|
||||
view.Generation,
|
||||
new RuntimeChatCommand(
|
||||
RuntimeChatChannel.Say,
|
||||
"[acdream headless observer active]")));
|
||||
StartMovement(view, commands);
|
||||
_stage = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.Clock.SimulationTimeSeconds
|
||||
< _movementDeadline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartMovement(view, commands);
|
||||
}
|
||||
|
||||
public void OnLifecycle(in RuntimeLifecycleDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCommand(in RuntimeCommandDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnEntity(in RuntimeEntityDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnInventory(in RuntimeInventoryDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnChat(in RuntimeChatDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnMovement(in RuntimeMovementDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnPortal(in RuntimePortalDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCombat(in RuntimeCombatDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private void StartMovement(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
{
|
||||
_turning = !_turning;
|
||||
MovementInput input = _turning
|
||||
? new MovementInput(TurnRight: true)
|
||||
: new MovementInput(Forward: true, Run: true);
|
||||
Require(commands.Movement.SetIntent(
|
||||
view.Generation,
|
||||
input));
|
||||
_movementDeadline =
|
||||
view.Clock.SimulationTimeSeconds
|
||||
+ (_turning ? 1.25 : 4.0);
|
||||
}
|
||||
|
||||
private static bool HasLocalPlayer(
|
||||
IGameRuntimeView view) =>
|
||||
view.Lifecycle.State
|
||||
is RuntimeLifecycleState.InWorld
|
||||
&& view.Lifecycle.PlayerGuid != 0u
|
||||
&& view.Entities.TryGet(
|
||||
view.Lifecycle.PlayerGuid,
|
||||
out _);
|
||||
|
||||
private static void Require(
|
||||
in RuntimeCommandResult result)
|
||||
{
|
||||
if (!result.Accepted)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Observer gate command was rejected with "
|
||||
+ $"{result.Status}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicit connected portal-route gate. It visits the same named locations
|
||||
/// used by the canonical graphical soak, then performs a typed lifestone
|
||||
/// recall. A later command is never issued until the preceding portal has
|
||||
/// produced a new completed Runtime portal generation.
|
||||
/// </summary>
|
||||
internal sealed class PortalRouteSmokeHeadlessBotPolicy
|
||||
: IHeadlessBotPolicy
|
||||
{
|
||||
private const uint AerlintheCell = 0x09040008u;
|
||||
private const uint CaulCell = 0xC95B0001u;
|
||||
private const uint RynthidCell = 0xF6820033u;
|
||||
|
||||
private static readonly RouteStop[] Stops =
|
||||
[
|
||||
new(
|
||||
"aerlinthe",
|
||||
"@teleloc 0x09040008 11.4 188.6 87.705 1 0 0 0",
|
||||
AerlintheCell),
|
||||
new(
|
||||
"caul",
|
||||
"@teleloc 0xC95B0001 14.8 0.3 12.005 1 0 0 0",
|
||||
CaulCell),
|
||||
new(
|
||||
"rynthid",
|
||||
"@teleloc 0xF6820033 145.7 49.855 58.010 1 0 0 0",
|
||||
RynthidCell),
|
||||
new("lifestone", null, null),
|
||||
];
|
||||
|
||||
private int _nextStop;
|
||||
private bool _awaitingPortal;
|
||||
private long _issuedAfterPortalGeneration;
|
||||
private RouteMovementStage _movementStage;
|
||||
private double _movementDeadline;
|
||||
|
||||
public bool IsComplete =>
|
||||
_nextStop == Stops.Length
|
||||
&& !_awaitingPortal
|
||||
&& _movementStage is RouteMovementStage.None;
|
||||
|
||||
public void Tick(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(view);
|
||||
ArgumentNullException.ThrowIfNull(commands);
|
||||
|
||||
if (!HasLocalPlayer(view))
|
||||
return;
|
||||
|
||||
if (_awaitingPortal)
|
||||
{
|
||||
RuntimePortalSnapshot portal = view.Portal.Snapshot;
|
||||
if (portal.Generation <= _issuedAfterPortalGeneration
|
||||
|| portal.Kind is not RuntimePortalKind.Portal
|
||||
|| !portal.Completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RouteStop completed = Stops[_nextStop];
|
||||
if (completed.ExpectedCell is uint expectedCell
|
||||
&& portal.DestinationCell != expectedCell)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Portal route '{completed.Name}' completed at "
|
||||
+ $"0x{portal.DestinationCell:X8}; expected "
|
||||
+ $"0x{expectedCell:X8}.");
|
||||
}
|
||||
|
||||
_awaitingPortal = false;
|
||||
StartMovement(
|
||||
view,
|
||||
commands,
|
||||
RouteMovementStage.ForwardOne);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_movementStage is not RouteMovementStage.None)
|
||||
{
|
||||
TickMovement(view, commands);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nextStop == Stops.Length)
|
||||
return;
|
||||
|
||||
RouteStop stop = Stops[_nextStop];
|
||||
_issuedAfterPortalGeneration = view.Portal.Snapshot.Generation;
|
||||
RuntimeCommandResult result = stop.ServerCommand is string text
|
||||
? commands.Chat.Execute(
|
||||
view.Generation,
|
||||
new RuntimeChatCommand(RuntimeChatChannel.Say, text))
|
||||
: commands.Portal.Execute(
|
||||
view.Generation,
|
||||
RuntimePortalCommand.RecallLifestone);
|
||||
Require(result, stop.Name);
|
||||
_awaitingPortal = true;
|
||||
}
|
||||
|
||||
private void TickMovement(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands)
|
||||
{
|
||||
if (view.Clock.SimulationTimeSeconds < _movementDeadline)
|
||||
return;
|
||||
|
||||
switch (_movementStage)
|
||||
{
|
||||
case RouteMovementStage.ForwardOne:
|
||||
StartMovement(
|
||||
view,
|
||||
commands,
|
||||
RouteMovementStage.TurnRight);
|
||||
break;
|
||||
case RouteMovementStage.TurnRight:
|
||||
StartMovement(
|
||||
view,
|
||||
commands,
|
||||
RouteMovementStage.ForwardTwo);
|
||||
break;
|
||||
case RouteMovementStage.ForwardTwo:
|
||||
Require(
|
||||
commands.Movement.ClearIntent(view.Generation),
|
||||
Stops[_nextStop].Name);
|
||||
_movementStage = RouteMovementStage.None;
|
||||
_nextStop++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartMovement(
|
||||
IGameRuntimeView view,
|
||||
IGameRuntimeCommands commands,
|
||||
RouteMovementStage stage)
|
||||
{
|
||||
MovementInput input;
|
||||
double durationSeconds;
|
||||
switch (stage)
|
||||
{
|
||||
case RouteMovementStage.ForwardOne:
|
||||
case RouteMovementStage.ForwardTwo:
|
||||
input = new MovementInput(Forward: true, Run: true);
|
||||
durationSeconds = 4.0;
|
||||
break;
|
||||
case RouteMovementStage.TurnRight:
|
||||
input = new MovementInput(TurnRight: true);
|
||||
durationSeconds = 2.0;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(stage));
|
||||
}
|
||||
|
||||
Require(
|
||||
commands.Movement.SetIntent(view.Generation, input),
|
||||
Stops[_nextStop].Name);
|
||||
_movementStage = stage;
|
||||
_movementDeadline =
|
||||
view.Clock.SimulationTimeSeconds + durationSeconds;
|
||||
}
|
||||
|
||||
public void OnLifecycle(in RuntimeLifecycleDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCommand(in RuntimeCommandDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnEntity(in RuntimeEntityDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnInventory(in RuntimeInventoryDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnChat(in RuntimeChatDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnMovement(in RuntimeMovementDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnPortal(in RuntimePortalDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCombat(in RuntimeCombatDelta delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private static bool HasLocalPlayer(IGameRuntimeView view) =>
|
||||
view.Lifecycle.State is RuntimeLifecycleState.InWorld
|
||||
&& view.Lifecycle.PlayerGuid != 0u
|
||||
&& view.Entities.TryGet(
|
||||
view.Lifecycle.PlayerGuid,
|
||||
out _);
|
||||
|
||||
private static void Require(
|
||||
in RuntimeCommandResult result,
|
||||
string stop)
|
||||
{
|
||||
if (!result.Accepted)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Portal route '{stop}' command was rejected with "
|
||||
+ $"{result.Status}.");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct RouteStop(
|
||||
string Name,
|
||||
string? ServerCommand,
|
||||
uint? ExpectedCell);
|
||||
|
||||
private enum RouteMovementStage
|
||||
{
|
||||
None,
|
||||
ForwardOne,
|
||||
TurnRight,
|
||||
ForwardTwo,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue