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:
parent
afebbe3eca
commit
854d9e9cd1
21 changed files with 2594 additions and 44 deletions
394
src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs
Normal file
394
src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue