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.Player;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using AcDream.UI.Abstractions.Panels.Chat;
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
|
@ -56,7 +57,6 @@ internal sealed record InteractionRetainedUiDependencies(
|
||||||
ILocalPlayerIdentitySource PlayerIdentity,
|
ILocalPlayerIdentitySource PlayerIdentity,
|
||||||
ILocalPlayerControllerSource PlayerController,
|
ILocalPlayerControllerSource PlayerController,
|
||||||
ILocalPlayerModeSource PlayerMode,
|
ILocalPlayerModeSource PlayerMode,
|
||||||
PlayerCharacterOptionsState CharacterOptions,
|
|
||||||
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
|
Func<DeferredSelectionViewPlaneSource, SelectionCameraSource>
|
||||||
SelectionCameraFactory,
|
SelectionCameraFactory,
|
||||||
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
DeferredRenderFrameDiagnosticsSource FrameDiagnostics,
|
||||||
|
|
@ -118,6 +118,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
|
||||||
private bool _deactivationStarted;
|
private bool _deactivationStarted;
|
||||||
|
|
||||||
public DeferredLiveSessionUiAuthority Session { get; } = new();
|
public DeferredLiveSessionUiAuthority Session { get; } = new();
|
||||||
|
public DeferredGameRuntimeStateCommands GameRuntime { get; } = new();
|
||||||
public DeferredSelectionUiAuthority Selection { get; } = new();
|
public DeferredSelectionUiAuthority Selection { get; } = new();
|
||||||
public DeferredSelectionViewPlaneSource SelectionViewPlane { get; } = new();
|
public DeferredSelectionViewPlaneSource SelectionViewPlane { get; } = new();
|
||||||
public DeferredRadarSnapshotSource Radar { get; } = new();
|
public DeferredRadarSnapshotSource Radar { get; } = new();
|
||||||
|
|
@ -176,6 +177,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
|
||||||
Radar.Deactivate();
|
Radar.Deactivate();
|
||||||
SelectionViewPlane.Deactivate();
|
SelectionViewPlane.Deactivate();
|
||||||
Selection.Deactivate();
|
Selection.Deactivate();
|
||||||
|
GameRuntime.Deactivate();
|
||||||
Session.Deactivate();
|
Session.Deactivate();
|
||||||
InventoryContainer.Deactivate();
|
InventoryContainer.Deactivate();
|
||||||
for (int i = _lateOwnerBindings.Count - 1; i >= 0; i--)
|
for (int i = _lateOwnerBindings.Count - 1; i >= 0; i--)
|
||||||
|
|
@ -294,7 +296,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
sendGive: (target, item, amount) =>
|
sendGive: (target, item, amount) =>
|
||||||
session.CurrentSession?.SendGiveObject(target, item, amount),
|
session.CurrentSession?.SendGiveObject(target, item, amount),
|
||||||
dragOnPlayerOpensSecureTrade: () =>
|
dragOnPlayerOpensSecureTrade: () =>
|
||||||
d.CharacterOptions.DragItemOnPlayerOpensSecureTrade,
|
d.Character.Options.DragItemOnPlayerOpensSecureTrade,
|
||||||
toast: d.Toast,
|
toast: d.Toast,
|
||||||
readyForInventoryRequest: () => session.IsInWorld,
|
readyForInventoryRequest: () => session.IsInWorld,
|
||||||
playerOnGround: () =>
|
playerOnGround: () =>
|
||||||
|
|
@ -375,15 +377,27 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||||
activeToonName: () => d.Settings.ActiveToonKey,
|
activeToonName: () => d.Settings.ActiveToonKey,
|
||||||
fallbackSheet: Studio.SampleData.SampleCharacter,
|
fallbackSheet: Studio.SampleData.SampleCharacter,
|
||||||
canSendRaise: () => late.Session.IsInWorld,
|
canSendRaise: () => late.GameRuntime.IsInWorld,
|
||||||
sendRaiseAttribute: (statId, cost) =>
|
sendRaiseAttribute: (statId, cost) =>
|
||||||
late.Session.CurrentSession?.SendRaiseAttribute(statId, cost),
|
late.GameRuntime.Advance(
|
||||||
|
RuntimeAdvancementKind.Attribute,
|
||||||
|
statId,
|
||||||
|
cost),
|
||||||
sendRaiseVital: (statId, cost) =>
|
sendRaiseVital: (statId, cost) =>
|
||||||
late.Session.CurrentSession?.SendRaiseVital(statId, cost),
|
late.GameRuntime.Advance(
|
||||||
|
RuntimeAdvancementKind.Vital,
|
||||||
|
statId,
|
||||||
|
cost),
|
||||||
sendRaiseSkill: (statId, cost) =>
|
sendRaiseSkill: (statId, cost) =>
|
||||||
late.Session.CurrentSession?.SendRaiseSkill(statId, cost),
|
late.GameRuntime.Advance(
|
||||||
|
RuntimeAdvancementKind.Skill,
|
||||||
|
statId,
|
||||||
|
cost),
|
||||||
sendTrainSkill: (statId, credits) =>
|
sendTrainSkill: (statId, credits) =>
|
||||||
late.Session.CurrentSession?.SendTrainSkill(statId, credits));
|
late.GameRuntime.Advance(
|
||||||
|
RuntimeAdvancementKind.TrainSkill,
|
||||||
|
statId,
|
||||||
|
credits));
|
||||||
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
|
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
|
||||||
|
|
||||||
MagicRuntime magic = MagicRuntime.Create(
|
MagicRuntime magic = MagicRuntime.Create(
|
||||||
|
|
@ -543,16 +557,13 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
|
guid => d.Selection.Select(guid, SelectionChangeSource.Inventory),
|
||||||
guid => late.Session.TryUseItem(guid, d.Log),
|
guid => late.Session.TryUseItem(guid, d.Log),
|
||||||
(tab, position, spellId) =>
|
(tab, position, spellId) =>
|
||||||
late.Session.CurrentSession?.SendAddSpellFavorite(
|
late.GameRuntime.AddFavorite(tab, position, spellId),
|
||||||
spellId,
|
|
||||||
position,
|
|
||||||
tab),
|
|
||||||
(tab, spellId) =>
|
(tab, spellId) =>
|
||||||
late.Session.CurrentSession?.SendRemoveSpellFavorite(spellId, tab),
|
late.GameRuntime.RemoveFavorite(tab, spellId),
|
||||||
filters => late.Session.CurrentSession?.SendSpellbookFilter(filters),
|
filters => late.GameRuntime.SetSpellbookFilter(filters),
|
||||||
spellId => late.Session.CurrentSession?.SendRemoveSpell(spellId),
|
spellId => late.GameRuntime.ForgetSpell(spellId),
|
||||||
(componentId, amount) =>
|
(componentId, amount) =>
|
||||||
late.Session.CurrentSession?.SendSetDesiredComponentLevel(
|
late.GameRuntime.SetDesiredComponent(
|
||||||
componentId,
|
componentId,
|
||||||
amount),
|
amount),
|
||||||
d.ClientTime),
|
d.ClientTime),
|
||||||
|
|
@ -589,8 +600,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Inventory.ItemMana,
|
d.Inventory.ItemMana,
|
||||||
d.CombatModeCommands.Toggle,
|
d.CombatModeCommands.Toggle,
|
||||||
itemInteraction,
|
itemInteraction,
|
||||||
entry => late.Session.CurrentSession?.SendAddShortcut(entry),
|
entry => late.GameRuntime.AddShortcut(entry),
|
||||||
index => late.Session.CurrentSession?.SendRemoveShortcut(index),
|
index => late.GameRuntime.RemoveShortcut(index),
|
||||||
d.Selection,
|
d.Selection,
|
||||||
handler => d.Combat.HealthChanged += handler,
|
handler => d.Combat.HealthChanged += handler,
|
||||||
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.Items;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
using AcDream.Runtime;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
namespace AcDream.App.Composition;
|
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>
|
/// <summary>
|
||||||
/// Early UI view over the later live-session owner. A binding lease clears only
|
/// 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.
|
/// 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>
|
/// <summary>
|
||||||
/// Probe scripts are mounted in Phase 5; reveal/resource facts become valid in
|
/// 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.
|
/// Phase 7. Calls remain inert and diagnostic until that exact owner binds.
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,8 @@ internal sealed record SessionPlayerDependencies(
|
||||||
LiveWorldOriginState WorldOrigin,
|
LiveWorldOriginState WorldOrigin,
|
||||||
WorldRenderRangeState RenderRange,
|
WorldRenderRangeState RenderRange,
|
||||||
LocalPlayerShadowState PlayerShadow,
|
LocalPlayerShadowState PlayerShadow,
|
||||||
LocalPlayerSkillState PlayerSkills,
|
|
||||||
ViewportAspectState ViewportAspect,
|
ViewportAspectState ViewportAspect,
|
||||||
PlayerApproachCompletionState PlayerApproachCompletions,
|
PlayerApproachCompletionState PlayerApproachCompletions,
|
||||||
PlayerCharacterOptionsState CharacterOptions,
|
|
||||||
RuntimeInventoryState Inventory,
|
RuntimeInventoryState Inventory,
|
||||||
PointerPositionState PointerPosition,
|
PointerPositionState PointerPosition,
|
||||||
DispatcherMovementInputSource MovementInput,
|
DispatcherMovementInputSource MovementInput,
|
||||||
|
|
@ -710,7 +708,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
gameplayInput,
|
gameplayInput,
|
||||||
liveSessionSource,
|
liveSessionSource,
|
||||||
d.MovementDiagnostics,
|
d.MovementDiagnostics,
|
||||||
d.PlayerSkills,
|
d.Character.MovementSkills,
|
||||||
d.ViewportAspect);
|
d.ViewportAspect);
|
||||||
if (d.SettingsDevTools.DevTools is { } devTools)
|
if (d.SettingsDevTools.DevTools is { } devTools)
|
||||||
{
|
{
|
||||||
|
|
@ -796,9 +794,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
new LiveSessionPlayerRuntime(
|
new LiveSessionPlayerRuntime(
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
d.PlayerSkills,
|
d.WorldOrigin),
|
||||||
d.WorldOrigin,
|
|
||||||
d.CharacterOptions),
|
|
||||||
new LiveSessionDomainRuntime(
|
new LiveSessionDomainRuntime(
|
||||||
d.EntityObjects,
|
d.EntityObjects,
|
||||||
d.Character,
|
d.Character,
|
||||||
|
|
@ -876,6 +872,8 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
d.EntityObjects,
|
d.EntityObjects,
|
||||||
|
d.Inventory,
|
||||||
|
d.Character,
|
||||||
d.Communication,
|
d.Communication,
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
|
|
@ -885,6 +883,9 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
gameplayInput,
|
gameplayInput,
|
||||||
combatCommand);
|
combatCommand);
|
||||||
bindings.Adopt("current game runtime adapter", gameRuntime);
|
bindings.Adopt("current game runtime adapter", gameRuntime);
|
||||||
|
bindings.Adopt(
|
||||||
|
"retained-UI game runtime commands",
|
||||||
|
interaction.LateBindings.GameRuntime.Bind(gameRuntime, gameRuntime));
|
||||||
|
|
||||||
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
var nearbyDiagnostics = new NearbyWorldDiagnosticDumper(
|
||||||
new RuntimeNearbyWorldDiagnosticSource(
|
new RuntimeNearbyWorldDiagnosticSource(
|
||||||
|
|
|
||||||
|
|
@ -140,38 +140,21 @@ internal sealed class LocalPlayerModeState : ILocalPlayerModeSource
|
||||||
/// delivery and player-mode construction share this owner so rebuilding the
|
/// delivery and player-mode construction share this owner so rebuilding the
|
||||||
/// local physics controller cannot fall back to stale defaults.
|
/// local physics controller cannot fall back to stale defaults.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LocalPlayerSkillState
|
internal static class LocalPlayerSkillProjection
|
||||||
{
|
{
|
||||||
public int RunSkill { get; private set; } = -1;
|
public static bool ApplyTo(
|
||||||
public int JumpSkill { get; private set; } = -1;
|
AcDream.Runtime.Gameplay.RuntimeMovementSkillState skills,
|
||||||
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
|
||||||
|
|
||||||
public void Update(
|
|
||||||
int runSkill,
|
|
||||||
int jumpSkill,
|
|
||||||
PlayerMovementController? controller)
|
PlayerMovementController? controller)
|
||||||
{
|
{
|
||||||
if (runSkill >= 0)
|
ArgumentNullException.ThrowIfNull(skills);
|
||||||
RunSkill = runSkill;
|
AcDream.Runtime.Gameplay.RuntimeMovementSkillSnapshot snapshot =
|
||||||
if (jumpSkill >= 0)
|
skills.Snapshot;
|
||||||
JumpSkill = jumpSkill;
|
if (controller is null || !snapshot.IsComplete)
|
||||||
ApplyTo(controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ApplyTo(PlayerMovementController? controller)
|
|
||||||
{
|
|
||||||
if (controller is null || !IsComplete)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
controller.SetCharacterSkills(RunSkill, JumpSkill);
|
controller.SetCharacterSkills(snapshot.RunSkill, snapshot.JumpSkill);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSession()
|
|
||||||
{
|
|
||||||
RunSkill = -1;
|
|
||||||
JumpSkill = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface IViewportAspectSource
|
internal interface IViewportAspectSource
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using AcDream.Content;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Physics.Motion;
|
using AcDream.Core.Physics.Motion;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@ internal sealed class PlayerModeController :
|
||||||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||||
private readonly ILiveInWorldSource _session;
|
private readonly ILiveInWorldSource _session;
|
||||||
private readonly MovementTruthDiagnosticController _movementDiagnostics;
|
private readonly MovementTruthDiagnosticController _movementDiagnostics;
|
||||||
private readonly LocalPlayerSkillState _skills;
|
private readonly RuntimeMovementSkillState _skills;
|
||||||
private readonly IViewportAspectSource _viewport;
|
private readonly IViewportAspectSource _viewport;
|
||||||
private PlayerModeAutoEntry? _autoEntry;
|
private PlayerModeAutoEntry? _autoEntry;
|
||||||
private IPlayerApproachCompletionSink? _approachLifetime;
|
private IPlayerApproachCompletionSink? _approachLifetime;
|
||||||
|
|
@ -67,7 +68,7 @@ internal sealed class PlayerModeController :
|
||||||
ILocalPlayerTeleportInputLifetime input,
|
ILocalPlayerTeleportInputLifetime input,
|
||||||
ILiveInWorldSource session,
|
ILiveInWorldSource session,
|
||||||
MovementTruthDiagnosticController movementDiagnostics,
|
MovementTruthDiagnosticController movementDiagnostics,
|
||||||
LocalPlayerSkillState skills,
|
RuntimeMovementSkillState skills,
|
||||||
IViewportAspectSource viewport)
|
IViewportAspectSource viewport)
|
||||||
{
|
{
|
||||||
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||||
|
|
@ -359,7 +360,7 @@ internal sealed class PlayerModeController :
|
||||||
exactMovement.CancelMoveTo(WeenieError.ActionCancelled);
|
exactMovement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_skills.ApplyTo(controller))
|
if (LocalPlayerSkillProjection.ApplyTo(_skills, controller))
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"live: {loggingTag} — applied server skills "
|
$"live: {loggingTag} — applied server skills "
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
@ -15,8 +16,64 @@ internal sealed record LiveSessionCommandBindings(
|
||||||
Action<string, string> SendTell,
|
Action<string, string> SendTell,
|
||||||
Action<uint, string> SendChannel,
|
Action<uint, string> SendChannel,
|
||||||
Action<uint, uint, uint, uint, string, uint> SendTurbineChat,
|
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);
|
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>
|
/// <summary>
|
||||||
/// Owns the command surface for one exact live-session generation. The router
|
/// Owns the command surface for one exact live-session generation. The router
|
||||||
/// itself is the published bus, so a retained reference becomes inert before
|
/// 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));
|
SendIfActive(() => bindings.SendTalk(command.Text));
|
||||||
});
|
});
|
||||||
commands.Register<SendChatCmd>(command => RouteChat(bindings, command));
|
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;
|
_commands = commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,7 @@ namespace AcDream.App.Net;
|
||||||
internal sealed record LiveSessionPlayerRuntime(
|
internal sealed record LiveSessionPlayerRuntime(
|
||||||
LocalPlayerIdentityState Identity,
|
LocalPlayerIdentityState Identity,
|
||||||
LocalPlayerControllerSlot Controller,
|
LocalPlayerControllerSlot Controller,
|
||||||
LocalPlayerSkillState Skills,
|
LiveWorldOriginState WorldOrigin);
|
||||||
LiveWorldOriginState WorldOrigin,
|
|
||||||
PlayerCharacterOptionsState CharacterOptions);
|
|
||||||
|
|
||||||
internal sealed record LiveSessionDomainRuntime(
|
internal sealed record LiveSessionDomainRuntime(
|
||||||
RuntimeEntityObjectLifetime EntityObjects,
|
RuntimeEntityObjectLifetime EntityObjects,
|
||||||
|
|
@ -215,8 +213,8 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
_domain.Communication.ResetChatIdentity();
|
_domain.Communication.ResetChatIdentity();
|
||||||
EntityVanishProbe.PlayerGuid = 0u;
|
EntityVanishProbe.PlayerGuid = 0u;
|
||||||
_interaction.Settings.ResetActiveCharacterKey();
|
_interaction.Settings.ResetActiveCharacterKey();
|
||||||
_player.CharacterOptions.Reset();
|
_domain.Character.Options.ResetSession();
|
||||||
_player.Skills.ResetSession();
|
_domain.Character.MovementSkills.ResetSession();
|
||||||
_world.NetworkUpdates.ResetSessionState();
|
_world.NetworkUpdates.ResetSessionState();
|
||||||
_world.Hydration.ResetSessionState();
|
_world.Hydration.ResetSessionState();
|
||||||
_domain.Inventory.ResetPlayerSnapshots();
|
_domain.Inventory.ResetPlayerSnapshots();
|
||||||
|
|
@ -282,27 +280,22 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||||
{
|
{
|
||||||
_player.Skills.Update(
|
if (LocalPlayerSkillProjection.ApplyTo(
|
||||||
runSkill,
|
_domain.Character.MovementSkills,
|
||||||
jumpSkill,
|
_player.Controller.Controller))
|
||||||
_player.Controller.Controller);
|
|
||||||
if (_player.Skills.IsComplete
|
|
||||||
&& _player.Controller.Controller is not null)
|
|
||||||
{
|
{
|
||||||
|
RuntimeMovementSkillSnapshot snapshot =
|
||||||
|
_domain.Character.MovementSkills.Snapshot;
|
||||||
_log(
|
_log(
|
||||||
$"player: applied server skills " +
|
$"player: applied server skills " +
|
||||||
$"run={_player.Skills.RunSkill} " +
|
$"run={snapshot.RunSkill} " +
|
||||||
$"jump={_player.Skills.JumpSkill}");
|
$"jump={snapshot.JumpSkill}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnConfirmationRequest: request =>
|
OnConfirmationRequest: request =>
|
||||||
_ui.RetailUi?.HandleConfirmationRequest(request),
|
_ui.RetailUi?.HandleConfirmationRequest(request),
|
||||||
OnConfirmationDone: done =>
|
OnConfirmationDone: done =>
|
||||||
_ui.RetailUi?.HandleConfirmationDone(done),
|
_ui.RetailUi?.HandleConfirmationDone(done),
|
||||||
OnCharacterOptions: (options1, _) =>
|
|
||||||
_player.CharacterOptions.Options =
|
|
||||||
(Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
|
||||||
options1,
|
|
||||||
ClientTime: ClientTimerNow);
|
ClientTime: ClientTimerNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,6 +384,26 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
senderGuid,
|
senderGuid,
|
||||||
text,
|
text,
|
||||||
cookie),
|
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);
|
Log: _log);
|
||||||
|
|
||||||
private static double ClientTimerNow() =>
|
private static double ClientTimerNow() =>
|
||||||
|
|
|
||||||
|
|
@ -440,12 +440,8 @@ public sealed class GameWindow :
|
||||||
get => _localPlayerIdentity.ServerGuid;
|
get => _localPlayerIdentity.ServerGuid;
|
||||||
set => _localPlayerIdentity.ServerGuid = value;
|
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.Physics.LocalPlayerShadowState _localPlayerShadow = new();
|
||||||
|
|
||||||
private readonly AcDream.App.Input.LocalPlayerSkillState _localPlayerSkills = new();
|
|
||||||
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
|
private readonly AcDream.App.Input.ViewportAspectState _viewportAspect = new();
|
||||||
private readonly FramebufferResizeController _framebufferResize;
|
private readonly FramebufferResizeController _framebufferResize;
|
||||||
private AcDream.App.Input.PlayerModeController? _playerModeController;
|
private AcDream.App.Input.PlayerModeController? _playerModeController;
|
||||||
|
|
@ -1269,7 +1265,6 @@ public sealed class GameWindow :
|
||||||
_localPlayerIdentity,
|
_localPlayerIdentity,
|
||||||
_playerControllerSlot,
|
_playerControllerSlot,
|
||||||
_localPlayerMode,
|
_localPlayerMode,
|
||||||
_characterOptions,
|
|
||||||
viewPlane => new SelectionCameraSource(
|
viewPlane => new SelectionCameraSource(
|
||||||
hostInputCamera.CameraController,
|
hostInputCamera.CameraController,
|
||||||
_window!,
|
_window!,
|
||||||
|
|
@ -1387,10 +1382,8 @@ public sealed class GameWindow :
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
_renderRange,
|
_renderRange,
|
||||||
_localPlayerShadow,
|
_localPlayerShadow,
|
||||||
_localPlayerSkills,
|
|
||||||
_viewportAspect,
|
_viewportAspect,
|
||||||
_playerApproachCompletions,
|
_playerApproachCompletions,
|
||||||
_characterOptions,
|
|
||||||
_runtimeInventory,
|
_runtimeInventory,
|
||||||
_pointerPosition,
|
_pointerPosition,
|
||||||
_movementInput,
|
_movementInput,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
LocalPlayerIdentityState playerIdentity,
|
LocalPlayerIdentityState playerIdentity,
|
||||||
LiveEntityRuntime entities,
|
LiveEntityRuntime entities,
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
|
RuntimeInventoryState inventory,
|
||||||
|
RuntimeCharacterState character,
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
ILocalPlayerControllerSource playerController,
|
ILocalPlayerControllerSource playerController,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
|
|
@ -51,6 +53,8 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
playerIdentity,
|
playerIdentity,
|
||||||
entities,
|
entities,
|
||||||
entityObjects,
|
entityObjects,
|
||||||
|
inventory,
|
||||||
|
character,
|
||||||
communication,
|
communication,
|
||||||
playerController,
|
playerController,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
|
|
@ -79,6 +83,9 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
public IGameRuntimeClock Clock => _view.Clock;
|
public IGameRuntimeClock Clock => _view.Clock;
|
||||||
public IRuntimeEntityView Entities => _view.Entities;
|
public IRuntimeEntityView Entities => _view.Entities;
|
||||||
public IRuntimeInventoryView Inventory => _view.Inventory;
|
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 IRuntimeChatView Chat => _view.Chat;
|
||||||
public IRuntimeMovementView Movement => _view.Movement;
|
public IRuntimeMovementView Movement => _view.Movement;
|
||||||
public IRuntimePortalView Portal => _view.Portal;
|
public IRuntimePortalView Portal => _view.Portal;
|
||||||
|
|
@ -92,6 +99,15 @@ internal sealed class CurrentGameRuntimeAdapter
|
||||||
IRuntimeChatCommands IGameRuntimeCommands.Chat => _commands;
|
IRuntimeChatCommands IGameRuntimeCommands.Chat => _commands;
|
||||||
public IRuntimePortalCommands PortalCommands => _commands;
|
public IRuntimePortalCommands PortalCommands => _commands;
|
||||||
IRuntimePortalCommands IGameRuntimeCommands.Portal => _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() =>
|
public RuntimeStateCheckpoint CaptureCheckpoint() =>
|
||||||
_view.CaptureCheckpoint();
|
_view.CaptureCheckpoint();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
@ -20,7 +21,11 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
IRuntimeCombatCommands,
|
IRuntimeCombatCommands,
|
||||||
IRuntimeMovementCommands,
|
IRuntimeMovementCommands,
|
||||||
IRuntimeChatCommands,
|
IRuntimeChatCommands,
|
||||||
IRuntimePortalCommands
|
IRuntimePortalCommands,
|
||||||
|
IRuntimeInventoryStateCommands,
|
||||||
|
IRuntimeSpellbookCommands,
|
||||||
|
IRuntimeCharacterCommands,
|
||||||
|
IRuntimeSocialCommands
|
||||||
{
|
{
|
||||||
private readonly LiveSessionController _session;
|
private readonly LiveSessionController _session;
|
||||||
private readonly LiveSessionHost _sessionHost;
|
private readonly LiveSessionHost _sessionHost;
|
||||||
|
|
@ -276,6 +281,340 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
return Result(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(
|
private RuntimeCommandStatus Validate(
|
||||||
RuntimeGenerationToken expectedGeneration,
|
RuntimeGenerationToken expectedGeneration,
|
||||||
bool requireWorld)
|
bool requireWorld)
|
||||||
|
|
@ -294,6 +633,17 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
||||||
uint objectId = 0u) =>
|
uint objectId = 0u) =>
|
||||||
new(status, _view.Generation, objectId);
|
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) =>
|
private RuntimeSessionStartResult RejectedStart(RuntimeCommandStatus status) =>
|
||||||
new(
|
new(
|
||||||
status == RuntimeCommandStatus.StaleGeneration
|
status == RuntimeCommandStatus.StaleGeneration
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
private readonly IGameRuntimeClock _clock;
|
private readonly IGameRuntimeClock _clock;
|
||||||
private readonly IRuntimeEntityView _entityView;
|
private readonly IRuntimeEntityView _entityView;
|
||||||
private readonly IRuntimeInventoryView _inventoryView;
|
private readonly IRuntimeInventoryView _inventoryView;
|
||||||
|
private readonly IRuntimeInventoryStateView _inventoryStateView;
|
||||||
|
private readonly IRuntimeCharacterView _characterView;
|
||||||
|
private readonly IRuntimeSocialView _socialView;
|
||||||
private readonly IRuntimeChatView _chatView;
|
private readonly IRuntimeChatView _chatView;
|
||||||
private readonly MovementView _movementView;
|
private readonly MovementView _movementView;
|
||||||
private readonly PortalView _portalView;
|
private readonly PortalView _portalView;
|
||||||
|
|
@ -34,6 +37,8 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
LocalPlayerIdentityState playerIdentity,
|
LocalPlayerIdentityState playerIdentity,
|
||||||
LiveEntityRuntime entities,
|
LiveEntityRuntime entities,
|
||||||
RuntimeEntityObjectLifetime entityObjects,
|
RuntimeEntityObjectLifetime entityObjects,
|
||||||
|
RuntimeInventoryState inventory,
|
||||||
|
RuntimeCharacterState character,
|
||||||
RuntimeCommunicationState communication,
|
RuntimeCommunicationState communication,
|
||||||
ILocalPlayerControllerSource playerController,
|
ILocalPlayerControllerSource playerController,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
|
|
@ -50,7 +55,12 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
|
|
||||||
_entityView = entityObjects.EntityView;
|
_entityView = entityObjects.EntityView;
|
||||||
_inventoryView = entityObjects.InventoryView;
|
_inventoryView = entityObjects.InventoryView;
|
||||||
|
_inventoryStateView = (
|
||||||
|
inventory ?? throw new ArgumentNullException(nameof(inventory))).View;
|
||||||
|
_characterView = (
|
||||||
|
character ?? throw new ArgumentNullException(nameof(character))).View;
|
||||||
_chatView = communication.View;
|
_chatView = communication.View;
|
||||||
|
_socialView = communication.SocialView;
|
||||||
_movementView = new MovementView(
|
_movementView = new MovementView(
|
||||||
playerController
|
playerController
|
||||||
?? throw new ArgumentNullException(nameof(playerController)),
|
?? throw new ArgumentNullException(nameof(playerController)),
|
||||||
|
|
@ -90,6 +100,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
public IGameRuntimeClock Clock => _clock;
|
public IGameRuntimeClock Clock => _clock;
|
||||||
public IRuntimeEntityView Entities => _entityView;
|
public IRuntimeEntityView Entities => _entityView;
|
||||||
public IRuntimeInventoryView Inventory => _inventoryView;
|
public IRuntimeInventoryView Inventory => _inventoryView;
|
||||||
|
public IRuntimeInventoryStateView InventoryState => _inventoryStateView;
|
||||||
|
public IRuntimeCharacterView Character => _characterView;
|
||||||
|
public IRuntimeSocialView Social => _socialView;
|
||||||
public IRuntimeChatView Chat => _chatView;
|
public IRuntimeChatView Chat => _chatView;
|
||||||
public IRuntimeMovementView Movement => _movementView;
|
public IRuntimeMovementView Movement => _movementView;
|
||||||
public IRuntimePortalView Portal => _portalView;
|
public IRuntimePortalView Portal => _portalView;
|
||||||
|
|
@ -103,6 +116,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_entities.MaterializedCount,
|
_entities.MaterializedCount,
|
||||||
_objects.ObjectCount,
|
_objects.ObjectCount,
|
||||||
_objects.ContainerCount,
|
_objects.ContainerCount,
|
||||||
|
_inventoryStateView.Snapshot,
|
||||||
|
_characterView.Snapshot,
|
||||||
|
_socialView.Snapshot,
|
||||||
_chatView.Revision,
|
_chatView.Revision,
|
||||||
_chatView.Count,
|
_chatView.Count,
|
||||||
_movementView.Snapshot,
|
_movementView.Snapshot,
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,16 @@ public sealed class WorldSession : IDisposable
|
||||||
SendGameAction(ClientCommandRequests.BuildEmote(seq, message));
|
SendGameAction(ClientCommandRequests.BuildEmote(seq, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send retail SetCharacterOptions (0x01A1) for the first character
|
||||||
|
/// option bitmap.
|
||||||
|
/// </summary>
|
||||||
|
public void SendSetCharacterOptions(uint options)
|
||||||
|
{
|
||||||
|
uint seq = NextGameActionSequence();
|
||||||
|
SendGameAction(SocialActions.BuildSetCharacterOptions(seq, options));
|
||||||
|
}
|
||||||
|
|
||||||
public void SendAddFriend(string name)
|
public void SendAddFriend(string name)
|
||||||
{
|
{
|
||||||
uint seq = NextGameActionSequence();
|
uint seq = NextGameActionSequence();
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ namespace AcDream.Core.Items;
|
||||||
public sealed class ItemManaState
|
public sealed class ItemManaState
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<uint, float> _manaByGuid = new();
|
private readonly ConcurrentDictionary<uint, float> _manaByGuid = new();
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
/// <summary>Fires for every valid or invalid query response.</summary>
|
/// <summary>Fires for every valid or invalid query response.</summary>
|
||||||
public event Action<uint /*guid*/, float /*fraction*/, bool /*valid*/>? ItemManaChanged;
|
public event Action<uint /*guid*/, float /*fraction*/, bool /*valid*/>? ItemManaChanged;
|
||||||
|
|
@ -20,6 +21,11 @@ public sealed class ItemManaState
|
||||||
_manaByGuid.TryGetValue(guid, out float percent) ? percent : 0f;
|
_manaByGuid.TryGetValue(guid, out float percent) ? percent : 0f;
|
||||||
|
|
||||||
public bool HasMana(uint guid) => _manaByGuid.ContainsKey(guid);
|
public bool HasMana(uint guid) => _manaByGuid.ContainsKey(guid);
|
||||||
|
public int Count => _manaByGuid.Count;
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
|
||||||
|
public bool TryGetManaPercent(uint guid, out float fraction) =>
|
||||||
|
_manaByGuid.TryGetValue(guid, out fraction);
|
||||||
|
|
||||||
public void OnQueryItemManaResponse(uint itemGuid, float manaPercent, bool valid)
|
public void OnQueryItemManaResponse(uint itemGuid, float manaPercent, bool valid)
|
||||||
{
|
{
|
||||||
|
|
@ -28,8 +34,13 @@ public sealed class ItemManaState
|
||||||
else
|
else
|
||||||
_manaByGuid.TryRemove(itemGuid, out _);
|
_manaByGuid.TryRemove(itemGuid, out _);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
ItemManaChanged?.Invoke(itemGuid, manaPercent, valid);
|
ItemManaChanged?.Invoke(itemGuid, manaPercent, valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear() => _manaByGuid.Clear();
|
public void Clear()
|
||||||
|
{
|
||||||
|
_manaByGuid.Clear();
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,29 @@ public sealed class FriendsState
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly List<FriendEntry> _entries = new();
|
private readonly List<FriendEntry> _entries = new();
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { lock (_gate) return _entries.Count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
|
||||||
public IReadOnlyList<FriendEntry> Snapshot()
|
public IReadOnlyList<FriendEntry> Snapshot()
|
||||||
{
|
{
|
||||||
lock (_gate) return _entries.ToArray();
|
lock (_gate) return _entries.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryGet(uint characterId, out FriendEntry? friend)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
friend = _entries.Find(candidate => candidate.Id == characterId);
|
||||||
|
return friend is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Apply(FriendsUpdate update)
|
public void Apply(FriendsUpdate update)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(update);
|
ArgumentNullException.ThrowIfNull(update);
|
||||||
|
|
@ -43,12 +60,17 @@ public sealed class FriendsState
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_gate) _entries.Clear();
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_entries.Clear();
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ public sealed class SquelchState
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private SquelchDatabase _database = SquelchDatabase.Empty;
|
private SquelchDatabase _database = SquelchDatabase.Empty;
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
|
||||||
public SquelchDatabase Snapshot()
|
public SquelchDatabase Snapshot()
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +17,11 @@ public sealed class SquelchState
|
||||||
public void Replace(SquelchDatabase database)
|
public void Replace(SquelchDatabase database)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(database);
|
ArgumentNullException.ThrowIfNull(database);
|
||||||
lock (_gate) _database = database;
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -25,7 +32,11 @@ public sealed class SquelchState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_gate) _database = SquelchDatabase.Empty;
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_database = SquelchDatabase.Empty;
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,114 @@ public interface IRuntimePortalCommands
|
||||||
RuntimePortalCommand command);
|
RuntimePortalCommand command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeShortcutCommand(
|
||||||
|
int Index,
|
||||||
|
uint ObjectId,
|
||||||
|
uint SpellId);
|
||||||
|
|
||||||
|
public interface IRuntimeInventoryStateCommands
|
||||||
|
{
|
||||||
|
RuntimeCommandResult AddShortcut(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeShortcutCommand command);
|
||||||
|
|
||||||
|
RuntimeCommandResult RemoveShortcut(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRuntimeSpellbookCommands
|
||||||
|
{
|
||||||
|
RuntimeCommandResult AddFavorite(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int tabIndex,
|
||||||
|
int position,
|
||||||
|
uint spellId);
|
||||||
|
|
||||||
|
RuntimeCommandResult RemoveFavorite(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int tabIndex,
|
||||||
|
uint spellId);
|
||||||
|
|
||||||
|
RuntimeCommandResult SetFilter(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint filters);
|
||||||
|
|
||||||
|
RuntimeCommandResult ForgetSpell(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint spellId);
|
||||||
|
|
||||||
|
RuntimeCommandResult SetDesiredComponent(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint componentId,
|
||||||
|
uint amount);
|
||||||
|
|
||||||
|
RuntimeCommandResult ClearDesiredComponents(
|
||||||
|
RuntimeGenerationToken expectedGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RuntimeAdvancementKind
|
||||||
|
{
|
||||||
|
Attribute,
|
||||||
|
Vital,
|
||||||
|
Skill,
|
||||||
|
TrainSkill,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind Kind,
|
||||||
|
uint StatId,
|
||||||
|
ulong Cost);
|
||||||
|
|
||||||
|
public interface IRuntimeCharacterCommands
|
||||||
|
{
|
||||||
|
RuntimeCommandResult Advance(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeAdvancementCommand command);
|
||||||
|
|
||||||
|
RuntimeCommandResult SetOptions1(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RuntimeFriendCommandKind
|
||||||
|
{
|
||||||
|
Add,
|
||||||
|
Remove,
|
||||||
|
Clear,
|
||||||
|
RequestLegacyList,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeFriendCommand(
|
||||||
|
RuntimeFriendCommandKind Kind,
|
||||||
|
uint CharacterId = 0u,
|
||||||
|
string? Name = null);
|
||||||
|
|
||||||
|
public enum RuntimeSquelchScope
|
||||||
|
{
|
||||||
|
Character,
|
||||||
|
Account,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeSquelchCommand(
|
||||||
|
RuntimeSquelchScope Scope,
|
||||||
|
bool Add,
|
||||||
|
uint CharacterId = 0u,
|
||||||
|
string? Name = null,
|
||||||
|
uint MessageType = 0u);
|
||||||
|
|
||||||
|
public interface IRuntimeSocialCommands
|
||||||
|
{
|
||||||
|
RuntimeCommandResult Execute(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeFriendCommand command);
|
||||||
|
|
||||||
|
RuntimeCommandResult Execute(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeSquelchCommand command);
|
||||||
|
}
|
||||||
|
|
||||||
public interface IGameRuntimeCommands
|
public interface IGameRuntimeCommands
|
||||||
{
|
{
|
||||||
IRuntimeSessionCommands Session { get; }
|
IRuntimeSessionCommands Session { get; }
|
||||||
|
|
@ -148,4 +256,12 @@ public interface IGameRuntimeCommands
|
||||||
IRuntimeChatCommands Chat { get; }
|
IRuntimeChatCommands Chat { get; }
|
||||||
|
|
||||||
IRuntimePortalCommands Portal { get; }
|
IRuntimePortalCommands Portal { get; }
|
||||||
|
|
||||||
|
IRuntimeInventoryStateCommands InventoryState { get; }
|
||||||
|
|
||||||
|
IRuntimeSpellbookCommands Spellbook { get; }
|
||||||
|
|
||||||
|
IRuntimeCharacterCommands Character { get; }
|
||||||
|
|
||||||
|
IRuntimeSocialCommands Social { get; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ public enum RuntimeCommandDomain
|
||||||
Movement,
|
Movement,
|
||||||
Chat,
|
Chat,
|
||||||
Portal,
|
Portal,
|
||||||
|
InventoryState,
|
||||||
|
Spellbook,
|
||||||
|
Character,
|
||||||
|
Social,
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct RuntimeLifecycleDelta(
|
public readonly record struct RuntimeLifecycleDelta(
|
||||||
|
|
@ -145,6 +149,19 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
||||||
System.Globalization.CultureInfo.InvariantCulture,
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
$"frame={checkpoint.FrameNumber};materialized={checkpoint.MaterializedEntityCount};" +
|
$"frame={checkpoint.FrameNumber};materialized={checkpoint.MaterializedEntityCount};" +
|
||||||
$"containers={checkpoint.InventoryContainerCount};chat={checkpoint.ChatCount};" +
|
$"containers={checkpoint.InventoryContainerCount};chat={checkpoint.ChatCount};" +
|
||||||
|
$"inventory-state={checkpoint.InventoryState.ShortcutCount}:" +
|
||||||
|
$"{checkpoint.InventoryState.ShortcutRevision}:" +
|
||||||
|
$"{checkpoint.InventoryState.ItemManaCount}:" +
|
||||||
|
$"{checkpoint.InventoryState.ItemManaRevision};" +
|
||||||
|
$"character={checkpoint.Character.CharacterRevision}:" +
|
||||||
|
$"{checkpoint.Character.SpellbookRevision}:" +
|
||||||
|
$"{checkpoint.Character.LearnedSpellCount}:" +
|
||||||
|
$"{checkpoint.Character.SkillCount}:" +
|
||||||
|
$"{checkpoint.Character.Options.Revision}:" +
|
||||||
|
$"{checkpoint.Character.MovementSkills.Revision};" +
|
||||||
|
$"social={checkpoint.Social.FriendsRevision}:" +
|
||||||
|
$"{checkpoint.Social.FriendCount}:" +
|
||||||
|
$"{checkpoint.Social.SquelchRevision};" +
|
||||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
110
src/AcDream.Runtime/GameRuntimeGameplayViews.cs
Normal file
110
src/AcDream.Runtime/GameRuntimeGameplayViews.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime;
|
||||||
|
|
||||||
|
public readonly record struct RuntimePendingInventoryRequestSnapshot(
|
||||||
|
ulong Token,
|
||||||
|
int Kind,
|
||||||
|
uint ItemId,
|
||||||
|
bool Dispatched);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeInventoryStateSnapshot(
|
||||||
|
uint RequestedExternalContainerId,
|
||||||
|
uint CurrentExternalContainerId,
|
||||||
|
int BusyCount,
|
||||||
|
bool CanBeginRequest,
|
||||||
|
RuntimePendingInventoryRequestSnapshot? PendingRequest,
|
||||||
|
int ShortcutCount,
|
||||||
|
long ShortcutRevision,
|
||||||
|
int ItemManaCount,
|
||||||
|
long ItemManaRevision);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeShortcutSnapshot(
|
||||||
|
int Index,
|
||||||
|
uint ObjectId,
|
||||||
|
uint SpellId);
|
||||||
|
|
||||||
|
public interface IRuntimeInventoryStateView
|
||||||
|
{
|
||||||
|
RuntimeInventoryStateSnapshot Snapshot { get; }
|
||||||
|
|
||||||
|
bool TryGetShortcut(int index, out RuntimeShortcutSnapshot shortcut);
|
||||||
|
|
||||||
|
bool TryGetItemMana(uint objectId, out float fraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCharacterSnapshot(
|
||||||
|
long CharacterRevision,
|
||||||
|
long SpellbookRevision,
|
||||||
|
RuntimeCharacterOptionsSnapshot Options,
|
||||||
|
RuntimeMovementSkillSnapshot MovementSkills,
|
||||||
|
int LearnedSpellCount,
|
||||||
|
int ActiveEnchantmentCount,
|
||||||
|
int DesiredComponentCount,
|
||||||
|
int SkillCount,
|
||||||
|
uint SpellbookFilters);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeVitalSnapshot(
|
||||||
|
int Kind,
|
||||||
|
uint Ranks,
|
||||||
|
uint Start,
|
||||||
|
uint Experience,
|
||||||
|
uint Current,
|
||||||
|
uint Maximum);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeAttributeSnapshot(
|
||||||
|
int Kind,
|
||||||
|
uint Ranks,
|
||||||
|
uint Start,
|
||||||
|
uint Experience,
|
||||||
|
uint Current);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeSkillSnapshot(
|
||||||
|
uint SkillId,
|
||||||
|
uint Ranks,
|
||||||
|
uint Status,
|
||||||
|
uint Experience,
|
||||||
|
uint Initial,
|
||||||
|
uint Resistance,
|
||||||
|
double LastUsed,
|
||||||
|
uint FormulaBonus,
|
||||||
|
uint CurrentLevel);
|
||||||
|
|
||||||
|
public interface IRuntimeCharacterView
|
||||||
|
{
|
||||||
|
RuntimeCharacterSnapshot Snapshot { get; }
|
||||||
|
|
||||||
|
bool TryGetVital(int kind, out RuntimeVitalSnapshot vital);
|
||||||
|
|
||||||
|
bool TryGetAttribute(int kind, out RuntimeAttributeSnapshot attribute);
|
||||||
|
|
||||||
|
bool TryGetSkill(uint skillId, out RuntimeSkillSnapshot skill);
|
||||||
|
|
||||||
|
bool KnowsSpell(uint spellId);
|
||||||
|
|
||||||
|
bool TryGetFavorite(int tabIndex, int position, out uint spellId);
|
||||||
|
|
||||||
|
bool TryGetDesiredComponent(uint componentId, out uint amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeSocialSnapshot(
|
||||||
|
long FriendsRevision,
|
||||||
|
int FriendCount,
|
||||||
|
long SquelchRevision,
|
||||||
|
int SquelchedAccountCount,
|
||||||
|
int SquelchedCharacterCount,
|
||||||
|
int GlobalSquelchTypeCount,
|
||||||
|
int NegotiatedChatRoomCount);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeFriendSnapshot(
|
||||||
|
uint Id,
|
||||||
|
string Name,
|
||||||
|
bool Online,
|
||||||
|
bool AppearOffline);
|
||||||
|
|
||||||
|
public interface IRuntimeSocialView
|
||||||
|
{
|
||||||
|
RuntimeSocialSnapshot Snapshot { get; }
|
||||||
|
|
||||||
|
bool TryGetFriend(uint characterId, out RuntimeFriendSnapshot friend);
|
||||||
|
}
|
||||||
|
|
@ -110,6 +110,9 @@ public readonly record struct RuntimeStateCheckpoint(
|
||||||
int MaterializedEntityCount,
|
int MaterializedEntityCount,
|
||||||
int InventoryObjectCount,
|
int InventoryObjectCount,
|
||||||
int InventoryContainerCount,
|
int InventoryContainerCount,
|
||||||
|
RuntimeInventoryStateSnapshot InventoryState,
|
||||||
|
RuntimeCharacterSnapshot Character,
|
||||||
|
RuntimeSocialSnapshot Social,
|
||||||
long ChatRevision,
|
long ChatRevision,
|
||||||
int ChatCount,
|
int ChatCount,
|
||||||
RuntimeMovementSnapshot Movement,
|
RuntimeMovementSnapshot Movement,
|
||||||
|
|
@ -127,6 +130,12 @@ public interface IGameRuntimeView
|
||||||
|
|
||||||
IRuntimeInventoryView Inventory { get; }
|
IRuntimeInventoryView Inventory { get; }
|
||||||
|
|
||||||
|
IRuntimeInventoryStateView InventoryState { get; }
|
||||||
|
|
||||||
|
IRuntimeCharacterView Character { get; }
|
||||||
|
|
||||||
|
IRuntimeSocialView Social { get; }
|
||||||
|
|
||||||
IRuntimeChatView Chat { get; }
|
IRuntimeChatView Chat { get; }
|
||||||
|
|
||||||
IRuntimeMovementView Movement { get; }
|
IRuntimeMovementView Movement { get; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Gameplay;
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
@ -11,15 +12,27 @@ namespace AcDream.Runtime.Gameplay;
|
||||||
public sealed class RuntimeCharacterState : IDisposable
|
public sealed class RuntimeCharacterState : IDisposable
|
||||||
{
|
{
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private long _characterRevision;
|
||||||
|
private long _spellbookRevision;
|
||||||
|
|
||||||
public RuntimeCharacterState(SpellTable? spellTable = null)
|
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||||
{
|
{
|
||||||
Spellbook = new Spellbook(spellTable);
|
Spellbook = new Spellbook(spellTable);
|
||||||
LocalPlayer = new LocalPlayerState(Spellbook);
|
LocalPlayer = new LocalPlayerState(Spellbook);
|
||||||
|
Options = new RuntimeCharacterOptionsState();
|
||||||
|
MovementSkills = new RuntimeMovementSkillState();
|
||||||
|
View = new CharacterView(this);
|
||||||
|
Spellbook.StateChanged += OnSpellbookChanged;
|
||||||
|
LocalPlayer.Changed += OnVitalChanged;
|
||||||
|
LocalPlayer.AttributeChanged += OnAttributeChanged;
|
||||||
|
LocalPlayer.CharacterChanged += OnCharacterChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Spellbook Spellbook { get; }
|
public Spellbook Spellbook { get; }
|
||||||
public LocalPlayerState LocalPlayer { get; }
|
public LocalPlayerState LocalPlayer { get; }
|
||||||
|
public RuntimeCharacterOptionsState Options { get; }
|
||||||
|
public RuntimeMovementSkillState MovementSkills { get; }
|
||||||
|
public IRuntimeCharacterView View { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -55,6 +68,8 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
List<Exception>? failures = null;
|
List<Exception>? failures = null;
|
||||||
Try(Spellbook.Clear, ref failures);
|
Try(Spellbook.Clear, ref failures);
|
||||||
Try(LocalPlayer.Clear, ref failures);
|
Try(LocalPlayer.Clear, ref failures);
|
||||||
|
Try(Options.ResetSession, ref failures);
|
||||||
|
Try(MovementSkills.ResetSession, ref failures);
|
||||||
if (failures is not null)
|
if (failures is not null)
|
||||||
{
|
{
|
||||||
throw new AggregateException(
|
throw new AggregateException(
|
||||||
|
|
@ -68,9 +83,25 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
ResetSession();
|
ResetSession();
|
||||||
|
Spellbook.StateChanged -= OnSpellbookChanged;
|
||||||
|
LocalPlayer.Changed -= OnVitalChanged;
|
||||||
|
LocalPlayer.AttributeChanged -= OnAttributeChanged;
|
||||||
|
LocalPlayer.CharacterChanged -= OnCharacterChanged;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSpellbookChanged() =>
|
||||||
|
Interlocked.Increment(ref _spellbookRevision);
|
||||||
|
|
||||||
|
private void OnVitalChanged(LocalPlayerState.VitalKind _) =>
|
||||||
|
Interlocked.Increment(ref _characterRevision);
|
||||||
|
|
||||||
|
private void OnAttributeChanged(LocalPlayerState.AttributeKind _) =>
|
||||||
|
Interlocked.Increment(ref _characterRevision);
|
||||||
|
|
||||||
|
private void OnCharacterChanged() =>
|
||||||
|
Interlocked.Increment(ref _characterRevision);
|
||||||
|
|
||||||
private static void Try(Action action, ref List<Exception>? failures)
|
private static void Try(Action action, ref List<Exception>? failures)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -82,4 +113,212 @@ public sealed class RuntimeCharacterState : IDisposable
|
||||||
(failures ??= []).Add(error);
|
(failures ??= []).Add(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class CharacterView(RuntimeCharacterState owner)
|
||||||
|
: IRuntimeCharacterView
|
||||||
|
{
|
||||||
|
public RuntimeCharacterSnapshot Snapshot => new(
|
||||||
|
Interlocked.Read(ref owner._characterRevision),
|
||||||
|
Interlocked.Read(ref owner._spellbookRevision),
|
||||||
|
owner.Options.Snapshot,
|
||||||
|
owner.MovementSkills.Snapshot,
|
||||||
|
owner.Spellbook.LearnedSpells.Count,
|
||||||
|
owner.Spellbook.ActiveEnchantments.Count(),
|
||||||
|
owner.Spellbook.DesiredComponents.Count,
|
||||||
|
owner.LocalPlayer.Skills.Count,
|
||||||
|
owner.Spellbook.SpellbookFilters);
|
||||||
|
|
||||||
|
public bool TryGetVital(int kind, out RuntimeVitalSnapshot vital)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined((LocalPlayerState.VitalKind)kind)
|
||||||
|
|| owner.LocalPlayer.Get((LocalPlayerState.VitalKind)kind)
|
||||||
|
is not LocalPlayerState.VitalSnapshot current)
|
||||||
|
{
|
||||||
|
vital = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vital = new RuntimeVitalSnapshot(
|
||||||
|
kind,
|
||||||
|
current.Ranks,
|
||||||
|
current.Start,
|
||||||
|
current.Xp,
|
||||||
|
current.Current,
|
||||||
|
owner.LocalPlayer.GetMaxApprox(
|
||||||
|
(LocalPlayerState.VitalKind)kind) ?? 0u);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetAttribute(
|
||||||
|
int kind,
|
||||||
|
out RuntimeAttributeSnapshot attribute)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined((LocalPlayerState.AttributeKind)kind)
|
||||||
|
|| owner.LocalPlayer.GetAttribute(
|
||||||
|
(LocalPlayerState.AttributeKind)kind)
|
||||||
|
is not LocalPlayerState.AttributeSnapshot current)
|
||||||
|
{
|
||||||
|
attribute = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute = new RuntimeAttributeSnapshot(
|
||||||
|
kind,
|
||||||
|
current.Ranks,
|
||||||
|
current.Start,
|
||||||
|
current.Xp,
|
||||||
|
current.Current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSkill(uint skillId, out RuntimeSkillSnapshot skill)
|
||||||
|
{
|
||||||
|
if (owner.LocalPlayer.GetSkill(skillId)
|
||||||
|
is not LocalPlayerState.SkillSnapshot current)
|
||||||
|
{
|
||||||
|
skill = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
skill = new RuntimeSkillSnapshot(
|
||||||
|
current.SkillId,
|
||||||
|
current.Ranks,
|
||||||
|
current.Status,
|
||||||
|
current.Xp,
|
||||||
|
current.Init,
|
||||||
|
current.Resistance,
|
||||||
|
current.LastUsed,
|
||||||
|
current.FormulaBonus,
|
||||||
|
current.CurrentLevel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool KnowsSpell(uint spellId) =>
|
||||||
|
owner.Spellbook.Knows(spellId);
|
||||||
|
|
||||||
|
public bool TryGetFavorite(
|
||||||
|
int tabIndex,
|
||||||
|
int position,
|
||||||
|
out uint spellId)
|
||||||
|
{
|
||||||
|
IReadOnlyList<uint> favorites =
|
||||||
|
owner.Spellbook.GetFavorites(tabIndex);
|
||||||
|
if ((uint)position >= (uint)favorites.Count)
|
||||||
|
{
|
||||||
|
spellId = 0u;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
spellId = favorites[position];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetDesiredComponent(
|
||||||
|
uint componentId,
|
||||||
|
out uint amount) =>
|
||||||
|
owner.Spellbook.DesiredComponents.TryGetValue(
|
||||||
|
componentId,
|
||||||
|
out amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeCharacterOptionsSnapshot(
|
||||||
|
uint Options1,
|
||||||
|
uint Options2,
|
||||||
|
long Revision)
|
||||||
|
{
|
||||||
|
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||||
|
(Options1
|
||||||
|
& (uint)PlayerDescriptionParser.CharacterOptions1
|
||||||
|
.DragItemOnPlayerOpensSecureTrade) != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical session-owned copy of retail's two character-option bitfields.
|
||||||
|
/// <c>PlayerModule::PlayerModule @ 0x005D51F0</c> installs the defaults.
|
||||||
|
/// Runtime reset restores the equivalent fresh-player-module state because
|
||||||
|
/// one Runtime owner survives across graphical and no-window sessions.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeCharacterOptionsState
|
||||||
|
{
|
||||||
|
public const uint DefaultOptions1 =
|
||||||
|
(uint)PlayerDescriptionParser.CharacterOptions1.Default;
|
||||||
|
public const uint DefaultOptions2 = 0x00948700u;
|
||||||
|
|
||||||
|
private uint _options1 = DefaultOptions1;
|
||||||
|
private uint _options2 = DefaultOptions2;
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public uint Options1 => Volatile.Read(ref _options1);
|
||||||
|
public uint Options2 => Volatile.Read(ref _options2);
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
public RuntimeCharacterOptionsSnapshot Snapshot =>
|
||||||
|
new(_options1, _options2, Revision);
|
||||||
|
|
||||||
|
public bool DragItemOnPlayerOpensSecureTrade =>
|
||||||
|
Snapshot.DragItemOnPlayerOpensSecureTrade;
|
||||||
|
|
||||||
|
public void Replace(uint options1, uint options2)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _options1, options1);
|
||||||
|
Volatile.Write(ref _options2, options2);
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _options1, DefaultOptions1);
|
||||||
|
Volatile.Write(ref _options2, DefaultOptions2);
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeMovementSkillSnapshot(
|
||||||
|
int RunSkill,
|
||||||
|
int JumpSkill,
|
||||||
|
long Revision)
|
||||||
|
{
|
||||||
|
public bool IsComplete => RunSkill >= 0 && JumpSkill >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-authoritative run/jump values retained independently of any
|
||||||
|
/// graphical movement controller. App applies this borrowed state whenever
|
||||||
|
/// its presentation/physics controller exists or is rebuilt.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeMovementSkillState
|
||||||
|
{
|
||||||
|
private int _runSkill = -1;
|
||||||
|
private int _jumpSkill = -1;
|
||||||
|
private long _revision;
|
||||||
|
|
||||||
|
public int RunSkill => Volatile.Read(ref _runSkill);
|
||||||
|
public int JumpSkill => Volatile.Read(ref _jumpSkill);
|
||||||
|
public bool IsComplete => _runSkill >= 0 && _jumpSkill >= 0;
|
||||||
|
public long Revision => Interlocked.Read(ref _revision);
|
||||||
|
public RuntimeMovementSkillSnapshot Snapshot =>
|
||||||
|
new(_runSkill, _jumpSkill, Revision);
|
||||||
|
|
||||||
|
public void Update(int runSkill, int jumpSkill)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (runSkill >= 0 && RunSkill != runSkill)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _runSkill, runSkill);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (jumpSkill >= 0 && JumpSkill != jumpSkill)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _jumpSkill, jumpSkill);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _runSkill, -1);
|
||||||
|
Volatile.Write(ref _jumpSkill, -1);
|
||||||
|
Interlocked.Increment(ref _revision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
Friends = new FriendsState();
|
Friends = new FriendsState();
|
||||||
Squelch = new SquelchState();
|
Squelch = new SquelchState();
|
||||||
View = new CommunicationView(Chat);
|
View = new CommunicationView(Chat);
|
||||||
|
SocialView = new CommunicationSocialView(
|
||||||
|
TurbineChat,
|
||||||
|
Friends,
|
||||||
|
Squelch);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatLog Chat { get; }
|
public ChatLog Chat { get; }
|
||||||
|
|
@ -44,6 +48,7 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
public FriendsState Friends { get; }
|
public FriendsState Friends { get; }
|
||||||
public SquelchState Squelch { get; }
|
public SquelchState Squelch { get; }
|
||||||
public IRuntimeChatView View { get; }
|
public IRuntimeChatView View { get; }
|
||||||
|
public IRuntimeSocialView SocialView { get; }
|
||||||
public IRuntimeCommunicationEventSource Events => _events;
|
public IRuntimeCommunicationEventSource Events => _events;
|
||||||
|
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
@ -79,6 +84,61 @@ public sealed class RuntimeCommunicationState : IDisposable
|
||||||
public long Revision => chat.Revision;
|
public long Revision => chat.Revision;
|
||||||
public int Count => chat.Count;
|
public int Count => chat.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class CommunicationSocialView(
|
||||||
|
TurbineChatState turbineChat,
|
||||||
|
FriendsState friends,
|
||||||
|
SquelchState squelch)
|
||||||
|
: IRuntimeSocialView
|
||||||
|
{
|
||||||
|
public RuntimeSocialSnapshot Snapshot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
SquelchDatabase database = squelch.Snapshot();
|
||||||
|
return new RuntimeSocialSnapshot(
|
||||||
|
friends.Revision,
|
||||||
|
friends.Count,
|
||||||
|
squelch.Revision,
|
||||||
|
database.Accounts.Count,
|
||||||
|
database.Characters.Count,
|
||||||
|
database.Global.MessageTypes.Count,
|
||||||
|
CountRooms(turbineChat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetFriend(
|
||||||
|
uint characterId,
|
||||||
|
out RuntimeFriendSnapshot friend)
|
||||||
|
{
|
||||||
|
if (!friends.TryGet(characterId, out FriendEntry? current)
|
||||||
|
|| current is null)
|
||||||
|
{
|
||||||
|
friend = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
friend = new RuntimeFriendSnapshot(
|
||||||
|
current.Id,
|
||||||
|
current.Name,
|
||||||
|
current.Online,
|
||||||
|
current.AppearOffline);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountRooms(TurbineChatState state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
if (state.AllegianceRoom != 0u) count++;
|
||||||
|
if (state.GeneralRoom != 0u) count++;
|
||||||
|
if (state.TradeRoom != 0u) count++;
|
||||||
|
if (state.LfgRoom != 0u) count++;
|
||||||
|
if (state.RoleplayRoom != 0u) count++;
|
||||||
|
if (state.SocietyRoom != 0u) count++;
|
||||||
|
if (state.OlthoiRoom != 0u) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class RuntimeCommunicationEventStream
|
internal sealed class RuntimeCommunicationEventStream
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
ItemMana = new ItemManaState();
|
ItemMana = new ItemManaState();
|
||||||
Shortcuts = new ShortcutState();
|
Shortcuts = new ShortcutState();
|
||||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||||
|
View = new InventoryStateView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientObjectTable Objects => _entityObjects.Objects;
|
public ClientObjectTable Objects => _entityObjects.Objects;
|
||||||
|
|
@ -28,6 +29,7 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
public ItemManaState ItemMana { get; }
|
public ItemManaState ItemMana { get; }
|
||||||
public ShortcutState Shortcuts { get; }
|
public ShortcutState Shortcuts { get; }
|
||||||
public InventoryTransactionState Transactions { get; }
|
public InventoryTransactionState Transactions { get; }
|
||||||
|
public IRuntimeInventoryStateView View { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
public void ResetExternalContainer() => ExternalContainers.Reset();
|
public void ResetExternalContainer() => ExternalContainers.Reset();
|
||||||
|
|
@ -66,6 +68,61 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
(failures ??= []).Add(error);
|
(failures ??= []).Add(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class InventoryStateView(RuntimeInventoryState owner)
|
||||||
|
: IRuntimeInventoryStateView
|
||||||
|
{
|
||||||
|
public RuntimeInventoryStateSnapshot Snapshot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
RuntimePendingInventoryRequestSnapshot? pending = null;
|
||||||
|
if (owner.Transactions.TryGetPending(
|
||||||
|
out PendingInventoryRequest request))
|
||||||
|
{
|
||||||
|
pending = new RuntimePendingInventoryRequestSnapshot(
|
||||||
|
request.Token,
|
||||||
|
(int)request.Kind,
|
||||||
|
request.ItemId,
|
||||||
|
request.Dispatched);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RuntimeInventoryStateSnapshot(
|
||||||
|
owner.ExternalContainers.RequestedContainerId,
|
||||||
|
owner.ExternalContainers.CurrentContainerId,
|
||||||
|
owner.Transactions.BusyCount,
|
||||||
|
owner.Transactions.CanBeginRequest,
|
||||||
|
pending,
|
||||||
|
owner.Shortcuts.Items.Count,
|
||||||
|
owner.Shortcuts.Revision,
|
||||||
|
owner.ItemMana.Count,
|
||||||
|
owner.ItemMana.Revision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetShortcut(
|
||||||
|
int index,
|
||||||
|
out RuntimeShortcutSnapshot shortcut)
|
||||||
|
{
|
||||||
|
IReadOnlyList<ShortcutEntry> shortcuts = owner.Shortcuts.Items;
|
||||||
|
for (int i = 0; i < shortcuts.Count; i++)
|
||||||
|
{
|
||||||
|
if (shortcuts[i].Index != index)
|
||||||
|
continue;
|
||||||
|
ShortcutEntry current = shortcuts[i];
|
||||||
|
shortcut = new RuntimeShortcutSnapshot(
|
||||||
|
current.Index,
|
||||||
|
current.ObjectId,
|
||||||
|
current.SpellId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
shortcut = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetItemMana(uint objectId, out float fraction) =>
|
||||||
|
owner.ItemMana.TryGetManaPercent(objectId, out fraction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ShortcutState
|
public sealed class ShortcutState
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ public sealed record LiveCharacterSessionBindings(
|
||||||
Action<int, int>? OnSkillsUpdated,
|
Action<int, int>? OnSkillsUpdated,
|
||||||
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
||||||
Action<GameEvents.CharacterConfirmationDone>? OnConfirmationDone,
|
Action<GameEvents.CharacterConfirmationDone>? OnConfirmationDone,
|
||||||
Action<uint, uint>? OnCharacterOptions,
|
|
||||||
Func<double>? ClientTime);
|
Func<double>? ClientTime);
|
||||||
|
|
||||||
public sealed record LiveSocialSessionBindings(
|
public sealed record LiveSocialSessionBindings(
|
||||||
|
|
@ -152,7 +151,13 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
social.Chat,
|
social.Chat,
|
||||||
character.Character.LocalPlayer,
|
character.Character.LocalPlayer,
|
||||||
social.TurbineChat,
|
social.TurbineChat,
|
||||||
onSkillsUpdated: character.OnSkillsUpdated,
|
onSkillsUpdated: (runSkill, jumpSkill) =>
|
||||||
|
{
|
||||||
|
character.Character.MovementSkills.Update(
|
||||||
|
runSkill,
|
||||||
|
jumpSkill);
|
||||||
|
character.OnSkillsUpdated?.Invoke(runSkill, jumpSkill);
|
||||||
|
},
|
||||||
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
||||||
onShortcuts: inventory.OnShortcuts,
|
onShortcuts: inventory.OnShortcuts,
|
||||||
playerGuid: inventory.PlayerGuid,
|
playerGuid: inventory.PlayerGuid,
|
||||||
|
|
@ -164,7 +169,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
friends: social.Friends,
|
friends: social.Friends,
|
||||||
squelch: social.Squelch,
|
squelch: social.Squelch,
|
||||||
onDesiredComponents: null,
|
onDesiredComponents: null,
|
||||||
onCharacterOptions: character.OnCharacterOptions,
|
onCharacterOptions: character.Character.Options.Replace,
|
||||||
clientTime: character.ClientTime,
|
clientTime: character.ClientTime,
|
||||||
externalContainers: inventory.ExternalContainers,
|
externalContainers: inventory.ExternalContainers,
|
||||||
accepting: IsAccepting));
|
accepting: IsAccepting));
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,6 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
PlayerIdentity: null!,
|
PlayerIdentity: null!,
|
||||||
PlayerController: null!,
|
PlayerController: null!,
|
||||||
PlayerMode: null!,
|
PlayerMode: null!,
|
||||||
CharacterOptions: null!,
|
|
||||||
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
|
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
|
||||||
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
|
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
|
||||||
ExistingVitals: null,
|
ExistingVitals: null,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using AcDream.App.UI.Layout;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
using AcDream.Runtime;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Composition;
|
namespace AcDream.App.Tests.Composition;
|
||||||
|
|
@ -67,6 +68,44 @@ public sealed class InteractionUiRuntimeSourcesTests
|
||||||
Assert.Throws<ObjectDisposedException>(() => source.Bind(query, interactions));
|
Assert.Throws<ObjectDisposedException>(() => source.Bind(query, interactions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameRuntimeCommandsCaptureTheExactCurrentGeneration()
|
||||||
|
{
|
||||||
|
var source = new DeferredGameRuntimeStateCommands();
|
||||||
|
var first = new RuntimeTarget(new RuntimeGenerationToken(7));
|
||||||
|
var second = new RuntimeTarget(new RuntimeGenerationToken(9));
|
||||||
|
|
||||||
|
Assert.False(source.IsInWorld);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Inactive,
|
||||||
|
source.Advance(RuntimeAdvancementKind.Skill, 6u, 100u).Status);
|
||||||
|
|
||||||
|
IDisposable stale = source.Bind(first, first);
|
||||||
|
Assert.True(source.IsInWorld);
|
||||||
|
Assert.True(source.Advance(
|
||||||
|
RuntimeAdvancementKind.Skill,
|
||||||
|
6u,
|
||||||
|
100u).Accepted);
|
||||||
|
Assert.Equal(new RuntimeGenerationToken(7), first.LastGeneration);
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
source.Bind(second, second));
|
||||||
|
|
||||||
|
stale.Dispose();
|
||||||
|
using IDisposable current = source.Bind(second, second);
|
||||||
|
stale.Dispose();
|
||||||
|
Assert.True(source.AddFavorite(0, 2, 42u).Accepted);
|
||||||
|
Assert.Equal(new RuntimeGenerationToken(9), second.LastGeneration);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Rejected,
|
||||||
|
source.RemoveShortcut(uint.MaxValue).Status);
|
||||||
|
|
||||||
|
source.Deactivate();
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Inactive,
|
||||||
|
source.RemoveShortcut(3u).Status);
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => source.Bind(first, first));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RadarNeverCachesAnUnboundBootstrapSnapshot()
|
public void RadarNeverCachesAnUnboundBootstrapSnapshot()
|
||||||
{
|
{
|
||||||
|
|
@ -172,6 +211,109 @@ public sealed class InteractionUiRuntimeSourcesTests
|
||||||
public ICommandBus Commands { get; } = new LiveCommandBus();
|
public ICommandBus Commands { get; } = new LiveCommandBus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RuntimeTarget
|
||||||
|
: IGameRuntimeView,
|
||||||
|
IGameRuntimeCommands,
|
||||||
|
IRuntimeInventoryStateCommands,
|
||||||
|
IRuntimeSpellbookCommands,
|
||||||
|
IRuntimeCharacterCommands
|
||||||
|
{
|
||||||
|
public RuntimeTarget(RuntimeGenerationToken generation)
|
||||||
|
{
|
||||||
|
Generation = generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeGenerationToken LastGeneration { get; private set; }
|
||||||
|
public RuntimeGenerationToken Generation { get; }
|
||||||
|
public RuntimeLifecycleSnapshot Lifecycle =>
|
||||||
|
new(Generation, RuntimeLifecycleState.InWorld, 1u, true);
|
||||||
|
public IGameRuntimeClock Clock => null!;
|
||||||
|
public IRuntimeEntityView Entities => null!;
|
||||||
|
public IRuntimeInventoryView Inventory => null!;
|
||||||
|
public IRuntimeInventoryStateView InventoryState => null!;
|
||||||
|
public IRuntimeCharacterView Character => null!;
|
||||||
|
public IRuntimeSocialView Social => null!;
|
||||||
|
public IRuntimeChatView Chat => null!;
|
||||||
|
public IRuntimeMovementView Movement => null!;
|
||||||
|
public IRuntimePortalView Portal => null!;
|
||||||
|
public IRuntimeSessionCommands Session => null!;
|
||||||
|
public IRuntimeSelectionCommands Selection => null!;
|
||||||
|
public IRuntimeCombatCommands Combat => null!;
|
||||||
|
IRuntimeMovementCommands IGameRuntimeCommands.Movement => null!;
|
||||||
|
IRuntimeChatCommands IGameRuntimeCommands.Chat => null!;
|
||||||
|
IRuntimePortalCommands IGameRuntimeCommands.Portal => null!;
|
||||||
|
IRuntimeInventoryStateCommands IGameRuntimeCommands.InventoryState => this;
|
||||||
|
IRuntimeSpellbookCommands IGameRuntimeCommands.Spellbook => this;
|
||||||
|
IRuntimeCharacterCommands IGameRuntimeCommands.Character => this;
|
||||||
|
IRuntimeSocialCommands IGameRuntimeCommands.Social => null!;
|
||||||
|
|
||||||
|
public RuntimeStateCheckpoint CaptureCheckpoint() => default;
|
||||||
|
|
||||||
|
public RuntimeCommandResult AddShortcut(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeShortcutCommand command) =>
|
||||||
|
Accepted(expectedGeneration, command.ObjectId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult RemoveShortcut(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int index) =>
|
||||||
|
Accepted(expectedGeneration);
|
||||||
|
|
||||||
|
public RuntimeCommandResult AddFavorite(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int tabIndex,
|
||||||
|
int position,
|
||||||
|
uint spellId) =>
|
||||||
|
Accepted(expectedGeneration, spellId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult RemoveFavorite(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
int tabIndex,
|
||||||
|
uint spellId) =>
|
||||||
|
Accepted(expectedGeneration, spellId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult SetFilter(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint filters) =>
|
||||||
|
Accepted(expectedGeneration);
|
||||||
|
|
||||||
|
public RuntimeCommandResult ForgetSpell(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint spellId) =>
|
||||||
|
Accepted(expectedGeneration, spellId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult SetDesiredComponent(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint componentId,
|
||||||
|
uint amount) =>
|
||||||
|
Accepted(expectedGeneration, componentId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult ClearDesiredComponents(
|
||||||
|
RuntimeGenerationToken expectedGeneration) =>
|
||||||
|
Accepted(expectedGeneration);
|
||||||
|
|
||||||
|
public RuntimeCommandResult Advance(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
in RuntimeAdvancementCommand command) =>
|
||||||
|
Accepted(expectedGeneration, command.StatId);
|
||||||
|
|
||||||
|
public RuntimeCommandResult SetOptions1(
|
||||||
|
RuntimeGenerationToken expectedGeneration,
|
||||||
|
uint options) =>
|
||||||
|
Accepted(expectedGeneration);
|
||||||
|
|
||||||
|
private RuntimeCommandResult Accepted(
|
||||||
|
RuntimeGenerationToken generation,
|
||||||
|
uint objectId = 0u)
|
||||||
|
{
|
||||||
|
LastGeneration = generation;
|
||||||
|
return new RuntimeCommandResult(
|
||||||
|
RuntimeCommandStatus.Accepted,
|
||||||
|
generation,
|
||||||
|
objectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SelectionQuery : IRetainedUiSelectionQuery
|
private sealed class SelectionQuery : IRetainedUiSelectionQuery
|
||||||
{
|
{
|
||||||
public bool ShouldShowHealth(uint serverGuid) => true;
|
public bool ShouldShowHealth(uint serverGuid) => true;
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,11 @@ public sealed class GameplayInputCommandControllerTests
|
||||||
public IGameRuntimeClock Clock => throw new NotSupportedException();
|
public IGameRuntimeClock Clock => throw new NotSupportedException();
|
||||||
public IRuntimeEntityView Entities => throw new NotSupportedException();
|
public IRuntimeEntityView Entities => throw new NotSupportedException();
|
||||||
public IRuntimeInventoryView Inventory => throw new NotSupportedException();
|
public IRuntimeInventoryView Inventory => throw new NotSupportedException();
|
||||||
|
public IRuntimeInventoryStateView InventoryState =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
public IRuntimeCharacterView Character =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
public IRuntimeSocialView Social => throw new NotSupportedException();
|
||||||
public IRuntimeChatView Chat => throw new NotSupportedException();
|
public IRuntimeChatView Chat => throw new NotSupportedException();
|
||||||
public IRuntimeMovementView Movement => throw new NotSupportedException();
|
public IRuntimeMovementView Movement => throw new NotSupportedException();
|
||||||
public IRuntimePortalView Portal => throw new NotSupportedException();
|
public IRuntimePortalView Portal => throw new NotSupportedException();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Social;
|
using AcDream.Core.Social;
|
||||||
using AcDream.UI.Abstractions;
|
using AcDream.UI.Abstractions;
|
||||||
|
|
||||||
|
|
@ -220,6 +221,35 @@ public sealed class LiveSessionCommandRouterTests
|
||||||
GC.KeepAlive(router);
|
GC.KeepAlive(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RuntimeStateCommandsUseActiveGenerationRouteAndBecomeInert()
|
||||||
|
{
|
||||||
|
var shortcuts = new List<ShortcutEntry>();
|
||||||
|
var options = new List<uint>();
|
||||||
|
LiveSessionCommandRouter router = NewRouter(
|
||||||
|
addShortcut: shortcuts.Add,
|
||||||
|
setCharacterOptions: options.Add);
|
||||||
|
|
||||||
|
router.Publish(new AddShortcutRuntimeCmd(
|
||||||
|
new ShortcutEntry(1, 0x80000001u, 0u)));
|
||||||
|
router.Activate();
|
||||||
|
router.Publish(new AddShortcutRuntimeCmd(
|
||||||
|
new ShortcutEntry(2, 0x80000002u, 0u)));
|
||||||
|
router.Publish(new SetCharacterOptionsRuntimeCmd(0x50C4A54Au));
|
||||||
|
router.Dispose();
|
||||||
|
router.Publish(new AddShortcutRuntimeCmd(
|
||||||
|
new ShortcutEntry(3, 0x80000003u, 0u)));
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
shortcuts,
|
||||||
|
entry =>
|
||||||
|
{
|
||||||
|
Assert.Equal(2, entry.Index);
|
||||||
|
Assert.Equal(0x80000002u, entry.ObjectId);
|
||||||
|
});
|
||||||
|
Assert.Equal([0x50C4A54Au], options);
|
||||||
|
}
|
||||||
|
|
||||||
private static LiveSessionCommandRouter NewRouter(
|
private static LiveSessionCommandRouter NewRouter(
|
||||||
ChatLog? chat = null,
|
ChatLog? chat = null,
|
||||||
TurbineChatState? turbine = null,
|
TurbineChatState? turbine = null,
|
||||||
|
|
@ -228,6 +258,8 @@ public sealed class LiveSessionCommandRouterTests
|
||||||
Action<string, string>? sendTell = null,
|
Action<string, string>? sendTell = null,
|
||||||
Action<uint, string>? sendChannel = null,
|
Action<uint, string>? sendChannel = null,
|
||||||
Action<uint, uint, uint, uint, string, uint>? sendTurbine = null,
|
Action<uint, uint, uint, uint, string, uint>? sendTurbine = null,
|
||||||
|
Action<ShortcutEntry>? addShortcut = null,
|
||||||
|
Action<uint>? setCharacterOptions = null,
|
||||||
Action<string>? log = null,
|
Action<string>? log = null,
|
||||||
ClientCommandController.Bindings? clientBindings = null) => new(
|
ClientCommandController.Bindings? clientBindings = null) => new(
|
||||||
new LiveSessionCommandBindings(
|
new LiveSessionCommandBindings(
|
||||||
|
|
@ -239,7 +271,27 @@ public sealed class LiveSessionCommandRouterTests
|
||||||
sendTell ?? ((_, _) => { }),
|
sendTell ?? ((_, _) => { }),
|
||||||
sendChannel ?? ((_, _) => { }),
|
sendChannel ?? ((_, _) => { }),
|
||||||
sendTurbine ?? ((_, _, _, _, _, _) => { }),
|
sendTurbine ?? ((_, _, _, _, _, _) => { }),
|
||||||
log));
|
AddShortcut: addShortcut ?? (_ => { }),
|
||||||
|
RemoveShortcut: _ => { },
|
||||||
|
AddFavorite: (_, _, _) => { },
|
||||||
|
RemoveFavorite: (_, _) => { },
|
||||||
|
SetSpellbookFilter: _ => { },
|
||||||
|
ForgetSpell: _ => { },
|
||||||
|
SetDesiredComponent: (_, _) => { },
|
||||||
|
ClearDesiredComponents: () => { },
|
||||||
|
RaiseAttribute: (_, _) => { },
|
||||||
|
RaiseVital: (_, _) => { },
|
||||||
|
RaiseSkill: (_, _) => { },
|
||||||
|
TrainSkill: (_, _) => { },
|
||||||
|
SetCharacterOptions: setCharacterOptions ?? (_ => { }),
|
||||||
|
AddFriend: _ => { },
|
||||||
|
RemoveFriend: _ => { },
|
||||||
|
ClearFriends: () => { },
|
||||||
|
RequestLegacyFriends: () => { },
|
||||||
|
ModifyCharacterSquelch: (_, _, _, _) => { },
|
||||||
|
ModifyAccountSquelch: (_, _) => { },
|
||||||
|
ModifyGlobalSquelch: (_, _) => { },
|
||||||
|
Log: log));
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
private static (LiveSessionCommandRouter, WeakReference<object>)
|
private static (LiveSessionCommandRouter, WeakReference<object>)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ using AcDream.Core.Net;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.Social;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
@ -363,6 +364,157 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Assert.Equal(0, harness.EntityObjects.Events.SubscriberCount);
|
Assert.Equal(0, harness.EntityObjects.Events.SubscriberCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdapterBorrowsExactJ4ViewsAndCheckpointState()
|
||||||
|
{
|
||||||
|
using var harness = new Harness();
|
||||||
|
harness.Character.Options.Replace(0x04000000u, 0x00948700u);
|
||||||
|
harness.Character.MovementSkills.Update(200, 175);
|
||||||
|
harness.Character.Spellbook.OnSpellLearned(42u);
|
||||||
|
harness.InventoryState.Shortcuts.Replace(
|
||||||
|
[
|
||||||
|
new ShortcutEntry(1, 0x80000001u, 0u),
|
||||||
|
]);
|
||||||
|
harness.Communication.Friends.Apply(new FriendsUpdate(
|
||||||
|
FriendsUpdateType.Full,
|
||||||
|
[
|
||||||
|
new FriendEntry(
|
||||||
|
0x50000003u,
|
||||||
|
"Borrowed",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
[]),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Assert.Same(harness.Character.View, harness.Runtime.Character);
|
||||||
|
Assert.Same(
|
||||||
|
harness.InventoryState.View,
|
||||||
|
harness.Runtime.InventoryState);
|
||||||
|
Assert.Same(
|
||||||
|
harness.Communication.SocialView,
|
||||||
|
harness.Runtime.Social);
|
||||||
|
|
||||||
|
RuntimeStateCheckpoint checkpoint =
|
||||||
|
harness.Runtime.CaptureCheckpoint();
|
||||||
|
Assert.Equal(1, checkpoint.Character.LearnedSpellCount);
|
||||||
|
Assert.True(checkpoint.Character.MovementSkills.IsComplete);
|
||||||
|
Assert.Equal(1, checkpoint.InventoryState.ShortcutCount);
|
||||||
|
Assert.Equal(1, checkpoint.Social.FriendCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void J4StateCommandsAreGenerationGatedAndReachCurrentTransport()
|
||||||
|
{
|
||||||
|
using var harness = new Harness();
|
||||||
|
var trace = new RuntimeTraceRecorder();
|
||||||
|
using IDisposable subscription = harness.Runtime.Subscribe(trace);
|
||||||
|
_ = harness.Runtime.Session.Start(harness.Runtime.Generation);
|
||||||
|
RuntimeGenerationToken generation = harness.Runtime.Generation;
|
||||||
|
IGameRuntimeCommands commands = harness.Runtime;
|
||||||
|
|
||||||
|
RuntimeCommandResult shortcut = commands.InventoryState.AddShortcut(
|
||||||
|
generation,
|
||||||
|
new RuntimeShortcutCommand(2, 0x80000001u, 0u));
|
||||||
|
RuntimeCommandResult favorite = commands.Spellbook.AddFavorite(
|
||||||
|
generation,
|
||||||
|
tabIndex: 0,
|
||||||
|
position: 0,
|
||||||
|
spellId: 42u);
|
||||||
|
RuntimeCommandResult filter = commands.Spellbook.SetFilter(
|
||||||
|
generation,
|
||||||
|
0x3FFEu);
|
||||||
|
RuntimeCommandResult desired = commands.Spellbook.SetDesiredComponent(
|
||||||
|
generation,
|
||||||
|
0x68000001u,
|
||||||
|
10u);
|
||||||
|
RuntimeCommandResult advancement = commands.Character.Advance(
|
||||||
|
generation,
|
||||||
|
new RuntimeAdvancementCommand(
|
||||||
|
RuntimeAdvancementKind.Skill,
|
||||||
|
StatId: 6u,
|
||||||
|
Cost: 500u));
|
||||||
|
RuntimeCommandResult options = commands.Character.SetOptions1(
|
||||||
|
generation,
|
||||||
|
0x50C4A54Au);
|
||||||
|
RuntimeCommandResult friend = commands.Social.Execute(
|
||||||
|
generation,
|
||||||
|
new RuntimeFriendCommand(
|
||||||
|
RuntimeFriendCommandKind.Add,
|
||||||
|
Name: "Runtime Friend"));
|
||||||
|
RuntimeCommandResult squelch = commands.Social.Execute(
|
||||||
|
generation,
|
||||||
|
new RuntimeSquelchCommand(
|
||||||
|
RuntimeSquelchScope.Global,
|
||||||
|
Add: true,
|
||||||
|
MessageType: 3u));
|
||||||
|
|
||||||
|
Assert.All(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
shortcut,
|
||||||
|
favorite,
|
||||||
|
filter,
|
||||||
|
desired,
|
||||||
|
advancement,
|
||||||
|
options,
|
||||||
|
friend,
|
||||||
|
squelch,
|
||||||
|
},
|
||||||
|
static result => Assert.True(result.Accepted));
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is AddShortcutRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is AddFavoriteRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is SetSpellbookFilterRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is SetDesiredComponentRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is RaiseSkillRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is SetCharacterOptionsRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is AddFriendRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
harness.Commands.Published,
|
||||||
|
static command => command is ModifyGlobalSquelchRuntimeCmd);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
|
&& (entry.Code >> 16)
|
||||||
|
== (int)RuntimeCommandDomain.InventoryState);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
|
&& (entry.Code >> 16)
|
||||||
|
== (int)RuntimeCommandDomain.Spellbook);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
|
&& (entry.Code >> 16)
|
||||||
|
== (int)RuntimeCommandDomain.Character);
|
||||||
|
Assert.Contains(
|
||||||
|
trace.Entries,
|
||||||
|
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||||
|
&& (entry.Code >> 16)
|
||||||
|
== (int)RuntimeCommandDomain.Social);
|
||||||
|
|
||||||
|
int published = harness.Commands.Published.Count;
|
||||||
|
RuntimeCommandResult stale = commands.Character.SetOptions1(
|
||||||
|
new RuntimeGenerationToken(generation.Value - 1),
|
||||||
|
0u);
|
||||||
|
Assert.Equal(RuntimeCommandStatus.StaleGeneration, stale.Status);
|
||||||
|
Assert.Equal(published, harness.Commands.Published.Count);
|
||||||
|
}
|
||||||
|
|
||||||
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
||||||
{
|
{
|
||||||
ObjectId = spawn.Guid,
|
ObjectId = spawn.Guid,
|
||||||
|
|
@ -407,6 +559,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
{
|
{
|
||||||
Identity = new LocalPlayerIdentityState();
|
Identity = new LocalPlayerIdentityState();
|
||||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||||
|
InventoryState = new RuntimeInventoryState(EntityObjects);
|
||||||
|
Character = new RuntimeCharacterState();
|
||||||
Entities = new LiveEntityRuntime(
|
Entities = new LiveEntityRuntime(
|
||||||
new GpuWorldState(),
|
new GpuWorldState(),
|
||||||
new NoopEntityResources(),
|
new NoopEntityResources(),
|
||||||
|
|
@ -448,7 +602,9 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
|
|
||||||
Options = LiveOptions();
|
Options = LiveOptions();
|
||||||
Commands = new RecordingCommandRouting();
|
Commands = new RecordingCommandRouting();
|
||||||
_session = new LiveSessionController(new SessionOperations());
|
Transport = new TestTransport();
|
||||||
|
_session = new LiveSessionController(
|
||||||
|
new SessionOperations(Transport));
|
||||||
Host = CreateHost(_session, Commands, Identity);
|
Host = CreateHost(_session, Commands, Identity);
|
||||||
Runtime = new CurrentGameRuntimeAdapter(
|
Runtime = new CurrentGameRuntimeAdapter(
|
||||||
_session,
|
_session,
|
||||||
|
|
@ -457,6 +613,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Identity,
|
Identity,
|
||||||
Entities,
|
Entities,
|
||||||
EntityObjects,
|
EntityObjects,
|
||||||
|
InventoryState,
|
||||||
|
Character,
|
||||||
Communication,
|
Communication,
|
||||||
new LocalPlayerControllerSlot(),
|
new LocalPlayerControllerSlot(),
|
||||||
WorldReveal,
|
WorldReveal,
|
||||||
|
|
@ -470,6 +628,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public RuntimeOptions Options { get; }
|
public RuntimeOptions Options { get; }
|
||||||
public LocalPlayerIdentityState Identity { get; }
|
public LocalPlayerIdentityState Identity { get; }
|
||||||
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||||
|
public RuntimeInventoryState InventoryState { get; }
|
||||||
|
public RuntimeCharacterState Character { get; }
|
||||||
public LiveEntityRuntime Entities { get; }
|
public LiveEntityRuntime Entities { get; }
|
||||||
public ClientObjectTable Objects { get; }
|
public ClientObjectTable Objects { get; }
|
||||||
public RuntimeCommunicationState Communication { get; }
|
public RuntimeCommunicationState Communication { get; }
|
||||||
|
|
@ -481,12 +641,15 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public UpdateFrameClock Clock { get; }
|
public UpdateFrameClock Clock { get; }
|
||||||
public WorldRevealCoordinator WorldReveal { get; }
|
public WorldRevealCoordinator WorldReveal { get; }
|
||||||
public RecordingCommandRouting Commands { get; }
|
public RecordingCommandRouting Commands { get; }
|
||||||
|
public TestTransport Transport { get; }
|
||||||
public LiveSessionHost Host { get; }
|
public LiveSessionHost Host { get; }
|
||||||
public CurrentGameRuntimeAdapter Runtime { get; }
|
public CurrentGameRuntimeAdapter Runtime { get; }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Runtime.Dispose();
|
Runtime.Dispose();
|
||||||
|
Character.Dispose();
|
||||||
|
InventoryState.Dispose();
|
||||||
Communication.Dispose();
|
Communication.Dispose();
|
||||||
_session.Dispose();
|
_session.Dispose();
|
||||||
_items.Dispose();
|
_items.Dispose();
|
||||||
|
|
@ -644,13 +807,14 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
Physics: physics);
|
Physics: physics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SessionOperations : ILiveSessionOperations
|
private sealed class SessionOperations(TestTransport transport)
|
||||||
|
: ILiveSessionOperations
|
||||||
{
|
{
|
||||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||||
new(IPAddress.Loopback, port);
|
new(IPAddress.Loopback, port);
|
||||||
|
|
||||||
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
||||||
new(endpoint, new TestTransport());
|
new(endpoint, transport);
|
||||||
|
|
||||||
public void Connect(WorldSession session, string user, string password)
|
public void Connect(WorldSession session, string user, string password)
|
||||||
{
|
{
|
||||||
|
|
@ -686,14 +850,18 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
public void DisposeSession(WorldSession session) => session.Dispose();
|
public void DisposeSession(WorldSession session) => session.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class TestTransport : IWorldSessionTransport
|
internal sealed class TestTransport : IWorldSessionTransport
|
||||||
{
|
{
|
||||||
|
public List<byte[]> Sent { get; } = [];
|
||||||
|
|
||||||
public void Send(ReadOnlySpan<byte> datagram)
|
public void Send(ReadOnlySpan<byte> datagram)
|
||||||
{
|
{
|
||||||
|
Sent.Add(datagram.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
|
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
|
||||||
{
|
{
|
||||||
|
Sent.Add(datagram.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Receive(
|
public int Receive(
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,37 @@ public sealed class RuntimeCharacterOwnershipTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppCharacterOptionAndMovementSkillOwnersWereDeleted()
|
||||||
|
{
|
||||||
|
string gameWindow = ReadSource("Rendering", "GameWindow.cs");
|
||||||
|
string sources = ReadSource(
|
||||||
|
"Composition",
|
||||||
|
"InteractionUiRuntimeSources.cs");
|
||||||
|
string playerState = ReadSource("Input", "LocalPlayerRuntimeState.cs");
|
||||||
|
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
||||||
|
string router = ReadRuntimeSource(
|
||||||
|
"Session",
|
||||||
|
"LiveSessionEventRouter.cs");
|
||||||
|
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"PlayerCharacterOptionsState",
|
||||||
|
gameWindow + sources + session,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"LocalPlayerSkillState",
|
||||||
|
gameWindow + playerState + session,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"character.Character.Options.Replace",
|
||||||
|
router,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"character.Character.MovementSkills.Update",
|
||||||
|
router,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ReadSource(params string[] relative) =>
|
private static string ReadSource(params string[] relative) =>
|
||||||
File.ReadAllText(Path.Combine(
|
File.ReadAllText(Path.Combine(
|
||||||
[FindRepositoryRoot(), "src", "AcDream.App", .. relative]));
|
[FindRepositoryRoot(), "src", "AcDream.App", .. relative]));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests;
|
namespace AcDream.Runtime.Tests;
|
||||||
|
|
||||||
public sealed class GameRuntimeContractTests
|
public sealed class GameRuntimeContractTests
|
||||||
|
|
@ -137,4 +140,68 @@ public sealed class GameRuntimeContractTests
|
||||||
Assert.Equal("General|Grey|hello", entry.Text);
|
Assert.Equal("General|Grey|hello", entry.Text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TraceRecorderIncludesJ4GameplayStateInCheckpoint()
|
||||||
|
{
|
||||||
|
var recorder = new RuntimeTraceRecorder();
|
||||||
|
var stamp = new RuntimeEventStamp(new(3), 1, 8);
|
||||||
|
var checkpoint = new RuntimeStateCheckpoint(
|
||||||
|
new RuntimeGenerationToken(3),
|
||||||
|
RuntimeLifecycleState.InWorld,
|
||||||
|
FrameNumber: 8,
|
||||||
|
EntityCount: 2,
|
||||||
|
MaterializedEntityCount: 2,
|
||||||
|
InventoryObjectCount: 1,
|
||||||
|
InventoryContainerCount: 1,
|
||||||
|
new RuntimeInventoryStateSnapshot(
|
||||||
|
0u, 0u, 0, true, null, 2, 4, 1, 3),
|
||||||
|
new RuntimeCharacterSnapshot(
|
||||||
|
CharacterRevision: 5,
|
||||||
|
SpellbookRevision: 6,
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
LearnedSpellCount: 7,
|
||||||
|
ActiveEnchantmentCount: 0,
|
||||||
|
DesiredComponentCount: 0,
|
||||||
|
SkillCount: 8,
|
||||||
|
SpellbookFilters: 0x3FFFu),
|
||||||
|
new RuntimeSocialSnapshot(9, 10, 11, 0, 0, 0, 0),
|
||||||
|
ChatRevision: 12,
|
||||||
|
ChatCount: 13,
|
||||||
|
default,
|
||||||
|
default);
|
||||||
|
|
||||||
|
recorder.AddCheckpoint(stamp, checkpoint);
|
||||||
|
|
||||||
|
RuntimeTraceEntry entry = Assert.Single(recorder.Entries);
|
||||||
|
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
||||||
|
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
||||||
|
Assert.Contains("social=9:10:11", entry.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void J4GameplayOwnersHaveNoStaticMutableSessionState()
|
||||||
|
{
|
||||||
|
Type[] owners =
|
||||||
|
[
|
||||||
|
typeof(RuntimeCommunicationState),
|
||||||
|
typeof(RuntimeInventoryState),
|
||||||
|
typeof(RuntimeCharacterState),
|
||||||
|
typeof(RuntimeCharacterOptionsState),
|
||||||
|
typeof(RuntimeMovementSkillState),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Type owner in owners)
|
||||||
|
{
|
||||||
|
FieldInfo[] mutable = owner
|
||||||
|
.GetFields(
|
||||||
|
BindingFlags.Static
|
||||||
|
| BindingFlags.Public
|
||||||
|
| BindingFlags.NonPublic)
|
||||||
|
.Where(static field => !field.IsLiteral)
|
||||||
|
.ToArray();
|
||||||
|
Assert.Empty(mutable);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Core.Player;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests.Gameplay;
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
@ -152,4 +153,105 @@ public sealed class RuntimeCharacterStateTests
|
||||||
|
|
||||||
Assert.True(state.IsDisposed);
|
Assert.True(state.IsDisposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnsRetailCharacterOptionsAndMovementSkillProjection()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||||
|
state.Options.Options1);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||||
|
state.Options.Options2);
|
||||||
|
Assert.False(state.MovementSkills.IsComplete);
|
||||||
|
|
||||||
|
state.Options.Replace(0x04000000u, 0x12345678u);
|
||||||
|
state.MovementSkills.Update(runSkill: 210, jumpSkill: -1);
|
||||||
|
state.MovementSkills.Update(runSkill: -1, jumpSkill: 165);
|
||||||
|
|
||||||
|
Assert.True(state.Options.DragItemOnPlayerOpensSecureTrade);
|
||||||
|
Assert.Equal(0x12345678u, state.Options.Options2);
|
||||||
|
Assert.Equal(
|
||||||
|
new RuntimeMovementSkillSnapshot(
|
||||||
|
210,
|
||||||
|
165,
|
||||||
|
state.MovementSkills.Revision),
|
||||||
|
state.MovementSkills.Snapshot);
|
||||||
|
Assert.True(state.MovementSkills.IsComplete);
|
||||||
|
|
||||||
|
state.ResetSession();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||||
|
state.Options.Options1);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||||
|
state.Options.Options2);
|
||||||
|
Assert.Equal(-1, state.MovementSkills.RunSkill);
|
||||||
|
Assert.Equal(-1, state.MovementSkills.JumpSkill);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CharacterViewBorrowsExactOwnersWithoutReconstructedState()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
state.LocalPlayer.OnAttributeUpdate(1u, 40u, 10u, 500u);
|
||||||
|
state.LocalPlayer.OnVitalUpdate(7u, 60u, 20u, 700u, 75u);
|
||||||
|
state.LocalPlayer.OnSkillUpdate(
|
||||||
|
6u,
|
||||||
|
30u,
|
||||||
|
2u,
|
||||||
|
800u,
|
||||||
|
10u,
|
||||||
|
0u,
|
||||||
|
5d,
|
||||||
|
12u);
|
||||||
|
state.Spellbook.OnSpellLearned(42u);
|
||||||
|
state.Spellbook.SetFavorite(0, 0, 42u);
|
||||||
|
state.Spellbook.SetDesiredComponent(0x68000001u, 11u);
|
||||||
|
|
||||||
|
RuntimeCharacterSnapshot summary = state.View.Snapshot;
|
||||||
|
|
||||||
|
Assert.Equal(1, summary.LearnedSpellCount);
|
||||||
|
Assert.Equal(1, summary.DesiredComponentCount);
|
||||||
|
Assert.Equal(1, summary.SkillCount);
|
||||||
|
Assert.True(state.View.KnowsSpell(42u));
|
||||||
|
Assert.True(state.View.TryGetFavorite(0, 0, out uint favorite));
|
||||||
|
Assert.Equal(42u, favorite);
|
||||||
|
Assert.True(state.View.TryGetDesiredComponent(
|
||||||
|
0x68000001u,
|
||||||
|
out uint desired));
|
||||||
|
Assert.Equal(11u, desired);
|
||||||
|
Assert.True(state.View.TryGetAttribute(
|
||||||
|
(int)LocalPlayerState.AttributeKind.Strength,
|
||||||
|
out RuntimeAttributeSnapshot attribute));
|
||||||
|
Assert.Equal(50u, attribute.Current);
|
||||||
|
Assert.True(state.View.TryGetVital(
|
||||||
|
(int)LocalPlayerState.VitalKind.Health,
|
||||||
|
out RuntimeVitalSnapshot vital));
|
||||||
|
Assert.Equal(75u, vital.Current);
|
||||||
|
Assert.True(state.View.TryGetSkill(6u, out RuntimeSkillSnapshot skill));
|
||||||
|
Assert.Equal(52u, skill.CurrentLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoRuntimeInstancesIsolateOptionsSkillsAndViewRevisions()
|
||||||
|
{
|
||||||
|
using var first = new RuntimeCharacterState();
|
||||||
|
using var second = new RuntimeCharacterState();
|
||||||
|
|
||||||
|
first.Options.Replace(1u, 2u);
|
||||||
|
first.MovementSkills.Update(100, 200);
|
||||||
|
first.Spellbook.OnSpellLearned(9u);
|
||||||
|
|
||||||
|
Assert.NotEqual(
|
||||||
|
first.View.Snapshot.Options,
|
||||||
|
second.View.Snapshot.Options);
|
||||||
|
Assert.True(first.View.Snapshot.MovementSkills.IsComplete);
|
||||||
|
Assert.False(second.View.Snapshot.MovementSkills.IsComplete);
|
||||||
|
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
|
||||||
|
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class RuntimeCommunicationStateTests
|
public sealed class RuntimeCommunicationStateTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SocialViewBorrowsFriendsAndSquelchOwners()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCommunicationState();
|
||||||
|
state.Friends.Apply(new AcDream.Core.Social.FriendsUpdate(
|
||||||
|
AcDream.Core.Social.FriendsUpdateType.Full,
|
||||||
|
[
|
||||||
|
new AcDream.Core.Social.FriendEntry(
|
||||||
|
0x50000001u,
|
||||||
|
"Friend",
|
||||||
|
Online: true,
|
||||||
|
AppearOffline: false,
|
||||||
|
Friends: [],
|
||||||
|
FriendOf: []),
|
||||||
|
]));
|
||||||
|
state.Squelch.Replace(new AcDream.Core.Social.SquelchDatabase(
|
||||||
|
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["account"] = 1u,
|
||||||
|
},
|
||||||
|
new Dictionary<uint, AcDream.Core.Social.SquelchInfo>(),
|
||||||
|
new AcDream.Core.Social.SquelchInfo(
|
||||||
|
string.Empty,
|
||||||
|
false,
|
||||||
|
new HashSet<uint> { 3u })));
|
||||||
|
|
||||||
|
RuntimeSocialSnapshot snapshot = state.SocialView.Snapshot;
|
||||||
|
|
||||||
|
Assert.Equal(1, snapshot.FriendCount);
|
||||||
|
Assert.Equal(1, snapshot.SquelchedAccountCount);
|
||||||
|
Assert.Equal(1, snapshot.GlobalSquelchTypeCount);
|
||||||
|
Assert.True(state.SocialView.TryGetFriend(
|
||||||
|
0x50000001u,
|
||||||
|
out RuntimeFriendSnapshot friend));
|
||||||
|
Assert.Equal("Friend", friend.Name);
|
||||||
|
Assert.True(friend.Online);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OwnsOneExactCommunicationGraphAndPublishesCommittedEntries()
|
public void OwnsOneExactCommunicationGraphAndPublishesCommittedEntries()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
public sealed class RuntimeInventoryStateTests
|
public sealed class RuntimeInventoryStateTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ViewBorrowsTransactionShortcutAndManaOwners()
|
||||||
|
{
|
||||||
|
using var entities = new RuntimeEntityObjectLifetime();
|
||||||
|
using var inventory = new RuntimeInventoryState(entities);
|
||||||
|
inventory.Shortcuts.Replace(
|
||||||
|
[
|
||||||
|
new AcDream.Core.Items.ShortcutEntry(3, 0x80000001u, 0u),
|
||||||
|
new AcDream.Core.Items.ShortcutEntry(4, 0u, 42u),
|
||||||
|
]);
|
||||||
|
inventory.ItemMana.OnQueryItemManaResponse(
|
||||||
|
0x80000001u,
|
||||||
|
0.75f,
|
||||||
|
valid: true);
|
||||||
|
inventory.Transactions.IncrementBusyCount();
|
||||||
|
|
||||||
|
RuntimeInventoryStateSnapshot snapshot = inventory.View.Snapshot;
|
||||||
|
|
||||||
|
Assert.Equal(2, snapshot.ShortcutCount);
|
||||||
|
Assert.Equal(1, snapshot.ItemManaCount);
|
||||||
|
Assert.Equal(1, snapshot.BusyCount);
|
||||||
|
Assert.False(snapshot.CanBeginRequest);
|
||||||
|
Assert.True(inventory.View.TryGetShortcut(
|
||||||
|
3,
|
||||||
|
out RuntimeShortcutSnapshot itemShortcut));
|
||||||
|
Assert.Equal(
|
||||||
|
new RuntimeShortcutSnapshot(3, 0x80000001u, 0u),
|
||||||
|
itemShortcut);
|
||||||
|
Assert.True(inventory.View.TryGetShortcut(
|
||||||
|
4,
|
||||||
|
out RuntimeShortcutSnapshot spellShortcut));
|
||||||
|
Assert.Equal(new RuntimeShortcutSnapshot(4, 0u, 42u), spellShortcut);
|
||||||
|
Assert.True(inventory.View.TryGetItemMana(
|
||||||
|
itemShortcut.ObjectId,
|
||||||
|
out float fraction));
|
||||||
|
Assert.Equal(0.75f, fraction);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OwnsOneGameplayGraphOverExactEntityObjectTable()
|
public void OwnsOneGameplayGraphOverExactEntityObjectTable()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,6 @@ public sealed class LiveSessionEventRouterTests
|
||||||
OnSkillsUpdated: null,
|
OnSkillsUpdated: null,
|
||||||
OnConfirmationRequest: null,
|
OnConfirmationRequest: null,
|
||||||
OnConfirmationDone: null,
|
OnConfirmationDone: null,
|
||||||
OnCharacterOptions: null,
|
|
||||||
ClientTime: () => 0d);
|
ClientTime: () => 0d);
|
||||||
|
|
||||||
private static LiveSocialSessionBindings NewSocialBindings(
|
private static LiveSocialSessionBindings NewSocialBindings(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue