acdream/src/AcDream.Headless/Policies/HeadlessBotPolicy.cs

191 lines
4.9 KiB
C#

using AcDream.Headless.Configuration;
using AcDream.Runtime;
namespace AcDream.Headless.Policies;
internal interface IHeadlessBotPolicy : IRuntimeEventObserver, IDisposable
{
bool IsComplete { get; }
void Tick(IGameRuntimeView view, IGameRuntimeCommands commands);
}
internal static class HeadlessBotPolicyFactory
{
internal static IHeadlessBotPolicy Create(string id) =>
id switch
{
"idle" => new IdleHeadlessBotPolicy(),
"lifecycle-smoke" => new LifecycleSmokeHeadlessBotPolicy(),
_ => throw new HeadlessConfigurationException(
$"Unknown headless bot policy '{id}'."),
};
}
internal sealed class IdleHeadlessBotPolicy : IHeadlessBotPolicy
{
public bool IsComplete => false;
public void Tick(
IGameRuntimeView view,
IGameRuntimeCommands commands)
{
ArgumentNullException.ThrowIfNull(view);
ArgumentNullException.ThrowIfNull(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()
{
}
}
/// <summary>
/// Explicit connected-gate policy: wait for the local player, issue one
/// harmless local-speech command and one lifestone recall, reconnect after
/// the exact portal completes, then terminate after the new generation has
/// materialized the player. It is never selected implicitly.
/// </summary>
internal sealed class LifecycleSmokeHeadlessBotPolicy
: IHeadlessBotPolicy
{
private int _stage;
private ulong _firstGeneration;
public bool IsComplete => _stage == 3;
public void Tick(
IGameRuntimeView view,
IGameRuntimeCommands commands)
{
ArgumentNullException.ThrowIfNull(view);
ArgumentNullException.ThrowIfNull(commands);
switch (_stage)
{
case 0:
if (!HasLocalPlayer(view))
return;
Require(commands.Chat.Execute(
view.Generation,
new RuntimeChatCommand(
RuntimeChatChannel.Say,
"[acdream headless lifecycle gate]")));
Require(commands.Portal.Execute(
view.Generation,
RuntimePortalCommand.RecallLifestone));
_firstGeneration = view.Generation.Value;
_stage = 1;
break;
case 1:
if (view.Portal.Snapshot.Kind
is not RuntimePortalKind.Portal
|| !view.Portal.Snapshot.Completed)
{
return;
}
RuntimeSessionStartResult reconnect =
commands.Session.Reconnect(view.Generation);
if (reconnect.Status
is not (
RuntimeSessionStartStatus.Connected
or RuntimeSessionStartStatus.Deferred))
{
throw new InvalidOperationException(
$"Lifecycle gate reconnect was rejected with {reconnect.Status}.");
}
_stage = 2;
break;
case 2:
if (view.Generation.Value <= _firstGeneration
|| !HasLocalPlayer(view))
{
return;
}
_stage = 3;
break;
}
}
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)
{
if (!result.Accepted)
{
throw new InvalidOperationException(
$"Lifecycle gate command was rejected with {result.Status}.");
}
}
}