using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank.Tests; public sealed class CraftingTests { [Fact] public void EmbeddedDatabaseContainsAllOfficialVtankCraftInteractions() { Assert.Equal(757, VtankCraftDatabase.Recipes.Count); VtankCraftRecipe recipe = Assert.Single( VtankCraftDatabase.ForResult("Plentiful Healing Kit")); Assert.Equal("Soft Bandages", recipe.FirstItem); Assert.Equal("Combined Hyssop and Mandrake", recipe.SecondItem); Assert.Equal(21u, recipe.RequiredSkill); Assert.Equal(157, recipe.Id); } [Fact] public void PlannerRecursivelyCraftsMissingPrerequisiteBeforeDesiredItem() { var character = new Character(trainedSkill: 21u); IReadOnlyList inventory = [ Item(1, "Soft Bandages"), Item(2, "Treated Mandrake"), Item(3, "Treated Hyssop"), ]; CraftingPlan first = Assert.IsType(CraftingPlanner.Plan( inventory, ["Plentiful Healing Kit"], character)); Assert.Equal("Combined Hyssop and Mandrake", first.Recipe.ResultItem); Assert.Equal(2u, first.FirstObjectId); Assert.Equal(3u, first.SecondObjectId); CraftingPlan final = Assert.IsType(CraftingPlanner.Plan( [inventory[0], Item(4, "Combined Hyssop and Mandrake")], ["Plentiful Healing Kit"], character)); Assert.Equal("Plentiful Healing Kit", final.Recipe.ResultItem); Assert.Equal(1u, final.FirstObjectId); Assert.Equal(4u, final.SecondObjectId); } [Fact] public void PlannerRejectsRecipesForUntrainedRequiredSkill() { CraftingPlan? plan = CraftingPlanner.Plan( [Item(1, "Soft Bandages"), Item(2, "Combined Hyssop and Mandrake")], ["Plentiful Healing Kit"], new Character(trainedSkill: 0u)); Assert.Null(plan); } [Fact] public void AllPeasSplitsTheFirstProfiledPeaBelowTheComponentThreshold() { CraftingPlan plan = Assert.IsType( CraftingPlanner.PlanPeaSplit( [ Item(1, "Splitting Tool"), Item(2, "Brimstone Pea"), ], new HashSet(StringComparer.OrdinalIgnoreCase) { CraftingPlanner.AllPeas, }, minimumComponentCount: 20)); Assert.Equal("Brimstone", plan.Recipe.ResultItem); Assert.Equal(1u, plan.FirstObjectId); Assert.Equal(2u, plan.SecondObjectId); } [Fact] public void PeaSplitStopsAtTheRequestedComponentCount() { PluginInventoryItem brimstone = Item(3, "Brimstone") with { StackSize = 20, }; Assert.Null(CraftingPlanner.PlanPeaSplit( [Item(1, "Splitting Tool"), Item(2, "Brimstone Pea"), brimstone], new HashSet(StringComparer.OrdinalIgnoreCase) { "Brimstone Pea", }, minimumComponentCount: 20)); } [Fact] public void SameInputRecipeStagesAnExactOneUnitSplitBeforeApplying() { PluginInventoryItem oil = Item(1, "Chorizite Oil") with { StackSize = 2, }; CraftingPlan plan = Assert.IsType(CraftingPlanner.Plan( [oil], ["Strong Chorizite Oil"], new Character(trainedSkill: 0u))); Assert.True(plan.RequiresSplitFirstStack); Assert.Equal(1u, plan.FirstObjectId); Assert.Equal(1u, plan.SplitContainerObjectId); Assert.Equal(0u, plan.SecondObjectId); } [Fact] public void ControllerWaitsForSplitReceiptAndBothPublishedStacksBeforeApplying() { var automation = new Automation { Inventory = [Item(20, "Chorizite Oil") with { ContainerObjectId = 1u, StackSize = 2, }], }; var settings = new InventorySettings { AutoCraftItems = true, SplitPeas = false, }; var profiles = new CombatSettings(); profiles.ConsumableNames.Add("Strong Chorizite Oil"); var controller = new CraftingController( new Host(automation), settings, profiles); Assert.True(controller.Tick(0d, canAct: true)); Assert.Equal([(20u, 1u, 1u)], automation.Moves); Assert.Empty(automation.Applies); automation.InventoryCompletion = new PluginInventoryCompletion( 1, PluginInventoryCommandKind.SplitToContainer, 20u, 0u); automation.Busy = false; Assert.True(controller.Tick(0.05d, canAct: true)); Assert.Empty(automation.Applies); Assert.Equal("AutoCraft waiting for split inventory", controller.Status); automation.Inventory = [ Item(20, "Chorizite Oil") with { ContainerObjectId = 1u, StackSize = 1, }, Item(21, "Chorizite Oil") with { ContainerObjectId = 1u, StackSize = 1, }, ]; Assert.True(controller.Tick(0.05d, canAct: true)); Assert.Equal([(20u, 21u)], automation.Applies); Assert.Equal("Crafting Strong Chorizite Oil", controller.Status); } [Fact] public void AmmunitionRequestCraftsEvenWhenGeneralAutoCraftIsDisabled() { var automation = new Automation { TrainedSkill = 37u, Inventory = [ Item(1u, "Wrapped Bundle of Deadly Fire Arrowheads"), Item(2u, "Wrapped Bundle of Arrowshafts"), ], }; var controller = new CraftingController( new Host(automation), new InventorySettings { AutoCraftItems = false }, new CombatSettings()); Assert.True(controller.Request("Deadly Fire Arrow")); Assert.Equal([(1u, 2u)], automation.Applies); Assert.Equal("Crafting Deadly Fire Arrow", controller.Status); } [Theory] [InlineData("Greater Stamina Kit", 0x00010000u, 0, (int)ConsumableCategory.StaminaKit)] [InlineData("Greater Mana Kit", 0x00010000u, 0, (int)ConsumableCategory.ManaKit)] [InlineData("Greater Healing Kit", 0x00010000u, 0, (int)ConsumableCategory.HealthKit)] [InlineData("Mana Food", 0u, 6, (int)ConsumableCategory.ManaFood)] [InlineData("Stamina Food", 0u, 4, (int)ConsumableCategory.StaminaFood)] [InlineData("Health Food", 0u, 2, (int)ConsumableCategory.HealthFood)] [InlineData("Intricate Lockpick", 0x00020000u, 0, (int)ConsumableCategory.Lockpick)] public void ConsumableClassificationUsesRetailPropertiesAndExactKitExceptions( string name, uint publicFlags, int boosterVital, int expected) { PluginInventoryItem item = Item(1u, name) with { PublicFlags = publicFlags, BoosterVital = boosterVital, }; Assert.Equal((ConsumableCategory)expected, ConsumableClassifier.Classify(item)); } [Fact] public void IdleCraftingRestocksTheConfiguredConsumableCategoryCount() { var automation = new Automation { TrainedSkill = 21u, Inventory = [ Item(1u, "Soft Bandages"), Item(2u, "Combined Hyssop and Mandrake"), Item(3u, "Plentiful Healing Kit"), ], }; var settings = new InventorySettings { AutoCraftItems = true, SplitPeas = false, IdleHealthKitCount = 2, }; var profiles = new CombatSettings(); profiles.ConsumableNames.Add("Plentiful Healing Kit"); profiles.ConsumableCategories["Plentiful Healing Kit"] = ConsumableCategory.HealthKit; var controller = new CraftingController( new Host(automation), settings, profiles); Assert.True(controller.TickIdle(0d, canAct: true)); Assert.Equal([(1u, 2u)], automation.Applies); Assert.Equal("Crafting Plentiful Healing Kit", controller.Status); } private static PluginInventoryItem Item(uint id, string name) => new( id, 0u, name, 0x80u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 1, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0); private sealed class Character(uint trainedSkill) : ICharacterInfo { public bool IsInWorld => true; public uint ObjectId => 1u; public uint CurrentHealth => 0; public uint MaxHealth => 0; public uint CurrentStamina => 0; public uint MaxStamina => 0; public uint CurrentMana => 0; public uint MaxMana => 0; public IReadOnlyList Skills => trainedSkill == 0u ? [] : [new(trainedSkill, "Craft", PluginSkillTraining.Trained, 300)]; public IReadOnlyList Attributes => []; public IReadOnlyList ActiveEnchantments => []; public bool TryGetSkill(uint skillId, out PluginSkillInfo skill) { if (skillId == trainedSkill && trainedSkill != 0u) { skill = Skills[0]; return true; } skill = default; return false; } } private sealed class Automation : IAutomationSurface, ICharacterInfo, IItemAutomation { public bool IsAvailable => true; public ICharacterInfo Character => this; public ISpellCatalog Spells => NoOpAutomationSurface.Instance; public IMagicCommands Magic => NoOpAutomationSurface.Instance; public IPluginChat Chat => NoOpAutomationSurface.Instance; public IItemAutomation Items => this; public bool IsInWorld => true; public uint ObjectId => 1u; public uint CurrentHealth => 0u; public uint MaxHealth => 0u; public uint CurrentStamina => 0u; public uint MaxStamina => 0u; public uint CurrentMana => 0u; public uint MaxMana => 0u; public uint TrainedSkill { get; set; } public IReadOnlyList Skills => TrainedSkill == 0u ? [] : [new(TrainedSkill, "Craft", PluginSkillTraining.Trained, 300)]; public IReadOnlyList Attributes => []; public IReadOnlyList ActiveEnchantments => []; public bool IsBusy => Busy; public bool Busy { get; set; } public PluginInventoryCompletion InventoryCompletion { get; set; } public PluginInventoryCompletion LastInventoryCompletion => InventoryCompletion; public IReadOnlyList Inventory { get; set; } = []; public List<(uint Source, uint Container, uint Amount)> Moves { get; } = []; public List<(uint Source, uint Target)> Applies { get; } = []; public IReadOnlyList CaptureOwnedItems() => Inventory; public PluginItemCommandResult MoveToContainer( uint objectId, uint containerObjectId, uint amount = 0u, int placement = 0) { Moves.Add((objectId, containerObjectId, amount)); Busy = true; return new(PluginItemCommandStatus.Started); } public PluginItemCommandResult Apply(uint objectId, uint targetObjectId) { Applies.Add((objectId, targetObjectId)); Busy = true; return new(PluginItemCommandStatus.Started); } public bool TryGetSkill(uint skillId, out PluginSkillInfo skill) { if (skillId == TrainedSkill && TrainedSkill != 0u) { skill = Skills[0]; return true; } skill = default; return false; } } private sealed class Host(IAutomationSurface automation) : IPluginHost { public bool HasUi => false; public IPluginLogger Log => Logger.Instance; public IGameState State => EmptyState.Instance; public IEvents Events => EmptyEvents.Instance; public ISelectionService Selection => EmptySelection.Instance; public IUiRegistry Ui => NoOpUiRegistry.Instance; public IAutomationSurface Automation { get; } = automation; } private sealed class Logger : IPluginLogger { public static Logger Instance { get; } = new(); public void Info(string message) { } public void Warn(string message) { } public void Error(string message, Exception? exception = null) { } } private sealed class EmptyState : IGameState { public static EmptyState Instance { get; } = new(); public IReadOnlyList Entities => []; } private sealed class EmptyEvents : IEvents { public static EmptyEvents Instance { get; } = new(); public event Action EntitySpawned { add { } remove { } } public event Action Tick { add { } remove { } } } private sealed class EmptySelection : ISelectionService { public static EmptySelection Instance { get; } = new(); public uint? SelectedObjectId => null; public uint? PreviousObjectId => null; public event Action Changed { add { } remove { } } public bool Select(uint objectId) => false; public bool Clear() => false; } }