diff --git a/.editorconfig b/.editorconfig
index 253faad..8d46053 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,14 +4,13 @@ root = true
[*]
charset = utf-8
-end_of_line = lf
+end_of_line = crlf
insert_final_newline = true
# 4 space indentation
indent_style = space
indent_size = 4
-
-# disable redundant style warnings
+tab_width = 4
# Microsoft .NET properties
csharp_indent_braces = false
@@ -21,6 +20,21 @@ csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = all
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_comma = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_comma = true
+csharp_space_after_cast = false
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = none
+csharp_space_between_square_brackets = false
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
@@ -64,21 +78,6 @@ dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
dotnet_style_object_initializer = false:suggestion
-csharp_space_between_method_call_empty_parameter_list_parentheses = false
-csharp_space_between_method_call_parameter_list_parentheses = false
-csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
-csharp_space_between_empty_square_brackets = false
-csharp_space_before_semicolon_in_for_statement = false
-csharp_space_before_open_square_brackets = false
-csharp_space_before_comma = false
-csharp_space_after_keywords_in_control_flow_statements = true
-csharp_space_after_comma = true
-csharp_space_after_cast = false
-csharp_space_around_binary_operators = before_and_after
-csharp_space_between_method_declaration_name_and_open_parenthesis = false
-csharp_space_between_method_declaration_parameter_list_parentheses = false
-csharp_space_between_parentheses = none
-csharp_space_between_square_brackets = false
# ReSharper properties
resharper_align_linq_query = true
@@ -166,11 +165,6 @@ csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
-
-[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
-indent_style = space
-indent_size = 4
-tab_width = 4
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs
index fa4e1c6..54d59fa 100644
--- a/Craftimizer/Service.cs
+++ b/Craftimizer/Service.cs
@@ -31,5 +31,4 @@ public sealed class Service
public static Configuration Configuration { get; internal set; }
public static WindowSystem WindowSystem => Plugin.WindowSystem;
#pragma warning restore CS8618
-
}
diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj
index dd73ca2..011fa03 100644
--- a/Solver/Craftimizer.Solver.csproj
+++ b/Solver/Craftimizer.Solver.csproj
@@ -18,4 +18,8 @@
+
+
+
+
diff --git a/Solver/Heuristics/ExpertFinisher.cs b/Solver/Heuristics/ExpertFinisher.cs
new file mode 100644
index 0000000..f247c86
--- /dev/null
+++ b/Solver/Heuristics/ExpertFinisher.cs
@@ -0,0 +1,117 @@
+using Craftimizer.Simulator;
+using Craftimizer.Simulator.Actions;
+using System.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+
+namespace Craftimizer.Solver.Heuristics;
+
+internal sealed class ExpertFinisher : IHeuristic
+{
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool ShouldUseAction(Simulator s, ActionType action, BaseAction baseAction)
+ {
+ if (!Normal.ShouldUseAction(s, action, baseAction))
+ return false;
+
+ // always use Trained Eye if it's available
+ if (action == ActionType.TrainedEye)
+ return baseAction.CanUse(s);
+
+ // only allow Focused moves after Observe
+ if (s.ActionStates.Observed &&
+ action != ActionType.FocusedSynthesis &&
+ action != ActionType.FocusedTouch)
+ return false;
+
+ // don't allow quality moves under Muscle Memory for difficult crafts
+ if (s.Input.Recipe.ClassJobLevel == 90 &&
+ s.HasEffect(EffectType.MuscleMemory) &&
+ baseAction.IncreasesQuality)
+ return false;
+
+ // use First Turn actions if it's available and the craft is difficult
+ if (s.IsFirstStep &&
+ s.Input.Recipe.ClassJobLevel == 90 &&
+ baseAction.Category != ActionCategory.FirstTurn &&
+ s.CP > 10)
+ return false;
+
+ // don't allow combo actions if the combo is already in progress
+ if (s.ActionStates.TouchComboIdx != 0 &&
+ (action == ActionType.StandardTouchCombo || action == ActionType.AdvancedTouchCombo))
+ return false;
+
+ // don't allow pure quality moves under Veneration
+ if (s.HasEffect(EffectType.Veneration) &&
+ !baseAction.IncreasesProgress &&
+ baseAction.IncreasesQuality)
+ return false;
+
+ // don't allow pure quality moves when it won't be able to finish the craft
+ if (baseAction.IncreasesQuality &&
+ s.CalculateDurabilityCost(baseAction.DurabilityCost) > s.Durability)
+ return false;
+
+ if (baseAction.IncreasesProgress)
+ {
+ var progressIncrease = s.CalculateProgressGain(baseAction.Efficiency(s));
+ var wouldFinish = s.Progress + progressIncrease >= s.Input.Recipe.MaxProgress;
+
+ if (wouldFinish)
+ {
+ // don't allow finishing the craft if there is significant quality remaining
+ if (s.Quality < s.Input.Recipe.MaxQuality / 5)
+ return false;
+ }
+ else
+ {
+ // don't allow pure progress moves under Innovation, if it wouldn't finish the craft
+ if (s.HasEffect(EffectType.Innovation) &&
+ !baseAction.IncreasesQuality &&
+ baseAction.IncreasesProgress)
+ return false;
+ }
+ }
+
+ // Only allow byregot at 2+ stacks
+ if (action == ActionType.ByregotsBlessing &&
+ s.GetEffectStrength(EffectType.InnerQuiet) <= 1)
+ return false;
+
+ // Don't execute waste not if a type is already active
+ if ((action == ActionType.WasteNot || action == ActionType.WasteNot2) &&
+ (s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)))
+ return false;
+
+ // Don't observe if you can't combo it into anything
+ if (action == ActionType.Observe &&
+ s.CP < 12)
+ return false;
+
+ // Do not Masters Mend if it would restore less than 25 dur
+ if (action == ActionType.MastersMend &&
+ s.Input.Recipe.MaxDurability - s.Durability < 25)
+ return false;
+
+ // Don't re-execute manipulation/great strides
+ if (action == ActionType.Manipulation &&
+ s.HasEffect(EffectType.Manipulation))
+ return false;
+
+ if (action == ActionType.GreatStrides &&
+ s.HasEffect(EffectType.GreatStrides))
+ return false;
+
+ // Don't overlap/reapply veneration/innovation if there is 2+ steps left
+ if ((action == ActionType.Veneration || action == ActionType.Innovation) &&
+ (s.GetEffectDuration(EffectType.Veneration) > 1 || s.GetEffectDuration(EffectType.Innovation) > 1))
+ return false;
+
+ return true;
+ }
+
+ [Pure]
+ public static ActionSet AvailableActions(Simulator s) =>
+ IHeuristic.AvailableActions(s, Normal.AcceptedActions);
+}
diff --git a/Solver/Heuristics/ExpertHydra.cs b/Solver/Heuristics/ExpertHydra.cs
new file mode 100644
index 0000000..c3d532c
--- /dev/null
+++ b/Solver/Heuristics/ExpertHydra.cs
@@ -0,0 +1,22 @@
+using Craftimizer.Simulator;
+using System.Diagnostics.Contracts;
+
+namespace Craftimizer.Solver.Heuristics;
+
+internal sealed class ExpertHydra // : IHeuristic
+{
+ [Pure]
+ public static ActionSet AvailableActions(Simulator s)
+ {
+ var qualityTarget = s.Input.Recipe.MaxQuality * 0.8f; // 80% quality at least
+ if (s.GetEffectStrength(EffectType.InnerQuiet) == 10 || s.Quality > qualityTarget)
+ return ExpertFinisher.AvailableActions(s);
+
+ qualityTarget = s.Input.Recipe.MaxQuality * 0.2f; // 20% quality at least
+ var progressTarget = s.Input.Recipe.MaxProgress - s.Input.BaseProgressGain * 2.5f; // 250% efficiency away from completion
+ if (s.Progress > progressTarget || s.Quality > qualityTarget)
+ return ExpertQuality.AvailableActions(s);
+
+ return ExpertOpener.AvailableActions(s);
+ }
+}
diff --git a/Solver/Heuristics/ExpertOpener.cs b/Solver/Heuristics/ExpertOpener.cs
new file mode 100644
index 0000000..30c3f7d
--- /dev/null
+++ b/Solver/Heuristics/ExpertOpener.cs
@@ -0,0 +1,84 @@
+using Craftimizer.Simulator;
+using Craftimizer.Simulator.Actions;
+using System.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+
+namespace Craftimizer.Solver.Heuristics;
+
+internal sealed class ExpertOpener : IHeuristic
+{
+ public static readonly ActionType[] AcceptedActions = new[]
+ {
+ ActionType.CarefulObservation,
+ ActionType.FinalAppraisal,
+ ActionType.Manipulation,
+ ActionType.MuscleMemory,
+ ActionType.PreciseTouch,
+ ActionType.RapidSynthesis,
+ ActionType.TricksOfTheTrade,
+ ActionType.Veneration,
+ ActionType.HeartAndSoul,
+ };
+
+ private static readonly BaseAction RapidSynthesis = ActionType.RapidSynthesis.Base();
+
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool ShouldUseAction(Simulator s, ActionType action, BaseAction baseAction)
+ {
+ // Make sure the first step is muscle memory
+ if (baseAction.IncreasesStepCount && s.IsFirstStep && action != ActionType.MuscleMemory)
+ return false;
+
+ // Roll for a malleable or sturdy before taking the first muscle memory step
+ if (s.Condition is not (Condition.Malleable or Condition.Sturdy) && s.IsFirstStep && action == ActionType.MuscleMemory && s.ActionStates.CarefulObservationCount != 3)
+ return false;
+
+ if (baseAction.IncreasesProgress)
+ {
+ var progressIncrease = s.CalculateProgressGain(baseAction.Efficiency(s));
+ var wouldFinish = s.Progress + progressIncrease >= s.Input.Recipe.MaxProgress;
+
+ if (wouldFinish)
+ return false;
+ }
+
+ if (action == ActionType.FinalAppraisal)
+ {
+ if (s.HasEffect(EffectType.FinalAppraisal))
+ return false;
+
+ var rapidProgressIncrease = s.CalculateProgressGain(RapidSynthesis.Efficiency(s));
+ var wouldRapidFinish = s.Progress + rapidProgressIncrease >= s.Input.Recipe.MaxProgress;
+ return wouldRapidFinish;
+ }
+
+ // Don't reapply manipulation if there is 2+ steps left
+ if (action == ActionType.Manipulation &&
+ s.GetEffectDuration(EffectType.Manipulation) > 1)
+ return false;
+
+ // Don't reapply veneration if there is 2+ steps left
+ if (action == ActionType.Veneration &&
+ s.GetEffectDuration(EffectType.Veneration) > 1)
+ return false;
+
+ return s.Condition switch
+ {
+ Condition.Centered or Condition.Sturdy or Condition.Normal or Condition.GoodOmen =>
+ action is ActionType.RapidSynthesis or ActionType.Veneration ||
+ action is ActionType.Manipulation && !s.HasEffect(EffectType.MuscleMemory),
+ Condition.Malleable =>
+ action is ActionType.RapidSynthesis,
+ Condition.Primed or Condition.Pliant =>
+ action is ActionType.RapidSynthesis or ActionType.Manipulation or ActionType.Veneration,
+ Condition.Good =>
+ action is ActionType.RapidSynthesis or ActionType.TricksOfTheTrade or ActionType.PreciseTouch,
+ _ => true
+ };
+ }
+
+ [Pure]
+ public static ActionSet AvailableActions(Simulator s) =>
+ IHeuristic.AvailableActions(s, AcceptedActions);
+}
diff --git a/Solver/Heuristics/ExpertQuality.cs b/Solver/Heuristics/ExpertQuality.cs
new file mode 100644
index 0000000..d716d2f
--- /dev/null
+++ b/Solver/Heuristics/ExpertQuality.cs
@@ -0,0 +1,117 @@
+using Craftimizer.Simulator;
+using Craftimizer.Simulator.Actions;
+using System.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+
+namespace Craftimizer.Solver.Heuristics;
+
+internal sealed class ExpertQuality : IHeuristic
+{
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool ShouldUseAction(Simulator s, ActionType action, BaseAction baseAction)
+ {
+ if (!Normal.ShouldUseAction(s, action, baseAction))
+ return false;
+
+ // always use Trained Eye if it's available
+ if (action == ActionType.TrainedEye)
+ return baseAction.CanUse(s);
+
+ // only allow Focused moves after Observe
+ if (s.ActionStates.Observed &&
+ action != ActionType.FocusedSynthesis &&
+ action != ActionType.FocusedTouch)
+ return false;
+
+ // don't allow quality moves under Muscle Memory for difficult crafts
+ if (s.Input.Recipe.ClassJobLevel == 90 &&
+ s.HasEffect(EffectType.MuscleMemory) &&
+ baseAction.IncreasesQuality)
+ return false;
+
+ // use First Turn actions if it's available and the craft is difficult
+ if (s.IsFirstStep &&
+ s.Input.Recipe.ClassJobLevel == 90 &&
+ baseAction.Category != ActionCategory.FirstTurn &&
+ s.CP > 10)
+ return false;
+
+ // don't allow combo actions if the combo is already in progress
+ if (s.ActionStates.TouchComboIdx != 0 &&
+ (action == ActionType.StandardTouchCombo || action == ActionType.AdvancedTouchCombo))
+ return false;
+
+ // don't allow pure quality moves under Veneration
+ if (s.HasEffect(EffectType.Veneration) &&
+ !baseAction.IncreasesProgress &&
+ baseAction.IncreasesQuality)
+ return false;
+
+ // don't allow pure quality moves when it won't be able to finish the craft
+ if (baseAction.IncreasesQuality &&
+ s.CalculateDurabilityCost(baseAction.DurabilityCost) > s.Durability)
+ return false;
+
+ if (baseAction.IncreasesProgress)
+ {
+ var progressIncrease = s.CalculateProgressGain(baseAction.Efficiency(s));
+ var wouldFinish = s.Progress + progressIncrease >= s.Input.Recipe.MaxProgress;
+
+ if (wouldFinish)
+ {
+ // don't allow finishing the craft if there is significant quality remaining
+ if (s.Quality < s.Input.Recipe.MaxQuality / 5)
+ return false;
+ }
+ else
+ {
+ // don't allow pure progress moves under Innovation, if it wouldn't finish the craft
+ if (s.HasEffect(EffectType.Innovation) &&
+ !baseAction.IncreasesQuality &&
+ baseAction.IncreasesProgress)
+ return false;
+ }
+ }
+
+ // Only allow byregot at 2+ stacks
+ if (action == ActionType.ByregotsBlessing &&
+ s.GetEffectStrength(EffectType.InnerQuiet) <= 1)
+ return false;
+
+ // Don't execute waste not if a type is already active
+ if ((action == ActionType.WasteNot || action == ActionType.WasteNot2) &&
+ (s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)))
+ return false;
+
+ // Don't observe if you can't combo it into anything
+ if (action == ActionType.Observe &&
+ s.CP < 12)
+ return false;
+
+ // Do not Masters Mend if it would restore less than 25 dur
+ if (action == ActionType.MastersMend &&
+ s.Input.Recipe.MaxDurability - s.Durability < 25)
+ return false;
+
+ // Don't re-execute manipulation/great strides
+ if (action == ActionType.Manipulation &&
+ s.HasEffect(EffectType.Manipulation))
+ return false;
+
+ if (action == ActionType.GreatStrides &&
+ s.HasEffect(EffectType.GreatStrides))
+ return false;
+
+ // Don't overlap/reapply veneration/innovation if there is 2+ steps left
+ if ((action == ActionType.Veneration || action == ActionType.Innovation) &&
+ (s.GetEffectDuration(EffectType.Veneration) > 1 || s.GetEffectDuration(EffectType.Innovation) > 1))
+ return false;
+
+ return true;
+ }
+
+ [Pure]
+ public static ActionSet AvailableActions(Simulator s) =>
+ IHeuristic.AvailableActions(s, Normal.AcceptedActions);
+}