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:
Erik 2026-07-26 08:39:02 +02:00
parent d362172620
commit d02a12ceac
16 changed files with 418 additions and 96 deletions

View file

@ -12,6 +12,7 @@ using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.Spells;
using AcDream.Core.Vfx;
using AcDream.Runtime.Gameplay;
using DatReaderWriter;
using Silk.NET.Input;
using Silk.NET.OpenGL;
@ -48,7 +49,7 @@ internal sealed record ContentEffectsAudioDependencies(
ResidencyBudgetOptions ResidencyBudgets,
PhysicsDataCache PhysicsDataCache,
bool DumpMotionEnabled,
Spellbook SpellBook,
RuntimeCharacterState Character,
AnimationHookRouter HookRouter,
EntityEffectPoseRegistry EffectPoses,
DeferredEntityEffectAdvanceSource EntityEffectAdvance,
@ -85,7 +86,9 @@ internal interface IContentEffectsAudioCompositionFactory
IDatReaderWriter dats,
Action<string> diagnostic);
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog);
void InstallSpellMetadata(
RuntimeCharacterState character,
MagicCatalog catalog);
int GetSpellCount(MagicCatalog catalog);
IAnimationLoader CreateAnimationLoader(
IDatReaderWriter dats,
@ -143,8 +146,10 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
MagicCatalog.Load(dats);
public void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog) =>
spellBook.InstallMetadata(catalog.SpellTable);
public void InstallSpellMetadata(
RuntimeCharacterState character,
MagicCatalog catalog) =>
character.InstallSpellMetadata(catalog.SpellTable);
public int GetSpellCount(MagicCatalog catalog) => catalog.SpellTable.Count;
@ -326,7 +331,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
_publication.PublishMagicCatalog(magic);
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
_factory.InstallSpellMetadata(_dependencies.SpellBook, magic);
_factory.InstallSpellMetadata(_dependencies.Character, magic);
_dependencies.Log(
$"spells: loaded {_factory.GetSpellCount(magic)} entries from portal.dat");
Fault(ContentEffectsAudioCompositionPoint.SpellMetadataInstalled);

View file

@ -48,9 +48,8 @@ internal sealed record InteractionRetainedUiDependencies(
SelectionState Selection,
RuntimeInventoryState Inventory,
MagicCatalog MagicCatalog,
Spellbook Spellbook,
RuntimeCharacterState Character,
RuntimeCommunicationState Communication,
LocalPlayerState LocalPlayer,
StackSplitQuantityState StackSplitQuantity,
BufferedUiRegistry? UiRegistry,
LiveCombatModeCommandSlot CombatModeCommands,
@ -350,7 +349,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
try
{
VitalsVM vitals = d.ExistingVitals
?? new VitalsVM(d.Combat, d.LocalPlayer);
?? new VitalsVM(d.Combat, d.Character.LocalPlayer);
UiHost host = lease.AcquireHost(
() => new UiHost(
d.Gl,
@ -372,7 +371,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
var characterSheet = new CharacterSheetProvider(
d.Inventory.Objects,
d.LocalPlayer,
d.Character.LocalPlayer,
playerGuid: () => d.PlayerIdentity.ServerGuid,
activeToonName: () => d.Settings.ActiveToonKey,
fallbackSheet: Studio.SampleData.SampleCharacter,
@ -389,7 +388,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
MagicRuntime magic = MagicRuntime.Create(
d.MagicCatalog,
d.Spellbook,
d.Character.Spellbook,
d.Inventory.Objects,
selectedObject: () => d.Selection.SelectedObjectId,
localPlayerId: () => d.PlayerIdentity.ServerGuid,
@ -421,7 +420,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
uint skillId = SkillId(school);
if (skillId != 0u)
return d.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
return d.Character.LocalPlayer.GetSkill(skillId)?.CurrentLevel ?? 0u;
uint highest = 0u;
foreach (MagicSchool candidate in new[]
@ -435,7 +434,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
{
highest = Math.Max(
highest,
d.LocalPlayer.GetSkill(SkillId(candidate))?.CurrentLevel ?? 0u);
d.Character.LocalPlayer.GetSkill(
SkillId(candidate))?.CurrentLevel ?? 0u);
}
return highest;
}
@ -527,7 +527,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
() => d.Settings.Gameplay,
d.Settings.SetCombatGameplay),
Magic: new MagicRuntimeBindings(
d.Spellbook,
d.Character.Spellbook,
magic.Casting,
d.Inventory.Objects,
() => d.PlayerIdentity.ServerGuid,
@ -569,10 +569,10 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
late.Selection.ResolveVividTargetInfo,
late.SelectionCamera.UiSnapshot),
Indicators: new IndicatorRuntimeBindings(
d.Spellbook,
d.Character.Spellbook,
d.Inventory.Objects,
() => d.PlayerIdentity.ServerGuid,
() => d.LocalPlayer.GetAttribute(
() => d.Character.LocalPlayer.GetAttribute(
LocalPlayerState.AttributeKind.Strength)
is { } strength ? (int?)strength.Current : null,
() => late.Session.LinkStatus,
@ -614,7 +614,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
() => d.PlayerIdentity.ServerGuid,
iconComposer.GetIcon,
iconComposer.GetDragIcon,
() => d.LocalPlayer.GetAttribute(
() => d.Character.LocalPlayer.GetAttribute(
LocalPlayerState.AttributeKind.Strength)
is { } strength ? (int?)strength.Current : null,
guid => late.Session.CurrentSession?.SendUse(guid),

View file

@ -85,8 +85,7 @@ internal sealed record SessionPlayerDependencies(
CombatState Combat,
CombatFeedbackSlot CombatFeedback,
RuntimeCommunicationState Communication,
LocalPlayerState LocalPlayer,
Spellbook Spellbook,
RuntimeCharacterState Character,
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
Action<string> Log);
@ -802,8 +801,7 @@ internal sealed class SessionPlayerCompositionPhase
d.CharacterOptions),
new LiveSessionDomainRuntime(
d.EntityObjects,
d.LocalPlayer,
d.Spellbook,
d.Character,
d.Combat,
d.Inventory,
d.Communication),