using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank.Tests; public sealed class MossTankPanelTests { [Fact] public void CorruptProfileIsPreservedAndReportedBeforeDefaultsLoad() { var storage = new MemoryStorage(); storage.Text["profiles/index.json"] = "{ this is not json"; var panel = new MossTankPanel( new FakeHost(new FakeAutomation(), storage)); Assert.Contains( "Raw data was preserved", panel.ProfileLifecycleNotice, StringComparison.Ordinal); KeyValuePair backup = Assert.Single( storage.Text, static pair => pair.Key.StartsWith( "recovery/macro/", StringComparison.Ordinal)); Assert.Contains("profiles/index.json", backup.Value, StringComparison.Ordinal); Assert.Contains("{ this is not json", backup.Value, StringComparison.Ordinal); } [Fact] public void FirstRunGuidanceExplainsProfilesImportsAndPersistentShelfOnce() { var storage = new MemoryStorage(); var automation = new FakeAutomation(); var panel = new MossTankPanel(new FakeHost(automation, storage)); panel.OnTick(0d); panel.OnTick(0d); string message = Assert.Single( automation.Messages, static value => value.Contains("First run:", StringComparison.Ordinal)); Assert.Contains("plugin shelf", message, StringComparison.Ordinal); Assert.Contains(".nav/.utl/.met", message, StringComparison.Ordinal); Assert.Equal("shown", storage.Text["onboarding/v1.txt"]); } [Fact] public void RunningMacroRebuffsNormallyAndUsesWiderIdleTopoffOnlyWhenEnabled() { var automation = new FakeAutomation { CurrentHealth = 100, MaxHealth = 100, CurrentStamina = 100, MaxStamina = 100, CurrentMana = 100, MaxMana = 100, Skills = [ new PluginSkillInfo( 1, "Life Magic", PluginSkillTraining.Trained, 300), ], KnownSelfBuffs = [ Spell( 1, 10, "Increases the caster's Life Magic skill by 10 points."), ], ActiveEnchantments = [new PluginActiveEnchantment(1, 10, 1, 600)], }; var panel = new MossTankPanel(new FakeHost(automation)); panel.ToggleCombat(); panel.OnTick(0d); Assert.Empty(automation.CastSpellIds); panel.ToggleIdleBuffTopoff(); panel.OnTick(1d); Assert.Equal([1u], automation.CastSpellIds); Assert.StartsWith("Buffing", panel.BuffStatus, StringComparison.Ordinal); } [Fact] public void FastCastBuffsHoldsForwardOnlyUntilInstantBuffCastEnds() { var automation = new FakeAutomation { Skills = [ new PluginSkillInfo( 1, "Life Magic", PluginSkillTraining.Trained, 300), ], KnownSelfBuffs = [ Spell( 1, 10, "Increases the caster's Life Magic skill by 10 points."), ], }; var panel = new MossTankPanel(new FakeHost(automation)); Command(panel, "opt set FastCastBuffs true"); panel.ForceBuff(); panel.OnTick(0d); PluginMovementIntent held = Assert.Single(automation.MovementIntents); Assert.True(held.Forward); Assert.Equal(0, automation.ClearMovementCount); panel.OnTick(0.25d); Assert.Equal(1, automation.ClearMovementCount); } [Fact] public void FastCastBuffsNeverAppliesForwardMovementToWarSpells() { var automation = new FakeAutomation { Skills = [ new PluginSkillInfo( 1, "Life Magic", PluginSkillTraining.Trained, 300), ], KnownSelfBuffs = [ Spell( 1, 10, "Increases the caster's Life Magic skill by 10 points.") with { School = 34u }, ], }; var panel = new MossTankPanel(new FakeHost(automation)); Command(panel, "opt set FastCastBuffs true"); panel.ForceBuff(); panel.OnTick(0d); Assert.Empty(automation.MovementIntents); } [Fact] public void RetainedLabelReads_UseUpdateSideSnapshotsWithoutAllocating() { var automation = new FakeAutomation { CurrentHealth = 90, MaxHealth = 100, CurrentStamina = 80, MaxStamina = 110, CurrentMana = 70, MaxMana = 120, Skills = [ new PluginSkillInfo(1, "Life Magic", PluginSkillTraining.Trained, 300), new PluginSkillInfo(2, "War Magic", PluginSkillTraining.Specialized, 350), new PluginSkillInfo(3, "Run", PluginSkillTraining.Untrained, 100), ], Attributes = [ new PluginAttributeInfo(0, "Strength", 100), new PluginAttributeInfo(1, "Endurance", 100), ], KnownSelfBuffs = [ Spell(1, 10, "Increases the caster's Life Magic skill by 10 points."), ], }; var panel = new MossTankPanel(new FakeHost(automation)); panel.OnTick(0.0); Assert.Equal("Health 90/100 Stam 80/110 Mana 70/120", panel.Vitals); Assert.Equal("2 attributes, 2 trained skills, 1 buff lines", panel.Coverage); string expectedVitals = panel.Vitals; string expectedCoverage = panel.Coverage; _ = panel.Vitals; _ = panel.Coverage; long before = GC.GetAllocatedBytesForCurrentThread(); bool sameReferences = true; for (int i = 0; i < 10_000; i++) { sameReferences &= ReferenceEquals(expectedVitals, panel.Vitals); sameReferences &= ReferenceEquals(expectedCoverage, panel.Coverage); } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.True(sameReferences); Assert.Equal(0, allocated); Assert.Equal(1, automation.KnownSelfBuffReads); } [Fact] public void UpdateTick_RefreshesRareCoverageAndChangedVitals() { var automation = new FakeAutomation { CurrentHealth = 90, MaxHealth = 100, CurrentStamina = 80, MaxStamina = 110, CurrentMana = 70, MaxMana = 120, Skills = [ new PluginSkillInfo(1, "Life Magic", PluginSkillTraining.Trained, 300), ], Attributes = [new PluginAttributeInfo(0, "Strength", 100)], KnownSelfBuffs = [ Spell(1, 10, "Increases the caster's Life Magic skill by 10 points."), ], }; var panel = new MossTankPanel(new FakeHost(automation)); panel.OnTick(0.0); automation.CurrentHealth = 75; automation.Skills = [ new PluginSkillInfo(1, "Life Magic", PluginSkillTraining.Trained, 300), new PluginSkillInfo(2, "War Magic", PluginSkillTraining.Specialized, 350), ]; panel.OnTick(0.5); Assert.StartsWith("Health 75/100", panel.Vitals, StringComparison.Ordinal); Assert.Equal("1 attributes, 1 trained skills, 1 buff lines", panel.Coverage); panel.OnTick(0.5); Assert.Equal("1 attributes, 2 trained skills, 1 buff lines", panel.Coverage); automation.KnownSelfBuffs = [ Spell(1, 10, "Increases the caster's Life Magic skill by 10 points."), Spell(2, 11, "Increases the caster's War Magic skill by 10 points."), ]; panel.OnTick(0.0); Assert.Equal("1 attributes, 2 trained skills, 2 buff lines", panel.Coverage); } [Fact] public void ItemsAndConsumablesTabsAddTheSelectedOwnedItemToRealProfiles() { var automation = new FakeAutomation { ItemEntries = [ Item(10, "Imperil Lens", 0x8000), Item(11, "Iron Phial of Imperil", 0x100), Item(12, "Black Marrow Pea", 0x20), ], }; var host = new FakeHost(automation); var panel = new MossTankPanel(host); host.Selection.Select(10); panel.AddSelectedItem(); Assert.Contains("Imperil Lens", panel.ItemProfileText, StringComparison.Ordinal); host.Selection.Select(11); panel.AddSelectedConsumable(); panel.AddAllPeas(); Assert.Contains( "Iron Phial of Imperil", panel.ConsumableProfileText, StringComparison.Ordinal); Assert.Contains( CraftingPlanner.AllPeas, panel.ConsumableProfileText, StringComparison.Ordinal); Assert.Contains("Imperil Lens", panel.ItemRows); panel.RemoveSelectedItem(); Assert.DoesNotContain("Imperil Lens", panel.ItemRows); Assert.Equal(2, panel.ConsumableRows.Count); panel.RemoveSelectedConsumable(); Assert.Single(panel.ConsumableRows); } [Fact] public void ItemProfilesPersistThroughHostScopedStorage() { var storage = new MemoryStorage(); var firstAutomation = new FakeAutomation { ItemEntries = [Item(10, "Imperil Lens", 0x8000)], }; var firstHost = new FakeHost(firstAutomation, storage); var first = new MossTankPanel(firstHost); firstHost.Selection.Select(10); first.AddSelectedItem(); var second = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); Assert.Contains("Imperil Lens", second.ItemProfileText, StringComparison.Ordinal); Assert.Contains( storage.Text.Keys, key => key.StartsWith("profiles/macro/", StringComparison.Ordinal)); } [Fact] public void VitalThresholdsPersistWithTheProfile() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); first.SetNormalHealth(0.42f); first.SetNoTargetMana(0.88f); first.ToggleHelpOthers(); var second = new MossTankPanel( new FakeHost(new FakeAutomation(), storage)); Assert.Equal(0.42f, second.NormalHealthValue, precision: 2); Assert.Equal(0.88f, second.NoTargetManaValue, precision: 2); Assert.False(second.HelpOthersEnabled); } [Fact] public void AutoStackAndAutoCramUseVtankDefaultsAndPersistPerProfile() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.True(first.AutoStackEnabled); Assert.False(first.AutoCramEnabled); Assert.True(first.AutoCraftItemsEnabled); Assert.True(first.RefillWornManaEnabled); first.ToggleAutoStack(); first.ToggleAutoCram(); first.ToggleAutoCraftItems(); first.ToggleRefillWornMana(); first.SetRefillWornMana(0.44f); var second = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.False(second.AutoStackEnabled); Assert.True(second.AutoCramEnabled); Assert.False(second.AutoCraftItemsEnabled); Assert.False(second.RefillWornManaEnabled); Assert.Equal(0.44f, second.RefillWornManaValue, precision: 2); } [Fact] public void LootingUsesVtankDefaultsAndPersistsTheOrderedRuleEditor() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.False(first.LootEnabled); Assert.False(first.LootPriorityBoostEnabled); Assert.Empty(first.LootRuleRows); first.ToggleLooting(); first.ToggleLootPriorityBoost(); first.AddLootRule(); first.SetLootExpressionDraft("name ~= coin && value >= 10"); first.ApplyLootRule(); first.SelectLootAction(nameof(LootAction.KeepUpTo)); first.LootKeepCountUp(); first.LootPriorityUp(); first.LootRangeDown(); var second = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.True(second.LootEnabled); Assert.True(second.LootPriorityBoostEnabled); Assert.Single(second.LootRuleRows); Assert.Contains("KeepUpTo", second.LootRuleRows[0], StringComparison.Ordinal); Assert.Contains("name ~= coin", second.LootRuleRows[0], StringComparison.Ordinal); Assert.Equal("Keep up to 2", second.LootKeepCountText); Assert.Equal("Priority 1", second.LootPriorityText); Assert.Equal("Corpse range 38m", second.LootRangeText); } [Fact] public void LootProfilesAreIndependentNamedDocuments() { var storage = new MemoryStorage(); var panel = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Looter" }, storage)); panel.AddLootRule(); panel.SetLootExpressionDraft("name ~= coin"); panel.ApplyLootRule(); panel.SetLootProfileNameDraft("Currency"); panel.CopyLootProfile(); panel.AddLootRule(); Assert.Equal("Currency", panel.LootProfileName); Assert.Equal(2, panel.LootRuleRows.Count); panel.SelectLootProfile(MossTankLootProfileStore.ByCharacter); Assert.Single(panel.LootRuleRows); panel.SelectLootProfile("Currency"); Assert.Equal(2, panel.LootRuleRows.Count); Assert.Contains( storage.Text.Keys, key => key.StartsWith("profiles/loot/", StringComparison.Ordinal)); } [Fact] public void LootClassifierSelectionIsVisibleAndPersistsWithMacroProfile() { var storage = new MemoryStorage(); var classifiers = new FakeLootClassifierRegistry( new PluginLootClassifierInfo("utility/loot", "Utility Loot")); var panel = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Looter" }, storage, classifiers)); Assert.Contains("Utility Loot [utility/loot]", panel.LootClassifierNames); panel.SelectLootClassifier("Utility Loot [utility/loot]"); Assert.Equal( "Utility Loot [utility/loot]", panel.SelectedLootClassifier); var restored = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Looter" }, storage, classifiers)); Assert.Equal( "Utility Loot [utility/loot]", restored.SelectedLootClassifier); restored.SelectLootClassifier("VTClassic"); Assert.Equal("VTClassic", restored.SelectedLootClassifier); } [Fact] public void VtankRecoveryAndFakeImperilCommandsHaveRealLocalSemantics() { var automation = new FakeAutomation { BusyReferences = 2, WorldObjects = [ new PluginWorldObject( 0x50000001u, 123u, "Drudge", PluginObjectClass.Monster, 0u, 0u, 0u), ], }; var host = new FakeHost(automation); var panel = new MossTankPanel(host); Command(panel, "clearbusy"); Assert.Equal(1, automation.BusyReferences); Assert.Contains(automation.Messages, text => text.Contains("2 -> 1", StringComparison.Ordinal)); Command(panel, "clearlocks"); Assert.Contains(automation.Messages, text => text.Contains("Action locks cleared", StringComparison.Ordinal)); host.Selection.Select(0x50000001u); Command(panel, "fakeimp"); Assert.Contains(automation.Messages, text => text.Contains("Fake cast complete", StringComparison.Ordinal)); } [Fact] public void LootCommandsImportAndExportExactVtclassicUtlFiles() { var storage = new MemoryStorage(); var legacy = new VtankLootProfile { Rules = [ new LootRule { Name = "Pyreal", Action = LootAction.KeepUpTo, KeepCount = 100, Priority = 7, VtankRequirements = [ new VtankLootRequirement { Type = 1, Payload = "^Pyreal$\r\n1\r\n", }, ], }, ], SalvageCombine = new VtankSalvageCombineSettings { DefaultCombineString = "1-5, 6-10", MaterialCombineStrings = new Dictionary { [61] = "1-10", }, MaterialValueModeValues = new Dictionary { [61] = 75_000, }, }, }; storage.Text["imports/Legacy.utl"] = VtankLootProfileSerializer.Write(legacy); var panel = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); Command(panel, "loot load Legacy.utl"); Assert.Equal("Legacy", panel.LootProfileName); Assert.Single(panel.LootRuleRows); Assert.Contains("KeepUpTo", panel.LootRuleRows[0], StringComparison.Ordinal); string exported = storage.Text["exports/Legacy.utl"]; Assert.True(VtankLootProfileSerializer.TryRead( exported, out VtankLootProfile roundTrip, out string error), error); Assert.Equal("1-5, 6-10", roundTrip.SalvageCombine.DefaultCombineString); Assert.Equal(75_000, roundTrip.SalvageCombine.MaterialValueModeValues[61]); Assert.Equal(1, Assert.Single(roundTrip.Rules).VtankRequirements[0].Type); } [Fact] public void NamedProfileCopyHotLoadsWithoutMixingByCharacterSettings() { var storage = new MemoryStorage(); var automation = new FakeAutomation { Name = "Moss Wart" }; var panel = new MossTankPanel(new FakeHost(automation, storage)); panel.SetNormalHealth(0.42f); panel.SetProfileNameDraft("Fellowship"); panel.CopyProfile(); Assert.Equal("Fellowship", panel.SelectedMacroProfile); Assert.Contains("Fellowship", panel.MacroProfileNames); panel.SetNormalHealth(0.88f); panel.SelectMacroProfile(MossTankProfileStore.ByCharacter); Assert.Equal(0.42f, panel.NormalHealthValue, precision: 2); panel.SelectMacroProfile("Fellowship"); Assert.Equal(0.88f, panel.NormalHealthValue, precision: 2); } [Fact] public void NavCommandsImportAndExportExactVtankNavFiles() { var storage = new MemoryStorage(); storage.Text["imports/Legacy.nav"] = """ uTank2 NAV 1.2 4 1 0 12.5 -3.25 0 0 """; var panel = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); Command(panel, "nav load Legacy.nav"); Assert.Equal("Legacy", panel.SelectedRouteProfile); Assert.Single(panel.RouteRows); Assert.Contains("12.5", panel.RouteRows[0], StringComparison.Ordinal); Command(panel, "nav save Exported.nav"); Assert.StartsWith( "uTank2 NAV 1.2\r\n", storage.Text["exports/Exported.nav"], StringComparison.Ordinal); } [Fact] public void MetaCommandsImportAndExportExactVtankMetFiles() { var storage = new MemoryStorage(); storage.Text["imports/Legacy.met"] = VtankMetaProfileSerializer.Save( new MetaProfile { Rules = [ new MetaRule { State = "Default", Condition = MetaCondition.Always(), Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say imported", }, }, ], }); var panel = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); Command(panel, "meta load Legacy.met"); Assert.Equal("Legacy", panel.SelectedMetaProfile); Assert.Single(panel.MetaRows); Assert.Contains("/say imported", panel.MetaRows[0], StringComparison.Ordinal); Command(panel, "meta save Exported.met"); Assert.StartsWith( "1\r\nCondAct\r\n5\r\n", storage.Text["exports/Exported.met"], StringComparison.Ordinal); Assert.True(VtankMetaProfileSerializer.TryLoad( storage.Text["exports/Exported.met"], out MetaProfile exported, out string error), error); Assert.Single(exported.Rules); } [Fact] public void ByCharacterProfilesAreIsolatedByCharacterName() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "One" }, storage)); first.SetNormalHealth(0.33f); var second = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Two" }, storage)); Assert.Equal(0.75f, second.NormalHealthValue, precision: 2); } [Fact] public void MonstersEditorMutatesAndPersistsExecutableOrderedRules() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Rule Maker" }, storage)); first.AddMonsterRule(); first.SetMonsterExpressionDraft("species==drudge"); first.ApplyMonsterRule(); first.ToggleMonsterImperil(); first.SelectMonsterDamage(nameof(MonsterDamageType.Fire)); first.MonsterPriorityUp(); Assert.Equal(1, first.SelectedMonsterRuleIndex); Assert.Equal(nameof(MonsterDamageType.Fire), first.SelectedDamageType); Assert.True(first.MonsterImperil); Assert.Contains("species==drudge", first.MonsterRows[1], StringComparison.Ordinal); var second = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Rule Maker" }, storage)); second.SelectMonsterRule(1); Assert.Equal(nameof(MonsterDamageType.Fire), second.SelectedDamageType); Assert.True(second.MonsterImperil); Assert.Equal("Priority 1", second.MonsterPriorityText); } [Fact] public void MonsterColumnsExposeRetailSpecificDamageCyclesAndLabels() { var panel = new MossTankPanel(new FakeHost(new FakeAutomation())); Assert.Equal( [ "Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold", "Fire", "Harm", "Auto", "Void Basic", "Drain Auto", "Prismatic", "Random", "Fists", ], panel.DamageTypeNames); Assert.Equal( [ "Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold", "Fire", "Auto", "None", ], panel.ExtraVulnerabilityNames); Assert.Equal( [ "Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold", "Fire", "PAuto", "Auto", "None", ], panel.PetDamageTypeNames); panel.SelectMonsterDamage("Void Basic"); panel.SelectMonsterExtraVulnerability("Lightning"); panel.SelectMonsterPetDamage("PAuto"); Assert.Equal("Void Basic", panel.SelectedDamageType); Assert.Equal("Lightning", panel.SelectedExtraVulnerability); Assert.Equal("PAuto", panel.SelectedPetDamage); } [Fact] public void MonsterEquipmentPersistsByNameAcrossSessionObjectIds() { var storage = new MemoryStorage(); var firstAutomation = new FakeAutomation { Name = "Rule Maker", ItemEntries = [Item(10, "Fire Sword", 1)], }; var firstHost = new FakeHost(firstAutomation, storage); var first = new MossTankPanel(firstHost); firstHost.Selection.Select(10); first.SetMonsterWeapon(); var second = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Rule Maker", ItemEntries = [Item(99, "Fire Sword", 1)], }, storage)); Assert.Contains("Fire Sword", second.MonsterEquipmentText); } [Fact] public void MetaTabEditsAndExecutesTheLiveStateMachine() { var panel = new MossTankPanel(new FakeHost(new FakeAutomation())); panel.ShowMeta(); Assert.True(panel.MetaVisible); panel.SetMetaStateDraft(MetaEngine.DefaultState); panel.SelectMetaCondition(nameof(MetaConditionKind.Always)); panel.SelectMetaAction(nameof(MetaActionKind.SetMetaState)); panel.SetMetaActionTextDraft("Hunt"); panel.AddMetaRule(); Assert.Single(panel.MetaRows); Assert.Contains("Hunt", panel.MetaRows[0], StringComparison.Ordinal); panel.ToggleMeta(); panel.ToggleCombat(); panel.OnTick(0.3); Assert.True(panel.MetaEnabled); Assert.Equal("Hunt", panel.MetaState); } [Fact] public void MetaProfilesAreIndependentDurableDocuments() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Meta Maker" }, storage)); first.SelectMetaAction(nameof(MetaActionKind.ChatCommand)); first.SetMetaActionTextDraft("/mt status"); first.AddMetaRule(); first.SetMetaProfileNameDraft("Hunting"); first.CopyMetaProfile(); Assert.Equal("Hunting", first.SelectedMetaProfile); Assert.Single(first.MetaRows); first.SelectMetaProfile(MossTankMetaProfileStore.ByCharacter); first.AddMetaRule(); Assert.Equal(2, first.MetaRows.Count); var second = new MossTankPanel(new FakeHost( new FakeAutomation { Name = "Meta Maker" }, storage)); Assert.Equal(MossTankMetaProfileStore.ByCharacter, second.SelectedMetaProfile); Assert.Equal(2, second.MetaRows.Count); second.SelectMetaProfile("Hunting"); Assert.Single(second.MetaRows); Assert.Contains("/mt status", second.MetaRows[0], StringComparison.Ordinal); } [Fact] public void UtilityBeltVtankExpressionsControlTheSameLiveOwners() { var panel = new MossTankPanel(new FakeHost(new FakeAutomation())); panel.SelectMetaCondition(nameof(MetaConditionKind.Expression)); panel.SetMetaConditionTextDraft("vtmacroenabled[] == 1"); panel.SelectMetaAction(nameof(MetaActionKind.ExpressionAction)); panel.SetMetaActionTextDraft( "vtsetsetting['MonsterRange',42] + vtsetmetastate['Expression State']"); panel.AddMetaRule(); panel.ToggleMeta(); panel.ToggleCombat(); panel.OnTick(0.3); Assert.Equal("Expression State", panel.MetaState); Assert.Equal("Maximum target range: 42m", panel.AttackRangeText); Assert.True(panel.EvaluateExpression("uboptset['MonsterRange',33]").IsTruthy); Assert.Equal(33d, panel.EvaluateExpression( "uboptget['MonsterRange']").AsNumber()); } [Fact] public void RunMacroAndEnableCombatAreIndependentVtankStates() { var panel = new MossTankPanel(new FakeHost(new FakeAutomation())); Assert.Equal("Run Macro", panel.CombatButtonText); Assert.True(panel.CombatEnabled); panel.ToggleCombat(); panel.ToggleCombatEnabled(); panel.OnTick(0d); Assert.Equal("Stop Macro", panel.CombatButtonText); Assert.False(panel.CombatEnabled); Assert.Equal("Combat disabled", panel.CombatStatus); Assert.True(panel.EvaluateExpression("vtmacroenabled[]").IsTruthy); Assert.False(panel.EvaluateExpression("uboptget['EnableCombat']").IsTruthy); } [Fact] public void SameCharacterReconnectClearsOnlySessionStateAndCanRestartCleanly() { var automation = new FakeAutomation { Name = "Relogger" }; var panel = new MossTankPanel(new FakeHost(automation)); panel.ToggleMeta(); panel.ToggleCombat(); panel.EvaluateExpression( "$session=11;@persistent=22;&global=33;" + "delayexec[60000,\"$late=1\"]"); panel.EvaluateExpression("vtsetmetastate['Hunt']"); automation.IsAvailable = false; panel.OnTick(0.1d); Assert.Equal("Run Macro", panel.CombatButtonText); Assert.False(panel.MetaEnabled); Assert.Equal(MetaEngine.DefaultState, panel.MetaState); Assert.Equal(0d, panel.EvaluateExpression("$session").AsNumber()); Assert.Equal(22d, panel.EvaluateExpression("@persistent").AsNumber()); Assert.Equal(33d, panel.EvaluateExpression("&global").AsNumber()); Assert.Equal("Lost the session.", panel.BuffStatus); automation.IsAvailable = true; panel.OnTick(0.1d); panel.OnTick(61d); Assert.Equal(0d, panel.EvaluateExpression("$late").AsNumber()); Assert.Equal("Idle.", panel.BuffStatus); panel.ToggleCombat(); Assert.Equal("Stop Macro", panel.CombatButtonText); } [Fact] public void OfficialVtankOptionDefaultsAndDynamicOverridesAreDurable() { var storage = new MemoryStorage(); var first = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.Equal(137, VtankOptionCatalog.Names.Length); Assert.Equal(10d, first.EvaluateExpression( "uboptget['ArrowheadFletchDiffExcessThreshold']").AsNumber()); Assert.Equal(0.0833333333333333d, first.EvaluateExpression( "uboptget['DoorIDRange']").AsNumber(), precision: 14); Assert.True(first.EvaluateExpression( "uboptget['ManaChargesWhenOff']").IsTruthy); Assert.Equal(4d, first.EvaluateExpression( "uboptget['SpellCompMin-Critical']").AsNumber()); Assert.Equal(20d, first.EvaluateExpression( "uboptget['SpellCompMin-Normal']").AsNumber()); Assert.Equal(20d, first.EvaluateExpression( "uboptget['SpellCompMin-Idle']").AsNumber()); Assert.Equal(2d, first.EvaluateExpression( "uboptget['IdleCraftCount_HealthKits']").AsNumber()); Assert.Equal(15d, first.EvaluateExpression( "uboptget['IdleCraftCount_ManaFood']").AsNumber()); Assert.True(first.EvaluateExpression( "uboptset['ArrowheadFletchDiffExcessThreshold',22]").IsTruthy); Assert.True(first.EvaluateExpression( "uboptset['IdleCraftCount_HealthKits',7]").IsTruthy); var second = new MossTankPanel(new FakeHost(new FakeAutomation(), storage)); Assert.Equal(22d, second.EvaluateExpression( "uboptget['arrowheadfletchdiffexcessthreshold']").AsNumber()); Assert.Equal(7d, second.EvaluateExpression( "uboptget['idlecraftcount_healthkits']").AsNumber()); } [Fact] public void VtankSetInAllRewritesEveryKnownMacroProfile() { var storage = new MemoryStorage(); var automation = new FakeAutomation(); var panel = new MossTankPanel(new FakeHost(automation, storage)); Command(panel, "settings save First"); Command(panel, "opt set AttackDistance 0.01"); Command(panel, "settings save Second"); Command(panel, "opt set AttackDistance 0.02"); Command(panel, "opt setinall AttackDistance 0.03"); Command(panel, "settings load First"); Assert.Equal(0.03d, panel.EvaluateExpression( "uboptget['AttackDistance']").AsNumber(), precision: 7); Command(panel, "settings load Second"); Assert.Equal(0.03d, panel.EvaluateExpression( "uboptget['AttackDistance']").AsNumber(), precision: 7); var reloaded = new MossTankPanel(new FakeHost( new FakeAutomation(), storage)); Command(reloaded, "settings load First"); Assert.Equal(0.03d, reloaded.EvaluateExpression( "uboptget['AttackDistance']").AsNumber(), precision: 7); } [Fact] public void VtankHelpAndExpressionsUseTheLocalCommandSurface() { var automation = new FakeAutomation(); var panel = new MossTankPanel(new FakeHost(automation)); panel.ExecuteVtankCommand(new PluginCommand("vt", "help", "/vt help")); panel.ExecuteVtankCommand(new PluginCommand( "vt", "mexec 1 + 2 * 3", "/vt mexec 1 + 2 * 3")); Assert.Equal(6, automation.Messages.Count); Assert.StartsWith("/vt commands (profiles):", automation.Messages[0], StringComparison.Ordinal); Assert.Equal("MExec evaluating expression: \"1 + 2 * 3\"", automation.Messages[4]); Assert.Equal("Result: 7", automation.Messages[5]); } [Fact] public void VtankJumpTurnsToTheRequestedHeadingBeforeChargingAndReleasing() { var automation = new FakeAutomation { NavigationSnapshot = NavigationAt(90f), }; var panel = new MossTankPanel(new FakeHost(automation)); panel.ExecuteVtankCommand(new PluginCommand( "vt", "jump 180 false 100", "/vt jump 180 false 100")); panel.OnTick(0.05d); Assert.True(automation.MovementIntents[^1].TurnRight); Assert.False(automation.MovementIntents[^1].Jump); automation.NavigationSnapshot = NavigationAt(180f); panel.OnTick(0.01d); Assert.True(automation.MovementIntents[^1].Jump); panel.OnTick(0.1d); Assert.False(automation.MovementIntents[^1].Jump); } [Fact] public void RegistersEveryAuditedUtilityBeltExpressionFunction() { var panel = new MossTankPanel(new FakeHost(new FakeAutomation())); string[] expected = [ "abs", "acos", "actiontryapplyitem", "actiontrycastbyid", "actiontrycastbyidontarget", "actiontrydrop", "actiontryequipanywand", "actiontrygiveitem", "actiontrygiveprofile", "actiontrymove", "actiontryselect", "actiontrysplit", "actiontryuseitem", "asin", "atan", "atan2", "ceiling", "chatbox", "chatboxpaste", "chr", "clearallgvars", "clearallpvars", "clearallvars", "clearexec", "cleargvar", "clearmotion", "clearnextlogin", "clearpvar", "clearvar", "cnumber", "componentdata", "componentname", "coordinatedistanceflat", "coordinatedistancewithz", "coordinategetns", "coordinategetwe", "coordinategetz", "coordinateparse", "coordinatetostring", "cos", "cosh", "cstr", "cstrf", "delayexec", "dictadditem", "dictclear", "dictcopy", "dictcreate", "dictgetitem", "dicthaskey", "dictkeys", "dictremovekey", "dictsize", "dictvalues", "echo", "exec", "floor", "getaccounthash", "getbusystate", "getcancastspell_buff", "getcancastspell_hunt", "getcharacterindex", "getcharattribute_base", "getcharattribute_buffed", "getcharboolprop", "getcharburden", "getchardoubleprop", "getcharintprop", "getcharquadprop", "getcharskill_base", "getcharskill_buffed", "getcharskill_traininglevel", "getcharstringprop", "getcharvital_base", "getcharvital_buffedmax", "getcharvital_current", "getcombatstate", "getcontaineritemcount", "getcooldownexpiration", "getcorpsesunopenedbyme", "getdatetimelocal", "getdatetimeutc", "getequippedweapontype", "getfellowid", "getfellowids", "getfellowname", "getfellownames", "getfellowshipcanrecruit", "getfellowshipcount", "getfellowshipisfull", "getfellowshipisleader", "getfellowshipisopen", "getfellowshipleaderid", "getfellowshiplocked", "getfellowshipname", "getfellowshipstatus", "getfreecontainerslots", "getfreeitemslots", "getgameday", "getgamehour", "getgamehourname", "getgamemonth", "getgamemonthname", "getgameticks", "getgameyear", "getgvar", "getheading", "getheadingto", "getinventorycountbytemplatetype", "getisday", "getisnight", "getisspellknown", "getitemcountininventorybyname", "getitemcountininventorybynamerx", "getknownspells", "getminutesuntilday", "getminutesuntilnight", "getmotion", "getobjectinternaltype", "getplayercoordinates", "getplayerlandblock", "getplayerlandcell", "getpvar", "getquestktprogress", "getquestktrequired", "getqueststatus", "getregexmatch", "getspellexpiration", "getspellexpirationbyname", "getunixtime", "getvar", "getworldname", "hascorpsebeenopenedbyme", "hexstr", "ifthen", "iif", "isfalse", "isportaling", "isrefreshingquests", "istrue", "listadd", "listclear", "listcontains", "listcopy", "listcount", "listcreate", "listfilter", "listfromrange", "listgetitem", "listindexof", "listinsert", "listlastindexof", "listmap", "listpop", "listreduce", "listremove", "listremoveat", "listreverse", "listsort", "lumavg", "lumtotal", "netclients", "ord", "randint", "round", "setcombatstate", "setgvar", "setmotion", "setnextlogin", "setpvar", "setvar", "sin", "sinh", "spelldata", "spellname", "sqrt", "statushud", "statushudcolored", "stopwatchcreate", "stopwatchelapsedseconds", "stopwatchstart", "stopwatchstop", "strlen", "tan", "tanh", "testgvar", "testpvar", "testquestflag", "testvar", "tostring", "touchgvar", "touchpvar", "touchvar", "uboptget", "uboptset", "uigetcontrol", "uisetlabel", "uisetvisible", "uiviewexists", "uiviewvisible", "ustadd", "ustopen", "ustsalvage", "vitae", "vtgetmeta", "vtgetmetastate", "vtgetsetting", "vtmacroenabled", "vtsetmetastate", "vtsetsetting", "wobjectfindall", "wobjectfindallbycontainer", "wobjectfindallbynamerx", "wobjectfindallbyobjectclass", "wobjectfindallbytemplatetype", "wobjectfindallinventory", "wobjectfindallinventorybynamerx", "wobjectfindallinventorybyobjectclass", "wobjectfindallinventorybytemplatetype", "wobjectfindalllandscape", "wobjectfindalllandscapebynamerx", "wobjectfindalllandscapebyobjectclass", "wobjectfindalllandscapebytemplatetype", "wobjectfindbyid", "wobjectfindininventorybyname", "wobjectfindininventorybynamerx", "wobjectfindininventorybytemplatetype", "wobjectfindnearestbynameandobjectclass", "wobjectfindnearestbyobjectclass", "wobjectfindnearestbytemplatetype", "wobjectfindnearestdoor", "wobjectfindnearestmonster", "wobjectgetactivespellids", "wobjectgetboolprop", "wobjectgetdoubleprop", "wobjectgethealth", "wobjectgethealthvalue", "wobjectgetid", "wobjectgetintprop", "wobjectgetisdooropen", "wobjectgetmanavalue", "wobjectgetname", "wobjectgetobjectclass", "wobjectgetopencontainer", "wobjectgetphysicscoordinates", "wobjectgetplayer", "wobjectgetselection", "wobjectgetspellids", "wobjectgetstaminavalue", "wobjectgetstringprop", "wobjectgettemplatetype", "wobjecthasdata", "wobjectisvalid", "wobjectlastidtime", "wobjectrequestdata", "xpavg", "xpduration", "xpmeter", "xpreset", "xptotal", ]; string[] missing = expected .Except(panel.ExpressionFunctionNames, StringComparer.OrdinalIgnoreCase) .Order(StringComparer.OrdinalIgnoreCase) .ToArray(); Assert.True( missing.Length == 0, "Missing UtilityBelt expression functions: " + string.Join(", ", missing)); } private static PluginInventoryItem Item( uint id, string name, uint itemType) => new( id, 0, name, itemType, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, 0); private static PluginSpellInfo Spell(uint id, uint family, string description) => new( id, $"Spell {id}", family, Tier: 1, Difficulty: 10, ManaCost: 5, DurationSeconds: 60f, School: 1, description, IsSelfTargeted: true, IsBeneficial: true); private static PluginNavigationSnapshot NavigationAt(float heading) => new( IsAvailable: true, IsPortalSpace: false, LocalObjectId: 1u, Position: new PluginNavigationPosition( 0x00010001u, 0d, 0d, 0d, heading, IsOutdoor: true), IsMoving: false, IsAirborne: false); private static void Command(MossTankPanel panel, string arguments) => panel.ExecuteVtankCommand(new PluginCommand( "vt", arguments, "/vt " + arguments)); private sealed class FakeHost( IAutomationSurface automation, IPluginStorage? storage = null, IPluginLootClassifierRegistry? lootClassifiers = null) : IPluginHost { public bool HasUi => false; public IPluginLogger Log { get; } = new FakeLogger(); public IGameState State { get; } = new FakeState(); public IEvents Events { get; } = new FakeEvents(); public ISelectionService Selection { get; } = new FakeSelection(); public IUiRegistry Ui => NoOpUiRegistry.Instance; public IPluginStorage Storage { get; } = storage ?? NoOpPluginStorage.Instance; public IAutomationSurface Automation { get; } = automation; public IPluginLootClassifierRegistry LootClassifiers { get; } = lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance; } private sealed class FakeLootClassifierRegistry( params PluginLootClassifierInfo[] available) : IPluginLootClassifierRegistry { public IReadOnlyList Available { get; } = available; } private sealed class FakeAutomation : IAutomationSurface, ICharacterInfo, ISpellCatalog, IMagicCommands, IPluginChat, IItemAutomation, INavigationAutomation, IWorldObjectAutomation, IRecoveryAutomation { private IReadOnlyList _knownSelfBuffs = []; public bool IsAvailable { get; set; } = true; public ICharacterInfo Character => this; public ISpellCatalog Spells => this; public IMagicCommands Magic => this; public IPluginChat Chat => this; public IItemAutomation Items => this; public INavigationAutomation Navigation => this; public IWorldObjectAutomation Objects => this; public IRecoveryAutomation Recovery => this; public bool IsInWorld => IsAvailable; public string Name { get; set; } = "Test Character"; public uint ObjectId { get; set; } = 1; public uint CurrentHealth { get; set; } public uint MaxHealth { get; set; } public uint CurrentStamina { get; set; } public uint MaxStamina { get; set; } public uint CurrentMana { get; set; } public uint MaxMana { get; set; } public IReadOnlyList Skills { get; set; } = []; public IReadOnlyList Attributes { get; set; } = []; public IReadOnlyList ActiveEnchantments { get; set; } = []; public IReadOnlyList ItemEntries { get; set; } = []; public List Messages { get; } = []; public List CastSpellIds { get; } = []; public List MovementIntents { get; } = []; public int ClearMovementCount { get; private set; } public IReadOnlyList WorldObjects { get; set; } = []; public int BusyReferences { get; set; } public PluginNavigationSnapshot NavigationSnapshot { get; set; } public PluginNavigationSnapshot Snapshot => NavigationSnapshot; public IReadOnlyList CaptureOwnedItems() => ItemEntries; public IReadOnlyList CaptureObjects() => WorldObjects; bool IWorldObjectAutomation.TryGet( uint objectId, out PluginWorldObject value) { foreach (PluginWorldObject candidate in WorldObjects) { if (candidate.ObjectId != objectId) continue; value = candidate; return true; } value = default; return false; } public PluginRecoveryResult ClearOneBusyReference() { int before = BusyReferences; BusyReferences = Math.Max(0, BusyReferences - 1); return new(true, before, BusyReferences); } public int KnownSelfBuffReads { get; private set; } public IReadOnlyList KnownSelfBuffs { get { KnownSelfBuffReads++; return _knownSelfBuffs; } set => _knownSelfBuffs = value; } public bool TryGetSkill(uint skillId, out PluginSkillInfo skill) { foreach (PluginSkillInfo candidate in Skills) { if (candidate.SkillId == skillId) { skill = candidate; return true; } } skill = default; return false; } public bool TryGet(uint spellId, out PluginSpellInfo info) { foreach (PluginSpellInfo candidate in _knownSelfBuffs) { if (candidate.SpellId == spellId) { info = candidate; return true; } } info = default; return false; } public bool IsCasting { get; set; } public PluginCastGate EvaluateGate(uint spellId) => PluginCastGate.Ready; public bool Cast(uint spellId) { CastSpellIds.Add(spellId); return true; } public void PostSystemMessage(string text) => Messages.Add(text); public bool TryGetObject(uint objectId, out PluginNavigationObject value) { value = default; return false; } public PluginNavigationCommandStatus SetMovementIntent( in PluginMovementIntent intent) { MovementIntents.Add(intent); return PluginNavigationCommandStatus.Accepted; } public PluginNavigationCommandStatus ClearMovementIntent() { ClearMovementCount++; return PluginNavigationCommandStatus.Accepted; } } private sealed class FakeLogger : IPluginLogger { public void Info(string message) { } public void Warn(string message) { } public void Error(string message, Exception? exception = null) { } } private sealed class MemoryStorage : IPluginStorage { public Dictionary Text { get; } = new(StringComparer.Ordinal); public bool IsAvailable => true; public string? ReadText(string key) => Text.TryGetValue(key, out string? value) ? value : null; public IReadOnlyList List(string prefix) => Text.Keys .Where(key => key.StartsWith(prefix + "/", StringComparison.Ordinal)) .OrderBy(static key => key, StringComparer.Ordinal) .ToArray(); public void WriteText(string key, string content) => Text[key] = content; public bool Delete(string key) => Text.Remove(key); } private sealed class FakeState : IGameState { public IReadOnlyList Entities => []; } private sealed class FakeEvents : IEvents { public event Action EntitySpawned { add { } remove { } } public event Action Tick { add { } remove { } } } private sealed class FakeSelection : ISelectionService { public uint? SelectedObjectId { get; private set; } public uint? PreviousObjectId { get; private set; } public event Action Changed { add { } remove { } } public bool Select(uint objectId) { PreviousObjectId = SelectedObjectId; SelectedObjectId = objectId; return true; } public bool Clear() { PreviousObjectId = SelectedObjectId; SelectedObjectId = null; return true; } } }