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
|
|
@ -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);
|
||||
IReadOnlyList<ShortcutEntry> shortcuts =
|
||||
[new ShortcutEntry(2, 0x50000001u, 0u)];
|
||||
IReadOnlyList<(uint Id, uint Amount)> components =
|
||||
[(0x68000001u, 12u)];
|
||||
|
||||
inventory.ExternalContainers.RequestOpen(0x70000001u);
|
||||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||
inventory.Shortcuts.Replace(shortcuts);
|
||||
inventory.DesiredComponents.Replace(components);
|
||||
|
||||
Assert.Same(entities.Objects, inventory.Objects);
|
||||
Assert.Equal(0x70000001u, inventory.ExternalContainers.CurrentContainerId);
|
||||
Assert.Equal(0.75f, inventory.ItemMana.GetManaPercent(0x50000001u));
|
||||
Assert.Same(shortcuts, inventory.Shortcuts.Items);
|
||||
Assert.Same(components, inventory.DesiredComponents.Items);
|
||||
Assert.Equal(1, inventory.Shortcuts.Revision);
|
||||
Assert.Equal(1, inventory.DesiredComponents.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -40,7 +35,6 @@ public sealed class RuntimeInventoryStateTests
|
|||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.75f, true);
|
||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
||||
inventory.Transactions.IncrementBusyCount();
|
||||
|
||||
inventory.ResetExternalContainer();
|
||||
|
|
@ -56,7 +50,6 @@ public sealed class RuntimeInventoryStateTests
|
|||
Assert.Equal(0, inventory.Transactions.BusyCount);
|
||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||
Assert.Empty(inventory.Shortcuts.Items);
|
||||
Assert.Empty(inventory.DesiredComponents.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -86,7 +79,6 @@ public sealed class RuntimeInventoryStateTests
|
|||
inventory.ExternalContainers.ApplyViewContents(0x70000001u);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(0x50000001u, 0.5f, true);
|
||||
inventory.Shortcuts.Replace([new ShortcutEntry(1, 2u, 3u)]);
|
||||
inventory.DesiredComponents.Replace([(4u, 5u)]);
|
||||
|
||||
inventory.Dispose();
|
||||
inventory.Dispose();
|
||||
|
|
@ -96,7 +88,6 @@ public sealed class RuntimeInventoryStateTests
|
|||
Assert.Equal(0u, inventory.ExternalContainers.CurrentContainerId);
|
||||
Assert.False(inventory.ItemMana.HasMana(0x50000001u));
|
||||
Assert.Empty(inventory.Shortcuts.Items);
|
||||
Assert.Empty(inventory.DesiredComponents.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using AcDream.Core.Player;
|
|||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
|
|
@ -227,18 +228,16 @@ public sealed class LiveSessionEventRouterTests
|
|||
|
||||
private static LiveInventorySessionBindings NewInventoryBindings() => new(
|
||||
new ClientObjectTable(),
|
||||
new LocalPlayerState(),
|
||||
PlayerGuid: () => 0x50000001u,
|
||||
OnShortcuts: null,
|
||||
OnUseDone: null,
|
||||
ItemMana: new ItemManaState(),
|
||||
OnDesiredComponents: null,
|
||||
ExternalContainers: new ExternalContainerState());
|
||||
|
||||
private static LiveCharacterSessionBindings NewCharacterBindings(
|
||||
CombatState? combat = null) => new(
|
||||
combat ?? new CombatState(),
|
||||
new Spellbook(),
|
||||
new RuntimeCharacterState(),
|
||||
ResolveSkillFormulaBonus: null,
|
||||
OnSkillsUpdated: null,
|
||||
OnConfirmationRequest: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue