refactor(runtime): expose canonical gameplay state

Move character options and movement skills into the Runtime-owned character graph, expose borrowed inventory, character, and social views, and route retained UI state commands through generation-gated typed Runtime contracts. Preserve the existing synchronous wire path while deleting the App-owned option and skill mirrors and extending normalized parity checkpoints.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 09:12:30 +02:00
parent 9d0d9b07e0
commit dcb61efb5a
34 changed files with 2076 additions and 103 deletions

View file

@ -18,6 +18,7 @@ using AcDream.Core.Items;
using AcDream.Core.Player;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Input;
using AcDream.UI.Abstractions.Panels.Chat;
@ -56,7 +57,6 @@ internal sealed record InteractionRetainedUiDependencies(
ILocalPlayerIdentitySource PlayerIdentity,
ILocalPlayerControllerSource PlayerController,
ILocalPlayerModeSource PlayerMode,
PlayerCharacterOptionsState CharacterOptions,
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
SelectionCameraFactory,
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
@ -118,6 +118,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
private bool _deactivationStarted;
public DeferredLiveSessionUiAuthority Session { get; } = new();
public DeferredGameRuntimeStateCommands GameRuntime { get; } = new();
public DeferredSelectionUiAuthority Selection { get; } = new();
public DeferredSelectionViewPlaneSource SelectionViewPlane { get; } = new();
public DeferredRadarSnapshotSource Radar { get; } = new();
@ -176,6 +177,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
Radar.Deactivate();
SelectionViewPlane.Deactivate();
Selection.Deactivate();
GameRuntime.Deactivate();
Session.Deactivate();
InventoryContainer.Deactivate();
for (int i = _lateOwnerBindings.Count - 1; i >= 0; i--)
@ -294,7 +296,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
sendGive: (target, item, amount) =>
session.CurrentSession?.SendGiveObject(target, item, amount),
dragOnPlayerOpensSecureTrade: () =>
d.CharacterOptions.DragItemOnPlayerOpensSecureTrade,
d.Character.Options.DragItemOnPlayerOpensSecureTrade,
toast: d.Toast,
readyForInventoryRequest: () => session.IsInWorld,
playerOnGround: () =>
@ -375,15 +377,27 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
playerGuid: () => d.PlayerIdentity.ServerGuid,
activeToonName: () => d.Settings.ActiveToonKey,
fallbackSheet: Studio.SampleData.SampleCharacter,
canSendRaise: () => late.Session.IsInWorld,
canSendRaise: () => late.GameRuntime.IsInWorld,
sendRaiseAttribute: (statId, cost) =>
late.Session.CurrentSession?.SendRaiseAttribute(statId, cost),
late.GameRuntime.Advance(
RuntimeAdvancementKind.Attribute,
statId,
cost),
sendRaiseVital: (statId, cost) =>
late.Session.CurrentSession?.SendRaiseVital(statId, cost),
late.GameRuntime.Advance(
RuntimeAdvancementKind.Vital,
statId,
cost),
sendRaiseSkill: (statId, cost) =>
late.Session.CurrentSession?.SendRaiseSkill(statId, cost),
late.GameRuntime.Advance(
RuntimeAdvancementKind.Skill,
statId,
cost),
sendTrainSkill: (statId, credits) =>
late.Session.CurrentSession?.SendTrainSkill(statId, credits));
late.GameRuntime.Advance(
RuntimeAdvancementKind.TrainSkill,
statId,
credits));
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
MagicRuntime magic = MagicRuntime.Create(
@ -543,16 +557,13 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
guid => late.Session.TryUseItem(guid, d.Log),
(tab, position, spellId) =>
late.Session.CurrentSession?.SendAddSpellFavorite(
spellId,
position,
tab),
late.GameRuntime.AddFavorite(tab, position, spellId),
(tab, spellId) =>
late.Session.CurrentSession?.SendRemoveSpellFavorite(spellId, tab),
filters => late.Session.CurrentSession?.SendSpellbookFilter(filters),
spellId => late.Session.CurrentSession?.SendRemoveSpell(spellId),
late.GameRuntime.RemoveFavorite(tab, spellId),
filters => late.GameRuntime.SetSpellbookFilter(filters),
spellId => late.GameRuntime.ForgetSpell(spellId),
(componentId, amount) =>
late.Session.CurrentSession?.SendSetDesiredComponentLevel(
late.GameRuntime.SetDesiredComponent(
componentId,
amount),
d.ClientTime),
@ -589,8 +600,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
d.Inventory.ItemMana,
d.CombatModeCommands.Toggle,
itemInteraction,
entry => late.Session.CurrentSession?.SendAddShortcut(entry),
index => late.Session.CurrentSession?.SendRemoveShortcut(index),
entry => late.GameRuntime.AddShortcut(entry),
index => late.GameRuntime.RemoveShortcut(index),
d.Selection,
handler => d.Combat.HealthChanged += handler,
handler => d.Combat.HealthChanged -= handler,

View file

@ -9,11 +9,186 @@ using AcDream.App.UI.Testing;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Runtime;
using AcDream.UI.Abstractions;
using Silk.NET.Windowing;
namespace AcDream.App.Composition;
/// <summary>
/// Early retained-UI projection over Slice J's later current-runtime seam.
/// Every call captures the view and command owner together and supplies that
/// exact generation, so a displaced session can never receive the command.
/// </summary>
internal sealed class DeferredGameRuntimeStateCommands
{
private readonly object _gate = new();
private IGameRuntimeView? _view;
private IGameRuntimeCommands? _commands;
private bool _deactivated;
public bool IsInWorld
{
get
{
lock (_gate)
return !_deactivated
&& _view?.Lifecycle.State == RuntimeLifecycleState.InWorld;
}
}
public IDisposable Bind(
IGameRuntimeView view,
IGameRuntimeCommands commands)
{
ArgumentNullException.ThrowIfNull(view);
ArgumentNullException.ThrowIfNull(commands);
lock (_gate)
{
ObjectDisposedException.ThrowIf(_deactivated, this);
if (_view is not null || _commands is not null)
{
throw new InvalidOperationException(
"The retained-UI game-runtime command seam is already bound.");
}
_view = view;
_commands = commands;
}
return new ExpectedRuntimeBinding(this, view, commands);
}
public RuntimeCommandResult AddShortcut(ShortcutEntry entry) =>
Invoke((commands, generation) => commands.InventoryState.AddShortcut(
generation,
new RuntimeShortcutCommand(
entry.Index,
entry.ObjectId,
entry.SpellId)));
public RuntimeCommandResult RemoveShortcut(uint index)
{
if (index > int.MaxValue)
return CurrentResult(RuntimeCommandStatus.Rejected);
return Invoke((commands, generation) =>
commands.InventoryState.RemoveShortcut(
generation,
(int)index));
}
public RuntimeCommandResult AddFavorite(
int tab,
int position,
uint spellId) =>
Invoke((commands, generation) => commands.Spellbook.AddFavorite(
generation,
tab,
position,
spellId));
public RuntimeCommandResult RemoveFavorite(int tab, uint spellId) =>
Invoke((commands, generation) => commands.Spellbook.RemoveFavorite(
generation,
tab,
spellId));
public RuntimeCommandResult SetSpellbookFilter(uint filters) =>
Invoke((commands, generation) => commands.Spellbook.SetFilter(
generation,
filters));
public RuntimeCommandResult ForgetSpell(uint spellId) =>
Invoke((commands, generation) => commands.Spellbook.ForgetSpell(
generation,
spellId));
public RuntimeCommandResult SetDesiredComponent(
uint componentId,
uint amount) =>
Invoke((commands, generation) =>
commands.Spellbook.SetDesiredComponent(
generation,
componentId,
amount));
public RuntimeCommandResult Advance(
RuntimeAdvancementKind kind,
uint statId,
ulong cost) =>
Invoke((commands, generation) => commands.Character.Advance(
generation,
new RuntimeAdvancementCommand(kind, statId, cost)));
public void Deactivate()
{
lock (_gate)
{
_deactivated = true;
_view = null;
_commands = null;
}
}
private RuntimeCommandResult Invoke(
Func<
IGameRuntimeCommands,
RuntimeGenerationToken,
RuntimeCommandResult> invoke)
{
IGameRuntimeCommands commands;
RuntimeGenerationToken generation;
lock (_gate)
{
if (_deactivated || _view is null || _commands is null)
{
return new RuntimeCommandResult(
RuntimeCommandStatus.Inactive,
_view?.Generation ?? default);
}
commands = _commands;
generation = _view.Generation;
}
return invoke(commands, generation);
}
private RuntimeCommandResult CurrentResult(RuntimeCommandStatus status)
{
lock (_gate)
{
return new RuntimeCommandResult(
status,
_view?.Generation ?? default);
}
}
private void Release(
IGameRuntimeView expectedView,
IGameRuntimeCommands expectedCommands)
{
lock (_gate)
{
if (!ReferenceEquals(_view, expectedView)
|| !ReferenceEquals(_commands, expectedCommands))
{
return;
}
_view = null;
_commands = null;
}
}
private sealed class ExpectedRuntimeBinding(
DeferredGameRuntimeStateCommands owner,
IGameRuntimeView view,
IGameRuntimeCommands commands)
: IDisposable
{
private DeferredGameRuntimeStateCommands? _owner = owner;
public void Dispose() =>
Interlocked.Exchange(ref _owner, null)?.Release(view, commands);
}
}
/// <summary>
/// Early UI view over the later live-session owner. A binding lease clears only
/// the exact owner it installed, so rollback cannot withdraw a replacement.
@ -394,19 +569,6 @@ internal sealed class DeferredInventoryContainerSource
}
}
internal sealed class PlayerCharacterOptionsState
{
public PlayerDescriptionParser.CharacterOptions1 Options { get; set; } =
PlayerDescriptionParser.CharacterOptions1.Default;
public bool DragItemOnPlayerOpensSecureTrade =>
(Options
& PlayerDescriptionParser.CharacterOptions1
.DragItemOnPlayerOpensSecureTrade) != 0;
public void Reset() => Options = PlayerDescriptionParser.CharacterOptions1.Default;
}
/// <summary>
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.

View file

@ -68,10 +68,8 @@ internal sealed record SessionPlayerDependencies(
LiveWorldOriginState WorldOrigin,
WorldRenderRangeState RenderRange,
LocalPlayerShadowState PlayerShadow,
LocalPlayerSkillState PlayerSkills,
ViewportAspectState ViewportAspect,
PlayerApproachCompletionState PlayerApproachCompletions,
PlayerCharacterOptionsState CharacterOptions,
RuntimeInventoryState Inventory,
PointerPositionState PointerPosition,
DispatcherMovementInputSource MovementInput,
@ -710,7 +708,7 @@ internal sealed class SessionPlayerCompositionPhase
gameplayInput,
liveSessionSource,
d.MovementDiagnostics,
d.PlayerSkills,
d.Character.MovementSkills,
d.ViewportAspect);
if (d.SettingsDevTools.DevTools is { } devTools)
{
@ -796,9 +794,7 @@ internal sealed class SessionPlayerCompositionPhase
new LiveSessionPlayerRuntime(
d.PlayerIdentity,
d.PlayerController,
d.PlayerSkills,
d.WorldOrigin,
d.CharacterOptions),
d.WorldOrigin),
new LiveSessionDomainRuntime(
d.EntityObjects,
d.Character,
@ -876,6 +872,8 @@ internal sealed class SessionPlayerCompositionPhase
d.PlayerIdentity,
live.LiveEntities,
d.EntityObjects,
d.Inventory,
d.Character,
d.Communication,
d.PlayerController,
worldReveal,
@ -885,6 +883,9 @@ internal sealed class SessionPlayerCompositionPhase
gameplayInput,
combatCommand);
bindings.Adopt("current game runtime adapter", gameRuntime);
bindings.Adopt(
"retained-UI game runtime commands",
interaction.LateBindings.GameRuntime.Bind(gameRuntime, gameRuntime));
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
new RuntimeNearbyWorldDiagnosticSource(

View file

@ -140,38 +140,21 @@ internal sealed class LocalPlayerModeState : ILocalPlayerModeSource
/// delivery and player-mode construction share this owner so rebuilding the
/// local physics controller cannot fall back to stale defaults.
/// </summary>
internal sealed class LocalPlayerSkillState
internal static class LocalPlayerSkillProjection
{
public int RunSkill { get; private set; } = -1;
public int JumpSkill { get; private set; } = -1;
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
public void Update(
int runSkill,
int jumpSkill,
public static bool ApplyTo(
AcDream.Runtime.Gameplay.RuntimeMovementSkillState skills,
PlayerMovementController? controller)
{
if (runSkill >= 0)
RunSkill = runSkill;
if (jumpSkill >= 0)
JumpSkill = jumpSkill;
ApplyTo(controller);
}
public bool ApplyTo(PlayerMovementController? controller)
{
if (controller is null || !IsComplete)
ArgumentNullException.ThrowIfNull(skills);
AcDream.Runtime.Gameplay.RuntimeMovementSkillSnapshot snapshot =
skills.Snapshot;
if (controller is null || !snapshot.IsComplete)
return false;
controller.SetCharacterSkills(RunSkill, JumpSkill);
controller.SetCharacterSkills(snapshot.RunSkill, snapshot.JumpSkill);
return true;
}
public void ResetSession()
{
RunSkill = -1;
JumpSkill = -1;
}
}
internal interface IViewportAspectSource

View file

@ -9,6 +9,7 @@ using AcDream.Content;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.Input;
@ -41,7 +42,7 @@ internal sealed class PlayerModeController :
private readonly ILocalPlayerTeleportInputLifetime _input;
private readonly ILiveInWorldSource _session;
private readonly MovementTruthDiagnosticController _movementDiagnostics;
private readonly LocalPlayerSkillState _skills;
private readonly RuntimeMovementSkillState _skills;
private readonly IViewportAspectSource _viewport;
private PlayerModeAutoEntry? _autoEntry;
private IPlayerApproachCompletionSink? _approachLifetime;
@ -67,7 +68,7 @@ internal sealed class PlayerModeController :
ILocalPlayerTeleportInputLifetime input,
ILiveInWorldSource session,
MovementTruthDiagnosticController movementDiagnostics,
LocalPlayerSkillState skills,
RuntimeMovementSkillState skills,
IViewportAspectSource viewport)
{
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
@ -359,7 +360,7 @@ internal sealed class PlayerModeController :
exactMovement.CancelMoveTo(WeenieError.ActionCancelled);
};
if (_skills.ApplyTo(controller))
if (LocalPlayerSkillProjection.ApplyTo(_skills, controller))
{
Console.WriteLine(
$"live: {loggingTag} — applied server skills "

View file

@ -1,5 +1,6 @@
using AcDream.App.UI;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Runtime.Session;
using AcDream.UI.Abstractions;
@ -15,8 +16,64 @@ internal sealed record LiveSessionCommandBindings(
Action<string, string> SendTell,
Action<uint, string> SendChannel,
Action<uint, uint, uint, uint, string, uint> SendTurbineChat,
Action<ShortcutEntry> AddShortcut,
Action<uint> RemoveShortcut,
Action<uint, int, int> AddFavorite,
Action<uint, int> RemoveFavorite,
Action<uint> SetSpellbookFilter,
Action<uint> ForgetSpell,
Action<uint, uint> SetDesiredComponent,
Action ClearDesiredComponents,
Action<uint, ulong> RaiseAttribute,
Action<uint, ulong> RaiseVital,
Action<uint, ulong> RaiseSkill,
Action<uint, uint> TrainSkill,
Action<uint> SetCharacterOptions,
Action<string> AddFriend,
Action<uint> RemoveFriend,
Action ClearFriends,
Action RequestLegacyFriends,
Action<bool, uint, string, uint> ModifyCharacterSquelch,
Action<bool, string> ModifyAccountSquelch,
Action<bool, uint> ModifyGlobalSquelch,
Action<string>? Log = null);
internal readonly record struct AddShortcutRuntimeCmd(ShortcutEntry Entry);
internal readonly record struct RemoveShortcutRuntimeCmd(uint Index);
internal readonly record struct AddFavoriteRuntimeCmd(
uint SpellId,
int Position,
int TabIndex);
internal readonly record struct RemoveFavoriteRuntimeCmd(
uint SpellId,
int TabIndex);
internal readonly record struct SetSpellbookFilterRuntimeCmd(uint Filters);
internal readonly record struct ForgetSpellRuntimeCmd(uint SpellId);
internal readonly record struct SetDesiredComponentRuntimeCmd(
uint ComponentId,
uint Amount);
internal readonly record struct ClearDesiredComponentsRuntimeCmd;
internal readonly record struct RaiseAttributeRuntimeCmd(uint StatId, ulong Cost);
internal readonly record struct RaiseVitalRuntimeCmd(uint StatId, ulong Cost);
internal readonly record struct RaiseSkillRuntimeCmd(uint StatId, ulong Cost);
internal readonly record struct TrainSkillRuntimeCmd(uint StatId, uint Cost);
internal readonly record struct SetCharacterOptionsRuntimeCmd(uint Options);
internal readonly record struct AddFriendRuntimeCmd(string Name);
internal readonly record struct RemoveFriendRuntimeCmd(uint CharacterId);
internal readonly record struct ClearFriendsRuntimeCmd;
internal readonly record struct RequestLegacyFriendsRuntimeCmd;
internal readonly record struct ModifyCharacterSquelchRuntimeCmd(
bool Add,
uint CharacterId,
string Name,
uint MessageType);
internal readonly record struct ModifyAccountSquelchRuntimeCmd(
bool Add,
string Name);
internal readonly record struct ModifyGlobalSquelchRuntimeCmd(
bool Add,
uint MessageType);
/// <summary>
/// Owns the command surface for one exact live-session generation. The router
/// itself is the published bus, so a retained reference becomes inert before
@ -52,6 +109,68 @@ internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting
SendIfActive(() => bindings.SendTalk(command.Text));
});
commands.Register<SendChatCmd>(command => RouteChat(bindings, command));
commands.Register<AddShortcutRuntimeCmd>(
command => SendIfActive(() => bindings.AddShortcut(command.Entry)));
commands.Register<RemoveShortcutRuntimeCmd>(
command => SendIfActive(() => bindings.RemoveShortcut(command.Index)));
commands.Register<AddFavoriteRuntimeCmd>(
command => SendIfActive(() => bindings.AddFavorite(
command.SpellId,
command.Position,
command.TabIndex)));
commands.Register<RemoveFavoriteRuntimeCmd>(
command => SendIfActive(() => bindings.RemoveFavorite(
command.SpellId,
command.TabIndex)));
commands.Register<SetSpellbookFilterRuntimeCmd>(
command => SendIfActive(() =>
bindings.SetSpellbookFilter(command.Filters)));
commands.Register<ForgetSpellRuntimeCmd>(
command => SendIfActive(() => bindings.ForgetSpell(command.SpellId)));
commands.Register<SetDesiredComponentRuntimeCmd>(
command => SendIfActive(() => bindings.SetDesiredComponent(
command.ComponentId,
command.Amount)));
commands.Register<ClearDesiredComponentsRuntimeCmd>(
_ => SendIfActive(bindings.ClearDesiredComponents));
commands.Register<RaiseAttributeRuntimeCmd>(
command => SendIfActive(() =>
bindings.RaiseAttribute(command.StatId, command.Cost)));
commands.Register<RaiseVitalRuntimeCmd>(
command => SendIfActive(() =>
bindings.RaiseVital(command.StatId, command.Cost)));
commands.Register<RaiseSkillRuntimeCmd>(
command => SendIfActive(() =>
bindings.RaiseSkill(command.StatId, command.Cost)));
commands.Register<TrainSkillRuntimeCmd>(
command => SendIfActive(() =>
bindings.TrainSkill(command.StatId, command.Cost)));
commands.Register<SetCharacterOptionsRuntimeCmd>(
command => SendIfActive(() =>
bindings.SetCharacterOptions(command.Options)));
commands.Register<AddFriendRuntimeCmd>(
command => SendIfActive(() => bindings.AddFriend(command.Name)));
commands.Register<RemoveFriendRuntimeCmd>(
command => SendIfActive(() =>
bindings.RemoveFriend(command.CharacterId)));
commands.Register<ClearFriendsRuntimeCmd>(
_ => SendIfActive(bindings.ClearFriends));
commands.Register<RequestLegacyFriendsRuntimeCmd>(
_ => SendIfActive(bindings.RequestLegacyFriends));
commands.Register<ModifyCharacterSquelchRuntimeCmd>(
command => SendIfActive(() => bindings.ModifyCharacterSquelch(
command.Add,
command.CharacterId,
command.Name,
command.MessageType)));
commands.Register<ModifyAccountSquelchRuntimeCmd>(
command => SendIfActive(() => bindings.ModifyAccountSquelch(
command.Add,
command.Name)));
commands.Register<ModifyGlobalSquelchRuntimeCmd>(
command => SendIfActive(() => bindings.ModifyGlobalSquelch(
command.Add,
command.MessageType)));
_commands = commands;
}

