using AcDream.App.Combat; using AcDream.App.Input; using AcDream.App.Interaction; using AcDream.App.Net; using AcDream.Core.Selection; using AcDream.Core.Items; using AcDream.Runtime; using AcDream.Runtime.Session; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Input; namespace AcDream.App.Runtime; /// /// Generation-gated synchronous command projection onto the same current /// owners used by graphical input. It introduces no queue or frame boundary. /// internal sealed class CurrentGameRuntimeCommandAdapter : IRuntimeSessionCommands, IRuntimeSelectionCommands, IRuntimeCombatCommands, IRuntimeMovementCommands, IRuntimeChatCommands, IRuntimePortalCommands, IRuntimeInventoryStateCommands, IRuntimeSpellbookCommands, IRuntimeCharacterCommands, IRuntimeSocialCommands { private readonly LiveSessionController _session; private readonly LiveSessionHost _sessionHost; private readonly ICommandBus _commands; 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( LiveSessionController session, LiveSessionHost sessionHost, ICommandBus commands, CurrentGameRuntimeViewAdapter view, SelectionState selectionState, SelectionInteractionController selection, GameplayInputFrameController gameplayInput, ILiveCombatModeCommand combat, ICurrentGameRuntimeEventSink events) { _session = session ?? throw new ArgumentNullException(nameof(session)); _sessionHost = sessionHost ?? throw new ArgumentNullException(nameof(sessionHost)); _commands = commands ?? throw new ArgumentNullException(nameof(commands)); _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); RuntimeSessionStartResult result = _sessionHost.Start(expectedGeneration); _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); RuntimeSessionStartResult result = _sessionHost.Reconnect(expectedGeneration); _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); } RuntimeTeardownAcknowledgement acknowledgement = _sessionHost.Stop(expectedGeneration); _events.EmitCommand( RuntimeCommandDomain.Session, operation: 2, acknowledgement.Status, text: acknowledgement.Error?.GetType().Name ?? string.Empty); _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); } _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); } _commands.Publish(new ExecuteClientCommandCmd( commandId.Value, string.Empty)); _events.EmitCommand( RuntimeCommandDomain.Portal, (int)command, RuntimeCommandStatus.Accepted); return Result(RuntimeCommandStatus.Accepted); } public RuntimeCommandResult AddShortcut( RuntimeGenerationToken expectedGeneration, in RuntimeShortcutCommand command) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if (command.Index < 0 || (command.ObjectId == 0u && command.SpellId == 0u)) { return EmitResult( RuntimeCommandDomain.InventoryState, operation: 0, RuntimeCommandStatus.Rejected, command.ObjectId); } _commands.Publish(new AddShortcutRuntimeCmd(new ShortcutEntry( command.Index, command.ObjectId, command.SpellId))); return EmitResult( RuntimeCommandDomain.InventoryState, operation: 0, RuntimeCommandStatus.Accepted, command.ObjectId); } public RuntimeCommandResult RemoveShortcut( RuntimeGenerationToken expectedGeneration, int index) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if (index < 0) { return EmitResult( RuntimeCommandDomain.InventoryState, operation: 1, RuntimeCommandStatus.Rejected); } _commands.Publish(new RemoveShortcutRuntimeCmd((uint)index)); return EmitResult( RuntimeCommandDomain.InventoryState, operation: 1, RuntimeCommandStatus.Accepted); } public RuntimeCommandResult AddFavorite( RuntimeGenerationToken expectedGeneration, int tabIndex, int position, uint spellId) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if ((uint)tabIndex >= 8u || position < 0 || spellId == 0u) { return EmitResult( RuntimeCommandDomain.Spellbook, operation: 0, RuntimeCommandStatus.Rejected, spellId); } _commands.Publish(new AddFavoriteRuntimeCmd( spellId, position, tabIndex)); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 0, RuntimeCommandStatus.Accepted, spellId); } public RuntimeCommandResult RemoveFavorite( RuntimeGenerationToken expectedGeneration, int tabIndex, uint spellId) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if ((uint)tabIndex >= 8u || spellId == 0u) { return EmitResult( RuntimeCommandDomain.Spellbook, operation: 1, RuntimeCommandStatus.Rejected, spellId); } _commands.Publish(new RemoveFavoriteRuntimeCmd(spellId, tabIndex)); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 1, RuntimeCommandStatus.Accepted, spellId); } public RuntimeCommandResult SetFilter( RuntimeGenerationToken expectedGeneration, uint filters) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); _commands.Publish(new SetSpellbookFilterRuntimeCmd(filters)); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 2, RuntimeCommandStatus.Accepted); } public RuntimeCommandResult ForgetSpell( RuntimeGenerationToken expectedGeneration, uint spellId) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if (spellId == 0u) { return EmitResult( RuntimeCommandDomain.Spellbook, operation: 3, RuntimeCommandStatus.Rejected, spellId); } _commands.Publish(new ForgetSpellRuntimeCmd(spellId)); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 3, RuntimeCommandStatus.Accepted, spellId); } public RuntimeCommandResult SetDesiredComponent( RuntimeGenerationToken expectedGeneration, uint componentId, uint amount) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if (componentId == 0u) { return EmitResult( RuntimeCommandDomain.Spellbook, operation: 4, RuntimeCommandStatus.Rejected, componentId); } _commands.Publish(new SetDesiredComponentRuntimeCmd( componentId, amount)); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 4, RuntimeCommandStatus.Accepted, componentId); } public RuntimeCommandResult ClearDesiredComponents( RuntimeGenerationToken expectedGeneration) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); _commands.Publish(new ClearDesiredComponentsRuntimeCmd()); return EmitResult( RuntimeCommandDomain.Spellbook, operation: 5, RuntimeCommandStatus.Accepted); } public RuntimeCommandResult Advance( RuntimeGenerationToken expectedGeneration, in RuntimeAdvancementCommand command) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); if (command.StatId == 0u || command.Cost == 0u) { return EmitResult( RuntimeCommandDomain.Character, (int)command.Kind, RuntimeCommandStatus.Rejected, command.StatId); } switch (command.Kind) { case RuntimeAdvancementKind.Attribute: _commands.Publish(new RaiseAttributeRuntimeCmd( command.StatId, command.Cost)); break; case RuntimeAdvancementKind.Vital: _commands.Publish(new RaiseVitalRuntimeCmd( command.StatId, command.Cost)); break; case RuntimeAdvancementKind.Skill: _commands.Publish(new RaiseSkillRuntimeCmd( command.StatId, command.Cost)); break; case RuntimeAdvancementKind.TrainSkill when command.Cost <= uint.MaxValue: _commands.Publish(new TrainSkillRuntimeCmd( command.StatId, (uint)command.Cost)); break; default: return EmitResult( RuntimeCommandDomain.Character, (int)command.Kind, RuntimeCommandStatus.Rejected, command.StatId); } return EmitResult( RuntimeCommandDomain.Character, (int)command.Kind, RuntimeCommandStatus.Accepted, command.StatId); } public RuntimeCommandResult SetOptions1( RuntimeGenerationToken expectedGeneration, uint options) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); _commands.Publish(new SetCharacterOptionsRuntimeCmd(options)); return EmitResult( RuntimeCommandDomain.Character, operation: 4, RuntimeCommandStatus.Accepted); } public RuntimeCommandResult Execute( RuntimeGenerationToken expectedGeneration, in RuntimeFriendCommand command) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); RuntimeCommandStatus status = RuntimeCommandStatus.Accepted; switch (command.Kind) { case RuntimeFriendCommandKind.Add when !string.IsNullOrWhiteSpace(command.Name): _commands.Publish(new AddFriendRuntimeCmd(command.Name)); break; case RuntimeFriendCommandKind.Remove when command.CharacterId != 0u: _commands.Publish(new RemoveFriendRuntimeCmd( command.CharacterId)); break; case RuntimeFriendCommandKind.Clear: _commands.Publish(new ClearFriendsRuntimeCmd()); break; case RuntimeFriendCommandKind.RequestLegacyList: _commands.Publish(new RequestLegacyFriendsRuntimeCmd()); break; default: status = RuntimeCommandStatus.Rejected; break; } return EmitResult( RuntimeCommandDomain.Social, (int)command.Kind, status, command.CharacterId, command.Name); } public RuntimeCommandResult Execute( RuntimeGenerationToken expectedGeneration, in RuntimeSquelchCommand command) { RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true); if (gate != RuntimeCommandStatus.Accepted) return Result(gate); RuntimeCommandStatus status = RuntimeCommandStatus.Accepted; switch (command.Scope) { case RuntimeSquelchScope.Character when command.CharacterId != 0u && !string.IsNullOrWhiteSpace(command.Name): _commands.Publish(new ModifyCharacterSquelchRuntimeCmd( command.Add, command.CharacterId, command.Name, command.MessageType)); break; case RuntimeSquelchScope.Account when !string.IsNullOrWhiteSpace(command.Name): _commands.Publish(new ModifyAccountSquelchRuntimeCmd( command.Add, command.Name)); break; case RuntimeSquelchScope.Global: _commands.Publish(new ModifyGlobalSquelchRuntimeCmd( command.Add, command.MessageType)); break; default: status = RuntimeCommandStatus.Rejected; break; } return EmitResult( RuntimeCommandDomain.Social, operation: 4 + (int)command.Scope, status, command.CharacterId, command.Name); } 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 RuntimeCommandResult EmitResult( RuntimeCommandDomain domain, int operation, RuntimeCommandStatus status, uint objectId = 0u, string? text = null) { _events.EmitCommand(domain, operation, status, objectId, text); return Result(status, objectId); } private RuntimeSessionStartResult RejectedStart(RuntimeCommandStatus status) => new( status == RuntimeCommandStatus.StaleGeneration ? RuntimeSessionStartStatus.StaleGeneration : RuntimeSessionStartStatus.Inactive, _view.Generation); 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; } }