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:
parent
9d0d9b07e0
commit
dcb61efb5a
34 changed files with 2076 additions and 103 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue