feat(headless): complete portable single-session host

This commit is contained in:
Erik 2026-07-27 07:36:53 +02:00
parent fbebb91848
commit f8cb840fb1
30 changed files with 3571 additions and 32 deletions

View file

@ -0,0 +1,427 @@
using AcDream.Core.Net;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Session;
/// <summary>
/// Typed command surface for a presentation-free Runtime host. K1 supplies
/// session, local chat, and recall operations; later Slice-K work extends the
/// same adapter rather than introducing a second bot command model.
/// </summary>
public sealed class DirectGameRuntimeCommandAdapter
: IGameRuntimeCommands,
IRuntimeSessionCommands,
IRuntimeSelectionCommands,
IRuntimeCombatCommands,
IRuntimeMagicCommands,
IRuntimeMovementCommands,
IRuntimeChatCommands,
IRuntimePortalCommands,
IRuntimeInventoryStateCommands,
IRuntimeSpellbookCommands,
IRuntimeCharacterCommands,
IRuntimeSocialCommands
{
private sealed class CommandRoute(
DirectGameRuntimeCommandAdapter owner,
WorldSession session,
RuntimeGenerationToken generation)
: ILiveSessionCommandRouting
{
private bool _active;
public void Activate()
{
if (_active)
return;
owner.Activate(session, generation, this);
_active = true;
}
public void Dispose()
{
owner.Deactivate(this);
_active = false;
}
}
private readonly object _gate = new();
private readonly GameRuntime _runtime;
private readonly IRuntimeSessionCommands _sessionCommands;
private WorldSession? _session;
private CommandRoute? _route;
private RuntimeGenerationToken _routeGeneration;
public DirectGameRuntimeCommandAdapter(
GameRuntime runtime,
IRuntimeSessionCommands sessionCommands)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_sessionCommands = sessionCommands
?? throw new ArgumentNullException(nameof(sessionCommands));
}
public IRuntimeSessionCommands Session => this;
public IRuntimeSelectionCommands Selection => this;
public IRuntimeCombatCommands Combat => this;
public IRuntimeMagicCommands Magic => this;
public IRuntimeMovementCommands Movement => this;
public IRuntimeChatCommands Chat => this;
public IRuntimePortalCommands Portal => this;
public IRuntimeInventoryStateCommands InventoryState => this;
public IRuntimeSpellbookCommands Spellbook => this;
public IRuntimeCharacterCommands Character => this;
public IRuntimeSocialCommands Social => this;
public ILiveSessionCommandRouting CreateRoute(WorldSession session)
{
ArgumentNullException.ThrowIfNull(session);
return new CommandRoute(this, session, _runtime.Generation);
}
public RuntimeSessionStartResult Start(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
RuntimeSessionStartResult result =
_sessionCommands.Start(expectedGeneration);
_runtime.EventSink.EmitLifecycle(
previous,
_runtime.Lifecycle.State);
return result;
}
public RuntimeSessionStartResult Reconnect(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
RuntimeSessionStartResult result =
_sessionCommands.Reconnect(expectedGeneration);
_runtime.EventSink.EmitLifecycle(
previous,
_runtime.Lifecycle.State);
return result;
}
public RuntimeTeardownAcknowledgement Stop(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
RuntimeTeardownAcknowledgement result =
_sessionCommands.Stop(expectedGeneration);
_runtime.EventSink.EmitLifecycle(
previous,
_runtime.Lifecycle.State);
return result;
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeChatCommand command)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (string.IsNullOrWhiteSpace(command.Text))
return EmitUnsupported(
RuntimeCommandDomain.Chat,
(int)command.Channel,
RuntimeCommandStatus.Rejected);
switch (command.Channel)
{
case RuntimeChatChannel.Say:
session!.SendTalk(command.Text);
break;
case RuntimeChatChannel.Tell
when !string.IsNullOrWhiteSpace(command.TargetName):
session!.SendTell(command.TargetName, command.Text);
break;
default:
return EmitUnsupported(
RuntimeCommandDomain.Chat,
(int)command.Channel);
}
_runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Chat,
(int)command.Channel,
RuntimeCommandStatus.Accepted,
text: command.Text);
return Result(RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimePortalCommand command)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out WorldSession? session);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
switch (command)
{
case RuntimePortalCommand.RecallLifestone:
session!.SendTeleportToLifestone();
break;
case RuntimePortalCommand.RecallMarketplace:
session!.SendTeleportToMarketplace();
break;
case RuntimePortalCommand.RecallHouse:
session!.SendTeleportToHouse();
break;
case RuntimePortalCommand.RecallMansion:
session!.SendTeleportToMansion();
break;
default:
return EmitUnsupported(
RuntimeCommandDomain.Portal,
(int)command);
}
_runtime.EventSink.EmitCommand(
RuntimeCommandDomain.Portal,
(int)command,
RuntimeCommandStatus.Accepted);
return Result(RuntimeCommandStatus.Accepted);
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeSelectionCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Selection,
(int)command);
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeCombatCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Combat,
(int)command);
public RuntimeCommandResult ExecuteAttack(
RuntimeGenerationToken expectedGeneration,
in RuntimeCombatAttackInput command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Combat,
(int)command.Command);
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeMagicCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Magic,
operation: 0,
command.SpellId);
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeMovementCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Movement,
(int)command);
public RuntimeCommandResult AddShortcut(
RuntimeGenerationToken expectedGeneration,
in RuntimeShortcutCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.InventoryState,
operation: 0,
command.ObjectId);
public RuntimeCommandResult RemoveShortcut(
RuntimeGenerationToken expectedGeneration,
int index) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.InventoryState,
operation: 1);
public RuntimeCommandResult AddFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
int position,
uint spellId) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 0,
spellId);
public RuntimeCommandResult RemoveFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
uint spellId) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 1,
spellId);
public RuntimeCommandResult SetFilter(
RuntimeGenerationToken expectedGeneration,
uint filters) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 2);
public RuntimeCommandResult ForgetSpell(
RuntimeGenerationToken expectedGeneration,
uint spellId) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 3,
spellId);
public RuntimeCommandResult SetDesiredComponent(
RuntimeGenerationToken expectedGeneration,
uint componentId,
uint amount) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 4,
componentId);
public RuntimeCommandResult ClearDesiredComponents(
RuntimeGenerationToken expectedGeneration) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Spellbook,
operation: 5);
public RuntimeCommandResult Advance(
RuntimeGenerationToken expectedGeneration,
in RuntimeAdvancementCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Character,
(int)command.Kind,
command.StatId);
public RuntimeCommandResult SetOptions1(
RuntimeGenerationToken expectedGeneration,
uint options) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Character,
operation: 4);
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeFriendCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Social,
(int)command.Kind,
command.CharacterId);
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeSquelchCommand command) =>
Unsupported(
expectedGeneration,
RuntimeCommandDomain.Social,
(int)command.Scope,
command.CharacterId);
private RuntimeCommandResult Unsupported(
RuntimeGenerationToken expectedGeneration,
RuntimeCommandDomain domain,
int operation,
uint primaryObjectId = 0u)
{
RuntimeCommandStatus gate =
Validate(expectedGeneration, out _);
return gate == RuntimeCommandStatus.Accepted
? EmitUnsupported(
domain,
operation,
RuntimeCommandStatus.Unsupported,
primaryObjectId)
: Result(gate);
}
private RuntimeCommandResult EmitUnsupported(
RuntimeCommandDomain domain,
int operation,
RuntimeCommandStatus status = RuntimeCommandStatus.Unsupported,
uint primaryObjectId = 0u)
{
_runtime.EventSink.EmitCommand(
domain,
operation,
status,
primaryObjectId);
return Result(status, primaryObjectId);
}
private RuntimeCommandStatus Validate(
RuntimeGenerationToken expectedGeneration,
out WorldSession? session)
{
lock (_gate)
{
if (expectedGeneration != _runtime.Generation
|| (_route is not null
&& _routeGeneration != expectedGeneration))
{
session = null;
return RuntimeCommandStatus.StaleGeneration;
}
session = _session;
return _route is not null
&& session is not null
&& _runtime.Session.IsInWorld
? RuntimeCommandStatus.Accepted
: RuntimeCommandStatus.Inactive;
}
}
private RuntimeCommandResult Result(
RuntimeCommandStatus status,
uint objectId = 0u) =>
new(status, _runtime.Generation, objectId);
private void Activate(
WorldSession session,
RuntimeGenerationToken generation,
CommandRoute route)
{
lock (_gate)
{
if (_route is not null)
{
throw new InvalidOperationException(
"A direct Runtime command route is already active.");
}
_session = session;
_routeGeneration = generation;
_route = route;
}
}
private void Deactivate(CommandRoute route)
{
lock (_gate)
{
if (!ReferenceEquals(_route, route))
return;
_route = null;
_session = null;
_routeGeneration = default;
}
}
}