View file

@ -36,9 +36,7 @@ namespace AcDream.App.Net;
internal sealed record LiveSessionPlayerRuntime(
LocalPlayerIdentityState Identity,
LocalPlayerControllerSlot Controller,
LocalPlayerSkillState Skills,
LiveWorldOriginState WorldOrigin,
PlayerCharacterOptionsState CharacterOptions);
LiveWorldOriginState WorldOrigin);
internal sealed record LiveSessionDomainRuntime(
RuntimeEntityObjectLifetime EntityObjects,
@ -215,8 +213,8 @@ internal sealed class LiveSessionRuntimeFactory
_domain.Communication.ResetChatIdentity();
EntityVanishProbe.PlayerGuid = 0u;
_interaction.Settings.ResetActiveCharacterKey();
_player.CharacterOptions.Reset();
_player.Skills.ResetSession();
_domain.Character.Options.ResetSession();
_domain.Character.MovementSkills.ResetSession();
_world.NetworkUpdates.ResetSessionState();
_world.Hydration.ResetSessionState();
_domain.Inventory.ResetPlayerSnapshots();
@ -282,27 +280,22 @@ internal sealed class LiveSessionRuntimeFactory
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
OnSkillsUpdated: (runSkill, jumpSkill) =>
{
_player.Skills.Update(
runSkill,
jumpSkill,
_player.Controller.Controller);
if (_player.Skills.IsComplete
&& _player.Controller.Controller is not null)
if (LocalPlayerSkillProjection.ApplyTo(
_domain.Character.MovementSkills,
_player.Controller.Controller))
{
RuntimeMovementSkillSnapshot snapshot =
_domain.Character.MovementSkills.Snapshot;
_log(
$"player: applied server skills " +
$"run={_player.Skills.RunSkill} " +
$"jump={_player.Skills.JumpSkill}");
$"run={snapshot.RunSkill} " +
$"jump={snapshot.JumpSkill}");
}
},
OnConfirmationRequest: request =>
_ui.RetailUi?.HandleConfirmationRequest(request),
OnConfirmationDone: done =>
_ui.RetailUi?.HandleConfirmationDone(done),
OnCharacterOptions: (options1, _) =>
_player.CharacterOptions.Options =
(Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
options1,
ClientTime: ClientTimerNow);
}
@ -391,6 +384,26 @@ internal sealed class LiveSessionRuntimeFactory
senderGuid,
text,
cookie),
AddShortcut: session.SendAddShortcut,
RemoveShortcut: session.SendRemoveShortcut,
AddFavorite: session.SendAddSpellFavorite,
RemoveFavorite: session.SendRemoveSpellFavorite,
SetSpellbookFilter: session.SendSpellbookFilter,
ForgetSpell: session.SendRemoveSpell,
SetDesiredComponent: session.SendSetDesiredComponentLevel,
ClearDesiredComponents: session.SendClearDesiredComponents,
RaiseAttribute: session.SendRaiseAttribute,
RaiseVital: session.SendRaiseVital,
RaiseSkill: session.SendRaiseSkill,
TrainSkill: session.SendTrainSkill,
SetCharacterOptions: session.SendSetCharacterOptions,
AddFriend: session.SendAddFriend,
RemoveFriend: session.SendRemoveFriend,
ClearFriends: session.SendClearFriends,
RequestLegacyFriends: session.SendLegacyFriendsListRequest,
ModifyCharacterSquelch: session.SendModifyCharacterSquelch,
ModifyAccountSquelch: session.SendModifyAccountSquelch,
ModifyGlobalSquelch: session.SendModifyGlobalSquelch,
Log: _log);
private static double ClientTimerNow() =>

View file

@ -440,12 +440,8 @@ public sealed class GameWindow :
get => _localPlayerIdentity.ServerGuid;
set => _localPlayerIdentity.ServerGuid = value;
}
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
// replaces this before any in-world drag can normally occur.
private readonly PlayerCharacterOptionsState _characterOptions = new();
private readonly AcDream.App.Physics.LocalPlayerShadowState _localPlayerShadow = new();
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new();
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
private readonly FramebufferResizeController _framebufferResize;
private AcDream.App.Input.PlayerModeController? _playerModeController;
@ -1269,7 +1265,6 @@ public sealed class GameWindow :
_localPlayerIdentity,
_playerControllerSlot,
_localPlayerMode,
_characterOptions,
viewPlane => new SelectionCameraSource(
hostInputCamera.CameraController,
_window!,
@ -1387,10 +1382,8 @@ public sealed class GameWindow :
_liveWorldOrigin,
_renderRange,
_localPlayerShadow,
_localPlayerSkills,
_viewportAspect,
_playerApproachCompletions,
_characterOptions,
_runtimeInventory,
_pointerPosition,
_movementInput,

View file

@ -37,6 +37,8 @@ internal sealed class CurrentGameRuntimeAdapter
LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory,
RuntimeCharacterState character,
RuntimeCommunicationState communication,
ILocalPlayerControllerSource playerController,
WorldRevealCoordinator worldReveal,
@ -51,6 +53,8 @@ internal sealed class CurrentGameRuntimeAdapter
playerIdentity,
entities,
entityObjects,
inventory,
character,
communication,
playerController,
worldReveal,
@ -79,6 +83,9 @@ internal sealed class CurrentGameRuntimeAdapter
public IGameRuntimeClock Clock => _view.Clock;
public IRuntimeEntityView Entities => _view.Entities;
public IRuntimeInventoryView Inventory => _view.Inventory;
public IRuntimeInventoryStateView InventoryState => _view.InventoryState;
public IRuntimeCharacterView Character => _view.Character;
public IRuntimeSocialView Social => _view.Social;
public IRuntimeChatView Chat => _view.Chat;
public IRuntimeMovementView Movement => _view.Movement;
public IRuntimePortalView Portal => _view.Portal;
@ -92,6 +99,15 @@ internal sealed class CurrentGameRuntimeAdapter
IRuntimeChatCommands IGameRuntimeCommands.Chat => _commands;
public IRuntimePortalCommands PortalCommands => _commands;
IRuntimePortalCommands IGameRuntimeCommands.Portal => _commands;
public IRuntimeInventoryStateCommands InventoryCommands => _commands;
IRuntimeInventoryStateCommands IGameRuntimeCommands.InventoryState =>
_commands;
public IRuntimeSpellbookCommands SpellbookCommands => _commands;
IRuntimeSpellbookCommands IGameRuntimeCommands.Spellbook => _commands;
public IRuntimeCharacterCommands CharacterCommands => _commands;
IRuntimeCharacterCommands IGameRuntimeCommands.Character => _commands;
public IRuntimeSocialCommands SocialCommands => _commands;
IRuntimeSocialCommands IGameRuntimeCommands.Social => _commands;
public RuntimeStateCheckpoint CaptureCheckpoint() =>
_view.CaptureCheckpoint();

View file

@ -3,6 +3,7 @@ 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;
@ -20,7 +21,11 @@ internal sealed class CurrentGameRuntimeCommandAdapter
IRuntimeCombatCommands,
IRuntimeMovementCommands,
IRuntimeChatCommands,
IRuntimePortalCommands
IRuntimePortalCommands,
IRuntimeInventoryStateCommands,
IRuntimeSpellbookCommands,
IRuntimeCharacterCommands,
IRuntimeSocialCommands
{
private readonly LiveSessionController _session;
private readonly LiveSessionHost _sessionHost;
@ -276,6 +281,340 @@ internal sealed class CurrentGameRuntimeCommandAdapter
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)
@ -294,6 +633,17 @@ internal sealed class CurrentGameRuntimeCommandAdapter
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

View file

@ -24,6 +24,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
private readonly IGameRuntimeClock _clock;
private readonly IRuntimeEntityView _entityView;
private readonly IRuntimeInventoryView _inventoryView;
private readonly IRuntimeInventoryStateView _inventoryStateView;
private readonly IRuntimeCharacterView _characterView;
private readonly IRuntimeSocialView _socialView;
private readonly IRuntimeChatView _chatView;
private readonly MovementView _movementView;
private readonly PortalView _portalView;
@ -34,6 +37,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
LocalPlayerIdentityState playerIdentity,
LiveEntityRuntime entities,
RuntimeEntityObjectLifetime entityObjects,
RuntimeInventoryState inventory,
RuntimeCharacterState character,
RuntimeCommunicationState communication,
ILocalPlayerControllerSource playerController,
WorldRevealCoordinator worldReveal,
@ -50,7 +55,12 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_entityView = entityObjects.EntityView;
_inventoryView = entityObjects.InventoryView;
_inventoryStateView = (
inventory ?? throw new ArgumentNullException(nameof(inventory))).View;
_characterView = (
character ?? throw new ArgumentNullException(nameof(character))).View;
_chatView = communication.View;
_socialView = communication.SocialView;
_movementView = new MovementView(
playerController
?? throw new ArgumentNullException(nameof(playerController)),
@ -90,6 +100,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
public IGameRuntimeClock Clock => _clock;
public IRuntimeEntityView Entities => _entityView;
public IRuntimeInventoryView Inventory => _inventoryView;
public IRuntimeInventoryStateView InventoryState => _inventoryStateView;
public IRuntimeCharacterView Character => _characterView;
public IRuntimeSocialView Social => _socialView;
public IRuntimeChatView Chat => _chatView;
public IRuntimeMovementView Movement => _movementView;
public IRuntimePortalView Portal => _portalView;
@ -103,6 +116,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
_entities.MaterializedCount,
_objects.ObjectCount,
_objects.ContainerCount,
_inventoryStateView.Snapshot,
_characterView.Snapshot,
_socialView.Snapshot,
_chatView.Revision,
_chatView.Count,
_movementView.Snapshot,