refactor(runtime): own magic and player state
Move the coupled Spellbook and LocalPlayerState into one Runtime-owned character graph, route content, live-session, retained UI, reset, and shutdown through that exact owner, and delete the duplicate desired-component snapshot from inventory state. Preserve synchronous retail update and reset ordering while adding independent-instance and retryable-failure coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
d362172620
commit
d02a12ceac
16 changed files with 418 additions and 96 deletions
|
|
@ -12,6 +12,7 @@ using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
@ -48,7 +49,7 @@ internal sealed record ContentEffectsAudioDependencies(
|
||||||
ResidencyBudgetOptions ResidencyBudgets,
|
ResidencyBudgetOptions ResidencyBudgets,
|
||||||
PhysicsDataCache PhysicsDataCache,
|
PhysicsDataCache PhysicsDataCache,
|
||||||
bool DumpMotionEnabled,
|
bool DumpMotionEnabled,
|
||||||
Spellbook SpellBook,
|
RuntimeCharacterState Character,
|
||||||
AnimationHookRouter HookRouter,
|
AnimationHookRouter HookRouter,
|
||||||
EntityEffectPoseRegistry EffectPoses,
|
EntityEffectPoseRegistry EffectPoses,
|
||||||
DeferredEntityEffectAdvanceSource EntityEffectAdvance,
|
DeferredEntityEffectAdvanceSource EntityEffectAdvance,
|
||||||
|
|
@ -85,7 +86,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
Action<string> diagnostic);
|
Action<string> diagnostic);
|
||||||
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
|
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
|
||||||
void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog);
|
void InstallSpellMetadata(
|
||||||
|
RuntimeCharacterState character,
|
||||||
|
MagicCatalog catalog);
|
||||||
int GetSpellCount(MagicCatalog catalog);
|
int GetSpellCount(MagicCatalog catalog);
|
||||||
IAnimationLoader CreateAnimationLoader(
|
IAnimationLoader CreateAnimationLoader(
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
|
|
@ -143,8 +146,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
||||||
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
|
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
|
||||||
MagicCatalog.Load(dats);
|
MagicCatalog.Load(dats);
|
||||||
|
|
||||||
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) =>
|
public void InstallSpellMetadata(
|
||||||
spellBook.InstallMetadata(catalog.SpellTable);
|
RuntimeCharacterState character,
|
||||||
|
MagicCatalog catalog) =>
|
||||||
|
character.InstallSpellMetadata(catalog.SpellTable);
|
||||||
|
|
||||||
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
|
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
|
||||||
|
|
||||||
|
|
@ -326,7 +331,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
||||||
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
|
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
|
||||||
_publication.PublishMagicCatalog(magic);
|
_publication.PublishMagicCatalog(magic);
|
||||||
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
|
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
|
||||||
_factory.InstallSpellMetadata(_dependencies.SpellBook, magic);
|
_factory.InstallSpellMetadata(_dependencies.Character, magic);
|
||||||
_dependencies.Log(
|
_dependencies.Log(
|
||||||
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
|
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
|
||||||
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);
|
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,8 @@ internal sealed record InteractionRetainedUiDependencies(
|
||||||
SelectionState Selection,
|
SelectionState Selection,
|
||||||
RuntimeInventoryState Inventory,
|
RuntimeInventoryState Inventory,
|
||||||
MagicCatalog MagicCatalog,
|
MagicCatalog MagicCatalog,
|
||||||
Spellbook Spellbook,
|
RuntimeCharacterState Character,
|
||||||
RuntimeCommunicationState Communication,
|
RuntimeCommunicationState Communication,
|
||||||
LocalPlayerState LocalPlayer,
|
|
||||||
StackSplitQuantityState StackSplitQuantity,
|
StackSplitQuantityState StackSplitQuantity,
|
||||||
BufferedUiRegistry? UiRegistry,
|
BufferedUiRegistry? UiRegistry,
|
||||||
LiveCombatModeCommandSlot CombatModeCommands,
|
LiveCombatModeCommandSlot CombatModeCommands,
|
||||||
|
|
@ -350,7 +349,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
VitalsVM vitals = d.ExistingVitals
|
VitalsVM vitals = d.ExistingVitals
|
||||||
?? new VitalsVM(d.Combat, d.LocalPlayer);
|
?? new VitalsVM(d.Combat, d.Character.LocalPlayer);
|
||||||
UiHost host = lease.AcquireHost(
|
UiHost host = lease.AcquireHost(
|
||||||
() => new UiHost(
|
() => new UiHost(
|
||||||
d.Gl,
|
d.Gl,
|
||||||
|
|
@ -372,7 +371,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
|
|
||||||
var characterSheet = new CharacterSheetProvider(
|
var characterSheet = new CharacterSheetProvider(
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
d.LocalPlayer,
|
d.Character.LocalPlayer,
|
||||||
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
playerGuid: () => d.PlayerIdentity.ServerGuid,
|
||||||
activeToonName: () => d.Settings.ActiveToonKey,
|
activeToonName: () => d.Settings.ActiveToonKey,
|
||||||
fallbackSheet: Studio.SampleData.SampleCharacter,
|
fallbackSheet: Studio.SampleData.SampleCharacter,
|
||||||
|
|
@ -389,7 +388,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
|
|
||||||
MagicRuntime magic = MagicRuntime.Create(
|
MagicRuntime magic = MagicRuntime.Create(
|
||||||
d.MagicCatalog,
|
d.MagicCatalog,
|
||||||
d.Spellbook,
|
d.Character.Spellbook,
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
selectedObject: () => d.Selection.SelectedObjectId,
|
selectedObject: () => d.Selection.SelectedObjectId,
|
||||||
localPlayerId: () => d.PlayerIdentity.ServerGuid,
|
localPlayerId: () => d.PlayerIdentity.ServerGuid,
|
||||||
|
|
@ -421,7 +420,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
|
|
||||||
uint skillId = SkillId(school);
|
uint skillId = SkillId(school);
|
||||||
if (skillId != 0u)
|
if (skillId != 0u)
|
||||||
return d.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
|
return d.Character.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
|
||||||
|
|
||||||
uint highest = 0u;
|
uint highest = 0u;
|
||||||
foreach (MagicSchool candidate in new[]
|
foreach (MagicSchool candidate in new[]
|
||||||
|
|
@ -435,7 +434,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
{
|
{
|
||||||
highest = Math.Max(
|
highest = Math.Max(
|
||||||
highest,
|
highest,
|
||||||
d.LocalPlayer.GetSkill(SkillId(candidate))?.CurrentLevel ?? 0u);
|
d.Character.LocalPlayer.GetSkill(
|
||||||
|
SkillId(candidate))?.CurrentLevel ?? 0u);
|
||||||
}
|
}
|
||||||
return highest;
|
return highest;
|
||||||
}
|
}
|
||||||
|
|
@ -527,7 +527,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
() => d.Settings.Gameplay,
|
() => d.Settings.Gameplay,
|
||||||
d.Settings.SetCombatGameplay),
|
d.Settings.SetCombatGameplay),
|
||||||
Magic: new MagicRuntimeBindings(
|
Magic: new MagicRuntimeBindings(
|
||||||
d.Spellbook,
|
d.Character.Spellbook,
|
||||||
magic.Casting,
|
magic.Casting,
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
() => d.PlayerIdentity.ServerGuid,
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
|
|
@ -569,10 +569,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
late.Selection.ResolveVividTargetInfo,
|
late.Selection.ResolveVividTargetInfo,
|
||||||
late.SelectionCamera.UiSnapshot),
|
late.SelectionCamera.UiSnapshot),
|
||||||
Indicators: new IndicatorRuntimeBindings(
|
Indicators: new IndicatorRuntimeBindings(
|
||||||
d.Spellbook,
|
d.Character.Spellbook,
|
||||||
d.Inventory.Objects,
|
d.Inventory.Objects,
|
||||||
() => d.PlayerIdentity.ServerGuid,
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
() => d.LocalPlayer.GetAttribute(
|
() => d.Character.LocalPlayer.GetAttribute(
|
||||||
LocalPlayerState.AttributeKind.Strength)
|
LocalPlayerState.AttributeKind.Strength)
|
||||||
is { } strength ? (int?)strength.Current : null,
|
is { } strength ? (int?)strength.Current : null,
|
||||||
() => late.Session.LinkStatus,
|
() => late.Session.LinkStatus,
|
||||||
|
|
@ -614,7 +614,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
() => d.PlayerIdentity.ServerGuid,
|
() => d.PlayerIdentity.ServerGuid,
|
||||||
iconComposer.GetIcon,
|
iconComposer.GetIcon,
|
||||||
iconComposer.GetDragIcon,
|
iconComposer.GetDragIcon,
|
||||||
() => d.LocalPlayer.GetAttribute(
|
() => d.Character.LocalPlayer.GetAttribute(
|
||||||
LocalPlayerState.AttributeKind.Strength)
|
LocalPlayerState.AttributeKind.Strength)
|
||||||
is { } strength ? (int?)strength.Current : null,
|
is { } strength ? (int?)strength.Current : null,
|
||||||
guid => late.Session.CurrentSession?.SendUse(guid),
|
guid => late.Session.CurrentSession?.SendUse(guid),
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,7 @@ internal sealed record SessionPlayerDependencies(
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
CombatFeedbackSlot CombatFeedback,
|
CombatFeedbackSlot CombatFeedback,
|
||||||
RuntimeCommunicationState Communication,
|
RuntimeCommunicationState Communication,
|
||||||
LocalPlayerState LocalPlayer,
|
RuntimeCharacterState Character,
|
||||||
Spellbook Spellbook,
|
|
||||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||||
Action<string> Log);
|
Action<string> Log);
|
||||||
|
|
||||||
|
|
@ -802,8 +801,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.CharacterOptions),
|
d.CharacterOptions),
|
||||||
new LiveSessionDomainRuntime(
|
new LiveSessionDomainRuntime(
|
||||||
d.EntityObjects,
|
d.EntityObjects,
|
||||||
d.LocalPlayer,
|
d.Character,
|
||||||
d.Spellbook,
|
|
||||||
d.Combat,
|
d.Combat,
|
||||||
d.Inventory,
|
d.Inventory,
|
||||||
d.Communication),
|
d.Communication),
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ internal sealed record LiveSessionPlayerRuntime(
|
||||||
|
|
||||||
internal sealed record LiveSessionDomainRuntime(
|
internal sealed record LiveSessionDomainRuntime(
|
||||||
RuntimeEntityObjectLifetime EntityObjects,
|
RuntimeEntityObjectLifetime EntityObjects,
|
||||||
LocalPlayerState LocalPlayer,
|
RuntimeCharacterState Character,
|
||||||
Spellbook Spellbook,
|
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
RuntimeInventoryState Inventory,
|
RuntimeInventoryState Inventory,
|
||||||
RuntimeCommunicationState Communication);
|
RuntimeCommunicationState Communication);
|
||||||
|
|
@ -170,12 +169,12 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
InventoryTransactions = _domain.Inventory.ResetTransactions,
|
InventoryTransactions = _domain.Inventory.ResetTransactions,
|
||||||
SelectionPresentation = _world.SelectionScene.Reset,
|
SelectionPresentation = _world.SelectionScene.Reset,
|
||||||
ObjectTable = _domain.EntityObjects.ClearObjects,
|
ObjectTable = _domain.EntityObjects.ClearObjects,
|
||||||
Spellbook = _domain.Spellbook.Clear,
|
Spellbook = _domain.Character.ResetSpellbook,
|
||||||
MagicRuntime = () => _ui.Magic?.Reset(),
|
MagicRuntime = () => _ui.Magic?.Reset(),
|
||||||
CombatAttack = _interaction.CombatAttack.ResetSession,
|
CombatAttack = _interaction.CombatAttack.ResetSession,
|
||||||
CombatState = _domain.Combat.Clear,
|
CombatState = _domain.Combat.Clear,
|
||||||
ItemMana = _domain.Inventory.ResetItemMana,
|
ItemMana = _domain.Inventory.ResetItemMana,
|
||||||
LocalPlayer = _domain.LocalPlayer.Clear,
|
LocalPlayer = _domain.Character.ResetLocalPlayer,
|
||||||
Friends = _domain.Communication.ResetFriends,
|
Friends = _domain.Communication.ResetFriends,
|
||||||
Squelch = _domain.Communication.ResetSquelch,
|
Squelch = _domain.Communication.ResetSquelch,
|
||||||
TurbineChat = _domain.Communication.ResetNegotiatedChannels,
|
TurbineChat = _domain.Communication.ResetNegotiatedChannels,
|
||||||
|
|
@ -256,7 +255,6 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
|
|
||||||
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
private LiveInventorySessionBindings CreateInventoryBindings() => new(
|
||||||
_domain.Inventory.Objects,
|
_domain.Inventory.Objects,
|
||||||
_domain.LocalPlayer,
|
|
||||||
PlayerGuid: () => _player.Identity.ServerGuid,
|
PlayerGuid: () => _player.Identity.ServerGuid,
|
||||||
OnShortcuts: _domain.Inventory.Shortcuts.Replace,
|
OnShortcuts: _domain.Inventory.Shortcuts.Replace,
|
||||||
OnUseDone: error =>
|
OnUseDone: error =>
|
||||||
|
|
@ -265,7 +263,6 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
_domain.Inventory.Transactions.CompleteUse(error);
|
_domain.Inventory.Transactions.CompleteUse(error);
|
||||||
},
|
},
|
||||||
_domain.Inventory.ItemMana,
|
_domain.Inventory.ItemMana,
|
||||||
OnDesiredComponents: _domain.Inventory.DesiredComponents.Replace,
|
|
||||||
ExternalContainers: _domain.Inventory.ExternalContainers,
|
ExternalContainers: _domain.Inventory.ExternalContainers,
|
||||||
OnAppraisal: appraisal =>
|
OnAppraisal: appraisal =>
|
||||||
{
|
{
|
||||||
|
|
@ -281,7 +278,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
var skillCreditResolver = new LiveSkillCreditResolver(skillTable);
|
var skillCreditResolver = new LiveSkillCreditResolver(skillTable);
|
||||||
return new LiveCharacterSessionBindings(
|
return new LiveCharacterSessionBindings(
|
||||||
_domain.Combat,
|
_domain.Combat,
|
||||||
_domain.Spellbook,
|
_domain.Character,
|
||||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||||
{
|
{
|
||||||
|
|
@ -337,7 +334,7 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
?? "unknown",
|
?? "unknown",
|
||||||
CurrentPosition: () => _player.Controller.Controller?.CellPosition,
|
CurrentPosition: () => _player.Controller.Controller?.CellPosition,
|
||||||
LastOutsideCorpsePosition: () =>
|
LastOutsideCorpsePosition: () =>
|
||||||
_domain.LocalPlayer.GetPosition(0x0Eu),
|
_domain.Character.LocalPlayer.GetPosition(0x0Eu),
|
||||||
ShowConfirmation: (message, completed) =>
|
ShowConfirmation: (message, completed) =>
|
||||||
_ui.RetailUi?.ShowConfirmation(message, completed),
|
_ui.RetailUi?.ShowConfirmation(message, completed),
|
||||||
Suicide: session.SendSuicide,
|
Suicide: session.SendSuicide,
|
||||||
|
|
@ -372,7 +369,6 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
ClearDesiredComponents: () =>
|
ClearDesiredComponents: () =>
|
||||||
{
|
{
|
||||||
session.SendClearDesiredComponents();
|
session.SendClearDesiredComponents();
|
||||||
_domain.Inventory.DesiredComponents.Clear();
|
|
||||||
},
|
},
|
||||||
HasOpenVendor: () => false,
|
HasOpenVendor: () => false,
|
||||||
FillComponentBuyList: (_, _) => { }),
|
FillComponentBuyList: (_, _) => { }),
|
||||||
|
|
|
||||||
|
|
@ -337,14 +337,14 @@ public sealed class GameWindow :
|
||||||
public readonly AcDream.Core.Combat.CombatState Combat = new();
|
public readonly AcDream.Core.Combat.CombatState Combat = new();
|
||||||
private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new();
|
private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new();
|
||||||
private readonly RuntimeInventoryState _runtimeInventory;
|
private readonly RuntimeInventoryState _runtimeInventory;
|
||||||
|
private readonly RuntimeCharacterState _runtimeCharacter;
|
||||||
public AcDream.Core.Items.ItemManaState ItemMana =>
|
public AcDream.Core.Items.ItemManaState ItemMana =>
|
||||||
_runtimeInventory.ItemMana;
|
_runtimeInventory.ItemMana;
|
||||||
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents =>
|
|
||||||
_runtimeInventory.DesiredComponents.Items;
|
|
||||||
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
|
||||||
// installs that immutable table once DatCollection opens in OnLoad.
|
// installs that immutable table once DatCollection opens in OnLoad.
|
||||||
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
public AcDream.Core.Spells.SpellTable SpellTable => SpellBook.Metadata;
|
||||||
public readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
|
public AcDream.Core.Spells.Spellbook SpellBook =>
|
||||||
|
_runtimeCharacter.Spellbook;
|
||||||
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
||||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts =>
|
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts =>
|
||||||
_runtimeInventory.Shortcuts.Items;
|
_runtimeInventory.Shortcuts.Items;
|
||||||
|
|
@ -352,7 +352,8 @@ public sealed class GameWindow :
|
||||||
// PlayerDescription so the Vitals HUD can render those bars.
|
// PlayerDescription so the Vitals HUD can render those bars.
|
||||||
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
||||||
// buffs into the max formula via Spellbook.GetVitalMod.
|
// buffs into the max formula via Spellbook.GetVitalMod.
|
||||||
public readonly AcDream.Core.Player.LocalPlayerState LocalPlayer = null!;
|
public AcDream.Core.Player.LocalPlayerState LocalPlayer =>
|
||||||
|
_runtimeCharacter.LocalPlayer;
|
||||||
|
|
||||||
// Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
|
// Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
|
||||||
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
||||||
|
|
@ -562,6 +563,7 @@ public sealed class GameWindow :
|
||||||
{
|
{
|
||||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||||
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
||||||
|
_runtimeCharacter = new RuntimeCharacterState();
|
||||||
var alphaScratchBudgets =
|
var alphaScratchBudgets =
|
||||||
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||||
_options.ResidencyBudgets.AlphaScratchBytes);
|
_options.ResidencyBudgets.AlphaScratchBytes);
|
||||||
|
|
@ -591,8 +593,6 @@ public sealed class GameWindow :
|
||||||
_uiRegistry = uiRegistry;
|
_uiRegistry = uiRegistry;
|
||||||
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
_animatedEntities = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(
|
||||||
_liveEntityRuntimeSlot);
|
_liveEntityRuntimeSlot);
|
||||||
SpellBook = new AcDream.Core.Spells.Spellbook();
|
|
||||||
LocalPlayer = new AcDream.Core.Player.LocalPlayerState(SpellBook);
|
|
||||||
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
// #184 Slice 2a: the extracted per-remote DR tick. Its stateful
|
||||||
// setup/motion dependency crosses the fail-fast construction bridge;
|
// setup/motion dependency crosses the fail-fast construction bridge;
|
||||||
// the server-velocity animation cycle is stateless shared policy.
|
// the server-velocity animation cycle is stateless shared policy.
|
||||||
|
|
@ -1149,7 +1149,7 @@ public sealed class GameWindow :
|
||||||
_options.ResidencyBudgets,
|
_options.ResidencyBudgets,
|
||||||
_physicsDataCache,
|
_physicsDataCache,
|
||||||
_animationDiagnostics.DumpMotionEnabled,
|
_animationDiagnostics.DumpMotionEnabled,
|
||||||
SpellBook,
|
_runtimeCharacter,
|
||||||
_hookRouter,
|
_hookRouter,
|
||||||
_effectPoses,
|
_effectPoses,
|
||||||
_entityEffectAdvance,
|
_entityEffectAdvance,
|
||||||
|
|
@ -1261,9 +1261,8 @@ public sealed class GameWindow :
|
||||||
_selection,
|
_selection,
|
||||||
_runtimeInventory,
|
_runtimeInventory,
|
||||||
contentEffectsAudio.MagicCatalog,
|
contentEffectsAudio.MagicCatalog,
|
||||||
SpellBook,
|
_runtimeCharacter,
|
||||||
_runtimeCommunication,
|
_runtimeCommunication,
|
||||||
LocalPlayer,
|
|
||||||
_stackSplitQuantity,
|
_stackSplitQuantity,
|
||||||
_uiRegistry,
|
_uiRegistry,
|
||||||
_liveCombatModeCommands,
|
_liveCombatModeCommands,
|
||||||
|
|
@ -1405,8 +1404,7 @@ public sealed class GameWindow :
|
||||||
Combat,
|
Combat,
|
||||||
_combatFeedback,
|
_combatFeedback,
|
||||||
_runtimeCommunication,
|
_runtimeCommunication,
|
||||||
LocalPlayer,
|
_runtimeCharacter,
|
||||||
SpellBook,
|
|
||||||
_portalTunnelFallback,
|
_portalTunnelFallback,
|
||||||
Console.WriteLine),
|
Console.WriteLine),
|
||||||
this).Compose(
|
this).Compose(
|
||||||
|
|
@ -1582,6 +1580,7 @@ public sealed class GameWindow :
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
_runtimeEntityObjects,
|
_runtimeEntityObjects,
|
||||||
_runtimeInventory,
|
_runtimeInventory,
|
||||||
|
_runtimeCharacter,
|
||||||
_runtimeCommunication,
|
_runtimeCommunication,
|
||||||
_renderSceneShadow,
|
_renderSceneShadow,
|
||||||
_livePresentationBindings,
|
_livePresentationBindings,
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ internal sealed record LiveShutdownRoots(
|
||||||
LiveEntityRuntime? LiveEntities,
|
LiveEntityRuntime? LiveEntities,
|
||||||
RuntimeEntityObjectLifetime EntityObjects,
|
RuntimeEntityObjectLifetime EntityObjects,
|
||||||
RuntimeInventoryState Inventory,
|
RuntimeInventoryState Inventory,
|
||||||
|
RuntimeCharacterState Character,
|
||||||
RuntimeCommunicationState Communication,
|
RuntimeCommunicationState Communication,
|
||||||
RenderSceneShadowRuntime? RenderSceneShadow,
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LivePresentationRuntimeBindings? PresentationBindings,
|
LivePresentationRuntimeBindings? PresentationBindings,
|
||||||
|
|
@ -391,6 +392,9 @@ internal static class GameWindowShutdownManifest
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("runtime entity/object stream",
|
new ResourceShutdownStage("runtime entity/object stream",
|
||||||
[
|
[
|
||||||
|
Hard(
|
||||||
|
"runtime character state",
|
||||||
|
live.Character.Dispose),
|
||||||
Hard(
|
Hard(
|
||||||
"runtime inventory state",
|
"runtime inventory state",
|
||||||
live.Inventory.Dispose),
|
live.Inventory.Dispose),
|
||||||
|
|
|
||||||
85
src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
Normal file
85
src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
using AcDream.Core.Player;
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical presentation-independent owner for the local character's magic
|
||||||
|
/// and player-sheet state. The two objects form one lifetime group because
|
||||||
|
/// vital maxima read active enchantments from this exact spellbook.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RuntimeCharacterState : IDisposable
|
||||||
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||||
|
{
|
||||||
|
Spellbook = new Spellbook(spellTable);
|
||||||
|
LocalPlayer = new LocalPlayerState(Spellbook);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Spellbook Spellbook { get; }
|
||||||
|
public LocalPlayerState LocalPlayer { get; }
|
||||||
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs immutable DAT metadata without transferring its ownership to
|
||||||
|
/// Runtime. The content host may install one table after portal.dat opens.
|
||||||
|
/// </summary>
|
||||||
|
public void InstallSpellMetadata(SpellTable spellTable)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
Spellbook.InstallMetadata(spellTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSpellbook()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
Spellbook.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetLocalPlayer()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
LocalPlayer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears both coupled owners while retaining every failed suffix for a
|
||||||
|
/// retry. State mutation happens before the existing synchronous
|
||||||
|
/// invalidation callbacks, so retrying is safe and convergent.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetSession()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
List<Exception>? failures = null;
|
||||||
|
Try(Spellbook.Clear, ref failures);
|
||||||
|
Try(LocalPlayer.Clear, ref failures);
|
||||||
|
if (failures is not null)
|
||||||
|
{
|
||||||
|
throw new AggregateException(
|
||||||
|
"Runtime character state did not converge during reset.",
|
||||||
|
failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
ResetSession();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Try(Action action, ref List<Exception>? failures)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
(failures ??= []).Add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
ExternalContainers = new ExternalContainerState();
|
ExternalContainers = new ExternalContainerState();
|
||||||
ItemMana = new ItemManaState();
|
ItemMana = new ItemManaState();
|
||||||
Shortcuts = new ShortcutState();
|
Shortcuts = new ShortcutState();
|
||||||
DesiredComponents = new DesiredComponentState();
|
|
||||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,7 +27,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
public ExternalContainerState ExternalContainers { get; }
|
public ExternalContainerState ExternalContainers { get; }
|
||||||
public ItemManaState ItemMana { get; }
|
public ItemManaState ItemMana { get; }
|
||||||
public ShortcutState Shortcuts { get; }
|
public ShortcutState Shortcuts { get; }
|
||||||
public DesiredComponentState DesiredComponents { get; }
|
|
||||||
public InventoryTransactionState Transactions { get; }
|
public InventoryTransactionState Transactions { get; }
|
||||||
public bool IsDisposed => _disposed;
|
public bool IsDisposed => _disposed;
|
||||||
|
|
||||||
|
|
@ -39,7 +37,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
public void ResetPlayerSnapshots()
|
public void ResetPlayerSnapshots()
|
||||||
{
|
{
|
||||||
Shortcuts.Clear();
|
Shortcuts.Clear();
|
||||||
DesiredComponents.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -50,7 +47,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
||||||
Try(() => ExternalContainers.Reset(), ref failures);
|
Try(() => ExternalContainers.Reset(), ref failures);
|
||||||
Try(ItemMana.Clear, ref failures);
|
Try(ItemMana.Clear, ref failures);
|
||||||
Try(Shortcuts.Clear, ref failures);
|
Try(Shortcuts.Clear, ref failures);
|
||||||
Try(DesiredComponents.Clear, ref failures);
|
|
||||||
Try(Transactions.Dispose, ref failures);
|
Try(Transactions.Dispose, ref failures);
|
||||||
if (failures is not null)
|
if (failures is not null)
|
||||||
throw new AggregateException(
|
throw new AggregateException(
|
||||||
|
|
@ -90,25 +86,3 @@ public sealed class ShortcutState
|
||||||
|
|
||||||
public void Clear() => Replace(Array.Empty<ShortcutEntry>());
|
public void Clear() => Replace(Array.Empty<ShortcutEntry>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DesiredComponentState
|
|
||||||
{
|
|
||||||
private IReadOnlyList<(uint Id, uint Amount)> _items =
|
|
||||||
Array.Empty<(uint Id, uint Amount)>();
|
|
||||||
private long _revision;
|
|
||||||
|
|
||||||
public IReadOnlyList<(uint Id, uint Amount)> Items =>
|
|
||||||
Volatile.Read(ref _items);
|
|
||||||
public long Revision => Interlocked.Read(ref _revision);
|
|
||||||
|
|
||||||
public void Replace(IReadOnlyList<(uint Id, uint Amount)>? items)
|
|
||||||
{
|
|
||||||
Volatile.Write(
|
|
||||||
ref _items,
|
|
||||||
items ?? Array.Empty<(uint Id, uint Amount)>());
|
|
||||||
Interlocked.Increment(ref _revision);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear() =>
|
|
||||||
Replace(Array.Empty<(uint Id, uint Amount)>());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Player;
|
using AcDream.Core.Player;
|
||||||
using AcDream.Core.Social;
|
using AcDream.Core.Social;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Session;
|
namespace AcDream.Runtime.Session;
|
||||||
|
|
||||||
|
|
@ -29,18 +30,16 @@ public sealed record LiveEnvironmentSessionSink(
|
||||||
|
|
||||||
public sealed record LiveInventorySessionBindings(
|
public sealed record LiveInventorySessionBindings(
|
||||||
ClientObjectTable Objects,
|
ClientObjectTable Objects,
|
||||||
LocalPlayerState LocalPlayer,
|
|
||||||
Func<uint> PlayerGuid,
|
Func<uint> PlayerGuid,
|
||||||
Action<IReadOnlyList<ShortcutEntry>>? OnShortcuts,
|
Action<IReadOnlyList<ShortcutEntry>>? OnShortcuts,
|
||||||
Action<uint>? OnUseDone,
|
Action<uint>? OnUseDone,
|
||||||
ItemManaState? ItemMana,
|
ItemManaState? ItemMana,
|
||||||
Action<IReadOnlyList<(uint Id, uint Amount)>>? OnDesiredComponents,
|
|
||||||
ExternalContainerState? ExternalContainers,
|
ExternalContainerState? ExternalContainers,
|
||||||
Action<AppraiseInfoParser.Parsed>? OnAppraisal = null);
|
Action<AppraiseInfoParser.Parsed>? OnAppraisal = null);
|
||||||
|
|
||||||
public sealed record LiveCharacterSessionBindings(
|
public sealed record LiveCharacterSessionBindings(
|
||||||
CombatState Combat,
|
CombatState Combat,
|
||||||
Spellbook Spellbook,
|
RuntimeCharacterState Character,
|
||||||
Func<uint, IReadOnlyDictionary<uint, uint>, uint>? ResolveSkillFormulaBonus,
|
Func<uint, IReadOnlyDictionary<uint, uint>, uint>? ResolveSkillFormulaBonus,
|
||||||
Action<int, int>? OnSkillsUpdated,
|
Action<int, int>? OnSkillsUpdated,
|
||||||
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
Action<GameEvents.CharacterConfirmationRequest>? OnConfirmationRequest,
|
||||||
|
|
@ -114,7 +113,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
session,
|
session,
|
||||||
inventory.Objects,
|
inventory.Objects,
|
||||||
inventory.PlayerGuid,
|
inventory.PlayerGuid,
|
||||||
inventory.LocalPlayer,
|
character.Character.LocalPlayer,
|
||||||
IsAccepting));
|
IsAccepting));
|
||||||
ConstructionCheckpoint();
|
ConstructionCheckpoint();
|
||||||
_subscriptions.Add(CombatStateWiring.Wire(
|
_subscriptions.Add(CombatStateWiring.Wire(
|
||||||
|
|
@ -149,9 +148,9 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
session.GameEvents,
|
session.GameEvents,
|
||||||
inventory.Objects,
|
inventory.Objects,
|
||||||
character.Combat,
|
character.Combat,
|
||||||
character.Spellbook,
|
character.Character.Spellbook,
|
||||||
social.Chat,
|
social.Chat,
|
||||||
inventory.LocalPlayer,
|
character.Character.LocalPlayer,
|
||||||
social.TurbineChat,
|
social.TurbineChat,
|
||||||
onSkillsUpdated: character.OnSkillsUpdated,
|
onSkillsUpdated: character.OnSkillsUpdated,
|
||||||
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
resolveSkillFormulaBonus: character.ResolveSkillFormulaBonus,
|
||||||
|
|
@ -164,7 +163,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
onConfirmationDone: character.OnConfirmationDone,
|
onConfirmationDone: character.OnConfirmationDone,
|
||||||
friends: social.Friends,
|
friends: social.Friends,
|
||||||
squelch: social.Squelch,
|
squelch: social.Squelch,
|
||||||
onDesiredComponents: inventory.OnDesiredComponents,
|
onDesiredComponents: null,
|
||||||
onCharacterOptions: character.OnCharacterOptions,
|
onCharacterOptions: character.OnCharacterOptions,
|
||||||
clientTime: character.ClientTime,
|
clientTime: character.ClientTime,
|
||||||
externalContainers: inventory.ExternalContainers,
|
externalContainers: inventory.ExternalContainers,
|
||||||
|
|
@ -202,7 +201,7 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
h => session.TurbineChatReceived -= h,
|
h => session.TurbineChatReceived -= h,
|
||||||
parsed => RouteTurbineChat(social.Chat, parsed));
|
parsed => RouteTurbineChat(social.Chat, parsed));
|
||||||
Subscribe<PrivateUpdateVital.ParsedFull>(h => session.VitalUpdated += h, h => session.VitalUpdated -= h, vital =>
|
Subscribe<PrivateUpdateVital.ParsedFull>(h => session.VitalUpdated += h, h => session.VitalUpdated -= h, vital =>
|
||||||
inventory.LocalPlayer.OnVitalUpdate(
|
character.Character.LocalPlayer.OnVitalUpdate(
|
||||||
vital.VitalId,
|
vital.VitalId,
|
||||||
vital.Ranks,
|
vital.Ranks,
|
||||||
vital.Start,
|
vital.Start,
|
||||||
|
|
@ -211,7 +210,9 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
Subscribe<PrivateUpdateVital.ParsedCurrent>(
|
Subscribe<PrivateUpdateVital.ParsedCurrent>(
|
||||||
h => session.VitalCurrentUpdated += h,
|
h => session.VitalCurrentUpdated += h,
|
||||||
h => session.VitalCurrentUpdated -= h,
|
h => session.VitalCurrentUpdated -= h,
|
||||||
vital => inventory.LocalPlayer.OnVitalCurrent(vital.VitalId, vital.Current));
|
vital => character.Character.LocalPlayer.OnVitalCurrent(
|
||||||
|
vital.VitalId,
|
||||||
|
vital.Current));
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref _lifecycleState, 2, 1) != 1)
|
if (Interlocked.CompareExchange(ref _lifecycleState, 2, 1) != 1)
|
||||||
throw new ObjectDisposedException(nameof(LiveSessionEventRouter));
|
throw new ObjectDisposedException(nameof(LiveSessionEventRouter));
|
||||||
|
|
@ -297,10 +298,9 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
||||||
ArgumentNullException.ThrowIfNull(environment.EnvironChanged);
|
ArgumentNullException.ThrowIfNull(environment.EnvironChanged);
|
||||||
ArgumentNullException.ThrowIfNull(environment.ServerTimeUpdated);
|
ArgumentNullException.ThrowIfNull(environment.ServerTimeUpdated);
|
||||||
ArgumentNullException.ThrowIfNull(inventory.Objects);
|
ArgumentNullException.ThrowIfNull(inventory.Objects);
|
||||||
ArgumentNullException.ThrowIfNull(inventory.LocalPlayer);
|
|
||||||
ArgumentNullException.ThrowIfNull(inventory.PlayerGuid);
|
ArgumentNullException.ThrowIfNull(inventory.PlayerGuid);
|
||||||
ArgumentNullException.ThrowIfNull(character.Combat);
|
ArgumentNullException.ThrowIfNull(character.Combat);
|
||||||
ArgumentNullException.ThrowIfNull(character.Spellbook);
|
ArgumentNullException.ThrowIfNull(character.Character);
|
||||||
ArgumentNullException.ThrowIfNull(social.Chat);
|
ArgumentNullException.ThrowIfNull(social.Chat);
|
||||||
ArgumentNullException.ThrowIfNull(social.TurbineChat);
|
ArgumentNullException.ThrowIfNull(social.TurbineChat);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
using Silk.NET.OpenAL;
|
using Silk.NET.OpenAL;
|
||||||
|
|
@ -257,7 +258,7 @@ public sealed class ContentEffectsAudioCompositionTests
|
||||||
ResidencyBudgetOptions.Default,
|
ResidencyBudgetOptions.Default,
|
||||||
new PhysicsDataCache(),
|
new PhysicsDataCache(),
|
||||||
false,
|
false,
|
||||||
new Spellbook(),
|
new RuntimeCharacterState(),
|
||||||
Router,
|
Router,
|
||||||
Poses,
|
Poses,
|
||||||
new DeferredEntityEffectAdvanceSource(),
|
new DeferredEntityEffectAdvanceSource(),
|
||||||
|
|
@ -289,7 +290,11 @@ public sealed class ContentEffectsAudioCompositionTests
|
||||||
throw new InvalidOperationException($"fault at {point}");
|
throw new InvalidOperationException($"fault at {point}");
|
||||||
});
|
});
|
||||||
|
|
||||||
public void Dispose() => Publication.Dispose();
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Publication.Dispose();
|
||||||
|
Dependencies.Character.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Publication :
|
private sealed class Publication :
|
||||||
|
|
@ -386,7 +391,9 @@ public sealed class ContentEffectsAudioCompositionTests
|
||||||
return _preparedAssets;
|
return _preparedAssets;
|
||||||
}
|
}
|
||||||
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) => _magic;
|
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) => _magic;
|
||||||
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) { }
|
public void InstallSpellMetadata(
|
||||||
|
RuntimeCharacterState character,
|
||||||
|
MagicCatalog catalog) { }
|
||||||
public int GetSpellCount(MagicCatalog catalog) => 0;
|
public int GetSpellCount(MagicCatalog catalog) => 0;
|
||||||
public IAnimationLoader CreateAnimationLoader(
|
public IAnimationLoader CreateAnimationLoader(
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
|
|
|
||||||
|
|
@ -191,9 +191,8 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
Selection: null!,
|
Selection: null!,
|
||||||
Inventory: null!,
|
Inventory: null!,
|
||||||
MagicCatalog: null!,
|
MagicCatalog: null!,
|
||||||
Spellbook: null!,
|
Character: null!,
|
||||||
Communication: null!,
|
Communication: null!,
|
||||||
LocalPlayer: null!,
|
|
||||||
StackSplitQuantity: null!,
|
StackSplitQuantity: null!,
|
||||||
UiRegistry: null,
|
UiRegistry: null,
|
||||||
CombatModeCommands: null!,
|
CombatModeCommands: null!,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
namespace AcDream.App.Tests.Runtime;
|
||||||
|
|
||||||
|
public sealed class RuntimeCharacterOwnershipTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProductionConstructsOnlyTheRuntimeCharacterOwner()
|
||||||
|
{
|
||||||
|
string source = ReadSource("Rendering", "GameWindow.cs");
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
"_runtimeCharacter = new RuntimeCharacterState();",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new AcDream.Core.Spells.Spellbook",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"new AcDream.Core.Player.LocalPlayerState",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"SpellBook =>\n _runtimeCharacter.Spellbook;",
|
||||||
|
Normalize(source),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"LocalPlayer =>\n _runtimeCharacter.LocalPlayer;",
|
||||||
|
Normalize(source),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SessionRoutingAndShutdownBorrowTheExactRuntimeOwner()
|
||||||
|
{
|
||||||
|
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
||||||
|
string router = ReadRuntimeSource(
|
||||||
|
"Session",
|
||||||
|
"LiveSessionEventRouter.cs");
|
||||||
|
string content = ReadSource(
|
||||||
|
"Composition",
|
||||||
|
"ContentEffectsAudioComposition.cs");
|
||||||
|
string ui = ReadSource(
|
||||||
|
"Composition",
|
||||||
|
"InteractionRetainedUiComposition.cs");
|
||||||
|
string inventory = ReadRuntimeSource(
|
||||||
|
"Gameplay",
|
||||||
|
"RuntimeInventoryState.cs");
|
||||||
|
string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs");
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
"RuntimeCharacterState Character",
|
||||||
|
session,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"character.Character.Spellbook",
|
||||||
|
router,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"character.Character.LocalPlayer",
|
||||||
|
router,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"_factory.InstallSpellMetadata(_dependencies.Character, magic)",
|
||||||
|
content,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"d.Character.Spellbook",
|
||||||
|
ui,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"d.Character.LocalPlayer",
|
||||||
|
ui,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"DesiredComponentState",
|
||||||
|
inventory,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"\"runtime character state\"",
|
||||||
|
shutdown,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadSource(params string[] relative) =>
|
||||||
|
File.ReadAllText(Path.Combine(
|
||||||
|
[FindRepositoryRoot(), "src", "AcDream.App", .. relative]));
|
||||||
|
|
||||||
|
private static string ReadRuntimeSource(params string[] relative) =>
|
||||||
|
File.ReadAllText(Path.Combine(
|
||||||
|
[FindRepositoryRoot(), "src", "AcDream.Runtime", .. relative]));
|
||||||
|
|
||||||
|
private static string Normalize(string source) =>
|
||||||
|
source.Replace("\r\n", "\n", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx")))
|
||||||
|
return current.FullName;
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
throw new DirectoryNotFoundException("AcDream.slnx was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,10 @@ public sealed class RuntimeInventoryOwnershipTests
|
||||||
"new AcDream.Core.Items.ExternalContainerState",
|
"new AcDream.Core.Items.ExternalContainerState",
|
||||||
source,
|
source,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"DesiredComponentState",
|
||||||
|
source,
|
||||||
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -50,8 +54,8 @@ public sealed class RuntimeInventoryOwnershipTests
|
||||||
"_domain.Inventory.Transactions.CompleteUse(error)",
|
"_domain.Inventory.Transactions.CompleteUse(error)",
|
||||||
session,
|
session,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.Contains(
|
Assert.DoesNotContain(
|
||||||
"OnDesiredComponents: _domain.Inventory.DesiredComponents.Replace",
|
"_domain.Inventory.DesiredComponents",
|
||||||
session,
|
session,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
using AcDream.Core.Spells;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Gameplay;
|
||||||
|
|
||||||
|
public sealed class RuntimeCharacterStateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OwnsOneCoupledSpellbookAndLocalPlayerGraph()
|
||||||
|
{
|
||||||
|
SpellTable table = SpellTable.Create(
|
||||||
|
[
|
||||||
|
new SpellMetadata(
|
||||||
|
SpellId: 1u,
|
||||||
|
Name: "Test",
|
||||||
|
School: "Life",
|
||||||
|
Family: 0u,
|
||||||
|
IconId: 0u,
|
||||||
|
SpellWords: "",
|
||||||
|
Duration: 60f,
|
||||||
|
ManaCost: 0,
|
||||||
|
IsDebuff: false,
|
||||||
|
IsFellowship: false,
|
||||||
|
Description: "",
|
||||||
|
SortKey: 0,
|
||||||
|
Difficulty: 0,
|
||||||
|
Flags: 0u,
|
||||||
|
Generation: 1,
|
||||||
|
IsFastWindup: false,
|
||||||
|
IsOffensive: false,
|
||||||
|
IsUntargeted: false,
|
||||||
|
Speed: 0f,
|
||||||
|
CasterEffect: 0u,
|
||||||
|
TargetEffect: 0u,
|
||||||
|
TargetMask: 0u,
|
||||||
|
SpellType: 0)
|
||||||
|
]);
|
||||||
|
using var state = new RuntimeCharacterState(table);
|
||||||
|
state.LocalPlayer.OnAttributeUpdate(
|
||||||
|
atType: 2u,
|
||||||
|
ranks: 90u,
|
||||||
|
start: 10u,
|
||||||
|
xp: 0u);
|
||||||
|
state.LocalPlayer.OnVitalUpdate(
|
||||||
|
vitalId: 7u,
|
||||||
|
ranks: 50u,
|
||||||
|
start: 50u,
|
||||||
|
xp: 0u,
|
||||||
|
current: 150u);
|
||||||
|
|
||||||
|
state.Spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord(
|
||||||
|
SpellId: 1u,
|
||||||
|
LayerId: 1u,
|
||||||
|
Duration: 60f,
|
||||||
|
CasterGuid: 2u,
|
||||||
|
Bucket: 2u,
|
||||||
|
StatModType: 0u,
|
||||||
|
StatModKey: EnchantmentMath.StatKey.MaxHealth,
|
||||||
|
StatModValue: 25f));
|
||||||
|
state.Spellbook.SetDesiredComponent(0x68000001u, 12u);
|
||||||
|
|
||||||
|
Assert.Equal(175u, state.LocalPlayer.GetMaxApprox(
|
||||||
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
Assert.Equal(12u, state.Spellbook.DesiredComponents[0x68000001u]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InstallsImmutableMetadataOnceOnTheCanonicalSpellbook()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
SpellTable table = SpellTable.Create(Array.Empty<SpellMetadata>());
|
||||||
|
|
||||||
|
state.InstallSpellMetadata(table);
|
||||||
|
state.InstallSpellMetadata(table);
|
||||||
|
|
||||||
|
Assert.Same(table, state.Spellbook.Metadata);
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => state.InstallSpellMetadata(SpellTable.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetMethodsPreserveTheOtherHalfOfTheLifetimeGroup()
|
||||||
|
{
|
||||||
|
using var state = new RuntimeCharacterState();
|
||||||
|
state.Spellbook.OnSpellLearned(7u);
|
||||||
|
state.LocalPlayer.OnVitalUpdate(7u, 1u, 9u, 0u, 10u);
|
||||||
|
|
||||||
|
state.ResetSpellbook();
|
||||||
|
|
||||||
|
Assert.False(state.Spellbook.Knows(7u));
|
||||||
|
Assert.NotNull(state.LocalPlayer.Get(
|
||||||
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
|
||||||
|
state.Spellbook.OnSpellLearned(8u);
|
||||||
|
state.ResetLocalPlayer();
|
||||||
|
|
||||||
|
Assert.True(state.Spellbook.Knows(8u));
|
||||||
|
Assert.Null(state.LocalPlayer.Get(
|
||||||
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndependentRuntimeInstancesNeverShareCharacterState()
|
||||||
|
{
|
||||||
|
using var first = new RuntimeCharacterState();
|
||||||
|
using var second = new RuntimeCharacterState();
|
||||||
|
|
||||||
|
first.Spellbook.OnSpellLearned(7u);
|
||||||
|
first.LocalPlayer.OnVitalUpdate(7u, 1u, 9u, 0u, 10u);
|
||||||
|
|
||||||
|
Assert.False(second.Spellbook.Knows(7u));
|
||||||
|
Assert.Null(second.LocalPlayer.Get(
|
||||||
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisposalRetriesObserverFailuresAndClearsBothOwners()
|
||||||
|
{
|
||||||
|
var state = new RuntimeCharacterState();
|
||||||
|
state.Spellbook.OnSpellLearned(7u);
|
||||||
|
state.LocalPlayer.OnVitalUpdate(7u, 1u, 9u, 0u, 10u);
|
||||||
|
bool failSpellbook = true;
|
||||||
|
bool failCharacter = true;
|
||||||
|
state.Spellbook.SpellbookChanged += () =>
|
||||||
|
{
|
||||||
|
if (failSpellbook)
|
||||||
|
{
|
||||||
|
failSpellbook = false;
|
||||||
|
throw new InvalidOperationException("spellbook");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
state.LocalPlayer.CharacterChanged += () =>
|
||||||
|
{
|
||||||
|
if (failCharacter)
|
||||||
|
{
|
||||||
|
failCharacter = false;
|
||||||
|
throw new InvalidOperationException("character");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AggregateException error = Assert.Throws<AggregateException>(
|
||||||
|
state.Dispose);
|
||||||
|
|
||||||
|
Assert.Equal(2, error.InnerExceptions.Count);
|
||||||
|
Assert.False(state.IsDisposed);
|
||||||
|
Assert.False(state.Spellbook.Knows(7u));
|
||||||
|
Assert.Null(state.LocalPlayer.Get(
|
||||||
|
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
|
||||||
|
|
||||||
|
state.Dispose();
|
||||||
|
state.Dispose();
|
||||||
|
|
||||||
|
Assert.True(state.IsDisposed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,22 +13,17 @@ public sealed class RuntimeInventoryStateTests
|
||||||
using var inventory = new RuntimeInventoryState(entities);
|
using var inventory = new RuntimeInventoryState(entities);
|
||||||
IReadOnlyList<ShortcutEntry> shortcuts =
|
IReadOnlyList<ShortcutEntry> shortcuts =
|
||||||
[new ShortcutEntry(2, 0x50000001u, 0u)];
|
[new ShortcutEntry(2, 0x50000001u, 0u)];
|
||||||
IReadOnlyList<(uint Id, uint Amount)> components =
|
|
||||||
[(0x68000001u, 12u)];
|
|
||||||
|
|
||||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||||
inventory.Shortcuts.Replace(shortcuts);
|
inventory.Shortcuts.Replace(shortcuts);
|
||||||
inventory.DesiredComponents.Replace(components);
|
|
||||||
|
|
||||||
Assert.Same(entities.Objects, inventory.Objects);
|
Assert.Same(entities.Objects, inventory.Objects);
|
||||||
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
||||||
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
||||||
Assert.Same(shortcuts, inventory.Shortcuts.Items);
|
Assert.Same(shortcuts, inventory.Shortcuts.Items);
|
||||||
Assert.Same(components, inventory.DesiredComponents.Items);
|
|
||||||
Assert.Equal(1, inventory.Shortcuts.Revision);
|
Assert.Equal(1, inventory.Shortcuts.Revision);
|
||||||
Assert.Equal(1, inventory.DesiredComponents.Revision);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -40,7 +35,6 @@ public sealed class RuntimeInventoryStateTests
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
|
||||||
inventory.Transactions.IncrementBusyCount();
|
inventory.Transactions.IncrementBusyCount();
|
||||||
|
|
||||||
inventory.ResetExternalContainer();
|
inventory.ResetExternalContainer();
|
||||||
|
|
@ -56,7 +50,6 @@ public sealed class RuntimeInventoryStateTests
|
||||||
Assert.Equal(0, inventory.Transactions.BusyCount);
|
Assert.Equal(0, inventory.Transactions.BusyCount);
|
||||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||||
Assert.Empty(inventory.Shortcuts.Items);
|
Assert.Empty(inventory.Shortcuts.Items);
|
||||||
Assert.Empty(inventory.DesiredComponents.Items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -86,7 +79,6 @@ public sealed class RuntimeInventoryStateTests
|
||||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
||||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
|
||||||
|
|
||||||
inventory.Dispose();
|
inventory.Dispose();
|
||||||
inventory.Dispose();
|
inventory.Dispose();
|
||||||
|
|
@ -96,7 +88,6 @@ public sealed class RuntimeInventoryStateTests
|
||||||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||||
Assert.Empty(inventory.Shortcuts.Items);
|
Assert.Empty(inventory.Shortcuts.Items);
|
||||||
Assert.Empty(inventory.DesiredComponents.Items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using AcDream.Core.Player;
|
||||||
using AcDream.Core.Social;
|
using AcDream.Core.Social;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using AcDream.Runtime.Session;
|
using AcDream.Runtime.Session;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests.Session;
|
namespace AcDream.Runtime.Tests.Session;
|
||||||
|
|
||||||
|
|
@ -227,18 +228,16 @@ public sealed class LiveSessionEventRouterTests
|
||||||
|
|
||||||
private static LiveInventorySessionBindings NewInventoryBindings() => new(
|
private static LiveInventorySessionBindings NewInventoryBindings() => new(
|
||||||
new ClientObjectTable(),
|
new ClientObjectTable(),
|
||||||
new LocalPlayerState(),
|
|
||||||
PlayerGuid: () => 0x50000001u,
|
PlayerGuid: () => 0x50000001u,
|
||||||
OnShortcuts: null,
|
OnShortcuts: null,
|
||||||
OnUseDone: null,
|
OnUseDone: null,
|
||||||
ItemMana: new ItemManaState(),
|
ItemMana: new ItemManaState(),
|
||||||
OnDesiredComponents: null,
|
|
||||||
ExternalContainers: new ExternalContainerState());
|
ExternalContainers: new ExternalContainerState());
|
||||||
|
|
||||||
private static LiveCharacterSessionBindings NewCharacterBindings(
|
private static LiveCharacterSessionBindings NewCharacterBindings(
|
||||||
CombatState? combat = null) => new(
|
CombatState? combat = null) => new(
|
||||||
combat ?? new CombatState(),
|
combat ?? new CombatState(),
|
||||||
new Spellbook(),
|
new RuntimeCharacterState(),
|
||||||
ResolveSkillFormulaBonus: null,
|
ResolveSkillFormulaBonus: null,
|
||||||
OnSkillsUpdated: null,
|
OnSkillsUpdated: null,
|
||||||
OnConfirmationRequest: null,
|
OnConfirmationRequest: null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue