feat(headless): complete portable single-session host
This commit is contained in:
parent
fbebb91848
commit
f8cb840fb1
30 changed files with 3571 additions and 32 deletions
181
src/AcDream.Headless/Policies/HeadlessBotPolicy.cs
Normal file
181
src/AcDream.Headless/Policies/HeadlessBotPolicy.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
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 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
|
||||
!= RuntimeSessionStartStatus.Connected)
|
||||
{
|
||||
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 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue