diff --git a/Benchmark/Craftimizer.Benchmark.csproj b/Benchmark/Craftimizer.Benchmark.csproj new file mode 100644 index 0000000..79298c0 --- /dev/null +++ b/Benchmark/Craftimizer.Benchmark.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + Exe + enable + enable + + + + + + + diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs new file mode 100644 index 0000000..9878759 --- /dev/null +++ b/Benchmark/Program.cs @@ -0,0 +1,32 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; + +namespace Craftimizer.Benchmark; + +internal class Program +{ + private static void Main(string[] args) + { + var input = new SimulationInput() + { + Stats = new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 }, + Recipe = new RecipeInfo() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 640, + ConditionsFlag = 15, + MaxDurability = 70, + MaxQuality = 14040, + MaxProgress = 6600, + QualityModifier = 70, + QualityDivider = 115, + ProgressModifier = 80, + ProgressDivider = 130, + } + }; + + var actions = new List(); + (actions, _) = Solver.Crafty.Solver.SearchStepwise(input, actions, a => Console.WriteLine(a)); + } +} diff --git a/Craftimizer.sln b/Craftimizer.sln index 6075d3c..3f50abb 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -1,19 +1,59 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer", "Craftimizer\Craftimizer.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Plugin", "Craftimizer\Craftimizer.Plugin.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Benchmark", "Benchmark\Craftimizer.Benchmark.csproj", "{057C4B64-4D99-4847-9BCF-966571CAE57C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Simulator", "Simulator\Craftimizer.Simulator.csproj", "{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solver\Craftimizer.Solver.csproj", "{2B0EA452-6DFC-48DB-9049-EA782E600C21}" + ProjectSection(ProjectDependencies) = postProject + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|Any CPU + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|Any CPU + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|Any CPU + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Craftimizer/Plugin/Configuration.cs b/Craftimizer/Configuration.cs similarity index 100% rename from Craftimizer/Plugin/Configuration.cs rename to Craftimizer/Configuration.cs diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.Plugin.csproj similarity index 96% rename from Craftimizer/Craftimizer.csproj rename to Craftimizer/Craftimizer.Plugin.csproj index 813cde2..99d8a00 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.Plugin.csproj @@ -31,6 +31,7 @@ + $(DalamudLibPath)FFXIVClientStructs.dll false diff --git a/Craftimizer/Craftimizer.json b/Craftimizer/Craftimizer.Plugin.json similarity index 100% rename from Craftimizer/Craftimizer.json rename to Craftimizer/Craftimizer.Plugin.json diff --git a/Craftimizer/Plugin/Icons.cs b/Craftimizer/Icons.cs similarity index 100% rename from Craftimizer/Plugin/Icons.cs rename to Craftimizer/Icons.cs diff --git a/Craftimizer/Plugin/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs similarity index 100% rename from Craftimizer/Plugin/ImGuiUtils.cs rename to Craftimizer/ImGuiUtils.cs diff --git a/Craftimizer/Plugin/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs similarity index 100% rename from Craftimizer/Plugin/LuminaSheets.cs rename to Craftimizer/LuminaSheets.cs diff --git a/Craftimizer/Plugin/Plugin.cs b/Craftimizer/Plugin.cs similarity index 100% rename from Craftimizer/Plugin/Plugin.cs rename to Craftimizer/Plugin.cs diff --git a/Craftimizer/Plugin/Service.cs b/Craftimizer/Service.cs similarity index 100% rename from Craftimizer/Plugin/Service.cs rename to Craftimizer/Service.cs diff --git a/Craftimizer/Simulator/Actions/ActionType.cs b/Craftimizer/Simulator/Actions/ActionType.cs deleted file mode 100644 index 98f619e..0000000 --- a/Craftimizer/Simulator/Actions/ActionType.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Craftimizer.Plugin; -using Dalamud.Utility; -using ImGuiScene; -using Lumina.Excel.GeneratedSheets; -using System; -using System.Linq; -using Action = Lumina.Excel.GeneratedSheets.Action; - -namespace Craftimizer.Simulator.Actions; - -public enum ActionType -{ - AdvancedTouch, - BasicSynthesis, - BasicTouch, - ByregotsBlessing, - CarefulObservation, - CarefulSynthesis, - DelicateSynthesis, - FinalAppraisal, - FocusedSynthesis, - FocusedTouch, - GreatStrides, - Groundwork, - HastyTouch, - HeartAndSoul, - Innovation, - IntensiveSynthesis, - Manipulation, - MastersMend, - MuscleMemory, - Observe, - PreciseTouch, - PreparatoryTouch, - PrudentSynthesis, - PrudentTouch, - RapidSynthesis, - Reflect, - StandardTouch, - TrainedEye, - TrainedFinesse, - TricksOfTheTrade, - Veneration, - WasteNot, - WasteNot2, -} - -internal static class ActionUtils -{ - private static readonly BaseAction[] Actions; - - static ActionUtils() - { - var types = typeof(BaseAction).Assembly.GetTypes() - .Where(t => t.IsAssignableTo(typeof(BaseAction)) && !t.IsAbstract); - Actions = Enum.GetNames() - .Select(a => types.First(t => t.Name == a)) - .Select(t => (Activator.CreateInstance(t) as BaseAction)!) - .ToArray(); - } - - private static BaseAction Action(this ActionType me) => Actions[(int)me]; - - public static BaseAction With(this ActionType me, SimulationState simulation) - { - BaseAction.TLSSimulation.Value = simulation; - return Action(me); - } - - public static int Level(this ActionType me) => - Action(me).Level; - - public static ActionCategory Category(this ActionType me) => - Action(me).Category; - - private static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) - { - var actionId = Action(me).ActionId; - if (LuminaSheets.CraftActionSheet.GetRow(actionId) is CraftAction baseCraftAction) - { - return (classJob switch - { - ClassJob.Carpenter => baseCraftAction.CRP.Value!, - ClassJob.Blacksmith => baseCraftAction.BSM.Value!, - ClassJob.Armorer => baseCraftAction.ARM.Value!, - ClassJob.Goldsmith => baseCraftAction.GSM.Value!, - ClassJob.Leatherworker => baseCraftAction.LTW.Value!, - ClassJob.Weaver => baseCraftAction.WVR.Value!, - ClassJob.Alchemist => baseCraftAction.ALC.Value!, - ClassJob.Culinarian => baseCraftAction.CUL.Value!, - _ => baseCraftAction - }, null); - } - else if (LuminaSheets.ActionSheet.GetRow(actionId) is Action baseAction) - { - return (null, - LuminaSheets.ActionSheet.First(r => - r.Icon == baseAction.Icon && - r.ActionCategory.Row == baseAction.ActionCategory.Row && - r.Name.RawString == baseAction.Name.RawString && - (r.ClassJobCategory.Value?.IsClassJob(classJob) ?? false) - )); - } - return (null, null); - } - - public static string GetName(this ActionType me, ClassJob classJob) - { - var (craftAction, action) = GetActionRow(me, classJob); - if (craftAction != null) - return craftAction.Name.ToDalamudString().TextValue; - else if (action != null) - return action.Name.ToDalamudString().TextValue; - return "Unknown"; - } - - public static TextureWrap GetIcon(this ActionType me, ClassJob classJob) - { - var (craftAction, action) = GetActionRow(me, classJob); - if (craftAction != null) - return Icons.GetIconFromId(craftAction.Icon); - else if (action != null) - return Icons.GetIconFromId(action.Icon); - // Old "Steady Hand" action icon - return Icons.GetIconFromId(1953); - } -} diff --git a/Craftimizer/Simulator/ClassJob.cs b/Craftimizer/Simulator/ClassJob.cs deleted file mode 100644 index 7b92b17..0000000 --- a/Craftimizer/Simulator/ClassJob.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Lumina.Excel.GeneratedSheets; - -namespace Craftimizer.Simulator; - -public enum ClassJob -{ - Carpenter, - Blacksmith, - Armorer, - Goldsmith, - Leatherworker, - Weaver, - Alchemist, - Culinarian -} - -internal static class ClassJobExtensions -{ - public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => - classJob switch - { - ClassJob.Carpenter => me.CRP, - ClassJob.Blacksmith => me.BSM, - ClassJob.Armorer => me.ARM, - ClassJob.Goldsmith => me.GSM, - ClassJob.Leatherworker => me.LTW, - ClassJob.Weaver => me.WVR, - ClassJob.Alchemist => me.ALC, - ClassJob.Culinarian => me.CUL, - _ => false - }; -} diff --git a/Craftimizer/Simulator/Condition.cs b/Craftimizer/Simulator/Condition.cs deleted file mode 100644 index 28d1159..0000000 --- a/Craftimizer/Simulator/Condition.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Craftimizer.Plugin; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Utility; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Craftimizer.Simulator; - -public enum Condition : ushort -{ - Poor = 0x0008, - Normal = 0x0001, - Good = 0x0002, - Excellent = 0x0004, - - Centered = 0x0010, - Sturdy = 0x0020, - Pliant = 0x0040, - Malleable = 0x0080, - Primed = 0x0100, - GoodOmen = 0x0200, -} - -internal static class ConditionUtils -{ - public static Condition[] GetPossibleConditions(ushort conditionsFlag) => - Enum.GetValues().Where(c => ((Condition)conditionsFlag).HasFlag(c)).ToArray(); - - public static (uint Name, uint Description) AddonIds(this Condition me) => - me switch - { - Condition.Poor => (229, 14203), - Condition.Normal => (226, 14200), - Condition.Good => (227, 14201), - Condition.Excellent => (228, 14202), - Condition.Centered => (239, 14204), - Condition.Sturdy => (240, 14205), - Condition.Pliant => (241, 14206), - Condition.Malleable => (13455, 14208), - Condition.Primed => (13454, 14207), - Condition.GoodOmen => (14214, 14215), - _ => (226, 14200) // Unknown - }; - - public static string Name(this Condition me) => - LuminaSheets.AddonSheet.GetRow(me.AddonIds().Name)!.Text.ToDalamudString().TextValue; - - public static string Description(this Condition me, bool isRelic) - { - var text = LuminaSheets.AddonSheet.GetRow(me.AddonIds().Description)!.Text.ToDalamudString(); - for (var i = 0; i < text.Payloads.Count; ++i) - if (text.Payloads[i] is RawPayload) - text.Payloads[i] = new TextPayload(isRelic ? "1.75" : "1.5"); - return text.TextValue; - } - -} diff --git a/Craftimizer/Simulator/Effect.cs b/Craftimizer/Simulator/Effect.cs deleted file mode 100644 index 7ff5a62..0000000 --- a/Craftimizer/Simulator/Effect.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Craftimizer.Plugin; -using Dalamud.Utility; -using ImGuiScene; -using System; -using System.Text; - -namespace Craftimizer.Simulator; - -public record Effect -{ - public EffectType Type { get; init; } - public int? Duration { get; set; } - public int? Strength { get; set; } - - public ushort IconId { get - { - var status = Type.Status(); - uint iconId = status.Icon; - if (status.MaxStacks != 0 && Strength != null) - iconId += (uint)Math.Clamp(Strength.Value, 1, status.MaxStacks) - 1; - return (ushort)iconId; - } - } - - public TextureWrap Icon => Icons.GetIconFromId(IconId); - - public string Tooltip { get - { - var status = Type.Status(); - var name = new StringBuilder(); - name.Append(status.Name.ToDalamudString().TextValue); - if (status.MaxStacks != 0 && Strength != null) - name.Append($" {Strength}"); - if (!status.IsPermanent && Duration != null) - name.Append($" > {Duration}"); - return name.ToString(); - } - } -} diff --git a/Craftimizer/Simulator/EffectType.cs b/Craftimizer/Simulator/EffectType.cs deleted file mode 100644 index 674b8a8..0000000 --- a/Craftimizer/Simulator/EffectType.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Craftimizer.Plugin; -using Lumina.Excel.GeneratedSheets; - -namespace Craftimizer.Simulator; - -public enum EffectType -{ - InnerQuiet, - WasteNot, - Veneration, - GreatStrides, - Innovation, - FinalAppraisal, - WasteNot2, - MuscleMemory, - Manipulation, - HeartAndSoul, -} - -internal static class EffectExtensions -{ - public static uint StatusId(this EffectType me) => - me switch - { - EffectType.InnerQuiet => 251, - EffectType.WasteNot => 252, - EffectType.Veneration => 2226, - EffectType.GreatStrides => 254, - EffectType.Innovation => 2189, - EffectType.FinalAppraisal => 2190, - EffectType.WasteNot2 => 257, - EffectType.MuscleMemory => 2191, - EffectType.Manipulation => 258, - EffectType.HeartAndSoul => 2665, - _ => 3412, - }; - - public static Status Status(this EffectType me) => - LuminaSheets.StatusSheet.GetRow(me.StatusId())!; -} diff --git a/Craftimizer/Simulator/SimulationInput.cs b/Craftimizer/Simulator/SimulationInput.cs deleted file mode 100644 index 62f7bde..0000000 --- a/Craftimizer/Simulator/SimulationInput.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Lumina.Excel.GeneratedSheets; -using System; - -namespace Craftimizer.Simulator; - -public readonly record struct SimulationInput -{ - public CharacterStats Stats { get; init; } - public Recipe Recipe { get; init; } - public Random Random { get; init; } - - public RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!; - public int RLvl => (int)RecipeTable.RowId; - public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(RecipeTable.ConditionsFlag); - - public int MaxDurability => RecipeTable.Durability * Recipe.DurabilityFactor / 100; - public int MaxQuality => (int)RecipeTable.Quality * Recipe.QualityFactor / 100; - public int MaxProgress => RecipeTable.Difficulty * Recipe.DifficultyFactor / 100; -} diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs new file mode 100644 index 0000000..1bb09c2 --- /dev/null +++ b/Craftimizer/SimulatorUtils.cs @@ -0,0 +1,164 @@ +using ImGuiScene; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using System.Linq; +using Craftimizer.Simulator.Actions; +using Action = Lumina.Excel.GeneratedSheets.Action; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using System; +using ClassJob = Craftimizer.Simulator.ClassJob; +using Condition = Craftimizer.Simulator.Condition; +using Craftimizer.Simulator; +using System.Text; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Plugin; + +internal static class ActionUtils +{ + private static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) + { + var actionId = me.WithUnsafe().ActionId; + if (LuminaSheets.CraftActionSheet.GetRow(actionId) is CraftAction baseCraftAction) + { + return (classJob switch + { + ClassJob.Carpenter => baseCraftAction.CRP.Value!, + ClassJob.Blacksmith => baseCraftAction.BSM.Value!, + ClassJob.Armorer => baseCraftAction.ARM.Value!, + ClassJob.Goldsmith => baseCraftAction.GSM.Value!, + ClassJob.Leatherworker => baseCraftAction.LTW.Value!, + ClassJob.Weaver => baseCraftAction.WVR.Value!, + ClassJob.Alchemist => baseCraftAction.ALC.Value!, + ClassJob.Culinarian => baseCraftAction.CUL.Value!, + _ => baseCraftAction + }, null); + } + else if (LuminaSheets.ActionSheet.GetRow(actionId) is Action baseAction) + { + return (null, + LuminaSheets.ActionSheet.First(r => + r.Icon == baseAction.Icon && + r.ActionCategory.Row == baseAction.ActionCategory.Row && + r.Name.RawString == baseAction.Name.RawString && + (r.ClassJobCategory.Value?.IsClassJob(classJob) ?? false) + )); + } + return (null, null); + } + + public static string GetName(this ActionType me, ClassJob classJob) + { + var (craftAction, action) = GetActionRow(me, classJob); + if (craftAction != null) + return craftAction.Name.ToDalamudString().TextValue; + else if (action != null) + return action.Name.ToDalamudString().TextValue; + return "Unknown"; + } + + public static TextureWrap GetIcon(this ActionType me, ClassJob classJob) + { + var (craftAction, action) = GetActionRow(me, classJob); + if (craftAction != null) + return Icons.GetIconFromId(craftAction.Icon); + else if (action != null) + return Icons.GetIconFromId(action.Icon); + // Old "Steady Hand" action icon + return Icons.GetIconFromId(1953); + } +} + +internal static class ClassJobExtensions +{ + public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => + classJob switch + { + ClassJob.Carpenter => me.CRP, + ClassJob.Blacksmith => me.BSM, + ClassJob.Armorer => me.ARM, + ClassJob.Goldsmith => me.GSM, + ClassJob.Leatherworker => me.LTW, + ClassJob.Weaver => me.WVR, + ClassJob.Alchemist => me.ALC, + ClassJob.Culinarian => me.CUL, + _ => false + }; +} + +internal static class ConditionUtils +{ + public static (uint Name, uint Description) AddonIds(this Condition me) => + me switch + { + Condition.Poor => (229, 14203), + Condition.Normal => (226, 14200), + Condition.Good => (227, 14201), + Condition.Excellent => (228, 14202), + Condition.Centered => (239, 14204), + Condition.Sturdy => (240, 14205), + Condition.Pliant => (241, 14206), + Condition.Malleable => (13455, 14208), + Condition.Primed => (13454, 14207), + Condition.GoodOmen => (14214, 14215), + _ => (226, 14200) // Unknown + }; + + public static string Name(this Condition me) => + LuminaSheets.AddonSheet.GetRow(me.AddonIds().Name)!.Text.ToDalamudString().TextValue; + + public static string Description(this Condition me, bool isRelic) + { + var text = LuminaSheets.AddonSheet.GetRow(me.AddonIds().Description)!.Text.ToDalamudString(); + for (var i = 0; i < text.Payloads.Count; ++i) + if (text.Payloads[i] is RawPayload) + text.Payloads[i] = new TextPayload(isRelic ? "1.75" : "1.5"); + return text.TextValue; + } +} + +internal static class EffectExtensions +{ + public static uint StatusId(this EffectType me) => + me switch + { + EffectType.InnerQuiet => 251, + EffectType.WasteNot => 252, + EffectType.Veneration => 2226, + EffectType.GreatStrides => 254, + EffectType.Innovation => 2189, + EffectType.FinalAppraisal => 2190, + EffectType.WasteNot2 => 257, + EffectType.MuscleMemory => 2191, + EffectType.Manipulation => 258, + EffectType.HeartAndSoul => 2665, + _ => 3412, + }; + + public static Status Status(this EffectType me) => + LuminaSheets.StatusSheet.GetRow(me.StatusId())!; + + public static ushort GetIconId(this Effect me) + { + var status = me.Type.Status(); + uint iconId = status.Icon; + if (status.MaxStacks != 0 && me.Strength != null) + iconId += (uint)Math.Clamp(me.Strength!.Value, 1, status.MaxStacks) - 1; + return (ushort)iconId; + } + + public static TextureWrap GetIcon(this Effect me) => + Icons.GetIconFromId(me.GetIconId()); + + public static string GetTooltip(this Effect me) + { + var status = me.Type.Status(); + var name = new StringBuilder(); + name.Append(status.Name.ToDalamudString().TextValue); + if (status.MaxStacks != 0 && me.Strength != null) + name.Append($" {me.Strength}"); + if (!status.IsPermanent && me.Duration != null) + name.Append($" > {me.Duration}"); + return name.ToString(); + } +} diff --git a/Craftimizer/Plugin/SimulatorWindow.cs b/Craftimizer/SimulatorWindow.cs similarity index 55% rename from Craftimizer/Plugin/SimulatorWindow.cs rename to Craftimizer/SimulatorWindow.cs index 0f3d9bb..cf42356 100644 --- a/Craftimizer/Plugin/SimulatorWindow.cs +++ b/Craftimizer/SimulatorWindow.cs @@ -3,15 +3,18 @@ using Craftimizer.Simulator.Actions; using Dalamud.Interface; using Dalamud.Interface.Windowing; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using System; using System.Linq; using System.Numerics; +using ClassJob = Craftimizer.Simulator.ClassJob; namespace Craftimizer.Plugin; public class SimulatorWindow : Window { - public SimulationState Simulation { get; } + public Simulator.Simulator Simulation { get; } + private SimulationState State { get; set; } private bool showOnlyGuaranteedActions = true; @@ -23,13 +26,37 @@ public class SimulatorWindow : Window MaximumSize = new Vector2(float.MaxValue, float.MaxValue) }; - Simulation = new(new() + State = new(new() { - Stats = new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 }, - Recipe = LuminaSheets.RecipeSheet.GetRow(35499)! + Stats = new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90, CLvl = CalculateCLvl(90) }, + Recipe = CreateRecipeInfo(LuminaSheets.RecipeSheet.GetRow(35499)!) }); + Simulation = new(State); } + private static RecipeInfo CreateRecipeInfo(Recipe recipe) + { + var recipeTable = recipe.RecipeLevelTable.Value!; + return new() { + IsExpert = recipe.IsExpert, + ClassJobLevel = recipeTable.ClassJobLevel, + RLvl = (int)recipeTable.RowId, + ConditionsFlag = recipeTable.ConditionsFlag, + MaxDurability = recipeTable.Durability * recipe.DurabilityFactor / 100, + MaxQuality = (int)recipeTable.Quality * recipe.QualityFactor / 100, + MaxProgress = recipeTable.Difficulty * recipe.DifficultyFactor / 100, + QualityModifier = recipeTable.QualityModifier, + QualityDivider = recipeTable.QualityDivider, + ProgressModifier = recipeTable.ProgressModifier, + ProgressDivider = recipeTable.ProgressDivider, + }; + } + + private static int CalculateCLvl(int characterLevel) => + characterLevel <= 80 + ? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel + : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; + public override void Draw() { ImGui.BeginTable("CraftimizerTable", 2, ImGuiTableFlags.Resizable); @@ -38,7 +65,7 @@ public class SimulatorWindow : Window ImGui.BeginChild("CraftimizerActions", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); ImGui.Checkbox("Show only guaranteed actions", ref showOnlyGuaranteedActions); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - foreach(var category in Enum.GetValues().GroupBy(a => a.Category())) + foreach (var category in Enum.GetValues().GroupBy(a => a.Category())) { var i = 0; ImGuiUtils.BeginGroupPanel(category.Key.GetDisplayName()); @@ -48,9 +75,9 @@ public class SimulatorWindow : Window if (showOnlyGuaranteedActions && !baseAction.IsGuaranteedAction) continue; - ImGui.BeginDisabled(!baseAction.CanUse); + ImGui.BeginDisabled(!baseAction.CanUse || Simulation.IsComplete); if (ImGui.ImageButton(action.GetIcon(ClassJob.Carpenter).ImGuiHandle, new Vector2(ImGui.GetFontSize() * 2))) - Simulation.Execute(action); + (_, State) = Simulation.Execute(State, action); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip($"{action.GetName(ClassJob.Carpenter)}\n{baseAction.GetTooltip(true)}"); ImGui.EndDisabled(); @@ -63,38 +90,38 @@ public class SimulatorWindow : Window ImGui.EndChild(); ImGui.TableNextColumn(); ImGui.BeginChild("CraftimizerSimulator", Vector2.Zero, true, ImGuiWindowFlags.NoDecoration); - ImGui.Text($"Step {Simulation.StepCount + 1}"); - ImGui.Text(Simulation.Condition.Name()); + ImGui.Text($"Step {State.StepCount + 1}"); + ImGui.Text(State.Condition.Name()); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(Simulation.Condition.Description(Simulation.Input.Stats.HasRelic)); - ImGui.Text($"{Simulation.HQPercent}%% HQ"); + ImGui.SetTooltip(State.Condition.Description(State.Input.Stats.HasRelic)); + ImGui.Text($"{State.HQPercent}%% HQ"); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Min((float)Simulation.Progress / Simulation.Input.MaxProgress, 1f), new Vector2(200, 20), $"{Simulation.Progress} / {Simulation.Input.MaxProgress}"); + ImGui.ProgressBar(Math.Min((float)State.Progress / State.Input.Recipe.MaxProgress, 1f), new Vector2(200, 20), $"{State.Progress} / {State.Input.Recipe.MaxProgress}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Min((float)Simulation.Quality / Simulation.Input.MaxQuality, 1f), new Vector2(200, 20), $"{Simulation.Quality} / {Simulation.Input.MaxQuality}"); + ImGui.ProgressBar(Math.Min((float)State.Quality / State.Input.Recipe.MaxQuality, 1f), new Vector2(200, 20), $"{State.Quality} / {State.Input.Recipe.MaxQuality}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)Simulation.Durability / Simulation.Input.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{Simulation.Durability} / {Simulation.Input.MaxDurability}"); + ImGui.ProgressBar(Math.Clamp((float)State.Durability / State.Input.Recipe.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{State.Durability} / {State.Input.Recipe.MaxDurability}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)Simulation.CP / Simulation.Input.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{Simulation.CP} / {Simulation.Input.Stats.CP}"); + ImGui.ProgressBar(Math.Clamp((float)State.CP / State.Input.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{State.CP} / {State.Input.Stats.CP}"); ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(5); ImGui.Text($"Effects:"); - foreach (var effect in Simulation.ActiveEffects) + foreach (var effect in State.ActiveEffects) { - var icon = effect.Icon; + var icon = effect.GetIcon(); var h = ImGui.GetFontSize() * 1.25f; var w = icon.Width * h / icon.Height; ImGui.Image(icon.ImGuiHandle, new Vector2(w, h)); ImGui.SameLine(); - ImGui.Text(effect.Tooltip); + ImGui.Text(effect.GetTooltip()); } ImGuiHelpers.ScaledDummy(5); { var i = 0; - foreach (var action in Simulation.ActionHistory) + foreach (var action in State.ActionHistory) { var baseAction = action.With(Simulation); ImGui.Image(action.GetIcon(ClassJob.Carpenter).ImGuiHandle, new Vector2(ImGui.GetFontSize() * 2f)); diff --git a/Craftimizer/packages.lock.json b/Craftimizer/packages.lock.json index 2426061..3544bdf 100644 --- a/Craftimizer/packages.lock.json +++ b/Craftimizer/packages.lock.json @@ -7,6 +7,9 @@ "requested": "[2.1.10, )", "resolved": "2.1.10", "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" + }, + "craftimizer.simulator": { + "type": "Project" } } } diff --git a/Craftimizer/Simulator/ActionCategory.cs b/Simulator/ActionCategory.cs similarity index 93% rename from Craftimizer/Simulator/ActionCategory.cs rename to Simulator/ActionCategory.cs index 5aeb8c6..3bb2632 100644 --- a/Craftimizer/Simulator/ActionCategory.cs +++ b/Simulator/ActionCategory.cs @@ -10,7 +10,7 @@ public enum ActionCategory Other } -internal static class ActionCategoryUtils +public static class ActionCategoryUtils { public static string GetDisplayName(this ActionCategory category) => category switch diff --git a/Craftimizer/Simulator/ActionResponse.cs b/Simulator/ActionResponse.cs similarity index 80% rename from Craftimizer/Simulator/ActionResponse.cs rename to Simulator/ActionResponse.cs index d71d4b2..b01a88d 100644 --- a/Craftimizer/Simulator/ActionResponse.cs +++ b/Simulator/ActionResponse.cs @@ -7,8 +7,5 @@ public enum ActionResponse NotEnoughCP, NoDurability, CannotUseAction, - UsedAction, - ProgressComplete, - NoMoreDurability, } diff --git a/Simulator/Actions/ActionType.cs b/Simulator/Actions/ActionType.cs new file mode 100644 index 0000000..b481ec8 --- /dev/null +++ b/Simulator/Actions/ActionType.cs @@ -0,0 +1,119 @@ +namespace Craftimizer.Simulator.Actions; + +public enum ActionType +{ + AdvancedTouch, + BasicSynthesis, + BasicTouch, + ByregotsBlessing, + CarefulObservation, + CarefulSynthesis, + DelicateSynthesis, + FinalAppraisal, + FocusedSynthesis, + FocusedTouch, + GreatStrides, + Groundwork, + HastyTouch, + HeartAndSoul, + Innovation, + IntensiveSynthesis, + Manipulation, + MastersMend, + MuscleMemory, + Observe, + PreciseTouch, + PreparatoryTouch, + PrudentSynthesis, + PrudentTouch, + RapidSynthesis, + Reflect, + StandardTouch, + TrainedEye, + TrainedFinesse, + TricksOfTheTrade, + Veneration, + WasteNot, + WasteNot2, +} + +public static class ActionUtils +{ + private static readonly BaseAction[] Actions; + + static ActionUtils() + { + var types = typeof(BaseAction).Assembly.GetTypes() + .Where(t => t.IsAssignableTo(typeof(BaseAction)) && !t.IsAbstract); + Actions = Enum.GetNames() + .Select(a => types.First(t => t.Name == a)) + .Select(t => (Activator.CreateInstance(t) as BaseAction)!) + .ToArray(); + } + + public static void SetSimulation(Simulator simulation) => + BaseAction.TLSSimulation.Value = simulation; + + public static BaseAction WithUnsafe(this ActionType me) => Actions[(int)me]; + + public static BaseAction With(this ActionType me, Simulator simulation) + { + SetSimulation(simulation); + return WithUnsafe(me); + } + + public static IEnumerable AvailableActions(Simulator simulation) + { + if (simulation.IsComplete) + return Enumerable.Empty(); + + SetSimulation(simulation); + return Enum.GetValues() + .Where(a => WithUnsafe(a).CanUse); + } + + public static int Level(this ActionType me) => + WithUnsafe(me).Level; + + public static ActionCategory Category(this ActionType me) => + WithUnsafe(me).Category; + + public static string IntName(this ActionType me) => + me switch + { + ActionType.AdvancedTouch => "Advanced Touch", + ActionType.BasicSynthesis => "Basic Synthesis", + ActionType.BasicTouch => "Basic Touch", + ActionType.ByregotsBlessing => "Byregot's Blessing", + ActionType.CarefulObservation => "Careful Observation", + ActionType.CarefulSynthesis => "Careful Synthesis", + ActionType.DelicateSynthesis => "Delicate Synthesis", + ActionType.FinalAppraisal => "Final Appraisal", + ActionType.FocusedSynthesis => "Focused Synthesis", + ActionType.FocusedTouch => "Focused Touch", + ActionType.GreatStrides => "Great Strides", + ActionType.Groundwork => "Groundwork", + ActionType.HastyTouch => "Hasty Touch", + ActionType.HeartAndSoul => "Heart And Soul", + ActionType.Innovation => "Innovation", + ActionType.IntensiveSynthesis => "Intensive Synthesis", + ActionType.Manipulation => "Manipulation", + ActionType.MastersMend => "Master's Mend", + ActionType.MuscleMemory => "Muscle Memory", + ActionType.Observe => "Observe", + ActionType.PreciseTouch => "Precise Touch", + ActionType.PreparatoryTouch => "Preparatory Touch", + ActionType.PrudentSynthesis => "Prudent Synthesis", + ActionType.PrudentTouch => "Prudent Touch", + ActionType.RapidSynthesis => "Rapid Synthesis", + ActionType.Reflect => "Reflect", + ActionType.StandardTouch => "Standard Touch", + ActionType.TrainedEye => "Trained Eye", + ActionType.TrainedFinesse => "Trained Finesse", + ActionType.TricksOfTheTrade => "Tricks Of The Trade", + ActionType.Veneration => "Veneration", + ActionType.WasteNot => "Waste Not", + ActionType.WasteNot2 => "Waste Not II", + _ => me.ToString(), + }; +} diff --git a/Craftimizer/Simulator/Actions/AdvancedTouch.cs b/Simulator/Actions/AdvancedTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/AdvancedTouch.cs rename to Simulator/Actions/AdvancedTouch.cs diff --git a/Craftimizer/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs similarity index 91% rename from Craftimizer/Simulator/Actions/BaseAction.cs rename to Simulator/Actions/BaseAction.cs index 05f3cd8..8e01da3 100644 --- a/Craftimizer/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -4,10 +4,10 @@ using System.Threading; namespace Craftimizer.Simulator.Actions; -internal abstract class BaseAction +public abstract class BaseAction { - internal static readonly ThreadLocal TLSSimulation = new(false); - protected static SimulationState Simulation => TLSSimulation.Value ?? throw new NullReferenceException(); + internal static readonly ThreadLocal TLSSimulation = new(false); + protected static Simulator Simulation => TLSSimulation.Value ?? throw new NullReferenceException(); public BaseAction() { } diff --git a/Craftimizer/Simulator/Actions/BaseBuffAction.cs b/Simulator/Actions/BaseBuffAction.cs similarity index 91% rename from Craftimizer/Simulator/Actions/BaseBuffAction.cs rename to Simulator/Actions/BaseBuffAction.cs index eccfa7c..5bc7de4 100644 --- a/Craftimizer/Simulator/Actions/BaseBuffAction.cs +++ b/Simulator/Actions/BaseBuffAction.cs @@ -1,4 +1,3 @@ -using System; using System.Text; namespace Craftimizer.Simulator.Actions; @@ -21,7 +20,7 @@ internal abstract class BaseBuffAction : BaseAction public override string GetTooltip(bool addUsability) { var builder = new StringBuilder(base.GetTooltip(addUsability)); - builder.AppendLine($"Effect: {Effect.Tooltip}"); + builder.AppendLine($"{Effect.Duration} Steps"); return builder.ToString(); } } diff --git a/Craftimizer/Simulator/Actions/BasicSynthesis.cs b/Simulator/Actions/BasicSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/BasicSynthesis.cs rename to Simulator/Actions/BasicSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/BasicTouch.cs b/Simulator/Actions/BasicTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/BasicTouch.cs rename to Simulator/Actions/BasicTouch.cs diff --git a/Craftimizer/Simulator/Actions/ByregotsBlessing.cs b/Simulator/Actions/ByregotsBlessing.cs similarity index 100% rename from Craftimizer/Simulator/Actions/ByregotsBlessing.cs rename to Simulator/Actions/ByregotsBlessing.cs diff --git a/Craftimizer/Simulator/Actions/CarefulObservation.cs b/Simulator/Actions/CarefulObservation.cs similarity index 100% rename from Craftimizer/Simulator/Actions/CarefulObservation.cs rename to Simulator/Actions/CarefulObservation.cs diff --git a/Craftimizer/Simulator/Actions/CarefulSynthesis.cs b/Simulator/Actions/CarefulSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/CarefulSynthesis.cs rename to Simulator/Actions/CarefulSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/DelicateSynthesis.cs b/Simulator/Actions/DelicateSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/DelicateSynthesis.cs rename to Simulator/Actions/DelicateSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/FinalAppraisal.cs b/Simulator/Actions/FinalAppraisal.cs similarity index 100% rename from Craftimizer/Simulator/Actions/FinalAppraisal.cs rename to Simulator/Actions/FinalAppraisal.cs diff --git a/Craftimizer/Simulator/Actions/FocusedSynthesis.cs b/Simulator/Actions/FocusedSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/FocusedSynthesis.cs rename to Simulator/Actions/FocusedSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/FocusedTouch.cs b/Simulator/Actions/FocusedTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/FocusedTouch.cs rename to Simulator/Actions/FocusedTouch.cs diff --git a/Craftimizer/Simulator/Actions/GreatStrides.cs b/Simulator/Actions/GreatStrides.cs similarity index 100% rename from Craftimizer/Simulator/Actions/GreatStrides.cs rename to Simulator/Actions/GreatStrides.cs diff --git a/Craftimizer/Simulator/Actions/Groundwork.cs b/Simulator/Actions/Groundwork.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Groundwork.cs rename to Simulator/Actions/Groundwork.cs diff --git a/Craftimizer/Simulator/Actions/HastyTouch.cs b/Simulator/Actions/HastyTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/HastyTouch.cs rename to Simulator/Actions/HastyTouch.cs diff --git a/Craftimizer/Simulator/Actions/HeartAndSoul.cs b/Simulator/Actions/HeartAndSoul.cs similarity index 100% rename from Craftimizer/Simulator/Actions/HeartAndSoul.cs rename to Simulator/Actions/HeartAndSoul.cs diff --git a/Craftimizer/Simulator/Actions/Innovation.cs b/Simulator/Actions/Innovation.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Innovation.cs rename to Simulator/Actions/Innovation.cs diff --git a/Craftimizer/Simulator/Actions/IntensiveSynthesis.cs b/Simulator/Actions/IntensiveSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/IntensiveSynthesis.cs rename to Simulator/Actions/IntensiveSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/Manipulation.cs b/Simulator/Actions/Manipulation.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Manipulation.cs rename to Simulator/Actions/Manipulation.cs diff --git a/Craftimizer/Simulator/Actions/MastersMend.cs b/Simulator/Actions/MastersMend.cs similarity index 100% rename from Craftimizer/Simulator/Actions/MastersMend.cs rename to Simulator/Actions/MastersMend.cs diff --git a/Craftimizer/Simulator/Actions/MuscleMemory.cs b/Simulator/Actions/MuscleMemory.cs similarity index 100% rename from Craftimizer/Simulator/Actions/MuscleMemory.cs rename to Simulator/Actions/MuscleMemory.cs diff --git a/Craftimizer/Simulator/Actions/Observe.cs b/Simulator/Actions/Observe.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Observe.cs rename to Simulator/Actions/Observe.cs diff --git a/Craftimizer/Simulator/Actions/PreciseTouch.cs b/Simulator/Actions/PreciseTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/PreciseTouch.cs rename to Simulator/Actions/PreciseTouch.cs diff --git a/Craftimizer/Simulator/Actions/PreparatoryTouch.cs b/Simulator/Actions/PreparatoryTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/PreparatoryTouch.cs rename to Simulator/Actions/PreparatoryTouch.cs diff --git a/Craftimizer/Simulator/Actions/PrudentSynthesis.cs b/Simulator/Actions/PrudentSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/PrudentSynthesis.cs rename to Simulator/Actions/PrudentSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/PrudentTouch.cs b/Simulator/Actions/PrudentTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/PrudentTouch.cs rename to Simulator/Actions/PrudentTouch.cs diff --git a/Craftimizer/Simulator/Actions/RapidSynthesis.cs b/Simulator/Actions/RapidSynthesis.cs similarity index 100% rename from Craftimizer/Simulator/Actions/RapidSynthesis.cs rename to Simulator/Actions/RapidSynthesis.cs diff --git a/Craftimizer/Simulator/Actions/Reflect.cs b/Simulator/Actions/Reflect.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Reflect.cs rename to Simulator/Actions/Reflect.cs diff --git a/Craftimizer/Simulator/Actions/StandardTouch.cs b/Simulator/Actions/StandardTouch.cs similarity index 100% rename from Craftimizer/Simulator/Actions/StandardTouch.cs rename to Simulator/Actions/StandardTouch.cs diff --git a/Craftimizer/Simulator/Actions/TrainedEye.cs b/Simulator/Actions/TrainedEye.cs similarity index 53% rename from Craftimizer/Simulator/Actions/TrainedEye.cs rename to Simulator/Actions/TrainedEye.cs index d2fa8bc..25a1df0 100644 --- a/Craftimizer/Simulator/Actions/TrainedEye.cs +++ b/Simulator/Actions/TrainedEye.cs @@ -9,8 +9,12 @@ internal class TrainedEye : BaseAction public override int CPCost => 250; public override bool IncreasesQuality => true; - public override bool CanUse => Simulation.IsFirstStep && base.CanUse; + public override bool CanUse => + Simulation.IsFirstStep && + !Simulation.Input.Recipe.IsExpert && + Simulation.Input.Stats.Level >= (Simulation.Input.Recipe.ClassJobLevel + 10) && + base.CanUse; public override void UseSuccess() => - Simulation.IncreaseQualityRaw(Simulation.Input.MaxQuality - Simulation.Quality); + Simulation.IncreaseQualityRaw(Simulation.Input.Recipe.MaxQuality - Simulation.Quality); } diff --git a/Craftimizer/Simulator/Actions/TrainedFinesse.cs b/Simulator/Actions/TrainedFinesse.cs similarity index 100% rename from Craftimizer/Simulator/Actions/TrainedFinesse.cs rename to Simulator/Actions/TrainedFinesse.cs diff --git a/Craftimizer/Simulator/Actions/TricksOfTheTrade.cs b/Simulator/Actions/TricksOfTheTrade.cs similarity index 100% rename from Craftimizer/Simulator/Actions/TricksOfTheTrade.cs rename to Simulator/Actions/TricksOfTheTrade.cs diff --git a/Craftimizer/Simulator/Actions/Veneration.cs b/Simulator/Actions/Veneration.cs similarity index 100% rename from Craftimizer/Simulator/Actions/Veneration.cs rename to Simulator/Actions/Veneration.cs diff --git a/Craftimizer/Simulator/Actions/WasteNot.cs b/Simulator/Actions/WasteNot.cs similarity index 100% rename from Craftimizer/Simulator/Actions/WasteNot.cs rename to Simulator/Actions/WasteNot.cs diff --git a/Craftimizer/Simulator/Actions/WasteNot2.cs b/Simulator/Actions/WasteNot2.cs similarity index 100% rename from Craftimizer/Simulator/Actions/WasteNot2.cs rename to Simulator/Actions/WasteNot2.cs diff --git a/Craftimizer/Simulator/CharacterStats.cs b/Simulator/CharacterStats.cs similarity index 54% rename from Craftimizer/Simulator/CharacterStats.cs rename to Simulator/CharacterStats.cs index afe5bb8..f216497 100644 --- a/Craftimizer/Simulator/CharacterStats.cs +++ b/Simulator/CharacterStats.cs @@ -1,6 +1,3 @@ -using System.Linq; -using Craftimizer.Plugin; - namespace Craftimizer.Simulator; public record CharacterStats @@ -11,8 +8,5 @@ public record CharacterStats public int Level { get; init; } public bool HasRelic { get; init; } public bool IsSpecialist { get; init; } - - public int CLvl => Level <= 80 - ? LuminaSheets.ParamGrowSheet.GetRow((uint)Level)!.CraftingLevel - : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == Level).RowId; + public int CLvl { get; init; } } diff --git a/Simulator/ClassJob.cs b/Simulator/ClassJob.cs new file mode 100644 index 0000000..31b6f40 --- /dev/null +++ b/Simulator/ClassJob.cs @@ -0,0 +1,13 @@ +namespace Craftimizer.Simulator; + +public enum ClassJob +{ + Carpenter, + Blacksmith, + Armorer, + Goldsmith, + Leatherworker, + Weaver, + Alchemist, + Culinarian +} diff --git a/Simulator/CompletionState.cs b/Simulator/CompletionState.cs new file mode 100644 index 0000000..84f2881 --- /dev/null +++ b/Simulator/CompletionState.cs @@ -0,0 +1,10 @@ +namespace Craftimizer.Simulator; + +public enum CompletionState +{ + Incomplete, + ProgressComplete, + NoMoreDurability, + + Other +} diff --git a/Simulator/Condition.cs b/Simulator/Condition.cs new file mode 100644 index 0000000..6ba2103 --- /dev/null +++ b/Simulator/Condition.cs @@ -0,0 +1,22 @@ +namespace Craftimizer.Simulator; + +public enum Condition : ushort +{ + Poor = 0x0008, + Normal = 0x0001, + Good = 0x0002, + Excellent = 0x0004, + + Centered = 0x0010, + Sturdy = 0x0020, + Pliant = 0x0040, + Malleable = 0x0080, + Primed = 0x0100, + GoodOmen = 0x0200, +} + +public static class ConditionUtils +{ + public static Condition[] GetPossibleConditions(ushort conditionsFlag) => + Enum.GetValues().Where(c => ((Condition)conditionsFlag).HasFlag(c)).ToArray(); +} diff --git a/Simulator/Craftimizer.Simulator.csproj b/Simulator/Craftimizer.Simulator.csproj new file mode 100644 index 0000000..cfadb03 --- /dev/null +++ b/Simulator/Craftimizer.Simulator.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/Simulator/Effect.cs b/Simulator/Effect.cs new file mode 100644 index 0000000..2384987 --- /dev/null +++ b/Simulator/Effect.cs @@ -0,0 +1,14 @@ +namespace Craftimizer.Simulator; + +public readonly record struct Effect +{ + public EffectType Type { get; init; } + public int? Duration { get; init; } + public int? Strength { get; init; } + + public bool HasDuration => Duration != null; + public bool HasStrength => Strength != null; + + public Effect DecrementDuration() => this with { Duration = Duration - 1 }; + public Effect IncrementStrength() => this with { Strength = Strength + 1 }; +} diff --git a/Simulator/EffectType.cs b/Simulator/EffectType.cs new file mode 100644 index 0000000..cf4ff72 --- /dev/null +++ b/Simulator/EffectType.cs @@ -0,0 +1,15 @@ +namespace Craftimizer.Simulator; + +public enum EffectType +{ + InnerQuiet, + WasteNot, + Veneration, + GreatStrides, + Innovation, + FinalAppraisal, + WasteNot2, + MuscleMemory, + Manipulation, + HeartAndSoul, +} diff --git a/Simulator/Recipe.cs b/Simulator/Recipe.cs new file mode 100644 index 0000000..c1562a2 --- /dev/null +++ b/Simulator/Recipe.cs @@ -0,0 +1,17 @@ +namespace Craftimizer.Simulator; + +public record RecipeInfo +{ + public bool IsExpert { get; init; } + public int ClassJobLevel { get; init; } + public int RLvl { get; init; } + public ushort ConditionsFlag { get; init; } + public int MaxDurability { get; init; } + public int MaxQuality { get; init; } + public int MaxProgress { get; init; } + + public int QualityModifier { get; init; } + public int QualityDivider { get; init; } + public int ProgressModifier { get; init; } + public int ProgressDivider { get; init; } +} diff --git a/Simulator/SimulationInput.cs b/Simulator/SimulationInput.cs new file mode 100644 index 0000000..5d8b47f --- /dev/null +++ b/Simulator/SimulationInput.cs @@ -0,0 +1,12 @@ +using System; + +namespace Craftimizer.Simulator; + +public readonly record struct SimulationInput +{ + public CharacterStats Stats { get; init; } + public RecipeInfo Recipe { get; init; } + public Random Random { get; init; } + + public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(Recipe.ConditionsFlag); +} diff --git a/Simulator/SimulationState.cs b/Simulator/SimulationState.cs new file mode 100644 index 0000000..9682744 --- /dev/null +++ b/Simulator/SimulationState.cs @@ -0,0 +1,46 @@ +using Craftimizer.Simulator.Actions; +using System; +using System.Collections.Generic; + +namespace Craftimizer.Simulator; + +public readonly record struct SimulationState +{ + public SimulationInput Input { get; init; } + + public int ActionCount => ActionHistory.Count; + + public int StepCount { get; init; } + public int Progress { get; init; } + public int Quality { get; init; } + public int Durability { get; init; } + public int CP { get; init; } + public Condition Condition { get; init; } + public List ActiveEffects { get; init; } + public List ActionHistory { get; init; } + + // https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2 + private static readonly int[] HQPercentTable = { + 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, + 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 17, 17, + 17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71, + 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 + }; + public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.Recipe.MaxQuality * 100, 0, 100)]; + + public bool IsFirstStep => StepCount == 0; + + public SimulationState(SimulationInput input) + { + Input = input; + + StepCount = 0; + Progress = 0; + Quality = 0; + Durability = Input.Recipe.MaxDurability; + CP = Input.Stats.CP; + Condition = Condition.Normal; + ActiveEffects = new(); + ActionHistory = new(); + } +} diff --git a/Craftimizer/Simulator/SimulationState.cs b/Simulator/Simulator.cs similarity index 66% rename from Craftimizer/Simulator/SimulationState.cs rename to Simulator/Simulator.cs index 63b47b3..468af7a 100644 --- a/Craftimizer/Simulator/SimulationState.cs +++ b/Simulator/Simulator.cs @@ -1,58 +1,76 @@ using Craftimizer.Simulator.Actions; -using System; -using System.Collections.Generic; -using System.Linq; namespace Craftimizer.Simulator; -public record struct SimulationState +public class Simulator { - public readonly SimulationInput Input { get; } - - public bool IsComplete { get; private set; } + public SimulationInput Input { get; private set; } public int StepCount { get; private set; } public int Progress { get; private set; } public int Quality { get; private set; } public int Durability { get; private set; } public int CP { get; private set; } public Condition Condition { get; private set; } - public List ActiveEffects { get; } - public List ActionHistory { get; } - - // https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2 - private static readonly int[] HQPercentTable = { - 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, - 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 17, 17, - 17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71, - 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 - }; - public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.MaxQuality * 100, 0, 100)]; + public List ActiveEffects { get; private set; } + public List ActionHistory { get; private set; } public bool IsFirstStep => StepCount == 0; - public SimulationState(SimulationInput input) + public CompletionState CompletionState { - Input = input; + get + { + if (Progress >= Input.Recipe.MaxProgress) + return CompletionState.ProgressComplete; + if (Durability <= 0) + return CompletionState.NoMoreDurability; + return CompletionState.Incomplete; + } + } + public virtual bool IsComplete => CompletionState != CompletionState.Incomplete; - IsComplete = false; - StepCount = 0; - Progress = 0; - Quality = 0; - Durability = Input.MaxDurability; - CP = Input.Stats.CP; - Condition = Condition.Normal; - ActiveEffects = new(); - ActionHistory = new(); + public IEnumerable AvailableActions => ActionUtils.AvailableActions(this); + +#pragma warning disable CS8618 // Emplace sets all the fields already + public Simulator(SimulationState state) +#pragma warning restore CS8618 + { + Emplace(state); } - public (ActionResponse Response, SimulationState NewState) Execute(ActionType action) + private void Emplace(SimulationState state) { - var newState = this; - var response = newState.ExecuteSelf(action); - return (response, newState); + Input = state.Input; + StepCount = state.StepCount; + Progress = state.Progress; + Quality = state.Quality; + Durability = state.Durability; + CP = state.CP; + Condition = state.Condition; + ActiveEffects = new(state.ActiveEffects); + ActionHistory = new(state.ActionHistory); } - public ActionResponse ExecuteSelf(ActionType action) + private SimulationState Displace() => new() + { + Input = Input, + StepCount = StepCount, + Progress = Progress, + Quality = Quality, + Durability = Durability, + CP = CP, + Condition = Condition, + ActiveEffects = ActiveEffects!, + ActionHistory = ActionHistory!, + }; + + public (ActionResponse Response, SimulationState NewState) Execute(SimulationState state, ActionType action) + { + Emplace(state); + return (Execute(action), Displace()); + } + + private ActionResponse Execute(ActionType action) { if (IsComplete) return ActionResponse.SimulationComplete; @@ -68,35 +86,31 @@ public record struct SimulationState } baseAction.Use(); - ActionHistory.Add(action); + ActionHistory!.Add(action); - for (var i = 0; i < ActiveEffects.Count; ++i) + for (var i = 0; i < ActiveEffects!.Count; ++i) { - var effect = ActiveEffects[i]; - effect.Duration--; + var effect = ActiveEffects[i].DecrementDuration(); if (effect.Duration == 0) { ActiveEffects.RemoveAt(i); --i; } - } - - if (Progress >= Input.MaxProgress) - { - IsComplete = true; - return ActionResponse.ProgressComplete; - } - if (Durability <= 0) - { - IsComplete = true; - return ActionResponse.NoMoreDurability; + else + ActiveEffects[i] = effect; } return ActionResponse.UsedAction; } - public Effect? GetEffect(EffectType effect) => - ActiveEffects.FirstOrDefault(e => e.Type == effect); + private int GetEffectIdx(EffectType effect) => + ActiveEffects!.FindIndex(e => e.Type == effect); + + public Effect? GetEffect(EffectType effect) + { + var idx = GetEffectIdx(effect); + return idx == -1 ? null : ActiveEffects![idx]; + } public void AddEffect(EffectType effect, int? duration = null, int? strength = null) { @@ -107,14 +121,13 @@ public record struct SimulationState if (duration != null) duration++; - var currentEffect = GetEffect(effect); - if (currentEffect != null) - { - currentEffect.Duration = duration; - currentEffect.Strength = strength; - } + var newEffect = new Effect { Type = effect, Duration = duration, Strength = strength }; + + var effectIdx = GetEffectIdx(effect); + if (effectIdx != -1) + ActiveEffects![effectIdx] = newEffect; else - ActiveEffects.Add(new Effect { Type = effect, Duration = duration, Strength = strength }); + ActiveEffects!.Add(newEffect); } public void StrengthenEffect(EffectType effect, int? duration = null) @@ -122,29 +135,29 @@ public record struct SimulationState if (duration != null) duration += 1; - var currentEffect = GetEffect(effect); - if (currentEffect != null) + var effectIdx = GetEffectIdx(effect); + if (effectIdx != -1) { - if (effect.Status().MaxStacks > currentEffect.Strength) - currentEffect.Strength++; + if (effect == EffectType.InnerQuiet && ActiveEffects![effectIdx].Strength < 10) + ActiveEffects[effectIdx] = ActiveEffects[effectIdx].IncrementStrength(); } else - AddEffect(effect, duration, 1); + ActiveEffects!.Add(new Effect { Type = effect, Duration = duration, Strength = 1 }); } public void RemoveEffect(EffectType effect) => - ActiveEffects.RemoveAll(e => e.Type == effect); + ActiveEffects!.RemoveAll(e => e.Type == effect); public bool HasEffect(EffectType effect) => - ActiveEffects.Any(e => e.Type == effect); + ActiveEffects!.Any(e => e.Type == effect); public bool IsPreviousAction(ActionType action, int stepsBack = 1) => - ActionHistory.Count >= stepsBack && ActionHistory[^stepsBack] == action; + ActionHistory!.Count >= stepsBack && ActionHistory[^stepsBack] == action; public int CountPreviousAction(ActionType action) => - ActionHistory.Count(a => a == action); + ActionHistory!.Count(a => a == action); - public bool RollSuccessRaw(float successRate) => + public virtual bool RollSuccessRaw(float successRate) => successRate >= Input.Random.NextSingle(); public bool RollSuccess(float successRate) => @@ -181,7 +194,7 @@ public record struct SimulationState return Condition.Normal; } - public void StepCondition() + public virtual void StepCondition() { Condition = Condition switch { @@ -197,8 +210,8 @@ public record struct SimulationState { Durability += amount; - if (Durability > Input.MaxDurability) - Durability = Input.MaxDurability; + if (Durability > Input.Recipe.MaxDurability) + Durability = Input.Recipe.MaxDurability; } public void RestoreCP(int amount) @@ -253,9 +266,9 @@ public record struct SimulationState }; // https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88 - var baseIncrease = (Input.Stats.Craftsmanship * 10f / Input.RecipeTable.ProgressDivider) + 2; - if (Input.Stats.CLvl <= Input.RLvl) - baseIncrease *= Input.RecipeTable.ProgressModifier / 100f; + var baseIncrease = (Input.Stats.Craftsmanship * 10f / Input.Recipe.ProgressDivider) + 2; + if (Input.Stats.CLvl <= Input.Recipe.RLvl) + baseIncrease *= Input.Recipe.ProgressModifier / 100f; baseIncrease = MathF.Floor(baseIncrease); var progressGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); @@ -284,9 +297,9 @@ public record struct SimulationState _ => 1.00f, }; - var baseIncrease = (Input.Stats.Control * 10f / Input.RecipeTable.QualityDivider) + 35; - if (Input.Stats.CLvl <= Input.RLvl) - baseIncrease *= Input.RecipeTable.QualityModifier / 100f; + var baseIncrease = (Input.Stats.Control * 10f / Input.Recipe.QualityDivider) + 35; + if (Input.Stats.CLvl <= Input.Recipe.RLvl) + baseIncrease *= Input.Recipe.QualityModifier / 100f; baseIncrease = MathF.Floor(baseIncrease); var qualityGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); @@ -303,9 +316,9 @@ public record struct SimulationState { Progress += progressGain; - if (HasEffect(EffectType.FinalAppraisal) && Progress >= Input.MaxProgress) + if (HasEffect(EffectType.FinalAppraisal) && Progress >= Input.Recipe.MaxProgress) { - Progress = Input.MaxProgress - 1; + Progress = Input.Recipe.MaxProgress - 1; RemoveEffect(EffectType.FinalAppraisal); } } diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj new file mode 100644 index 0000000..77caee8 --- /dev/null +++ b/Solver/Craftimizer.Solver.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/Solver/Crafty/Arena.cs b/Solver/Crafty/Arena.cs new file mode 100644 index 0000000..26537f3 --- /dev/null +++ b/Solver/Crafty/Arena.cs @@ -0,0 +1,29 @@ +namespace Craftimizer.Solver.Crafty; + +public class Arena where T : struct +{ + public readonly record struct Node + { + public int? Parent { get; init; } + public int Index { get; init; } + public List Children { get; init; } + public T State { get; init; } + } + + public List Nodes { get; } = new(); + + public Arena(T initialState = default) + { + Nodes.Add(new() { Parent = null, Index = 0, Children = new(), State = initialState }); + } + + public int Insert(int parentIndex, T state) + { + var index = Nodes.Count; + Nodes.Add(new() { Parent = parentIndex, Index = index, Children = new(), State = state }); + Nodes[parentIndex].Children.Add(index); + return index; + } + + public Node Get(int index) => Nodes[index]; +} diff --git a/Solver/Crafty/CompletionState.cs b/Solver/Crafty/CompletionState.cs new file mode 100644 index 0000000..09284ae --- /dev/null +++ b/Solver/Crafty/CompletionState.cs @@ -0,0 +1,20 @@ +using CompState = Craftimizer.Simulator.CompletionState; + +namespace Craftimizer.Solver.Crafty; + +public enum CompletionState +{ + Incomplete, + ProgressComplete, + NoMoreDurability, + + InvalidAction, + MaxActionCountReached, + NoMoreActions +} + +internal static class CompletionStateUtils +{ + public static CompState IntoBase(this CompletionState me) => + (CompState)me >= CompState.Other ? CompState.Other : (CompState)me; +} diff --git a/Solver/Crafty/NodeScores.cs b/Solver/Crafty/NodeScores.cs new file mode 100644 index 0000000..bee1631 --- /dev/null +++ b/Solver/Crafty/NodeScores.cs @@ -0,0 +1,8 @@ +namespace Craftimizer.Solver.Crafty; + +public class NodeScores +{ + public float ScoreSum { get; set; } = 0; + public float MaxScore { get; set; } = 0; + public float Visits { get; set; } = 0; +} diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs new file mode 100644 index 0000000..8843858 --- /dev/null +++ b/Solver/Crafty/SimulationNode.cs @@ -0,0 +1,66 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; + +namespace Craftimizer.Solver.Crafty; + +public readonly record struct SimulationNode +{ + public SimulationState State { get; init; } + public ActionType? Action { get; init; } + public List AvailableActions { get; init; } + public CompletionState SimulationCompletionState { get; init; } + public CompletionState CompletionState => + AvailableActions.Count == 0 && SimulationCompletionState == CompletionState.Incomplete ? + CompletionState.NoMoreActions : + SimulationCompletionState; + + public NodeScores Scores { get; init; } + + public bool IsComplete => CompletionState != CompletionState.Incomplete; + + public float? CalculateScore() + { + if (CompletionState != CompletionState.ProgressComplete) + return null; + + static float Apply(float bonus, float value, float target) => + bonus * Math.Min(1f, value / target); + + var progressBonus = 0.20f; + var qualityBonus = 0.65f; + var durabilityBonus = 0.05f; + var cpBonus = 0.05f; + var fewerStepsBonus = 0.05f; + + var progressScore = Apply( + progressBonus, + State.Progress, + State.Input.Recipe.MaxProgress + ); + + var qualityScore = Apply( + qualityBonus, + State.Quality, + State.Input.Recipe.MaxQuality + ); + + var durabilityScore = Apply( + durabilityBonus, + State.Durability, + State.Input.Recipe.MaxDurability + ); + + var cpScore = Apply( + cpBonus, + State.CP, + State.Input.Stats.CP + ); + + var fewerStepsScore = + fewerStepsBonus * (1f - ((float)(State.ActionCount + 1) / Solver.MaxStepCount)); + + Solver.WriteLine($"score: {progressScore:0.00000} {qualityScore:0.00000} {durabilityScore:0.00000} {cpScore:0.00000} {fewerStepsScore:0.00000}"); + + return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; + } +} diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs new file mode 100644 index 0000000..11410a5 --- /dev/null +++ b/Solver/Crafty/Simulator.cs @@ -0,0 +1,161 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Sim = Craftimizer.Simulator.Simulator; + +namespace Craftimizer.Solver.Crafty; + +public class Simulator : Sim +{ + public new CompletionState CompletionState => + ActionHistory.Count >= Solver.MaxStepCount ? + CompletionState.MaxActionCountReached : + (CompletionState)base.CompletionState; + public override bool IsComplete => CompletionState != CompletionState.Incomplete; + + public Simulator(SimulationState state) : base(state) + { + } + + // Disable randomization + public override bool RollSuccessRaw(float successRate) => successRate == 1; + public override void StepCondition() { } + + private static readonly ActionType[] AcceptedActions = new[] + { + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.FocusedTouch, + ActionType.FocusedSynthesis, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.Observe, + ActionType.MastersMend, + ActionType.BasicTouch, + }; + + // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L137 + public List AvailableActionsHeuristic(bool strict) + { + if (IsComplete) + return new(); + + ActionUtils.SetSimulation(this); + return AcceptedActions.Where(action => + { + var baseAction = action.WithUnsafe(); + + if (!baseAction.CanUse) + return false; + + if (CalculateSuccessRate(baseAction.SuccessRate) != 1) + return false; + + // don't allow quality moves at max quality + if (Quality >= Input.Recipe.MaxQuality && baseAction.IncreasesQuality) + return false; + + if (action == ActionType.Observe && + IsPreviousAction(ActionType.Observe)) + return false; + + if (action == ActionType.Groundwork && + Durability < baseAction.DurabilityCost) + return false; + + if (action == ActionType.FinalAppraisal) + return false; + + if (strict) + { + // always used Trained Eye if it's available + if (action == ActionType.TrainedEye) + return true; + + // only allow Focused moves after Observe + if (IsPreviousAction(ActionType.Observe) && + action != ActionType.FocusedSynthesis && + action != ActionType.FocusedTouch) + return false; + + // don't allow quality moves under Muscle Memory for difficult crafts + if (Input.Recipe.ClassJobLevel == 90 && + HasEffect(EffectType.MuscleMemory) && + baseAction.IncreasesQuality) + return false; + + // don't allow pure quality moves under Veneration + if (HasEffect(EffectType.Veneration) && + !baseAction.IncreasesProgress && + baseAction.IncreasesQuality) + return false; + + if (baseAction.IncreasesProgress) + { + var progress_increase = CalculateProgressGain(baseAction.Efficiency); + var would_finish = Progress + progress_increase >= Input.Recipe.MaxProgress; + + if (would_finish) + { + // don't allow finishing the craft if there is significant quality remaining + if (Quality < (Input.Recipe.MaxQuality / 5)) + return false; + } + else + { + // don't allow pure progress moves under Innovation, if it wouldn't finish the craft + if (HasEffect(EffectType.Innovation) && + !baseAction.IncreasesQuality && + baseAction.IncreasesProgress) + return false; + } + } + + if (action == ActionType.ByregotsBlessing && + GetEffect(EffectType.InnerQuiet)?.Strength <= 1) + return false; + + if ((action == ActionType.WasteNot || action == ActionType.WasteNot2) && + (HasEffect(EffectType.WasteNot) || HasEffect(EffectType.WasteNot2))) + return false; + + if (action == ActionType.Observe && + CP < 5) + return false; + + if (action == ActionType.MastersMend && + Input.Recipe.MaxDurability - Durability < 25) + return false; + + if (action == ActionType.Manipulation && + HasEffect(EffectType.Manipulation)) + return false; + + if (action == ActionType.GreatStrides && + HasEffect(EffectType.GreatStrides)) + return false; + + if ((action == ActionType.Veneration || action == ActionType.Innovation) && + (GetEffect(EffectType.Veneration)?.Duration > 1 || GetEffect(EffectType.Innovation)?.Duration > 1)) + return false; + } + + return true; + }).ToList(); + } +} diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs new file mode 100644 index 0000000..5150304 --- /dev/null +++ b/Solver/Crafty/Solver.cs @@ -0,0 +1,304 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; + +namespace Craftimizer.Solver.Crafty; + +// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs +public class Solver +{ + public Simulator Simulator; + public Arena Tree; + + //public Random Random => Simulator.Input.Random; + + public const int Iterations = 100000; + public const float ScoreStorageThreshold = 1f; + public const float MaxScoreWeightingConstant = 0.1f; + public const float ExplorationConstant = 4f; + public const int MaxStepCount = 25; + + public static void Write(string data) + { + if (false) + Console.Write(data); + } + public static void WriteLine(string data) + { + if (false) + Console.WriteLine(data); + } + + public Solver(SimulationState state, bool strict) + { + Simulator = new(state); + Tree = new(new() + { + State = state, + Action = null, + SimulationCompletionState = Simulator.CompletionState, + AvailableActions = Simulator.AvailableActionsHeuristic(strict), + Scores = new() + }); + } + + public Solver(SimulationInput input) : this(new(input), false) + { + } + + private SimulationNode Execute(SimulationState state, ActionType action, bool strict) + { + (_, var newState) = Simulator.Execute(state, action); + return new() + { + State = newState, + Action = action, + SimulationCompletionState = Simulator.CompletionState, + AvailableActions = Simulator.AvailableActionsHeuristic(strict), + Scores = new() + }; + } + + public (int Index, CompletionState State) ExecuteActions(int startIndex, List actions, bool strict = false) + { + var currentIndex = startIndex; + foreach (var action in actions) + { + var node = Tree.Get(currentIndex).State; + if (node.IsComplete) + return (currentIndex, node.CompletionState); + + if (!node.AvailableActions.Remove(action)) + return (currentIndex, CompletionState.InvalidAction); + + currentIndex = Tree.Insert(currentIndex, Execute(node.State, action, strict)); + } + + var currentNode = Tree.Get(currentIndex).State; + return (currentIndex, currentNode.CompletionState); + } + + public static float Eval(NodeScores node, NodeScores parent) + { + var w = MaxScoreWeightingConstant; + var c = ExplorationConstant; + + var visits = node.Visits; + var average_score = node.ScoreSum / visits; + + var exploitation = ((1f - w) * average_score) + (w * node.MaxScore); + var exploration = MathF.Sqrt(c * MathF.Log(parent.Visits) / visits); + + WriteLine($"a {node.ScoreSum} {node.MaxScore}"); + WriteLine($"b {exploitation} {exploration}"); + + return exploitation + exploration; + } + + private enum Ordering + { + Less, + Equal, + Greater + } + + private static V? RustMaxBy(List source, Func into) + { + static Func compare_into(Func compare, Func into) => + (a, b) => compare(into(a), into(b)); + + static Func compare(IComparer comparer) => + (x, y) => comparer.Compare(x, y) switch + { + < 0 => Ordering.Less, + 0 => Ordering.Equal, + > 0 => Ordering.Greater, + }; + + static Func max_by_fold(Func compare) => + (x, y) => compare(x, y) switch + { + Ordering.Less or Ordering.Equal => y, + Ordering.Greater => x, + _ => x + }; + + static V? reduce(List d, Func f) + { + V? accum = default!; + for (var i = 0; i < d.Count; ++i) + accum = i == 0 ? d[i] : f(accum, d[i]); + return accum; + } + + var comparer = compare_into(compare(Comparer.Default), into); + return reduce(source, max_by_fold(comparer)); + } + + public int Select(int currentIndex) + { + var selectedIndex = currentIndex; + while (true) + { + var selectedNode = Tree.Get(selectedIndex); + + var expandable = selectedNode.State.AvailableActions.Count != 0; + var likelyTerminal = selectedNode.Children.Count == 0; + WriteLine("select:"); + WriteLine($"{expandable} {likelyTerminal}".ToLower()); + if (expandable || likelyTerminal) { + break; + } + + // select the node with the highest score + selectedIndex = RustMaxBy(selectedNode.Children, n => Eval(Tree.Get(n).State.Scores, selectedNode.State.Scores)); + WriteLine($"{selectedIndex}"); + } + return selectedIndex; + } + + public (int Index, CompletionState State, float Score) ExpandAndRollout(int initialIndex) + { + WriteLine("expand_and_rollout"); + WriteLine($"{initialIndex}"); + // expand once + var initialNode = Tree.Get(initialIndex).State; + if (initialNode.IsComplete) + { + WriteLine($"ret {initialIndex} {initialNode.CompletionState}"); + return (initialIndex, initialNode.CompletionState, initialNode.CalculateScore() ?? 0); + } + var randomAction = initialNode.AvailableActions.ElementAt(0); + initialNode.AvailableActions.Remove(randomAction); + WriteLine($"pick {randomAction.IntName()}"); + var expandedState = Execute(initialNode.State, randomAction, true); + var expandedIndex = Tree.Insert(initialIndex, expandedState); + WriteLine($"ins {expandedIndex}"); + + // playout to a terminal state + var currentState = Tree.Get(expandedIndex).State; + var preCount = currentState.State.ActionCount; + while (true) + { + if (currentState.IsComplete) + break; + randomAction = currentState.AvailableActions.ElementAt(0); + currentState = Execute(currentState.State, randomAction, true); + } + + // store the result if a max score was reached + var score = currentState.CalculateScore() ?? 0; + if (currentState.CompletionState == CompletionState.ProgressComplete) + { + WriteLine($"calc: {score:0.00000}"); + if (score >= ScoreStorageThreshold && score >= Tree.Get(0).State.Scores.MaxScore) + { + WriteLine("exp_a"); + foreach (var action in currentState.State.ActionHistory.Skip(preCount)) + Write($">{action.IntName()}"); + WriteLine(""); + + (var terminalIndex, _) = ExecuteActions(expandedIndex, currentState.State.ActionHistory.Skip(preCount).ToList(), true); + return (terminalIndex, currentState.CompletionState, score); + } + } + return (expandedIndex, currentState.CompletionState, score); + } + + public void Backpropagate(int startIndex, int targetIndex, float score) + { + WriteLine($"back {startIndex}->{targetIndex} {score}"); + var currentIndex = startIndex; + while (true) + { + var currentNode = Tree.Get(currentIndex); + var currentScores = currentNode.State.Scores; + currentScores.Visits++; + currentScores.ScoreSum += score; + currentScores.MaxScore = Math.Max(currentScores.MaxScore, score); + WriteLine($"bak {currentIndex} {currentScores.Visits} {currentScores.ScoreSum} {currentScores.MaxScore}"); + + if (currentIndex == targetIndex) + break; + + currentIndex = currentNode.Parent!.Value; + } + } + + public void Search(int startIndex) + { + for (var i = 0; i < Iterations; i++) + { + WriteLine($"search {i}"); + var selectedIndex = Select(startIndex); + var (endIndex, state, score) = ExpandAndRollout(selectedIndex); + + WriteLine($"backp {endIndex} {score}"); + Backpropagate(endIndex, startIndex, score); + } + } + + public (List Actions, SimulationNode Node) Solution() + { + WriteLine("sol"); + var actions = new List(); + var node = Tree.Get(0); + while (node.Children.Count != 0) { + var next_index = RustMaxBy(node.Children, n => Tree.Get(n).State.Scores.MaxScore); + WriteLine($"next: {next_index}"); + node = Tree.Get(next_index); + if (node.State.Action != null) + { + WriteLine($"act: {node.State.Action.Value.IntName()}"); + actions.Add(node.State.Action.Value); + } + } + + return (actions, node.State); + } + + public static (SimulationState SimState, CompletionState State) Simulate(SimulationInput input, List actions) + { + var solver = new Solver(input); + var (index, result) = solver.ExecuteActions(0, actions); + return (solver.Tree.Get(index).State.State, result); + } + + public static (List Actions, SimulationState State) SearchStepwise(SimulationInput input, List actions, Action? actionCallback) + { + var (state, result) = Simulate(input, actions); + if (result != CompletionState.Incomplete) { + return (actions, state); + } + + var solver = new Solver(state, true); + while (!solver.Simulator.IsComplete) + { + solver.Search(0); + var (solution_actions, solution_node) = solver.Solution(); + + if (solution_node.Scores.MaxScore >= 1.0) { + actions.AddRange(solution_actions); + return (actions, solution_node.State); + } + + var chosen_action = solution_actions[0]; + (_, state) = solver.Simulator.Execute(state, chosen_action); + actions.Add(chosen_action); + + actionCallback?.Invoke(chosen_action); + + solver = new Solver(state, true); + } + + return (actions, state); + } + + public static (List Actions, SimulationState State) SearchOneshot(SimulationInput input, List actions) + { + var solver = new Solver(input); + solver.Search(0); + var (solution_actions, solution_node) = solver.Solution(); + actions.AddRange(solution_actions); + return (actions, solution_node.State); + } +}