diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 7838d75..4102c7d 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -620,10 +620,7 @@ internal static class ImGuiUtils var lineSize = font.CalcWordWrapPositionA(1, textBuf, currentWrapWidth) ?? textBuf.Length; var lineBuf = textBuf[..lineSize]; ImGui.Text(lineBuf.ToString()); - var remainingBuf = textBuf[lineSize..]; - - while (!remainingBuf.IsEmpty && char.IsWhiteSpace(remainingBuf[0])) - remainingBuf = remainingBuf[1..]; + var remainingBuf = textBuf[lineSize..].TrimStart(); if (!remainingBuf.IsEmpty) { diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 1861829..6ad24b6 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -843,15 +843,14 @@ public sealed unsafe class RecipeNote : Window, IDisposable if (state.MacroName is { } macroName) { + using var _ = ImRaii2.TextWrapPos(panelWidth); if (state.MacroUrl is { } macroUrl) { ImGuiUtils.AlignCentered(ImGui.CalcTextSize(macroName).X, panelWidth); ImGuiUtils.Hyperlink(macroName, macroUrl, false); } else - { ImGuiUtils.TextCentered(macroName, panelWidth); - } } using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index e1d2d92..8391188 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -1,3 +1,5 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Craftimizer.Solver; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -8,8 +10,10 @@ using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Text; namespace Craftimizer.Plugin.Windows; @@ -558,6 +562,21 @@ public sealed class Settings : Window, IDisposable ); } + using (var panel = ImRaii2.GroupPanel("Action Pool", -1, out var poolWidth)) + { + poolWidth -= ImGui.GetStyle().ItemSpacing.X * 2; + + ImGui.Text("Select the actions you want the solver to choose from."); + + var pool = config.ActionPool; + DrawActionPool(ref pool, poolWidth, out var isPoolDirty); + if (isPoolDirty) + { + config = config with { ActionPool = pool }; + isDirty = true; + } + } + using (var panel = ImRaii2.GroupPanel("Advanced", -1, out _)) { DrawOption( @@ -676,6 +695,120 @@ public sealed class Settings : Window, IDisposable configRef = config; } + private static void DrawActionPool(ref ActionType[] actionPool, float poolWidth, out bool isDirty) + { + isDirty = false; + + var recipeData = Service.Plugin.GetDefaultStats().Recipe; + HashSet pool = new(actionPool); + + var imageSize = ImGui.GetFrameHeight() * 2; + var spacing = ImGui.GetStyle().ItemSpacing.Y; + + using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); + using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); + using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero); + using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f); + foreach (var category in Enum.GetValues()) + { + if (category == ActionCategory.Combo) + continue; + + var actions = category.GetActions(); + using var panel = ImRaii2.GroupPanel(category.GetDisplayName(), poolWidth, out var availSpace); + var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing)); + var itemCount = actions.Count; + var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow); + for (var i = 0; i < iterCount; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + var actionBase = actions[i].Base(); + var isEnabled = pool.Contains(actions[i]); + var isInefficient = SolverConfig.InefficientActions.Contains(actions[i]); + var isRisky = SolverConfig.RiskyActions.Contains(actions[i]); + var iconTint = Vector4.One; + if (!isEnabled) + iconTint = new(1, 1, 1, ImGui.GetStyle().DisabledAlpha); + else if (isInefficient) + iconTint = new(1, 1f, .5f, 1); + else if (isRisky) + iconTint = new(1, .5f, .5f, 1); + if (ImGui.ImageButton(actions[i].GetIcon(recipeData.ClassJob).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, iconTint)) + { + isDirty = true; + if (isEnabled) + pool.Remove(actions[i]); + else + pool.Add(actions[i]); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + var s = new StringBuilder(); + s.AppendLine(actions[i].GetName(recipeData.ClassJob)); + if (isInefficient) + s.AppendLine( + "Not recommended. This action may be randomly used in a " + + "detrimental way to the rest of the craft. Always use " + + "your best judgement if enabling this action."); + if (isRisky) + s.AppendLine( + "Useless; the solver currently doesn't take any risks in " + + "its crafts. It only takes steps that have a 100% chance of " + + "succeeding. If you want have a moment where you want to take " + + "risks in your craft (like in expert recipes), don't rely " + + "on the solver during that time."); + ImGuiUtils.TooltipWrapped(s.ToString()); + } + } + else + ImGui.Dummy(new(imageSize)); + } + } + + if (isDirty) + { + bool InPool(BaseComboAction action) + { + if (action.ActionTypeA.Base() is BaseComboAction { } aCombo) + { + if (!InPool(aCombo)) + return false; + } + else + { + if (!pool.Contains(action.ActionTypeA)) + return false; + } + if (action.ActionTypeB.Base() is BaseComboAction { } bCombo) + { + if (!InPool(bCombo)) + return false; + } + else + { + if (!pool.Contains(action.ActionTypeB)) + return false; + } + return true; + } + + foreach(var combo in ActionCategory.Combo.GetActions()) + { + if (combo.Base() is BaseComboAction { } comboAction) + { + if (!InPool(comboAction)) + pool.Remove(combo); + else + pool.Add(combo); + } + } + actionPool = pool.ToArray(); + } + } + private void DrawTabSimulator() { using var tab = TabItem("Simulator"); diff --git a/Simulator/ActionCategory.cs b/Simulator/ActionCategory.cs index 56ce764..727772e 100644 --- a/Simulator/ActionCategory.cs +++ b/Simulator/ActionCategory.cs @@ -22,7 +22,6 @@ public static class ActionCategoryUtils { SortedActions = new( Enum.GetValues() - .Where(a => a.Category() != ActionCategory.Combo) .GroupBy(a => a.Category()) .ToDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray())); } diff --git a/Simulator/Actions/ActionType.cs b/Simulator/Actions/ActionType.cs index 1c6e589..d7dd285 100644 --- a/Simulator/Actions/ActionType.cs +++ b/Simulator/Actions/ActionType.cs @@ -63,12 +63,6 @@ public static class ActionUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] public static BaseAction Base(this ActionType me) => Actions[(int)me]; - public static IEnumerable AvailableActions(Simulator simulation) => - simulation.IsComplete - ? Enumerable.Empty() - : Enum.GetValues() - .Where(a => a.Base().CanUse(simulation)); - public static int Level(this ActionType me) => me.Base().Level; diff --git a/Simulator/Actions/AdvancedTouchCombo.cs b/Simulator/Actions/AdvancedTouchCombo.cs index 6a147a0..66e6123 100644 --- a/Simulator/Actions/AdvancedTouchCombo.cs +++ b/Simulator/Actions/AdvancedTouchCombo.cs @@ -4,4 +4,6 @@ internal sealed class AdvancedTouchCombo : BaseComboAction ActionType.StandardTouchCombo; public override ActionType ActionTypeB => ActionType.AdvancedTouch; + + public override int CPCost(Simulator s) => 18 * 3; } diff --git a/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs index 07c4cee..e650a00 100644 --- a/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -25,8 +25,19 @@ public abstract class BaseAction public virtual int Efficiency(Simulator s) => 0; public virtual float SuccessRate(Simulator s) => 1f; - public virtual bool CanUse(Simulator s) => - s.Input.Stats.Level >= Level && s.CP >= CPCost(s); + // Return true if it can be in the action pool now or in the future + // e.g. if Heart and Soul is already used, it is impossible to use it again + // or if it's a first step action and IsFirstStep is false + public virtual bool IsPossible(Simulator s) => + s.Input.Stats.Level >= Level; + + // Return true if it can be used now + // This already assumes that IsPossible returns true *at some point before* + public virtual bool CouldUse(Simulator s) => + s.CP >= CPCost(s); + + public bool CanUse(Simulator s) => + IsPossible(s) && CouldUse(s); public virtual void Use(Simulator s) { diff --git a/Simulator/Actions/BaseComboAction.cs b/Simulator/Actions/BaseComboAction.cs index 16876c5..5151f4b 100644 --- a/Simulator/Actions/BaseComboAction.cs +++ b/Simulator/Actions/BaseComboAction.cs @@ -7,8 +7,8 @@ public abstract class BaseComboAction : BaseAction public sealed override ActionCategory Category => ActionCategory.Combo; - protected bool BaseCanUse(Simulator s) => - base.CanUse(s); + protected bool BaseCouldUse(Simulator s) => + base.CouldUse(s); private static bool VerifyDurability2(int durabilityA, int durability, in Effects effects) { diff --git a/Simulator/Actions/BaseComboActionImpl.cs b/Simulator/Actions/BaseComboActionImpl.cs index 623fd53..6f87f35 100644 --- a/Simulator/Actions/BaseComboActionImpl.cs +++ b/Simulator/Actions/BaseComboActionImpl.cs @@ -13,8 +13,10 @@ internal abstract class BaseComboAction : BaseComboAction where A : BaseAc public override int CPCost(Simulator s) => ActionA.CPCost(s) + ActionB.CPCost(s); - public override bool CanUse(Simulator s) => - BaseCanUse(s) && VerifyDurability2(s, ActionA.DurabilityCost); + public override bool IsPossible(Simulator s) => ActionA.IsPossible(s) && ActionB.IsPossible(s); + + public override bool CouldUse(Simulator s) => + BaseCouldUse(s) && VerifyDurability2(s, ActionA.DurabilityCost); public override void Use(Simulator s) { diff --git a/Simulator/Actions/ByregotsBlessing.cs b/Simulator/Actions/ByregotsBlessing.cs index 257db8e..7e29798 100644 --- a/Simulator/Actions/ByregotsBlessing.cs +++ b/Simulator/Actions/ByregotsBlessing.cs @@ -11,7 +11,7 @@ internal sealed class ByregotsBlessing : BaseAction public override int CPCost(Simulator s) => 24; public override int Efficiency(Simulator s) => 100 + (20 * s.GetEffectStrength(EffectType.InnerQuiet)); - public override bool CanUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CanUse(s); + public override bool CouldUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/CarefulObservation.cs b/Simulator/Actions/CarefulObservation.cs index ebee287..5ae64d8 100644 --- a/Simulator/Actions/CarefulObservation.cs +++ b/Simulator/Actions/CarefulObservation.cs @@ -12,10 +12,13 @@ internal sealed class CarefulObservation : BaseAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; + public override bool IsPossible(Simulator s) => + base.IsPossible(s) && s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; + + public override bool CouldUse(Simulator s) => s.ActionStates.CarefulObservationCount < 3; public override void UseSuccess(Simulator s) => s.StepCondition(); public override string GetTooltip(Simulator s, bool addUsability) => - $"{base.GetTooltip(s, addUsability)}Specialist Only"; + $"{base.GetTooltip(s, addUsability)}Specialist Only\n"; } diff --git a/Simulator/Actions/HeartAndSoul.cs b/Simulator/Actions/HeartAndSoul.cs index 16e7b1a..06e183b 100644 --- a/Simulator/Actions/HeartAndSoul.cs +++ b/Simulator/Actions/HeartAndSoul.cs @@ -13,8 +13,11 @@ internal sealed class HeartAndSoul : BaseBuffAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; + public override bool IsPossible(Simulator s) => + base.IsPossible(s) && s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; + + public override bool CouldUse(Simulator s) => !s.ActionStates.UsedHeartAndSoul; public override string GetTooltip(Simulator s, bool addUsability) => - $"{GetBaseTooltip(s, addUsability)}Specialist Only"; + $"{GetBaseTooltip(s, addUsability)}Specialist Only\n"; } diff --git a/Simulator/Actions/IntensiveSynthesis.cs b/Simulator/Actions/IntensiveSynthesis.cs index 54db7b0..e54f3d2 100644 --- a/Simulator/Actions/IntensiveSynthesis.cs +++ b/Simulator/Actions/IntensiveSynthesis.cs @@ -11,9 +11,9 @@ internal sealed class IntensiveSynthesis : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 400; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/Manipulation.cs b/Simulator/Actions/Manipulation.cs index 804ee65..56dfa30 100644 --- a/Simulator/Actions/Manipulation.cs +++ b/Simulator/Actions/Manipulation.cs @@ -10,7 +10,9 @@ internal sealed class Manipulation : BaseBuffAction public override byte Duration => 8; public override int CPCost(Simulator s) => 96; - public override bool CanUse(Simulator s) => s.Input.Stats.CanUseManipulation && base.CanUse(s); + + public override bool IsPossible(Simulator s) => + s.Input.Stats.CanUseManipulation && base.IsPossible(s); public override void Use(Simulator s) { diff --git a/Simulator/Actions/MuscleMemory.cs b/Simulator/Actions/MuscleMemory.cs index f6eb09d..bde7941 100644 --- a/Simulator/Actions/MuscleMemory.cs +++ b/Simulator/Actions/MuscleMemory.cs @@ -11,7 +11,9 @@ internal sealed class MuscleMemory : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 300; - public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); + public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/PreciseTouch.cs b/Simulator/Actions/PreciseTouch.cs index 1be9c2d..1b4766e 100644 --- a/Simulator/Actions/PreciseTouch.cs +++ b/Simulator/Actions/PreciseTouch.cs @@ -11,9 +11,9 @@ internal sealed class PreciseTouch : BaseAction public override int CPCost(Simulator s) => 18; public override int Efficiency(Simulator s) => 150; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/PrudentSynthesis.cs b/Simulator/Actions/PrudentSynthesis.cs index 1ac3a22..a441fe9 100644 --- a/Simulator/Actions/PrudentSynthesis.cs +++ b/Simulator/Actions/PrudentSynthesis.cs @@ -12,7 +12,7 @@ internal sealed class PrudentSynthesis : BaseAction public override int CPCost(Simulator s) => 18; public override int Efficiency(Simulator s) => 180; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/PrudentTouch.cs b/Simulator/Actions/PrudentTouch.cs index f01f814..2298f10 100644 --- a/Simulator/Actions/PrudentTouch.cs +++ b/Simulator/Actions/PrudentTouch.cs @@ -12,7 +12,7 @@ internal sealed class PrudentTouch : BaseAction public override int CPCost(Simulator s) => 25; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/Reflect.cs b/Simulator/Actions/Reflect.cs index 9a7dbdc..28fc811 100644 --- a/Simulator/Actions/Reflect.cs +++ b/Simulator/Actions/Reflect.cs @@ -11,7 +11,9 @@ internal sealed class Reflect : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); + public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/StandardTouchCombo.cs b/Simulator/Actions/StandardTouchCombo.cs index 5fcea98..d932c85 100644 --- a/Simulator/Actions/StandardTouchCombo.cs +++ b/Simulator/Actions/StandardTouchCombo.cs @@ -4,4 +4,6 @@ internal sealed class StandardTouchCombo : BaseComboAction ActionType.BasicTouch; public override ActionType ActionTypeB => ActionType.StandardTouch; + + public override int CPCost(Simulator s) => 18 * 2; } diff --git a/Simulator/Actions/TrainedEye.cs b/Simulator/Actions/TrainedEye.cs index 494fb0c..94c4990 100644 --- a/Simulator/Actions/TrainedEye.cs +++ b/Simulator/Actions/TrainedEye.cs @@ -10,11 +10,12 @@ internal sealed class TrainedEye : BaseAction public override int CPCost(Simulator s) => 250; - public override bool CanUse(Simulator s) => - s.IsFirstStep && + public override bool IsPossible(Simulator s) => s.IsFirstStep && !s.Input.Recipe.IsExpert && s.Input.Stats.Level >= (s.Input.Recipe.ClassJobLevel + 10) && - base.CanUse(s); + base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) => s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality); diff --git a/Simulator/Actions/TrainedFinesse.cs b/Simulator/Actions/TrainedFinesse.cs index 28026c2..724d0aa 100644 --- a/Simulator/Actions/TrainedFinesse.cs +++ b/Simulator/Actions/TrainedFinesse.cs @@ -12,7 +12,7 @@ internal sealed class TrainedFinesse : BaseAction public override int CPCost(Simulator s) => 32; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => s.GetEffectStrength(EffectType.InnerQuiet) == 10 - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/TricksOfTheTrade.cs b/Simulator/Actions/TricksOfTheTrade.cs index ae9f5d2..22a375f 100644 --- a/Simulator/Actions/TricksOfTheTrade.cs +++ b/Simulator/Actions/TricksOfTheTrade.cs @@ -10,9 +10,9 @@ internal sealed class TricksOfTheTrade : BaseAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Condition.cs b/Simulator/Condition.cs index 6ba2103..0f9e6d7 100644 --- a/Simulator/Condition.cs +++ b/Simulator/Condition.cs @@ -1,22 +1,38 @@ namespace Craftimizer.Simulator; -public enum Condition : ushort +public enum Condition : byte { - Poor = 0x0008, - Normal = 0x0001, - Good = 0x0002, - Excellent = 0x0004, + Normal, + Good, + Excellent, + Poor, - Centered = 0x0010, - Sturdy = 0x0020, - Pliant = 0x0040, - Malleable = 0x0080, - Primed = 0x0100, - GoodOmen = 0x0200, + Centered, + Sturdy, + Pliant, + Malleable, + Primed, + GoodOmen, } public static class ConditionUtils { + [Flags] + private enum ConditionMask : ushort + { + Normal = 1 << 0, // 0x0001 + Good = 1 << 1, // 0x0002 + Excellent = 1 << 2, // 0x0004 + Poor = 1 << 3, // 0x0008 + + Centered = 1 << 4, // 0x0010 + Sturdy = 1 << 5, // 0x0020 + Pliant = 1 << 6, // 0x0040 + Malleable = 1 << 7, // 0x0080 + Primed = 1 << 8, // 0x0100 + GoodOmen = 1 << 9, // 0x0200 + } + public static Condition[] GetPossibleConditions(ushort conditionsFlag) => - Enum.GetValues().Where(c => ((Condition)conditionsFlag).HasFlag(c)).ToArray(); + Enum.GetValues().Where(c => ((ConditionMask)conditionsFlag).HasFlag((ConditionMask)(1 << (ushort)c))).ToArray(); } diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index 75364f5..b984d9b 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -35,7 +35,12 @@ public class Simulator } public bool IsComplete => CompletionState != CompletionState.Incomplete; - public IEnumerable AvailableActions => ActionUtils.AvailableActions(this); + public SimulationState ExecuteUnchecked(in SimulationState state, ActionType action) + { + this.state = state; + ExecuteUnchecked(action); + return this.state; + } public (ActionResponse Response, SimulationState NewState) Execute(in SimulationState state, ActionType action) { @@ -43,6 +48,10 @@ public class Simulator return (Execute(action), this.state); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ExecuteUnchecked(ActionType action) => + action.Base().Use(this); + private ActionResponse Execute(ActionType action) { if (IsComplete) diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 9c7710c..796657e 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -7,69 +7,19 @@ namespace Craftimizer.Solver; public struct ActionSet { - private uint bits; - - internal static ReadOnlySpan AcceptedActions => new[] - { - ActionType.StandardTouchCombo, - ActionType.AdvancedTouchCombo, - ActionType.FocusedTouchCombo, - ActionType.FocusedSynthesisCombo, - ActionType.TrainedFinesse, - ActionType.PrudentSynthesis, - ActionType.Groundwork, - ActionType.AdvancedTouch, - ActionType.CarefulSynthesis, - ActionType.TrainedEye, - ActionType.DelicateSynthesis, - ActionType.PreparatoryTouch, - ActionType.Reflect, - ActionType.PrudentTouch, - ActionType.Manipulation, - ActionType.MuscleMemory, - ActionType.ByregotsBlessing, - ActionType.WasteNot2, - ActionType.BasicSynthesis, - ActionType.Innovation, - ActionType.GreatStrides, - ActionType.StandardTouch, - ActionType.Veneration, - ActionType.WasteNot, - ActionType.MastersMend, - ActionType.BasicTouch, - }; - - public static readonly int[] AcceptedActionsLUT; - - static ActionSet() - { - AcceptedActionsLUT = new int[Enum.GetValues().Length]; - for (var i = 0; i < AcceptedActionsLUT.Length; i++) - AcceptedActionsLUT[i] = -1; - for (var i = 0; i < AcceptedActions.Length; i++) - AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; - } + private ulong bits; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FromAction(ActionType action) - { - var ret = AcceptedActionsLUT[(byte)action]; - if (ret == -1) - throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}."); - return ret; - } + private static int FromAction(ActionType action) => (byte)action; + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActionType ToAction(int index) - { - if (index < 0 || index >= AcceptedActions.Length) - throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for {nameof(ActionSet)}."); - return AcceptedActions[index]; - } + private static ActionType ToAction(int index) => (ActionType)index; + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); + private static ulong ToMask(ActionType action) => 1ul << FromAction(action); // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -96,7 +46,7 @@ public struct ActionSet public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index) - 1); + public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index)); [Pure] public readonly int Count => BitOperations.PopCount(bits); diff --git a/Solver/ArenaBuffer.cs b/Solver/ArenaBuffer.cs index 6405a85..dda988a 100644 --- a/Solver/ArenaBuffer.cs +++ b/Solver/ArenaBuffer.cs @@ -4,31 +4,34 @@ using System.Runtime.CompilerServices; namespace Craftimizer.Solver; -// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs -public struct ArenaBuffer where T : struct +public struct ArenaBuffer { // Technically 25, but it's very unlikely to actually get to there. // The benchmark reaches 20 at most, but here we have a little leeway just in case. - private const int MaxSize = 24; + internal const int MaxSize = 32; - private static readonly int BatchSize = Vector.Count; - private static readonly int BatchSizeBits = int.Log2(BatchSize); - private static readonly int BatchSizeMask = BatchSize - 1; + internal static readonly int BatchSize = Vector.Count; + internal static readonly int BatchSizeBits = int.Log2(BatchSize); + internal static readonly int BatchSizeMask = BatchSize - 1; - private static readonly int BatchCount = MaxSize / BatchSize; + internal static readonly int BatchCount = MaxSize / BatchSize; +} +// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs +public struct ArenaBuffer where T : struct +{ public ArenaNode[][] Data; public int Count { get; private set; } public void Add(ArenaNode node) { - Data ??= new ArenaNode[BatchCount][]; + Data ??= new ArenaNode[ArenaBuffer.BatchCount][]; var idx = Count++; var (arrayIdx, subIdx) = GetArrayIndex(idx); - Data[arrayIdx] ??= new ArenaNode[BatchSize]; + Data[arrayIdx] ??= new ArenaNode[ArenaBuffer.BatchSize]; node.ChildIdx = (arrayIdx, subIdx); Data[arrayIdx][subIdx] = node; @@ -37,5 +40,5 @@ public struct ArenaBuffer where T : struct [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => - (idx >> BatchSizeBits, idx & BatchSizeMask); + (idx >> ArenaBuffer.BatchSizeBits, idx & ArenaBuffer.BatchSizeMask); } diff --git a/Solver/ArenaNode.cs b/Solver/ArenaNode.cs index 5d65e9f..ebafacf 100644 --- a/Solver/ArenaNode.cs +++ b/Solver/ArenaNode.cs @@ -12,7 +12,7 @@ public sealed class ArenaNode where T : struct public NodeScoresBuffer? ParentScores => Parent?.ChildScores; - public ArenaNode(T state, ArenaNode? parent = null) + public ArenaNode(in T state, ArenaNode? parent = null) { State = state; Children = new(); @@ -24,9 +24,9 @@ public sealed class ArenaNode where T : struct Children.Data?[at.arrayIdx]?[at.subIdx]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArenaNode Add(T state) + public ArenaNode Add(in T state) { - var node = new ArenaNode(state, this); + var node = new ArenaNode(in state, this); ChildScores.Add(); Children.Add(node); return node; diff --git a/Solver/Intrinsics.cs b/Solver/Intrinsics.cs index df946a7..d901f7f 100644 --- a/Solver/Intrinsics.cs +++ b/Solver/Intrinsics.cs @@ -92,11 +92,46 @@ internal static class Intrinsics return _base; } + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSetScalar(ulong value, int n) + { + var mask = 0x00000000FFFFFFFFul; + var size = 32; + var _base = 0; + + if (n++ >= BitOperations.PopCount(value)) + return 64; + + while (size > 0) + { + var count = BitOperations.PopCount(value & mask); + if (n > count) + { + _base += size; + size >>= 1; + mask |= mask << size; + } + else + { + size >>= 1; + mask >>= size; + } + } + + return _base; + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int NthBitSetBMI2(uint value, int n) => BitOperations.TrailingZeroCount(Bmi2.ParallelBitDeposit(1u << n, value)); + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSetBMI2(ulong value, int n) => + BitOperations.TrailingZeroCount(Bmi2.X64.ParallelBitDeposit(1ul << n, value)); + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int NthBitSet(uint value, int n) @@ -109,6 +144,18 @@ internal static class Intrinsics NthBitSetScalar(value, n); } + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int NthBitSet(ulong value, int n) + { + if (n >= BitOperations.PopCount(value)) + return 64; + + return Bmi2.X64.IsSupported ? + NthBitSetBMI2(value, n) : + NthBitSetScalar(value, n); + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector ReciprocalSqrt(Vector data) diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index 472909e..a22a5ad 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -3,7 +3,6 @@ using Craftimizer.Simulator; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; -using System.Text; using Node = Craftimizer.Solver.ArenaNode; namespace Craftimizer.Solver; @@ -23,7 +22,7 @@ public sealed class MCTS public MCTS(in MCTSConfig config, in SimulationState state) { this.config = config; - var sim = new Simulator(config.MaxStepCount) { State = state }; + var sim = new Simulator(config.ActionPool, config.MaxStepCount, state); rootNode = new(new( state, null, @@ -35,7 +34,7 @@ public sealed class MCTS private static SimulationNode Execute(Simulator simulator, in SimulationState state, ActionType action, bool strict) { - (_, var newState) = simulator.Execute(state, action); + var newState = simulator.ExecuteUnchecked(state, action); return new( newState, action, @@ -194,7 +193,6 @@ public sealed class MCTS var currentCompletionState = expandedNode.State.SimulationCompletionState; var currentActions = expandedNode.State.AvailableActions; - byte actionCount = 0; Span actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)]; while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && @@ -202,7 +200,7 @@ public sealed class MCTS { var nextAction = currentActions.SelectRandom(random); actions[actionCount++] = nextAction; - (_, currentState) = simulator.Execute(currentState, nextAction); + currentState = simulator.ExecuteUnchecked(currentState, nextAction); currentCompletionState = simulator.CompletionState; if (currentCompletionState != CompletionState.Incomplete) break; @@ -262,17 +260,14 @@ public sealed class MCTS return !NodesIncomplete(rootNode, new()); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Search(int iterations, ref int progress, CancellationToken token) { - Simulator simulator = new(config.MaxStepCount); + var simulator = new Simulator(config.ActionPool, config.MaxStepCount, rootNode.State.State); var random = rootNode.State.State.Input.Random; var staleCounter = 0; var i = 0; for (; i < iterations || MaxScore == 0; i++) { - token.ThrowIfCancellationRequested(); - var selectedNode = Select(); var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); if (MaxScore == 0) @@ -293,7 +288,10 @@ public sealed class MCTS Backpropagate(endNode, score); if ((i & (ProgressUpdateFrequency - 1)) == ProgressUpdateFrequency - 1) + { + token.ThrowIfCancellationRequested(); Interlocked.Add(ref progress, ProgressUpdateFrequency); + } } Interlocked.Add(ref progress, i & (ProgressUpdateFrequency - 1)); } diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs index 1bf4e3d..8f7f091 100644 --- a/Solver/MCTSConfig.cs +++ b/Solver/MCTSConfig.cs @@ -1,3 +1,4 @@ +using Craftimizer.Simulator.Actions; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -21,6 +22,8 @@ public readonly record struct MCTSConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } + public ActionType[] ActionPool { get; init; } + public MCTSConfig(in SolverConfig config) { MaxStepCount = config.MaxStepCount; @@ -36,5 +39,7 @@ public readonly record struct MCTSConfig ScoreDurability = config.ScoreDurability; ScoreCP = config.ScoreCP; ScoreSteps = config.ScoreSteps; + + ActionPool = config.ActionPool; } } diff --git a/Solver/NodeScoresBuffer.cs b/Solver/NodeScoresBuffer.cs index 5d45e59..1178267 100644 --- a/Solver/NodeScoresBuffer.cs +++ b/Solver/NodeScoresBuffer.cs @@ -1,12 +1,13 @@ using System.Diagnostics.Contracts; -using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Craftimizer.Solver; // Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs public struct NodeScoresBuffer { + [StructLayout(LayoutKind.Auto)] public readonly struct ScoresBatch { public readonly Memory ScoreSum; @@ -15,28 +16,18 @@ public struct NodeScoresBuffer public ScoresBatch() { - ScoreSum = new float[BatchSize]; - MaxScore = new float[BatchSize]; - Visits = new int[BatchSize]; + ScoreSum = new float[ArenaBuffer.BatchSize]; + MaxScore = new float[ArenaBuffer.BatchSize]; + Visits = new int[ArenaBuffer.BatchSize]; } } - // Technically 25, but it's very unlikely to actually get to there. - // The benchmark reaches 20 at most, but here we have a little leeway just in case. - private const int MaxSize = 24; - - private static readonly int BatchSize = Vector.Count; - private static readonly int BatchSizeBits = int.Log2(BatchSize); - private static readonly int BatchSizeMask = BatchSize - 1; - - private static readonly int BatchCount = MaxSize / BatchSize; - public ScoresBatch[] Data; public int Count { get; private set; } public void Add() { - Data ??= new ScoresBatch[BatchCount]; + Data ??= new ScoresBatch[ArenaBuffer.BatchCount]; var idx = Count++; @@ -59,5 +50,5 @@ public struct NodeScoresBuffer [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => - (idx >> BatchSizeBits, idx & BatchSizeMask); + (idx >> ArenaBuffer.BatchSizeBits, idx & ArenaBuffer.BatchSizeMask); } diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index ea0404f..843e86c 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -7,6 +7,7 @@ namespace Craftimizer.Solver; internal sealed class Simulator : SimulatorNoRandom { + private readonly (BaseAction Data, ActionType Action)[] actionPoolObjects; private readonly int maxStepCount; public override CompletionState CompletionState @@ -20,8 +21,15 @@ internal sealed class Simulator : SimulatorNoRandom } } - public Simulator(int maxStepCount) + public Simulator(ActionType[] actionPool, int maxStepCount, SimulationState? filteringState = null) { + var pool = actionPool.Select(x => (x.Base(), x)); + if (filteringState is { } state) + { + State = state; + pool = pool.Where(x => x.Item1.IsPossible(this)); + } + actionPoolObjects = pool.OrderBy(x => x.x).ToArray(); this.maxStepCount = maxStepCount; } @@ -30,11 +38,9 @@ internal sealed class Simulator : SimulatorNoRandom [MethodImpl(MethodImplOptions.AggressiveInlining)] // It's just a bunch of if statements, I would assume this is actually quite simple to follow #pragma warning disable MA0051 // Method is too long - private bool CanUseAction(ActionType action, bool strict) + private bool CouldUseAction(ActionType action, BaseAction baseAction, bool strict) #pragma warning restore MA0051 // Method is too long { - var baseAction = action.Base(); - if (CalculateSuccessRate(baseAction.SuccessRate(this)) != 1) return false; @@ -46,7 +52,7 @@ internal sealed class Simulator : SimulatorNoRandom { // always use Trained Eye if it's available if (action == ActionType.TrainedEye) - return baseAction.CanUse(this); + return baseAction.CouldUse(this); // don't allow quality moves under Muscle Memory for difficult crafts if (Input.Recipe.ClassJobLevel == 90 && @@ -123,7 +129,7 @@ internal sealed class Simulator : SimulatorNoRandom return false; } - return baseAction.CanUse(this); + return baseAction.CouldUse(this); } // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L137 @@ -133,10 +139,9 @@ internal sealed class Simulator : SimulatorNoRandom return new(); var ret = new ActionSet(); - foreach (var action in ActionSet.AcceptedActions) - if (CanUseAction(action, strict)) + foreach (var (data, action) in actionPoolObjects) + if (CouldUseAction(action, data, strict)) ret.AddAction(action); return ret; } - } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index cc43434..6cc53a5 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -145,7 +145,7 @@ public sealed class Solver : IDisposable var bestSims = new List<(float Score, SolverSolution Result)>(); var state = State; - var sim = new Simulator(Config.MaxStepCount); + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount); var activeStates = new List() { new(Array.Empty(), state) }; @@ -272,7 +272,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount, state); while (true) { Token.ThrowIfCancellationRequested(); @@ -338,7 +338,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount, state); while (true) { Token.ThrowIfCancellationRequested(); diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index 391ea79..4522c50 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -1,3 +1,4 @@ +using Craftimizer.Simulator.Actions; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -31,6 +32,7 @@ public readonly record struct SolverConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } + public ActionType[] ActionPool { get; init; } public SolverAlgorithm Algorithm { get; init; } public SolverConfig() @@ -54,9 +56,56 @@ public readonly record struct SolverConfig ScoreCP = .05f; ScoreSteps = .05f; + ActionPool = DefaultActionPool; Algorithm = SolverAlgorithm.StepwiseFurcated; } + public static ActionType[] OptimizeActionPool(IEnumerable actions) => + actions.Order().ToArray(); + + public static readonly ActionType[] DefaultActionPool = OptimizeActionPool(new[] + { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.MastersMend, + ActionType.BasicTouch, + }); + + public static readonly IReadOnlySet InefficientActions = new HashSet(new[] + { + ActionType.CarefulObservation, + ActionType.HeartAndSoul, + ActionType.FinalAppraisal + }); + + public static readonly IReadOnlySet RiskyActions = new HashSet(new[] + { + ActionType.RapidSynthesis, + ActionType.HastyTouch, + }); + public static readonly SolverConfig SimulatorDefault = new SolverConfig() with { diff --git a/Test/Solver/ActionSet.cs b/Test/Solver/ActionSet.cs index 7d96f6a..ec170fa 100644 --- a/Test/Solver/ActionSet.cs +++ b/Test/Solver/ActionSet.cs @@ -3,21 +3,6 @@ namespace Craftimizer.Test.Solver; [TestClass] public class ActionSetTests { - [TestMethod] - public void TestAcceptedActions() - { - var actions = ActionSet.AcceptedActions; - var lut = ActionSet.AcceptedActionsLUT; - - Assert.IsTrue(actions.Length <= 32); - foreach (var i in Enum.GetValues()) - { - var idx = lut[(byte)i]; - if (idx != -1) - Assert.AreEqual(i, actions[idx]); - } - } - [TestMethod] public void TestSize() { @@ -87,18 +72,18 @@ public class ActionSetTests Assert.AreEqual(4, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); - Assert.AreEqual(ActionType.Reflect, set.ElementAt(1)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(2)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(3)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); + Assert.AreEqual(ActionType.Reflect, set.ElementAt(3)); set.RemoveAction(ActionType.Reflect); Assert.AreEqual(3, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); } [TestMethod]