using AcDream.Core.Spells; using AcDream.Core.Player; 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()); state.InstallSpellMetadata(table); state.InstallSpellMetadata(table); Assert.Same(table, state.Spellbook.Metadata); Assert.Throws( () => 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 DisposalReportsObserverFailuresAfterTerminalConvergence() { 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( state.Dispose); Assert.Equal(2, error.InnerExceptions.Count); Assert.True(state.IsDisposed); Assert.False(state.Spellbook.Knows(7u)); Assert.Null(state.LocalPlayer.Get( AcDream.Core.Player.LocalPlayerState.VitalKind.Health)); Assert.True(state.CaptureOwnership().IsConverged); state.Dispose(); 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); } [Fact] public void SpellbookCommandsFollowRetailLocalAndOutboundOrder() { using var state = new RuntimeCharacterState(); var order = new List(); state.Spellbook.SpellbookChanged += () => order.Add("local"); state.Spellbook.DesiredComponentsChanged += () => order.Add("local"); Assert.True(state.TryAddFavorite( 0, 0, 42u, () => order.Add("send"))); Assert.Equal(["local", "send"], order); Assert.Equal([42u], state.Spellbook.GetFavorites(0)); order.Clear(); state.SetSpellbookFilter(0x3FFEu, () => order.Add("send")); Assert.Equal(["local", "send"], order); Assert.Equal(0x3FFEu, state.Spellbook.SpellbookFilters); order.Clear(); Assert.True(state.TrySetDesiredComponent( 0x68000001u, 0u, () => order.Add("send"))); Assert.Equal(["send", "local"], order); Assert.True(state.Spellbook.DesiredComponents.ContainsKey( 0x68000001u)); Assert.Equal(0u, state.Spellbook.DesiredComponents[0x68000001u]); order.Clear(); state.ClearDesiredComponents(() => order.Add("send")); Assert.Equal(["send", "local"], order); Assert.Empty(state.Spellbook.DesiredComponents); Assert.Throws( () => state.TrySetDesiredComponent( 0x68000002u, 10u, () => throw new InvalidOperationException("transport"))); Assert.Equal(10u, state.Spellbook.DesiredComponents[0x68000002u]); } [Fact] public void InvalidSpellbookCommandsDoNotPublishOrMutate() { using var state = new RuntimeCharacterState(); int sends = 0; Assert.False(state.TryAddFavorite( 8, 0, 42u, () => sends++)); Assert.False(state.TryRemoveFavorite( -1, 42u, () => sends++)); Assert.False(state.TrySetDesiredComponent( 0u, 1u, () => sends++)); Assert.False(state.TrySetDesiredComponent( 1u, 5001u, () => sends++)); Assert.Equal(0, sends); Assert.Empty(state.Spellbook.GetFavorites(0)); Assert.Empty(state.Spellbook.DesiredComponents); } }