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);
+ }
+}