feat(runtime): define borrowed views commands and ordered events

Establish the J1 presentation-independent contract with instance-scoped clocks and generations, immutable borrowed views, typed generation-gated commands, normalized ordered diagnostics, and teardown acknowledgements. Route graphical startup plus press-time selection, movement, and combat through focused App adapters over the exact existing owners without adding a queue or mirrored world.

Validated by the Release solution build, 13 Runtime tests, 3,838 App tests with three existing skips, and the complete 8,424-test Release suite with five existing skips.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 19:08:42 +02:00
parent afebbe3eca
commit 854d9e9cd1
21 changed files with 2594 additions and 44 deletions

View file

@ -0,0 +1,394 @@
using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Input;
namespace AcDream.App.Runtime;
/// <summary>
/// Generation-gated synchronous command projection onto the same current
/// owners used by graphical input. It introduces no queue or frame boundary.
/// </summary>
internal sealed class CurrentGameRuntimeCommandAdapter
: IRuntimeSessionCommands,
IRuntimeSelectionCommands,
IRuntimeCombatCommands,
IRuntimeMovementCommands,
IRuntimeChatCommands,
IRuntimePortalCommands
{
private readonly RuntimeOptions _options;
private readonly LiveSessionController _session;
private readonly LiveSessionHost _sessionHost;
private readonly CurrentGameRuntimeViewAdapter _view;
private readonly SelectionState _selectionState;
private readonly SelectionInteractionController _selection;
private readonly GameplayInputFrameController _gameplayInput;
private readonly ILiveCombatModeCommand _combat;
private readonly ICurrentGameRuntimeEventSink _events;
public CurrentGameRuntimeCommandAdapter(
RuntimeOptions options,
LiveSessionController session,
LiveSessionHost sessionHost,
CurrentGameRuntimeViewAdapter view,
SelectionState selectionState,
SelectionInteractionController selection,
GameplayInputFrameController gameplayInput,
ILiveCombatModeCommand combat,
ICurrentGameRuntimeEventSink events)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_session = session ?? throw new ArgumentNullException(nameof(session));
_sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost));
_view = view ?? throw new ArgumentNullException(nameof(view));
_selectionState = selectionState
?? throw new ArgumentNullException(nameof(selectionState));
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_gameplayInput = gameplayInput
?? throw new ArgumentNullException(nameof(gameplayInput));
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
_events = events ?? throw new ArgumentNullException(nameof(events));
}
public RuntimeSessionStartResult Start(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _view.Lifecycle.State;
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: false);
if (gate != RuntimeCommandStatus.Accepted)
return RejectedStart(gate);
LiveSessionStartResult direct = _sessionHost.Start(_options);
RuntimeSessionStartResult result = Convert(direct);
_events.EmitCommand(
RuntimeCommandDomain.Session,
operation: 0,
ToCommandStatus(result.Status),
result.CharacterId,
result.CharacterName);
_events.EmitLifecycle(previous);
return result;
}
public RuntimeSessionStartResult Reconnect(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _view.Lifecycle.State;
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: false);
if (gate != RuntimeCommandStatus.Accepted)
return RejectedStart(gate);
LiveSessionStartResult direct = _sessionHost.Reconnect(_options);
RuntimeSessionStartResult result = Convert(direct);
_events.EmitCommand(
RuntimeCommandDomain.Session,
operation: 1,
ToCommandStatus(result.Status),
result.CharacterId,
result.CharacterName);
_events.EmitLifecycle(previous);
return result;
}
public RuntimeTeardownAcknowledgement Stop(
RuntimeGenerationToken expectedGeneration)
{
RuntimeLifecycleState previous = _view.Lifecycle.State;
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: false);
if (gate != RuntimeCommandStatus.Accepted)
{
return new RuntimeTeardownAcknowledgement(
expectedGeneration,
_view.Generation,
gate,
RuntimeTeardownStage.None);
}
try
{
_session.Stop();
RuntimeGenerationToken current = _view.Generation;
RuntimeTeardownStage completed =
_session.CurrentSession is null && !_session.IsInWorld
? RuntimeTeardownStage.Complete
: RuntimeTeardownStage.CommandsInert
| RuntimeTeardownStage.InboundDetached;
var acknowledgement = new RuntimeTeardownAcknowledgement(
expectedGeneration,
current,
RuntimeCommandStatus.Accepted,
completed);
_events.EmitCommand(
RuntimeCommandDomain.Session,
operation: 2,
RuntimeCommandStatus.Accepted);
_events.EmitLifecycle(previous);
return acknowledgement;
}
catch (Exception error)
{
var acknowledgement = new RuntimeTeardownAcknowledgement(
expectedGeneration,
_view.Generation,
RuntimeCommandStatus.Rejected,
RuntimeTeardownStage.None,
error);
_events.EmitCommand(
RuntimeCommandDomain.Session,
operation: 2,
RuntimeCommandStatus.Rejected,
text: error.GetType().Name);
_events.EmitLifecycle(previous);
return acknowledgement;
}
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeSelectionCommand command)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
InputAction action = command switch
{
RuntimeSelectionCommand.SelectClosestHostile =>
InputAction.SelectionClosestMonster,
RuntimeSelectionCommand.SelectPrevious =>
InputAction.SelectionPreviousSelection,
RuntimeSelectionCommand.ExamineSelected =>
InputAction.SelectionExamine,
RuntimeSelectionCommand.UseSelected =>
InputAction.UseSelected,
RuntimeSelectionCommand.PickUpSelected =>
InputAction.SelectionPickUp,
_ => InputAction.None,
};
RuntimeCommandStatus status = action != InputAction.None
&& _selection.HandleInputAction(action)
? RuntimeCommandStatus.Accepted
: RuntimeCommandStatus.Unsupported;
uint selected = _selectionState.SelectedObjectId ?? 0u;
_events.EmitCommand(
RuntimeCommandDomain.Selection,
(int)command,
status,
selected);
return Result(status, selected);
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeCombatCommand command)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
RuntimeCommandStatus status;
if (command == RuntimeCombatCommand.ToggleMode)
{
_combat.Toggle();
status = RuntimeCommandStatus.Accepted;
}
else
{
status = RuntimeCommandStatus.Unsupported;
}
_events.EmitCommand(RuntimeCommandDomain.Combat, (int)command, status);
return Result(status);
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
RuntimeMovementCommand command)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
InputAction action = command switch
{
RuntimeMovementCommand.ToggleRunLock => InputAction.MovementRunLock,
RuntimeMovementCommand.Stop => InputAction.MovementStop,
_ => InputAction.None,
};
RuntimeCommandStatus status;
if (action == InputAction.None)
{
status = RuntimeCommandStatus.Unsupported;
}
else
{
_gameplayInput.HandlePressedMovementAction(action);
status = RuntimeCommandStatus.Accepted;
}
_events.EmitCommand(RuntimeCommandDomain.Movement, (int)command, status);
return Result(status);
}
public RuntimeCommandResult Execute(
RuntimeGenerationToken expectedGeneration,
in RuntimeChatCommand command)
{
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
if (!TryMap(command.Channel, out ChatChannelKind channel))
{
_events.EmitCommand(
RuntimeCommandDomain.Chat,
(int)command.Channel,
RuntimeCommandStatus.Unsupported);
return Result(RuntimeCommandStatus.Unsupported);
}
_session.Commands.Publish(new SendChatCmd(
channel,
command.TargetName,
command.Text));
_events.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, requireWorld: true);
if (gate != RuntimeCommandStatus.Accepted)
return Result(gate);
ClientCommandId? commandId = command switch
{
RuntimePortalCommand.RecallLifestone =>
ClientCommandId.LifestoneRecall,
RuntimePortalCommand.RecallMarketplace =>
ClientCommandId.MarketplaceRecall,
RuntimePortalCommand.RecallHouse =>
ClientCommandId.HouseRecall,
RuntimePortalCommand.RecallMansion =>
ClientCommandId.MansionRecall,
_ => null,
};
if (commandId is null)
{
_events.EmitCommand(
RuntimeCommandDomain.Portal,
(int)command,
RuntimeCommandStatus.Unsupported);
return Result(RuntimeCommandStatus.Unsupported);
}
_session.Commands.Publish(new ExecuteClientCommandCmd(
commandId.Value,
string.Empty));
_events.EmitCommand(
RuntimeCommandDomain.Portal,
(int)command,
RuntimeCommandStatus.Accepted);
return Result(RuntimeCommandStatus.Accepted);
}
private RuntimeCommandStatus Validate(
RuntimeGenerationToken expectedGeneration,
bool requireWorld)
{
if (!_view.IsActive)
return RuntimeCommandStatus.Inactive;
if (expectedGeneration != _view.Generation)
return RuntimeCommandStatus.StaleGeneration;
if (requireWorld && !_session.IsInWorld)
return RuntimeCommandStatus.Inactive;
return RuntimeCommandStatus.Accepted;
}
private RuntimeCommandResult Result(
RuntimeCommandStatus status,
uint objectId = 0u) =>
new(status, _view.Generation, objectId);
private RuntimeSessionStartResult RejectedStart(RuntimeCommandStatus status) =>
new(
status == RuntimeCommandStatus.StaleGeneration
? RuntimeSessionStartStatus.StaleGeneration
: RuntimeSessionStartStatus.Inactive,
_view.Generation);
private RuntimeSessionStartResult Convert(LiveSessionStartResult result)
{
RuntimeSessionStartStatus status = result.Status switch
{
LiveSessionStartStatus.Disabled => RuntimeSessionStartStatus.Disabled,
LiveSessionStartStatus.MissingCredentials =>
RuntimeSessionStartStatus.MissingCredentials,
LiveSessionStartStatus.NoCharacters =>
RuntimeSessionStartStatus.NoCharacters,
LiveSessionStartStatus.Connected =>
RuntimeSessionStartStatus.Connected,
LiveSessionStartStatus.Deferred =>
RuntimeSessionStartStatus.Deferred,
LiveSessionStartStatus.Failed =>
RuntimeSessionStartStatus.Failed,
_ => throw new ArgumentOutOfRangeException(
nameof(result),
result.Status,
"Unknown live-session start result."),
};
return new RuntimeSessionStartResult(
status,
_view.Generation,
result.Selection?.CharacterId ?? 0u,
result.Selection?.CharacterName ?? string.Empty,
result.Error);
}
private static RuntimeCommandStatus ToCommandStatus(
RuntimeSessionStartStatus status) =>
status switch
{
RuntimeSessionStartStatus.Failed => RuntimeCommandStatus.Rejected,
RuntimeSessionStartStatus.Inactive => RuntimeCommandStatus.Inactive,
RuntimeSessionStartStatus.StaleGeneration =>
RuntimeCommandStatus.StaleGeneration,
_ => RuntimeCommandStatus.Accepted,
};
private static bool TryMap(
RuntimeChatChannel channel,
out ChatChannelKind result)
{
result = channel switch
{
RuntimeChatChannel.Say => ChatChannelKind.Say,
RuntimeChatChannel.Tell => ChatChannelKind.Tell,
RuntimeChatChannel.Fellowship => ChatChannelKind.Fellowship,
RuntimeChatChannel.Allegiance => ChatChannelKind.Allegiance,
RuntimeChatChannel.Vassals => ChatChannelKind.Vassals,
RuntimeChatChannel.Patron => ChatChannelKind.Patron,
RuntimeChatChannel.Monarch => ChatChannelKind.Monarch,
RuntimeChatChannel.CoVassals => ChatChannelKind.CoVassals,
RuntimeChatChannel.General => ChatChannelKind.General,
RuntimeChatChannel.Trade => ChatChannelKind.Trade,
RuntimeChatChannel.LookingForGroup => ChatChannelKind.Lfg,
RuntimeChatChannel.Roleplay => ChatChannelKind.Roleplay,
RuntimeChatChannel.Society => ChatChannelKind.Society,
RuntimeChatChannel.Olthoi => ChatChannelKind.Olthoi,
_ => ChatChannelKind.Unknown,
};
return result != ChatChannelKind.Unknown;
}
}