191 lines
4.9 KiB
C#
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}.");
|
|
}
|
|
}
|
|
}
|