Campaign CT slice CT2: the client now learns the character's earned titles and current display title from the server, owns that state in Runtime, and can send a display-title change. No UI (CT3/CT4). Wire (Core.Net): - GameEvents.ParseCharacterTitleTable (0x0029 CharacterTitle): retail CharacterTitleTable::UnPack @0x005c6e90 skips a leading u32 into no field — its own Pack @0x005c6e40 always writes the literal 1 there, matching ACE's unconditional Writer.Write(1u) — then reads displayTitleId, then a count-prefixed PList<uint> of earned ids. - GameEvents.ParseUpdateTitle (0x002B UpdateTitle): titleId + setAsDisplay, per CM_Social::DispatchUI_AddOrSetCharacterTitle @0x006a54c0 -> Handle_Social__AddOrSetCharacterTitle @0x00564260, which ALWAYS adds (SendNotice_AddCharacterTitle, unconditional) and additionally sets display only when setAsDisplay != 0 (SendNotice_SetDisplayCharacterTitle, gated). - SocialActions.BuildTitleSet / WorldSession.SendSetTitle: outbound TitleSet (0x002C), u32 titleId, matching ACE's GameActionSetTitle. - GameEventWiring gains onCharacterTitleTable/onUpdateTitle delegate holes (Core.Net cannot reference AcDream.Runtime directly). Runtime: - New RuntimeCharacterTitleState (RuntimeCharacterState.Titles): earned title id set + display title id, TableReplaced/TitleAdded/ DisplayTitleChanged events matching retail's unconditional-add / gated-display-set contract, clears at generation reset. RuntimeCharacterOwnershipSnapshot/CaptureOwnership/IsConverged and RuntimeCharacterSnapshot extended (trailing optional fields, no existing call site broken). - IRuntimeCharacterCommands.SetTitle: generation-gated, sends TitleSet only — NO optimistic local mutation. Verified against retail's own CM_Social::Event_SetDisplayCharacterTitle @0x006a5720, which sends the wire message and touches no local field; the display title updates only from the server's own echo (the CA-campaign lesson: never re-add an optimistic write). Implemented on both hosts (DirectGameRuntimeCommandAdapter direct-send; CurrentGameRuntimeCommandAdapter via LiveCommandBus / LiveSessionCommandRouter's new SetTitleRuntimeCmd). - LiveSessionEventRouter wires the two inbound events unconditionally (RuntimeCharacterState.Titles is a required child, not an optional sibling like Fellowship/Allegiance). App (non-UI plumbing + resolver): - CharacterTitleResolver (src/AcDream.App/UI/Layout/): ports CharacterTitleTable::GetCharacterTitleFromID @0x005c6ed0 — titleId -> EnumMapper(0x22000041) canonical key -> compute_str_hash -> StringTable(0x2300000E) localized text. Runtime stays id-only; CT3/ CT4 consume this for display. DIDs hardcoded per the RetailKeyNames precedent (CT1 verified them end-to-end). Register: no new row. Retail's send path is non-optimistic and so is ours — no deviation to record for this slice. Tests: wire conformance (byte-exact + truncation) in CharacterTitleEventsTests.cs + SocialActionsTests.cs; Runtime owner unit tests in RuntimeCharacterTitleStateTests.cs plus integration in RuntimeCharacterStateTests.cs; a no-local-mutation command test in DirectGameRuntimeCommandAdapterTests.cs; an InstalledDat pin (CharacterTitleResolverLiveDatTests.cs, ids 0/1/2/3/5/13/14, run green with ACDREAM_RUN_INSTALLED_DAT_TESTS=1). Full solution build green; hermetic filtered suite green (15,380 passed / 0 failed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1160 lines
42 KiB
C#
1160 lines
42 KiB
C#
using AcDream.App.Input;
|
|
using AcDream.App.Interaction;
|
|
using AcDream.App.Net;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Session;
|
|
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,
|
|
IRuntimeMagicCommands,
|
|
IRuntimeMovementCommands,
|
|
IRuntimeChatCommands,
|
|
IRuntimePortalCommands,
|
|
IRuntimeInventoryStateCommands,
|
|
IRuntimeSpellbookCommands,
|
|
IRuntimeCharacterCommands,
|
|
IRuntimeSocialCommands,
|
|
IRuntimeFellowshipCommands,
|
|
IRuntimeAllegianceCommands
|
|
{
|
|
private readonly LiveSessionController _session;
|
|
private readonly LiveSessionHost _sessionHost;
|
|
private readonly ICommandBus _commands;
|
|
private readonly IGameRuntimeView _view;
|
|
private readonly RuntimeInventoryState _inventory;
|
|
private readonly RuntimeCharacterState _character;
|
|
private readonly RuntimeActionState _actions;
|
|
private readonly RuntimeLocalPlayerMovementState _movement;
|
|
private readonly RuntimeFellowshipState _fellowship;
|
|
private readonly SelectionInteractionController _selection;
|
|
private readonly IGameRuntimeEventSink _events;
|
|
|
|
public CurrentGameRuntimeCommandAdapter(
|
|
LiveSessionController session,
|
|
LiveSessionHost sessionHost,
|
|
ICommandBus commands,
|
|
IGameRuntimeView view,
|
|
RuntimeInventoryState inventory,
|
|
RuntimeCharacterState character,
|
|
RuntimeActionState actions,
|
|
RuntimeLocalPlayerMovementState movement,
|
|
RuntimeFellowshipState fellowship,
|
|
SelectionInteractionController selection,
|
|
IGameRuntimeEventSink 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));
|
|
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
|
_character = character ?? throw new ArgumentNullException(nameof(character));
|
|
_actions = actions
|
|
?? throw new ArgumentNullException(nameof(actions));
|
|
_movement = movement
|
|
?? throw new ArgumentNullException(nameof(movement));
|
|
_fellowship = fellowship
|
|
?? throw new ArgumentNullException(nameof(fellowship));
|
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
|
_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, _view.Lifecycle.State);
|
|
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, _view.Lifecycle.State);
|
|
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, _view.Lifecycle.State);
|
|
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 = _actions.Selection.SelectedObjectId ?? 0u;
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Selection,
|
|
(int)command,
|
|
status,
|
|
selected);
|
|
return Result(status, selected);
|
|
}
|
|
|
|
public RuntimeCommandResult SelectObject(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint objectId)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
|
|
RuntimeCommandStatus status =
|
|
objectId != 0u
|
|
&& _inventory.Objects.Get(objectId) is not null
|
|
? RuntimeCommandStatus.Accepted
|
|
: RuntimeCommandStatus.Rejected;
|
|
if (status == RuntimeCommandStatus.Accepted)
|
|
{
|
|
_actions.Selection.Select(
|
|
objectId,
|
|
SelectionChangeSource.Plugin);
|
|
}
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Selection,
|
|
operation: 0x100,
|
|
status,
|
|
objectId);
|
|
return Result(status, objectId);
|
|
}
|
|
|
|
public RuntimeCommandResult Clear(
|
|
RuntimeGenerationToken expectedGeneration)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_actions.Selection.Clear(SelectionChangeSource.Plugin);
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Selection,
|
|
operation: 0x101,
|
|
RuntimeCommandStatus.Accepted);
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
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)
|
|
{
|
|
RuntimeCombatModeRequestResult result =
|
|
_actions.CombatMode.Toggle();
|
|
status = result.Status switch
|
|
{
|
|
RuntimeCombatModeRequestStatus.Sent =>
|
|
RuntimeCommandStatus.Accepted,
|
|
RuntimeCombatModeRequestStatus.Inactive =>
|
|
RuntimeCommandStatus.Inactive,
|
|
_ => RuntimeCommandStatus.Rejected,
|
|
};
|
|
// #260 probe: the toggle's own Inactive exit comes from a
|
|
// DIFFERENT IsInWorld source than Validate's (the operations
|
|
// slot, which reads false when unbound). Logging the result
|
|
// here separates "gate passed, toggle refused" from "gate
|
|
// rejected" — the two look identical to the player.
|
|
if (AcDream.Core.Net.NetDiagnostics.ProbeNet)
|
|
{
|
|
Console.WriteLine(
|
|
$"[cmd-gate] combat toggle result={result.Status}"
|
|
+ $" mode={result.Mode}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
status = RuntimeCommandStatus.Unsupported;
|
|
}
|
|
|
|
_events.EmitCommand(RuntimeCommandDomain.Combat, (int)command, status);
|
|
return Result(status);
|
|
}
|
|
|
|
public RuntimeCommandResult ExecuteAttack(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
in RuntimeCombatAttackInput command)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
|
|
RuntimeCommandStatus status = _actions.CombatAttack.HandleCommand(
|
|
command)
|
|
? RuntimeCommandStatus.Accepted
|
|
: RuntimeCommandStatus.Unsupported;
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Combat,
|
|
operation: 0x100 + (int)command.Command,
|
|
status);
|
|
return Result(status);
|
|
}
|
|
|
|
public RuntimeCommandResult Execute(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
in RuntimeMagicCommand command)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
|
|
CastRequestResult cast = _actions.SpellCast.Cast(command.SpellId);
|
|
RuntimeCommandStatus status = cast == CastRequestResult.Sent
|
|
? RuntimeCommandStatus.Accepted
|
|
: RuntimeCommandStatus.Rejected;
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Magic,
|
|
operation: (int)cast,
|
|
status,
|
|
command.SpellId);
|
|
return Result(status, command.SpellId);
|
|
}
|
|
|
|
public RuntimeCommandResult Execute(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
RuntimeMovementCommand command)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
|
|
RuntimeCommandStatus status = _movement.Execute(command)
|
|
? RuntimeCommandStatus.Accepted
|
|
: RuntimeCommandStatus.Unsupported;
|
|
|
|
_events.EmitCommand(RuntimeCommandDomain.Movement, (int)command, status);
|
|
return Result(status);
|
|
}
|
|
|
|
public RuntimeCommandResult SetIntent(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
in MovementInput input)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_movement.SetCommandInput(input);
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Movement,
|
|
operation: 0x100,
|
|
RuntimeCommandStatus.Accepted);
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult ClearIntent(
|
|
RuntimeGenerationToken expectedGeneration)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(
|
|
expectedGeneration,
|
|
requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_movement.ClearCommandInput();
|
|
_events.EmitCommand(
|
|
RuntimeCommandDomain.Movement,
|
|
operation: 0x101,
|
|
RuntimeCommandStatus.Accepted);
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
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);
|
|
var entry = new ShortcutEntry(
|
|
command.Index,
|
|
command.ObjectId,
|
|
command.SpellId);
|
|
if (!_inventory.TryAddShortcut(
|
|
entry,
|
|
() => _commands.Publish(new AddShortcutRuntimeCmd(entry))))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.InventoryState,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Rejected,
|
|
command.ObjectId);
|
|
}
|
|
|
|
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 (!_inventory.TryRemoveShortcut(
|
|
index,
|
|
() => _commands.Publish(
|
|
new RemoveShortcutRuntimeCmd((uint)index))))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.InventoryState,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Rejected);
|
|
}
|
|
|
|
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 (!_character.TryAddFavorite(
|
|
tabIndex,
|
|
position,
|
|
spellId,
|
|
() => _commands.Publish(new AddFavoriteRuntimeCmd(
|
|
spellId,
|
|
position,
|
|
tabIndex))))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Spellbook,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Rejected,
|
|
spellId);
|
|
}
|
|
|
|
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 (!_character.TryRemoveFavorite(
|
|
tabIndex,
|
|
spellId,
|
|
() => _commands.Publish(
|
|
new RemoveFavoriteRuntimeCmd(spellId, tabIndex))))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Spellbook,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Rejected,
|
|
spellId);
|
|
}
|
|
|
|
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);
|
|
_character.SetSpellbookFilter(
|
|
filters,
|
|
() => _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 (!_character.TrySetDesiredComponent(
|
|
componentId,
|
|
amount,
|
|
() => _commands.Publish(new SetDesiredComponentRuntimeCmd(
|
|
componentId,
|
|
amount))))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Spellbook,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Rejected,
|
|
componentId);
|
|
}
|
|
|
|
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);
|
|
_character.ClearDesiredComponents(
|
|
() => _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 SetSingleOption(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint optionId,
|
|
bool value)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
// OP1 (Campaign OP, 2026-08-10): reject an id outside
|
|
// CharacterOptionTable at THIS seam too — the router's
|
|
// SetSingleCharacterOptionRuntimeCmd handler ultimately funnels into
|
|
// RuntimeCharacterOptionsState.TrySetOption (the SAME shared write-
|
|
// then-send/dirty policy the direct host uses), which would also
|
|
// reject it, but LiveCommandBus.Publish has no return value for that
|
|
// rejection to travel back on.
|
|
if (!CharacterOptionTable.TryGet(optionId, out _))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Character,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Rejected);
|
|
}
|
|
_commands.Publish(new SetSingleCharacterOptionRuntimeCmd(optionId, value));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Character,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult SaveOptions(
|
|
RuntimeGenerationToken expectedGeneration)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_commands.Publish(new SaveCharacterOptionsRuntimeCmd());
|
|
// S4 (OP1 review fix, blast lens, 2026-08-11): objectId 0 — the bus
|
|
// has no return channel to report whether the deferred flush
|
|
// actually fired, so this host reports the SAME shape
|
|
// DirectGameRuntimeCommandAdapter.SaveOptions now does (Accepted,
|
|
// objectId 0) rather than a host-specific encoding.
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Character,
|
|
operation: 5,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult SetTitle(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint titleId)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (titleId == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Character,
|
|
operation: 6,
|
|
RuntimeCommandStatus.Rejected);
|
|
}
|
|
// CT2: NO optimistic local mutation — matches
|
|
// DirectGameRuntimeCommandAdapter.SetTitle; RuntimeCharacterState.
|
|
// Titles updates only from the server's own echo.
|
|
_commands.Publish(new SetTitleRuntimeCmd(titleId));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Character,
|
|
operation: 6,
|
|
RuntimeCommandStatus.Accepted,
|
|
titleId);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ────────
|
|
|
|
public RuntimeCommandResult Create(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
string fellowshipName,
|
|
bool shareXp)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (string.IsNullOrWhiteSpace(fellowshipName))
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Rejected);
|
|
}
|
|
_commands.Publish(new FellowshipCreateRuntimeCmd(fellowshipName, shareXp));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult Recruit(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint targetGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (targetGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Rejected,
|
|
targetGuid);
|
|
}
|
|
_commands.Publish(new FellowshipRecruitRuntimeCmd(targetGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Accepted,
|
|
targetGuid);
|
|
}
|
|
|
|
public RuntimeCommandResult Dismiss(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint targetGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (targetGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 2,
|
|
RuntimeCommandStatus.Rejected,
|
|
targetGuid);
|
|
}
|
|
_commands.Publish(new FellowshipDismissRuntimeCmd(targetGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 2,
|
|
RuntimeCommandStatus.Accepted,
|
|
targetGuid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail's Quit-button leader hand-off (lane B §2.5/§3.6) — see
|
|
/// <see cref="DirectGameRuntimeCommandAdapter.Quit"/>'s identical rule.
|
|
/// </summary>
|
|
public RuntimeCommandResult Quit(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
bool disband)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (_fellowship.RequiresLeaderHandoffBeforeQuit(
|
|
_view.Lifecycle.PlayerGuid,
|
|
disband,
|
|
out uint newLeaderGuid))
|
|
{
|
|
_commands.Publish(new FellowshipAssignNewLeaderRuntimeCmd(newLeaderGuid));
|
|
}
|
|
_commands.Publish(new FellowshipQuitRuntimeCmd(disband));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 3,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult AssignLeader(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint newLeaderGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (newLeaderGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Rejected,
|
|
newLeaderGuid);
|
|
}
|
|
_commands.Publish(new FellowshipAssignNewLeaderRuntimeCmd(newLeaderGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Accepted,
|
|
newLeaderGuid);
|
|
}
|
|
|
|
public RuntimeCommandResult SetOpen(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
bool isOpen)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_commands.Publish(new FellowshipChangeOpennessRuntimeCmd(isOpen));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 5,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult SetPanelOpen(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
bool panelOpen)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_commands.Publish(new FellowshipUpdateRequestRuntimeCmd(panelOpen));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Fellowship,
|
|
operation: 6,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult Swear(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint patronGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (patronGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Rejected,
|
|
patronGuid);
|
|
}
|
|
_commands.Publish(new AllegianceSwearRuntimeCmd(patronGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 0,
|
|
RuntimeCommandStatus.Accepted,
|
|
patronGuid);
|
|
}
|
|
|
|
public RuntimeCommandResult Break(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint targetGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (targetGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Rejected,
|
|
targetGuid);
|
|
}
|
|
_commands.Publish(new AllegianceBreakRuntimeCmd(targetGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 1,
|
|
RuntimeCommandStatus.Accepted,
|
|
targetGuid);
|
|
}
|
|
|
|
public RuntimeCommandResult Kick(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
uint vassalGuid)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
if (vassalGuid == 0u)
|
|
{
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 2,
|
|
RuntimeCommandStatus.Rejected,
|
|
vassalGuid);
|
|
}
|
|
_commands.Publish(new AllegianceKickRuntimeCmd(vassalGuid));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 2,
|
|
RuntimeCommandStatus.Accepted,
|
|
vassalGuid);
|
|
}
|
|
|
|
public RuntimeCommandResult RequestInfo(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
string playerName)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_commands.Publish(new AllegianceInfoRequestRuntimeCmd(playerName ?? string.Empty));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 3,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
public RuntimeCommandResult SetUpdateSubscription(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
bool on)
|
|
{
|
|
RuntimeCommandStatus gate = Validate(expectedGeneration, requireWorld: true);
|
|
if (gate != RuntimeCommandStatus.Accepted)
|
|
return Result(gate);
|
|
_commands.Publish(new AllegianceUpdateRequestRuntimeCmd(on));
|
|
return EmitResult(
|
|
RuntimeCommandDomain.Allegiance,
|
|
operation: 4,
|
|
RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandStatus Validate(
|
|
RuntimeGenerationToken expectedGeneration,
|
|
bool requireWorld)
|
|
{
|
|
RuntimeCommandStatus status;
|
|
if (_view.Lifecycle.State == RuntimeLifecycleState.Disposed)
|
|
status = RuntimeCommandStatus.Inactive;
|
|
else if (expectedGeneration != _view.Generation)
|
|
status = RuntimeCommandStatus.StaleGeneration;
|
|
else if (requireWorld && !_session.IsInWorld)
|
|
status = RuntimeCommandStatus.Inactive;
|
|
else
|
|
status = RuntimeCommandStatus.Accepted;
|
|
|
|
// #260 probe: a rejected generation-gated command is otherwise
|
|
// COMPLETELY silent (no log, no event) — which is exactly how the
|
|
// portal-network wedge hid. Log every rejection with the full gate
|
|
// state so the failing predicate names itself.
|
|
if (status != RuntimeCommandStatus.Accepted
|
|
&& AcDream.Core.Net.NetDiagnostics.ProbeNet)
|
|
{
|
|
Console.WriteLine(
|
|
$"[cmd-gate] REJECT status={status}"
|
|
+ $" expected={expectedGeneration}"
|
|
+ $" view={_view.Generation}"
|
|
+ $" lifecycle={_view.Lifecycle.State}"
|
|
+ $" inWorld={_session.IsInWorld}");
|
|
}
|
|
return status;
|
|
}
|
|
|
|
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.AllegianceBroadcast => ChatChannelKind.AllegianceBroadcast,
|
|
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;
|
|
}
|
|
}
|