1.9.0.0 (Testing) Release

This commit is contained in:
Asriel Camora
2023-10-17 03:36:31 -07:00
parent 398c7f0500
commit 234eb3a7ab
37 changed files with 3692 additions and 1314 deletions
+81 -9
View File
@@ -1,7 +1,7 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver;
using Dalamud.Configuration;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -10,8 +10,64 @@ namespace Craftimizer.Plugin;
[Serializable]
public class Macro
{
public static event Action<Macro>? OnMacroChanged;
public string Name { get; set; } = string.Empty;
public List<ActionType> Actions { get; set; } = new();
[JsonProperty(PropertyName = "Actions")]
private List<ActionType> actions { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<ActionType> Actions
{
get => actions;
set => ActionEnumerable = value;
}
[JsonIgnore]
public IEnumerable<ActionType> ActionEnumerable
{
set
{
actions = new(value);
OnMacroChanged?.Invoke(this);
}
}
}
[Serializable]
public class MacroCopyConfiguration
{
public enum CopyType
{
OpenWindow, // useful for big macros
CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much)
CopyToClipboard,
}
public CopyType Type { get; set; } = CopyType.OpenWindow;
// CopyToMacro
public bool CopyDown { get; set; }
public bool SharedMacro { get; set; }
public int StartMacroIdx { get; set; } = 1;
public int MaxMacroCount { get; set; } = 5;
// Add /nextmacro [down]
public bool UseNextMacro { get; set; }
// Add /mlock
public bool UseMacroLock { get; set; }
public bool AddNotification { get; set; } = true;
// Requires AddNotification
public bool AddNotificationSound { get; set; } = true;
public int IntermediateNotificationSound { get; set; } = 10;
public int EndNotificationSound { get; set; } = 6;
// For SND
public bool RemoveWaitTimes { get; set; }
// For SND; Cannot use CopyToMacro
public bool CombineMacro { get; set; }
}
[Serializable]
@@ -19,9 +75,12 @@ public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 1;
public bool OverrideUncraftability { get; set; } = true;
public bool HideUnlearnedActions { get; set; } = true;
public List<Macro> Macros { get; set; } = new();
public static event Action? OnMacroListChanged;
[JsonProperty(PropertyName = "Macros")]
private List<Macro> macros { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<Macro> Macros => macros;
public bool ConditionRandomness { get; set; } = true;
public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault;
public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault;
@@ -29,10 +88,23 @@ public class Configuration : IPluginConfiguration
public bool ShowOptimalMacroStat { get; set; } = true;
public int SynthHelperStepCount { get; set; } = 5;
public Simulator.Simulator CreateSimulator(SimulationState state) =>
ConditionRandomness ?
new Simulator.Simulator(state) :
new SimulatorNoRandom(state);
public MacroCopyConfiguration MacroCopy { get; set; } = new();
public void AddMacro(Macro macro)
{
macros.Add(macro);
Save();
OnMacroListChanged?.Invoke();
}
public void RemoveMacro(Macro macro)
{
if (macros.Remove(macro))
{
Save();
OnMacroListChanged?.Invoke();
}
}
public void Save() =>
Service.PluginInterface.SavePluginConfig(this);
+1 -1
View File
@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Asriel Camora</Authors>
<Version>1.2.0.0</Version>
<Version>1.9.0.0</Version>
<PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl>
</PropertyGroup>
+333 -96
View File
@@ -1,130 +1,149 @@
using Craftimizer.Utils;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
namespace Craftimizer.Plugin;
internal static class ImGuiUtils
{
private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new();
private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new();
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
public static void BeginGroupPanel(float width = -1, bool addPadding = true)
// width = -1 -> size to parent
// width = 0 -> size to content
// returns available width (better since it accounts for the right side padding)
// ^ only useful if width = -1
public static float BeginGroupPanel(string name, float width)
{
// container group
ImGui.BeginGroup();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight();
width = width < 0 ? ImGui.GetContentRegionAvail().X - (2 * itemSpacing.X) : width;
var fullWidth = width > 0 ? width + (2 * itemSpacing.X) : 0;
{
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
ImGui.SameLine(0, 0);
// inner group
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(fullWidth, 0));
ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x
ImGui.SameLine(0, 0);
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
// label group
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2
ImGui.SameLine(0, 0);
var textFrameHeight = ImGui.GetFrameHeight();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(name);
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y
ImGui.BeginGroup();
// content group
ImGui.BeginGroup();
}
ImGui.PopStyleVar(2);
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
}
public static void BeginGroupPanel(string name, float width = -1, bool addPadding = true)
{
ImGui.BeginGroup();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight();
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
ImGui.SameLine(0, 0);
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
ImGui.SameLine(0, 0);
ImGui.TextUnformatted(name);
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
ImGui.BeginGroup();
ImGui.PopStyleVar(2);
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
return width;
}
public static void EndGroupPanel()
{
ImGui.PopItemWidth();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight();
ImGui.EndGroup();
ImGui.EndGroup();
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y));
ImGui.EndGroup();
var itemMin = ImGui.GetItemRectMin();
var itemMax = ImGui.GetItemRectMax();
var labelRect = GroupPanelLabelStack.Pop();
var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f;
(Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0));
labelRect.Min.X -= itemSpacing.X;
labelRect.Max.X += itemSpacing.X;
for (var i = 0; i < 4; ++i)
{
var (minClip, maxClip) = i switch
{
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
_ => (Vector2.Zero, Vector2.Zero)
};
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.PushClipRect(minClip, maxClip, true);
ImGui.GetWindowDrawList().AddRect(
frameRect.Min, frameRect.Max,
ImGui.GetColorU32(ImGuiCol.Border),
halfFrame.X);
ImGui.PopClipRect();
// content group
ImGui.EndGroup();
// label group
ImGui.EndGroup();
ImGui.SameLine(0, 0);
// shifts full size by is (for rect placement)
ImGui.Dummy(new(itemSpacing.X, 0));
ImGui.Dummy(new(0, itemSpacing.Y * 2)); // * 2 for some reason (otherwise the bottom is too skinny)
// inner group
ImGui.EndGroup();
var labelRect = GroupPanelLabelStack.Pop();
var innerMin = ImGui.GetItemRectMin() + new Vector2(0, labelRect.TopPadding);
var innerMax = ImGui.GetItemRectMax();
(Vector2 Min, Vector2 Max) frameRect = (innerMin, innerMax);
// add itemspacing padding on the label's sides
labelRect.Min.X -= itemSpacing.X / 2;
labelRect.Max.X += itemSpacing.X / 2;
for (var i = 0; i < 4; ++i)
{
var (minClip, maxClip) = i switch
{
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
_ => (Vector2.Zero, Vector2.Zero)
};
ImGui.PushClipRect(minClip, maxClip, true);
ImGui.GetWindowDrawList().AddRect(
frameRect.Min, frameRect.Max,
ImGui.GetColorU32(ImGuiCol.Border),
itemSpacing.X);
ImGui.PopClipRect();
}
ImGui.Dummy(Vector2.Zero);
}
ImGui.PopStyleVar(2);
ImGui.Dummy(Vector2.Zero);
ImGui.EndGroup();
}
private struct EndUnconditionally : ImRaii.IEndObject, IDisposable
{
private Action EndAction { get; }
public bool Success { get; }
public bool Disposed { get; private set; }
public EndUnconditionally(Action endAction, bool success)
{
EndAction = endAction;
Success = success;
Disposed = false;
}
public void Dispose()
{
if (!Disposed)
{
EndAction();
Disposed = true;
}
}
}
public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth)
{
internalWidth = BeginGroupPanel(name, width);
return new EndUnconditionally(EndGroupPanel, true);
}
private static Vector2 UnitCircle(float theta)
{
var (s, c) = MathF.SinCos(theta);
@@ -156,7 +175,7 @@ internal static class ImGuiUtils
var offset = ImGui.GetCursorScreenPos() + new Vector2(radius);
var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius * 2);
var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius);
var incrementAngle = MathF.Tau / segments;
var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0;
@@ -215,6 +234,216 @@ internal static class ImGuiUtils
Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor);
}
private sealed class SearchableComboData<T> where T : class
{
public readonly ImmutableArray<T> items;
public List<T> filteredItems;
public T selectedItem;
public string input;
public bool wasTextActive;
public bool wasPopupActive;
public CancellationTokenSource? cts;
public Task? task;
private readonly Func<T, string> getString;
public SearchableComboData(IEnumerable<T> items, T selectedItem, Func<T, string> getString)
{
this.items = items.ToImmutableArray();
filteredItems = new() { selectedItem };
this.selectedItem = selectedItem;
this.getString = getString;
input = GetString(selectedItem);
}
public void SetItem(T selectedItem)
{
if (this.selectedItem != selectedItem)
{
input = GetString(selectedItem);
this.selectedItem = selectedItem;
}
}
public string GetString(T item) => getString(item);
public void Filter()
{
cts?.Cancel();
var inp = input;
cts = new();
var token = cts.Token;
task = Task.Run(() => FilterTask(inp, token), token)
.ContinueWith(t =>
{
if (cts.IsCancellationRequested)
return;
try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (Exception e)
{
Log.Error(e, "Filtering recipes failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
private void FilterTask(string input, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(input))
{
filteredItems = items.ToList();
return;
}
var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts);
var query = items.AsParallel().Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant())))
.Where(t => t.Score > 0)
.OrderByDescending(t => t.Score)
.Select(t => t.Item);
token.ThrowIfCancellationRequested();
filteredItems = query.ToList();
}
}
private static readonly Dictionary<uint, object> ComboData = new();
private static SearchableComboData<T> GetComboData<T>(uint comboKey, IEnumerable<T> items, T selectedItem, Func<T, string> getString) where T : class =>
(SearchableComboData<T>)(
ComboData.TryGetValue(comboKey, out var data)
? data
: ComboData[comboKey] = new SearchableComboData<T>(items, selectedItem, getString));
// https://github.com/ocornut/imgui/issues/718#issuecomment-1563162222
public static bool SearchableCombo<T>(string id, ref T selectedItem, IEnumerable<T> items, ImFontPtr selectableFont, float width, Func<T, string> getString, Func<T, string> getId, Action<T> draw) where T : class
{
var comboKey = ImGui.GetID(id);
var data = GetComboData(comboKey, items, selectedItem, getString);
data.SetItem(selectedItem);
using var pushId = ImRaii.PushId(id);
width = width == 0 ? ImGui.GetContentRegionAvail().X : width;
var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width);
ImGui.SetNextItemWidth(availableSpace);
var isInputTextEnterPressed = ImGui.InputText("##input", ref data.input, 256, ImGuiInputTextFlags.EnterReturnsTrue);
var min = ImGui.GetItemRectMin();
var size = ImGui.GetItemRectSize();
size.X = Math.Min(size.X, availableSpace);
var isInputTextActivated = ImGui.IsItemActivated();
if (isInputTextActivated)
{
ImGui.SetNextWindowPos(min - ImGui.GetStyle().WindowPadding);
ImGui.OpenPopup("##popup");
data.wasTextActive = false;
}
using (var popup = ImRaii.Popup("##popup", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings))
{
if (popup)
{
data.wasPopupActive = true;
if (isInputTextActivated)
{
ImGui.SetKeyboardFocusHere(0);
data.Filter();
}
ImGui.SetNextItemWidth(size.X);
if (ImGui.InputText("##input_popup", ref data.input, 256))
data.Filter();
var isActive = ImGui.IsItemActive();
if (!isActive && data.wasTextActive && ImGui.IsKeyPressed(ImGuiKey.Enter))
isInputTextEnterPressed = true;
data.wasTextActive = isActive;
using (var scrollingRegion = ImRaii.Child("scrollingRegion", new Vector2(size.X, size.Y * 10), false, ImGuiWindowFlags.HorizontalScrollbar))
{
T? _selectedItem = default;
var height = ImGui.GetTextLineHeight();
var r = ListClip(data.filteredItems, height, t =>
{
var name = getString(t);
using (var selectFont = ImRaii.PushFont(selectableFont))
{
if (ImGui.Selectable($"##{getId(t)}"))
{
_selectedItem = t;
return true;
}
}
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f);
draw(t);
return false;
});
if (r)
{
selectedItem = _selectedItem!;
data.SetItem(selectedItem);
ImGui.CloseCurrentPopup();
return true;
}
}
if (isInputTextEnterPressed || ImGui.IsKeyPressed(ImGuiKey.Escape))
{
if (isInputTextEnterPressed && data.filteredItems.Count > 0)
{
selectedItem = data.filteredItems[0];
data.SetItem(selectedItem);
}
ImGui.CloseCurrentPopup();
return true;
}
}
else
{
if (data.wasPopupActive)
{
data.wasPopupActive = false;
data.input = getString(selectedItem);
}
}
}
return false;
}
private static bool ListClip<T>(IReadOnlyList<T> data, float lineHeight, Predicate<T> func)
{
ImGuiListClipperPtr imGuiListClipperPtr;
unsafe
{
imGuiListClipperPtr = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
}
try
{
imGuiListClipperPtr.Begin(data.Count, lineHeight);
while (imGuiListClipperPtr.Step())
{
for (var i = imGuiListClipperPtr.DisplayStart; i <= imGuiListClipperPtr.DisplayEnd; i++)
{
if (i >= data.Count)
return false;
if (i >= 0)
{
if (func(data[i]))
return true;
}
}
}
return false;
}
finally
{
imGuiListClipperPtr.End();
imGuiListClipperPtr.Destroy();
}
}
public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size)
{
ImGui.PushFont(UiBuilder.IconFont);
@@ -254,6 +483,14 @@ internal static class ImGuiUtils
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2);
}
public static void AlignRight(float width, float availWidth = default)
{
if (availWidth == default)
availWidth = ImGui.GetContentRegionAvail().X;
if (availWidth > width)
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + availWidth - width);
}
public static void AlignMiddle(Vector2 size, Vector2 availSize = default)
{
if (availSize == default)
@@ -271,9 +508,9 @@ internal static class ImGuiUtils
ImGui.TextUnformatted(text);
}
public static void TextMiddle(string text, Vector2 availSize = default)
public static void TextRight(string text, float availWidth = default)
{
AlignMiddle(ImGui.CalcTextSize(text), availSize);
AlignRight(ImGui.CalcTextSize(text).X, availWidth);
ImGui.TextUnformatted(text);
}
+66 -15
View File
@@ -1,21 +1,22 @@
using Craftimizer.Plugin.Utils;
using Craftimizer.Plugin.Windows;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Craftimizer.Windows;
using Dalamud.Game.Command;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Reflection;
using ClassJob = Craftimizer.Simulator.ClassJob;
namespace Craftimizer.Plugin;
public sealed class Plugin : IDalamudPlugin
{
public string Name => "Craftimizer";
public string Version { get; }
public string Author { get; }
public string BuildConfiguration { get; }
@@ -23,23 +24,23 @@ public sealed class Plugin : IDalamudPlugin
public WindowSystem WindowSystem { get; }
public Settings SettingsWindow { get; }
public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; }
public Craft SynthesisWindow { get; }
public RecipeNote RecipeNoteWindow { get; }
public MacroList ListWindow { get; private set; }
public MacroEditor? EditorWindow { get; private set; }
public MacroClipboard? ClipboardWindow { get; private set; }
public Configuration Configuration { get; }
public Hooks Hooks { get; }
public Craftimizer.Utils.RecipeNote RecipeNote { get; }
public IconManager IconManager { get; }
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
{
Service.Initialize(this, pluginInterface);
WindowSystem = new("Craftimizer");
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
Hooks = new();
RecipeNote = new();
IconManager = new();
WindowSystem = new(Name);
var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
@@ -49,16 +50,41 @@ public sealed class Plugin : IDalamudPlugin
SettingsWindow = new();
RecipeNoteWindow = new();
SynthesisWindow = new();
ListWindow = new();
// Trigger static constructors so a huge hitch doesn't occur on first RecipeNote frame.
FoodStatus.Initialize();
Gearsets.Initialize();
ActionUtils.Initialize();
Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw;
Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow;
Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog;
Service.CommandManager.AddHandler("/craftimizer", new CommandInfo((_, _) => OpenSettingsWindow())
{
HelpMessage = "Open the settings window.",
});
Service.CommandManager.AddHandler("/craftmacros", new CommandInfo((_, _) => OpenMacroListWindow())
{
HelpMessage = "Open the crafting macros window.",
});
Service.CommandManager.AddHandler("/crafteditor", new CommandInfo((_, _) => OpenSettingsWindow())
{
HelpMessage = "Open the crafting macro editor.",
});
}
public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter)
{
EditorWindow?.Dispose();
EditorWindow = new(characterStats, recipeData, buffs, actions, setter);
}
public void OpenSettingsWindow()
{
SettingsWindow.IsOpen = true;
SettingsWindow.BringToFront();
if (SettingsWindow.IsOpen ^= true)
SettingsWindow.BringToFront();
}
public void OpenSettingsTab(string selectedTabLabel)
@@ -67,11 +93,36 @@ public sealed class Plugin : IDalamudPlugin
SettingsWindow.SelectTab(selectedTabLabel);
}
public void OpenMacroListWindow()
{
ListWindow.IsOpen = true;
ListWindow.BringToFront();
}
public void OpenCraftingLog()
{
Chat.SendMessage("/craftinglog");
}
public void OpenMacroClipboard(List<string> macros)
{
ClipboardWindow?.Dispose();
ClipboardWindow = new(macros);
}
public void CopyMacro(IReadOnlyList<ActionType> actions) =>
MacroCopy.Copy(actions);
public void Dispose()
{
SimulatorWindow?.Dispose();
SynthesisWindow.Dispose();
RecipeNote.Dispose();
Service.CommandManager.RemoveHandler("/craftimizer");
Service.CommandManager.RemoveHandler("/craftmacros");
Service.CommandManager.RemoveHandler("/crafteditor");
SettingsWindow.Dispose();
RecipeNoteWindow.Dispose();
ListWindow.Dispose();
EditorWindow?.Dispose();
ClipboardWindow?.Dispose();
Hooks.Dispose();
IconManager.Dispose();
}
-1
View File
@@ -1,6 +1,5 @@
using Craftimizer.Utils;
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
+7 -3
View File
@@ -6,7 +6,6 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Globalization;
@@ -64,6 +63,8 @@ internal static class ActionUtils
}
}
public static void Initialize() { }
public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) =>
ActionRows[(int)me, (int)classJob];
@@ -309,11 +310,14 @@ internal static class EffectUtils
EffectType.FinalAppraisal => 2190,
EffectType.WasteNot2 => 257,
EffectType.MuscleMemory => 2191,
EffectType.Manipulation => 258,
EffectType.Manipulation => 1164,
EffectType.HeartAndSoul => 2665,
_ => 3412,
_ => throw new ArgumentOutOfRangeException(nameof(me)),
};
public static bool IsIndefinite(this EffectType me) =>
me is EffectType.InnerQuiet or EffectType.HeartAndSoul;
public static Status Status(this EffectType me) =>
LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
+15 -14
View File
@@ -1,6 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Runtime.InteropServices;
using System.Text;
@@ -8,7 +9,7 @@ using System.Text;
namespace Craftimizer.Plugin.Utils;
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
public static class Chat
public static unsafe class Chat
{
private static class Signatures
{
@@ -16,11 +17,11 @@ public static class Chat
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
}
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private delegate void ProcessChatBoxDelegate(UIModule* uiModule, IntPtr message, IntPtr unused, byte a4);
private static ProcessChatBoxDelegate? ProcessChatBox { get; }
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> SanitiseString = null!;
static Chat()
{
@@ -33,7 +34,7 @@ public static class Chat
{
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr))
{
_sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sanitisePtr;
SanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sanitisePtr;
}
}
}
@@ -58,7 +59,7 @@ public static class Chat
throw new InvalidOperationException("Could not find signature for chat sending");
}
var uiModule = (IntPtr)Framework.Instance()->GetUiModule();
var uiModule = Framework.Instance()->GetUiModule();
using var payload = new ChatPayload(message);
var mem1 = Marshal.AllocHGlobal(400);
@@ -118,14 +119,14 @@ public static class Chat
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public static unsafe string SanitiseText(string text)
{
if (_sanitiseString == null)
if (SanitiseString == null)
{
throw new InvalidOperationException("Could not find signature for chat sanitisation");
}
var uText = Utf8String.FromString(text);
_sanitiseString(uText, 0x27F, IntPtr.Zero);
SanitiseString(uText, 0x27F, IntPtr.Zero);
var sanitised = uText->ToString();
uText->Dtor();
@@ -151,19 +152,19 @@ public static class Chat
internal ChatPayload(byte[] stringBytes)
{
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
Marshal.WriteByte(textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1);
textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
unk1 = 64;
unk2 = 0;
}
public void Dispose()
{
Marshal.FreeHGlobal(this.textPtr);
Marshal.FreeHGlobal(textPtr);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
using System.Numerics;
namespace Craftimizer.Utils;
public static class Colors
{
public static readonly Vector4 Progress = new(0.44f, 0.65f, 0.18f, 1f);
public static readonly Vector4 Quality = new(0.26f, 0.71f, 0.69f, 1f);
public static readonly Vector4 Durability = new(0.13f, 0.52f, 0.93f, 1f);
public static readonly Vector4 HQ = new(0.592f, 0.863f, 0.376f, 1f);
public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f);
}
+121
View File
@@ -0,0 +1,121 @@
using Craftimizer.Plugin;
using Craftimizer.Plugin.Utils;
using Lumina.Excel.GeneratedSheets;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
namespace Craftimizer.Utils;
public static class FoodStatus
{
private static readonly ReadOnlyDictionary<uint, uint> ItemFoodToItemLUT;
private static readonly ReadOnlyDictionary<uint, Food> FoodItems;
private static readonly ReadOnlyDictionary<uint, Food> MedicineItems;
private static readonly ImmutableArray<uint> FoodOrder;
private static readonly ImmutableArray<uint> MedicineOrder;
public readonly record struct FoodStat(bool IsRelative, int Value, int Max, int ValueHQ, int MaxHQ);
public readonly record struct Food(Item Item, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP);
static FoodStatus()
{
var lut = new Dictionary<uint, uint>();
foreach (var item in LuminaSheets.ItemSheet)
{
var isFood = item.ItemUICategory.Row == 46;
var isMedicine = item.ItemUICategory.Row == 44;
if (!isFood && !isMedicine)
continue;
if (item.ItemAction.Value == null)
continue;
if (!(item.ItemAction.Value.Type is 844 or 845 or 846))
continue;
var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]);
if (itemFood == null)
continue;
lut.TryAdd(itemFood.RowId, item.RowId);
}
ItemFoodToItemLUT = lut.AsReadOnly();
var foods = new Dictionary<uint, Food>();
var medicines = new Dictionary<uint, Food>();
foreach (var item in LuminaSheets.ItemSheet)
{
var isFood = item.ItemUICategory.Row == 46;
var isMedicine = item.ItemUICategory.Row == 44;
if (!isFood && !isMedicine)
continue;
if (item.ItemAction.Value == null)
continue;
if (!(item.ItemAction.Value.Type is 844 or 845 or 846))
continue;
var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]);
if (itemFood == null)
continue;
FoodStat? craftsmanship = null, control = null, cp = null;
foreach (var stat in itemFood.UnkData1)
{
if (stat.BaseParam == 0)
continue;
var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ);
switch (stat.BaseParam)
{
case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break;
case Gearsets.ParamControl: control = foodStat; break;
case Gearsets.ParamCP: cp = foodStat; break;
default: continue;
}
}
if (craftsmanship != null || control != null || cp != null)
{
var food = new Food(item, craftsmanship, control, cp);
if (isFood)
foods.Add(item.RowId, food);
if (isMedicine)
medicines.Add(item.RowId, food);
}
}
FoodItems = foods.AsReadOnly();
MedicineItems = medicines.AsReadOnly();
FoodOrder = FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray();
MedicineOrder = MedicineItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray();
}
public static void Initialize() { }
public static IEnumerable<Food> OrderedFoods => FoodOrder.Select(id => FoodItems[id]);
public static IEnumerable<Food> OrderedMedicines => MedicineOrder.Select(id => MedicineItems[id]);
public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param)
{
var isHq = param > 10000;
param -= 10000;
if (!ItemFoodToItemLUT.TryGetValue(param, out var itemId))
return null;
return (itemId, isHq);
}
public static Food? TryGetFood(uint itemId)
{
if (FoodItems.TryGetValue(itemId, out var food))
return food;
if (MedicineItems.TryGetValue(itemId, out food))
return food;
return null;
}
}
+224
View File
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Craftimizer.Utils;
internal readonly struct FuzzyMatcher
{
private const bool IsBorderMatching = true;
private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>();
private readonly string needleString = string.Empty;
private readonly int needleFinalPosition = -1;
private readonly (int Start, int End)[] needleSegments = EmptySegArray;
private readonly MatchMode mode = MatchMode.Simple;
public FuzzyMatcher(string term, MatchMode matchMode)
{
needleString = term;
needleFinalPosition = needleString.Length - 1;
mode = matchMode;
needleSegments = matchMode switch
{
MatchMode.FuzzyParts => FindNeedleSegments(needleString),
MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray,
_ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, "Invalid match mode"),
};
}
private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan<char> span)
{
var segments = new List<(int, int)>();
var wordStart = -1;
for (var i = 0; i < span.Length; i++)
{
if (span[i] is not ' ' and not '\u3000')
{
if (wordStart < 0)
wordStart = i;
}
else if (wordStart >= 0)
{
segments.Add((wordStart, i - 1));
wordStart = -1;
}
}
if (wordStart >= 0)
segments.Add((wordStart, span.Length - 1));
return segments.ToArray();
}
public int Matches(string value)
{
if (needleFinalPosition < 0)
return 0;
if (mode == MatchMode.Simple)
return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase) ? 1 : 0;
if (mode == MatchMode.Fuzzy)
return GetRawScore(value, 0, needleFinalPosition);
if (mode == MatchMode.FuzzyParts)
{
if (needleSegments.Length < 2)
return GetRawScore(value, 0, needleFinalPosition);
var total = 0;
for (var i = 0; i < needleSegments.Length; i++)
{
var (start, end) = needleSegments[i];
var cur = GetRawScore(value, start, end);
if (cur == 0)
return 0;
total += cur;
}
return total;
}
return 8;
}
public int MatchesAny(params string[] values)
{
var max = 0;
for (var i = 0; i < values.Length; i++)
{
var cur = Matches(values[i]);
if (cur > max)
max = cur;
}
return max;
}
private int GetRawScore(ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
{
var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd);
if (startPos < 0)
return 0;
var needleSize = needleEnd - needleStart + 1;
var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
(startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd);
var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
return int.Max(score, revScore);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches)
{
var score = 100
+ needleSize * 3
+ borderMatches * 3
+ consecutive * 5
- startPos
- gaps * 2;
if (startPos == 0)
score += 5;
return score < 1 ? 1 : score;
}
private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward(
ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
{
var needleIndex = needleStart;
var lastMatchIndex = -10;
var startPos = 0;
var gaps = 0;
var consecutive = 0;
var borderMatches = 0;
for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++)
{
if (haystack[haystackIndex] == needleString[needleIndex])
{
if (IsBorderMatching)
{
if (haystackIndex > 0)
{
if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
borderMatches++;
}
}
needleIndex++;
if (haystackIndex == lastMatchIndex + 1)
consecutive++;
if (needleIndex > needleEnd)
return (startPos, gaps, consecutive, borderMatches, haystackIndex);
lastMatchIndex = haystackIndex;
}
else
{
if (needleIndex > needleStart)
gaps++;
else
startPos++;
}
}
return (-1, 0, 0, 0, 0);
}
private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse(
ReadOnlySpan<char> haystack, int haystackLastMatchIndex, int needleStart, int needleEnd)
{
var needleIndex = needleEnd;
var revLastMatchIndex = haystack.Length + 10;
var gaps = 0;
var consecutive = 0;
var borderMatches = 0;
for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--)
{
if (haystack[haystackIndex] == needleString[needleIndex])
{
if (IsBorderMatching)
{
if (haystackIndex > 0)
{
if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
borderMatches++;
}
}
needleIndex--;
if (haystackIndex == revLastMatchIndex - 1)
consecutive++;
if (needleIndex < needleStart)
return (haystackIndex, gaps, consecutive, borderMatches);
revLastMatchIndex = haystackIndex;
}
else
gaps++;
}
return (-1, 0, 0, 0);
}
}
internal enum MatchMode
{
Simple,
Fuzzy,
FuzzyParts,
}
+22 -4
View File
@@ -20,6 +20,24 @@ public static unsafe class Gearsets
public const int ParamCraftsmanship = 70;
public const int ParamControl = 71;
private static readonly int[] LevelToCLvlLUT;
static Gearsets()
{
LevelToCLvlLUT = new int[90];
for (uint i = 0; i < 80; ++i) {
var level = i + 1;
LevelToCLvlLUT[i] = LuminaSheets.ParamGrowSheet.GetRow(level)!.CraftingLevel;
}
for (var i = 80; i < 90; ++i)
{
var level = i + 1;
LevelToCLvlLUT[i] = (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == level).RowId;
}
}
public static void Initialize() { }
public static GearsetItem[] GetGearsetItems(InventoryContainer* container)
{
var items = new GearsetItem[(int)container->Size];
@@ -128,10 +146,10 @@ public static unsafe class Gearsets
public static bool IsSplendorousTool(GearsetItem item) =>
LuminaSheets.ItemSheetEnglish.GetRow(item.itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal);
public static int CalculateCLvl(int characterLevel) =>
characterLevel <= 80
? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel
: (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId;
public static int CalculateCLvl(int level) =>
(level > 0 && level <= 90) ?
LevelToCLvlLUT[level - 1] :
throw new ArgumentOutOfRangeException(nameof(level), level, "Level is out of range.");
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265
private static int CalculateParamCap(Item item, int paramId)
+16 -1
View File
@@ -1,6 +1,6 @@
using Craftimizer.Plugin;
using Dalamud.Interface.Internal;
using ImGuiScene;
using Dalamud.Plugin.Services;
using System;
using System.Collections.Generic;
using System.IO;
@@ -11,6 +11,7 @@ namespace Craftimizer.Utils;
public sealed class IconManager : IDisposable
{
private readonly Dictionary<uint, IDalamudTextureWrap> iconCache = new();
private readonly Dictionary<uint, IDalamudTextureWrap> hqIconCache = new();
private readonly Dictionary<string, IDalamudTextureWrap> textureCache = new();
private readonly Dictionary<string, IDalamudTextureWrap> assemblyCache = new();
@@ -22,6 +23,16 @@ public sealed class IconManager : IDisposable
return ret;
}
public IDalamudTextureWrap GetHqIcon(uint id, bool isHq = true)
{
if (!isHq)
return GetIcon(id);
if (!hqIconCache.TryGetValue(id, out var ret))
hqIconCache.Add(id, ret = Service.TextureProvider.GetIcon(id, ITextureProvider.IconFlags.HiRes | ITextureProvider.IconFlags.ItemHighQuality) ??
throw new ArgumentException($"Invalid hq icon id {id}", nameof(id)));
return ret;
}
public IDalamudTextureWrap GetTexture(string path)
{
if (!textureCache.TryGetValue(path, out var ret))
@@ -55,6 +66,10 @@ public sealed class IconManager : IDisposable
image.Dispose();
iconCache.Clear();
foreach (var image in hqIconCache.Values)
image.Dispose();
hqIconCache.Clear();
foreach (var image in textureCache.Values)
image.Dispose();
textureCache.Clear();
+152
View File
@@ -0,0 +1,152 @@
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using ImGuiNET;
using System;
using System.Collections.Generic;
namespace Craftimizer.Utils;
public static class MacroCopy
{
private const ClassJob DefaultJob = ClassJob.Carpenter;
private const int MacroSize = 15;
public static void Copy(IReadOnlyList<ActionType> actions)
{
if (actions.Count == 0)
{
Service.PluginInterface.UiBuilder.AddNotification("Could not copy macro. It's empty!", "Craftimizer Macro Not Copied", NotificationType.Error);
return;
}
var config = Service.Configuration.MacroCopy;
var macros = new List<string>();
var s = new List<string>();
foreach(var action in actions)
{
if (s.Count == 0)
{
if (config.UseMacroLock)
s.Add("/mlock");
}
s.Add(GetActionCommand(action, config));
if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro)
{
if (s.Count == MacroSize - 1)
{
if (GetEndCommand(macros.Count, true, config) is { } endCommand)
s.Add(endCommand);
}
if (s.Count == MacroSize)
{
macros.Add(string.Join("\n", s));
s.Clear();
}
}
}
if (s.Count > 0)
{
if (GetEndCommand(macros.Count, true, config) is { } endCommand)
s.Add(endCommand);
macros.Add(string.Join("\n", s));
}
switch (config.Type)
{
case MacroCopyConfiguration.CopyType.OpenWindow:
Service.Plugin.OpenMacroClipboard(macros);
break;
case MacroCopyConfiguration.CopyType.CopyToMacro:
CopyToMacro(macros, config);
break;
case MacroCopyConfiguration.CopyType.CopyToClipboard:
CopyToClipboard(macros, config);
break;
}
}
private static string GetActionCommand(ActionType action, MacroCopyConfiguration config)
{
var actionBase = action.Base();
if (actionBase is BaseComboAction)
throw new ArgumentException("Combo actions are not supported", nameof(action));
if (config.Type != MacroCopyConfiguration.CopyType.CopyToMacro && config.RemoveWaitTimes)
return $"/ac \"{action.GetName(DefaultJob)}\"";
else
return $"/ac \"{action.GetName(DefaultJob)}\" <wait.{actionBase.MacroWaitTime}>";
}
private static string? GetEndCommand(int macroIdx, bool isEnd, MacroCopyConfiguration config)
{
if (config.UseNextMacro && !isEnd)
{
if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro && config.CopyDown)
return $"/nextmacro down";
else
return $"/nextmacro";
}
if (config.AddNotification)
{
if (isEnd)
{
if (config.AddNotificationSound)
return $"/echo Craft complete! <se.{config.EndNotificationSound}>";
else
return $"/echo Craft complete!";
}
else
{
if (config.AddNotificationSound)
return $"/echo Macro #{macroIdx + 1} complete! <se.{config.IntermediateNotificationSound}>";
else
return $"/echo Macro #{macroIdx + 1} complete!";
}
}
return null;
}
private static void CopyToMacro(List<string> macros, MacroCopyConfiguration config)
{
int i, macroIdx;
for (
i = 0, macroIdx = config.StartMacroIdx;
i < macros.Count && i < config.MaxMacroCount && macroIdx < 100;
i++, macroIdx += config.CopyDown ? 10 : 1)
SetMacro(macroIdx, config.SharedMacro, macros[i]);
Service.PluginInterface.UiBuilder.AddNotification(i > 1 ? "Copied macro to User Macros." : $"Copied {i} macros to User Macros.", "Craftimizer Macro Copied", NotificationType.Success);
if (i < macros.Count)
{
Service.Plugin.OpenMacroClipboard(macros);
var rest = macros.Count - i;
Service.PluginInterface.UiBuilder.AddNotification($"Couldn't copy {rest} macro{(rest == 1 ? "" : "s")}, so a window was opened with all of them.", "Craftimizer Macro Copied", NotificationType.Info);
}
}
private static unsafe void SetMacro(int idx, bool isShared, string macroText)
{
if (idx >= 100 || idx < 0)
throw new ArgumentOutOfRangeException(nameof(idx), "Macro index must be between 0 and 99");
var module = RaptureMacroModule.Instance();
var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx);
var text = Utf8String.FromString(macroText);
module->ReplaceMacroLines(macro, text);
text->Dtor();
IMemorySpace.Free(text);
}
private static void CopyToClipboard(List<string> macros, MacroCopyConfiguration config)
{
ImGui.SetClipboardText(string.Join("\n\n", macros));
Service.PluginInterface.UiBuilder.AddNotification(macros.Count > 1 ? "Copied macro to clipboard." : $"Copied {macros.Count} macros to clipboard.", "Craftimizer Macro Copied", NotificationType.Success);
}
}
+82
View File
@@ -0,0 +1,82 @@
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Linq;
using ClassJob = Craftimizer.Simulator.ClassJob;
namespace Craftimizer.Utils;
public sealed record RecipeData
{
public ushort RecipeId { get; }
public Recipe Recipe { get; }
public RecipeLevelTable Table { get; }
public ClassJob ClassJob { get; }
public RecipeInfo RecipeInfo { get; }
public IReadOnlyList<(Item Item, int Amount)> Ingredients { get; }
public int MaxStartingQuality { get; }
private int TotalHqILvls { get; }
public RecipeData(ushort recipeId)
{
RecipeId = recipeId;
Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ??
throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId));
Table = Recipe.RecipeLevelTable.Value!;
ClassJob = (ClassJob)Recipe.CraftType.Row;
RecipeInfo = new()
{
IsExpert = Recipe.IsExpert,
ClassJobLevel = Table.ClassJobLevel,
RLvl = (int)Table.RowId,
ConditionsFlag = Table.ConditionsFlag,
MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100,
MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0,
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
QualityModifier = Table.QualityModifier,
QualityDivider = Table.QualityDivider,
ProgressModifier = Table.ProgressModifier,
ProgressDivider = Table.ProgressDivider,
};
Ingredients = Recipe.UnkData5.Take(6)
.Where(i => i != null && i.ItemIngredient != 0)
.Select(i => (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)!, (int)i.AmountIngredient))
.Where(i => i.Item1 != null).ToList();
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f);
TotalHqILvls = (int)Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.Row * i.Amount);
}
public int CalculateItemStartingQuality(int itemIdx, int amount)
{
if (itemIdx >= Ingredients.Count)
throw new ArgumentOutOfRangeException(nameof(itemIdx));
var ingredient = Ingredients[itemIdx];
if (amount > ingredient.Amount)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!ingredient.Item.CanBeHq)
return 0;
var iLvls = ingredient.Item.LevelItem.Row * amount;
return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality);
}
public int CalculateStartingQuality(IEnumerable<int> hqQuantities)
{
if (TotalHqILvls == 0)
return 0;
var iLvls = Ingredients.Zip(hqQuantities).Sum(i => i.First.Item.LevelItem.Row * i.Second);
return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality);
}
}
-203
View File
@@ -1,203 +0,0 @@
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Dalamud.Game;
using Dalamud.Logging;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Linq;
using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob;
using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote;
namespace Craftimizer.Utils;
public record RecipeData
{
public ushort RecipeId { get; }
public Recipe Recipe { get; }
public RecipeLevelTable Table { get; }
public ClassJob ClassJob { get; }
public RecipeInfo RecipeInfo { get; }
public int HQIngredientCount { get; }
public int MaxStartingQuality { get; }
public RecipeData(ushort recipeId)
{
RecipeId = recipeId;
Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ??
throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId));
Table = Recipe.RecipeLevelTable.Value!;
ClassJob = (ClassJob)Recipe.CraftType.Row;
RecipeInfo = new()
{
IsExpert = Recipe.IsExpert,
ClassJobLevel = Table.ClassJobLevel,
RLvl = (int)Table.RowId,
ConditionsFlag = Table.ConditionsFlag,
MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100,
MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0,
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
QualityModifier = Table.QualityModifier,
QualityDivider = Table.QualityDivider,
ProgressModifier = Table.ProgressModifier,
ProgressDivider = Table.ProgressDivider,
};
HQIngredientCount = Recipe.UnkData5
.Where(i =>
i != null &&
i.ItemIngredient != 0 &&
(LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false)
).Sum(i => i.AmountIngredient);
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f);
}
}
public sealed unsafe class RecipeNote : IDisposable
{
public AddonRecipeNote* AddonRecipe { get; private set; }
public AddonSynthesis* AddonSynthesis { get; private set; }
public bool IsCrafting { get; private set; }
public ushort RecipeId { get; private set; }
public Recipe Recipe { get; private set; } = null!;
public bool HasValidRecipe { get; private set; }
public RecipeLevelTable Table { get; private set; } = null!;
public RecipeInfo Info { get; private set; } = null!;
public ClassJob ClassJob { get; private set; }
public short CharacterLevel { get; private set; }
public bool CanUseManipulation { get; private set; }
public int HQIngredientCount { get; private set; }
public int MaxStartingQuality { get; private set; }
public RecipeNote()
{
Service.Framework.Update += FrameworkUpdate;
}
private void FrameworkUpdate(IFramework f)
{
HasValidRecipe = false;
try
{
HasValidRecipe = Update();
}
catch (Exception e)
{
Log.Error(e, "RecipeNote framework update failed");
}
}
public bool Update()
{
if (Service.ClientState.LocalPlayer == null)
return false;
AddonRecipe = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote");
AddonSynthesis = (AddonSynthesis*)Service.GameGui.GetAddonByName("Synthesis");
var recipeId = GetRecipeIdFromList();
if (recipeId == null)
{
recipeId = GetRecipeIdFromAgent();
if (recipeId == null)
return false;
else
IsCrafting = true;
}
else
IsCrafting = false;
var isNewRecipe = RecipeId != recipeId.Value;
RecipeId = recipeId.Value;
var recipe = LuminaSheets.RecipeSheet.GetRow(RecipeId);
if (recipe == null)
return false;
Recipe = recipe;
if (isNewRecipe)
CalculateStats();
return true;
}
private static ushort? GetRecipeIdFromList()
{
var instance = CSRecipeNote.Instance();
var list = instance->RecipeList;
if (list == null)
return null;
var recipeEntry = list->SelectedRecipe;
if (recipeEntry == null)
return null;
return recipeEntry->RecipeId;
}
private static ushort? GetRecipeIdFromAgent()
{
var instance = AgentRecipeNote.Instance();
var recipeId = instance->ActiveCraftRecipeId;
if (recipeId == 0)
return null;
return (ushort)recipeId;
}
private void CalculateStats()
{
Table = Recipe.RecipeLevelTable.Value!;
Info = CreateInfo();
ClassJob = (ClassJob)Recipe.CraftType.Row;
CharacterLevel = PlayerState.Instance()->ClassJobLevelArray[ClassJob.GetExpArrayIdx()];
CanUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(ClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address);
HQIngredientCount = Recipe.UnkData5
.Where(i =>
i != null &&
i.ItemIngredient != 0 &&
(LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false)
).Sum(i => i.AmountIngredient);
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * Info.MaxQuality / 100f);
}
private RecipeInfo CreateInfo() =>
new()
{
IsExpert = Recipe.IsExpert,
ClassJobLevel = Table.ClassJobLevel,
RLvl = (int)Table.RowId,
ConditionsFlag = Table.ConditionsFlag,
MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100,
MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100,
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
QualityModifier = Table.QualityModifier,
QualityDivider = Table.QualityDivider,
ProgressModifier = Table.ProgressModifier,
ProgressDivider = Table.ProgressDivider,
};
public void Dispose()
{
Service.Framework.Update -= FrameworkUpdate;
}
}
+13 -5
View File
@@ -1,15 +1,16 @@
using Dalamud.Game.Text;
using Dalamud.Game.Text;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Craftimizer.Utils;
public static class SqText
{
private static ReadOnlyDictionary<char, SeIconChar> levelNumReplacements = new(new Dictionary<char, SeIconChar>
public static SeIconChar LevelPrefix => SeIconChar.LevelEn;
public static readonly ReadOnlyDictionary<char, SeIconChar> LevelNumReplacements = new(new Dictionary<char, SeIconChar>
{
['0'] = SeIconChar.Number0,
['1'] = SeIconChar.Number1,
@@ -26,8 +27,15 @@ public static class SqText
public static string ToLevelString<T>(T value) where T : IBinaryInteger<T>
{
var str = value.ToString() ?? throw new FormatException("Failed to format value");
foreach(var (k, v) in levelNumReplacements)
foreach(var (k, v) in LevelNumReplacements)
str = str.Replace(k, v.ToIconChar());
return SeIconChar.LevelEn.ToIconChar() + str;
return str;
}
public static bool TryParseLevelString(string str, out int result)
{
foreach(var (k, v) in LevelNumReplacements)
str = str.Replace(v.ToIconChar(), k);
return int.TryParse(str, out result);
}
}
-226
View File
@@ -1,226 +0,0 @@
using Craftimizer.Plugin.Utils;
using Craftimizer.Utils;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET;
using System;
using System.Numerics;
namespace Craftimizer.Plugin.Windows;
public sealed unsafe partial class Craft : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
| ImGuiWindowFlags.AlwaysAutoResize
| ImGuiWindowFlags.NoSavedSettings
| ImGuiWindowFlags.NoFocusOnAppearing
| ImGuiWindowFlags.NoNavFocus;
private const float WindowWidth = 300;
private const int ActionsPerRow = 5;
private static readonly Vector2 CraftProgressBarSize = new(WindowWidth, 15);
private static Configuration Config => Service.Configuration;
private static Random Random { get; } = new();
private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote;
private bool WasOpen { get; set; }
public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true)
{
Service.WindowSystem.AddWindow(this);
Service.Plugin.Hooks.OnActionUsed += OnActionUsed;
IsOpen = true;
}
public override void Draw()
{
SolveTick();
DequeueSolver();
DrawActions();
ImGui.SameLine(0, 0);
ImGui.Dummy(default);
ImGuiHelpers.ScaledDummy(5);
Simulator.DrawAllProgressBars(SolverLatestState, CraftProgressBarSize);
ImGuiHelpers.ScaledDummy(5);
ImGui.PushFont(UiBuilder.IconFont);
var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X + (ImGui.GetStyle().FramePadding.X * 2);
ImGui.PopFont();
DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGui.GetFrameHeight()));
ImGui.SameLine();
if (ImGuiComponents.IconButton("synthSettingsButton", FontAwesomeIcon.Cog))
Service.Plugin.OpenSettingsTab(Settings.TabSynthHelper);
}
private void DrawActions()
{
var actionSize = new Vector2((WindowWidth / ActionsPerRow) - (ImGui.GetStyle().ItemSpacing.X * ((ActionsPerRow - 1f) / ActionsPerRow)));
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
ImGui.Dummy(new(0, actionSize.Y));
ImGui.SameLine(0, 0);
for (var i = 0; i < SolverActions.Count; ++i)
{
var (action, tooltip, state) = SolverActions[i];
ImGui.PushID(i);
if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0))
{
if (i == 0)
Chat.SendMessage($"/ac \"{action.GetName(RecipeUtils.ClassJob)}\"");
}
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text($"{action.GetName(RecipeUtils.ClassJob)}\n{tooltip}");
Simulator.DrawAllProgressTooltips(state);
if (i == 0)
ImGui.Text("Click to Execute");
ImGui.EndTooltip();
}
ImGui.PopID();
if (i % ActionsPerRow != (ActionsPerRow - 1))
ImGui.SameLine();
}
ImGui.PopStyleColor(3);
}
private void DrawSolveButton(Vector2 buttonSize)
{
string buttonText;
string tooltipText;
bool isEnabled;
var taskCompleted = SolverTask?.IsCompleted ?? true;
var taskCancelled = SolverTaskToken?.IsCancellationRequested ?? false;
if (!taskCompleted)
{
if (taskCancelled)
{
buttonText = "Cancelling...";
tooltipText = "Cancelling action generation. This shouldn't take long.";
isEnabled = false;
}
else
{
buttonText = "Cancel";
tooltipText = "Cancel action generation";
isEnabled = true;
}
}
else
{
buttonText = "Retry";
tooltipText = "Retry and regenerate a new set of recommended actions to finish the craft.";
isEnabled = true;
}
ImGui.BeginDisabled(!isEnabled);
if (ImGui.Button(buttonText, buttonSize))
{
if (!taskCompleted)
{
if (!taskCancelled)
SolverTaskToken?.Cancel();
}
else
QueueSolve(GetNextState()!.Value);
}
ImGui.EndDisabled();
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(tooltipText);
}
public override void PreDraw()
{
var addon = RecipeUtils.AddonSynthesis;
ref var unit = ref addon->AtkUnitBase;
var scale = unit.Scale;
var pos = new Vector2(unit.X, unit.Y);
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
var node = unit.GetNodeById(79);
Position = pos + new Vector2(size.X, node->Y * scale);
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new(-1),
MaximumSize = new(10000, 10000)
};
if (Input == null)
return;
base.PreDraw();
}
private bool DrawConditionsInner()
{
if (!RecipeUtils.HasValidRecipe)
return false;
if (!RecipeUtils.IsCrafting)
return false;
if (RecipeUtils.AddonSynthesis == null)
return false;
// Check if Synthesis addon is visible
if (RecipeUtils.AddonSynthesis->AtkUnitBase.WindowNode == null)
return false;
if (RecipeUtils.AddonSynthesis->AtkUnitBase.GetNodeById(79) == null)
return false;
return base.DrawConditions();
}
public override bool DrawConditions()
{
if (!Config.EnableSynthHelper)
return false;
var ret = DrawConditionsInner();
if (ret && !WasOpen)
ResetSimulation();
WasOpen = ret;
return ret;
}
private void ResetSimulation()
{
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
if (container == null)
return;
CharacterStats = Gearsets.CalculateCharacterStats(Gearsets.CalculateGearsetCurrentStats(), Gearsets.GetGearsetItems(container), RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation);
Input = new(CharacterStats, RecipeUtils.Info, 0, Random);
ActionCount = 0;
ActionStates = new();
}
public void Dispose()
{
StopSolve();
SolverTaskToken?.Cancel();
SolverTask?.TryWait();
SolverTask?.Dispose();
SolverTaskToken?.Dispose();
Service.Plugin.Hooks.OnActionUsed -= OnActionUsed;
}
}
-157
View File
@@ -1,157 +0,0 @@
using Craftimizer.Simulator;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Windowing;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Craftimizer.Plugin.Windows;
public sealed unsafe partial class Craft : Window, IDisposable
{
// State variables, manually kept track of outside of the addon
private CharacterStats CharacterStats = null!;
private SimulationInput Input = null!;
private int ActionCount;
private ActionStates ActionStates;
private sealed class AddonValues
{
public AddonSynthesis* Addon { get; }
public AtkValue* Values => Addon->AtkUnitBase.AtkValues;
public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount;
public AddonValues(AddonSynthesis* addon)
{
Addon = addon;
if (ValueCount != 26)
throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon));
}
public unsafe AtkValue* this[int i] => Values + i;
// Always 0?
private uint Unk0 => GetUInt(0);
// Always true?
private bool Unk1 => GetBool(1);
public SeString ItemName => GetString(2);
public uint ItemIconId => GetUInt(3);
public uint ItemCount => GetUInt(4);
public uint Progress => GetUInt(5);
public uint MaxProgress => GetUInt(6);
public uint Durability => GetUInt(7);
public uint MaxDurability => GetUInt(8);
public uint Quality => GetUInt(9);
public uint HQChance => GetUInt(10);
private uint IsShowingCollectibleInfoValue => GetUInt(11);
private uint ConditionValue => GetUInt(12);
public SeString ConditionName => GetString(13);
public SeString ConditionNameAndTooltip => GetString(14);
public uint StepCount => GetUInt(15);
public uint ResultItemId => GetUInt(16);
public uint MaxQuality => GetUInt(17);
public uint RequiredQuality => GetUInt(18);
private uint IsCollectibleValue => GetUInt(19);
public uint Collectability => GetUInt(20);
public uint MaxCollectability => GetUInt(21);
public uint CollectabilityCheckpoint1 => GetUInt(22);
public uint CollectabilityCheckpoint2 => GetUInt(23);
public uint CollectabilityCheckpoint3 => GetUInt(24);
public bool IsExpertRecipe => GetBool(25);
public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0;
public Condition Condition => (Condition)(1 << (int)ConditionValue);
public bool IsCollectible => IsCollectibleValue != 0;
private uint GetUInt(int i)
{
var value = this[i];
return value->Type == ValueType.UInt ?
value->UInt :
throw new ArgumentException($"Value {i} is not a uint", nameof(i));
}
private bool GetBool(int i)
{
var value = this[i];
return value->Type == ValueType.Bool ?
value->Byte != 0 :
throw new ArgumentException($"Value {i} is not a boolean", nameof(i));
}
private SeString GetString(int i)
{
var value = this[i];
return value->Type switch
{
ValueType.AllocatedString or
ValueType.String =>
MemoryHelper.ReadSeStringNullTerminated((nint)value->String),
_ => throw new ArgumentException($"Value {i} is not a string", nameof(i))
};
}
}
private const ushort StatusInnerQuiet = 251;
private const ushort StatusWasteNot = 252;
private const ushort StatusVeneration = 2226;
private const ushort StatusGreatStrides = 254;
private const ushort StatusInnovation = 2189;
private const ushort StatusFinalAppraisal = 2190;
private const ushort StatusWasteNot2 = 257;
private const ushort StatusMuscleMemory = 2191;
private const ushort StatusManipulation = 1164;
private const ushort StatusHeartAndSoul = 2665;
private SimulationState GetAddonSimulationState()
{
var player = Service.ClientState.LocalPlayer!;
var values = new AddonValues(RecipeUtils.AddonSynthesis);
var statusManager = ((Character*)player.Address)->GetStatusManager();
byte GetEffectStack(ushort id)
{
foreach (var status in statusManager->StatusSpan)
if (status.StatusID == id)
return status.StackCount;
return 0;
}
bool HasEffect(ushort id)
{
foreach (var status in statusManager->StatusSpan)
if (status.StatusID == id)
return true;
return false;
}
return new(Input)
{
ActionCount = ActionCount,
StepCount = (int)values.StepCount - 1,
Progress = (int)values.Progress,
Quality = (int)values.Quality,
Durability = (int)values.Durability,
CP = (int)player.CurrentCp,
Condition = values.Condition,
ActiveEffects = new()
{
InnerQuiet = GetEffectStack(StatusInnerQuiet),
WasteNot = GetEffectStack(StatusWasteNot),
Veneration = GetEffectStack(StatusVeneration),
GreatStrides = GetEffectStack(StatusGreatStrides),
Innovation = GetEffectStack(StatusInnovation),
FinalAppraisal = GetEffectStack(StatusFinalAppraisal),
WasteNot2 = GetEffectStack(StatusWasteNot2),
MuscleMemory = GetEffectStack(StatusMuscleMemory),
Manipulation = GetEffectStack(StatusManipulation),
HeartAndSoul = HasEffect(StatusHeartAndSoul),
},
ActionStates = ActionStates
};
}
}
-99
View File
@@ -1,99 +0,0 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Dalamud.Interface.Windowing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Craftimizer.Plugin.Windows;
public sealed unsafe partial class Craft : Window, IDisposable
{
private SimulationState? SolverState { get; set; }
private Solver.Solver? SolverTask { get; set; }
private CancellationTokenSource? SolverTaskToken { get; set; }
private ConcurrentQueue<ActionType> SolverActionQueue { get; } = new();
// State is the state of the simulation *after* its corresponding action is executed.
private List<(ActionType Action, string Tooltip, SimulationState State)> SolverActions { get; } = new();
private SimulatorNoRandom SolverSim { get; set; } = null!;
private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State;
private void StopSolve()
{
if (SolverTask == null || SolverTaskToken == null)
return;
if (!SolverTask.IsCompleted)
SolverTaskToken.Cancel();
else
{
SolverTaskToken.Dispose();
SolverTask.Dispose();
SolverTask = null;
SolverTaskToken = null;
}
}
private void QueueSolve(SimulationState state)
{
StopSolve();
SolverActionQueue.Clear();
SolverActions.Clear();
SolverState = state;
SolverSim = new(state);
SolverTaskToken = new();
SolverTask = new(Config.SynthHelperSolverConfig, state) { Token = SolverTaskToken.Token };
SolverTask.OnLog += s => Log.Debug(s);
SolverTask.OnNewAction += SolverActionQueue.Enqueue;
SolverTask.Start();
}
private void SolveTick()
{
var newState = GetNextState();
if (SolverState == newState)
return;
if (newState == null)
StopSolve();
else
QueueSolve(newState.Value);
}
private void DequeueSolver()
{
while (SolverActionQueue.TryDequeue(out var poppedAction))
AppendSolverAction(poppedAction);
}
private void AppendSolverAction(ActionType action)
{
var actionBase = action.Base();
if (actionBase is BaseComboAction comboActionBase)
{
AppendSolverAction(comboActionBase.ActionTypeA);
AppendSolverAction(comboActionBase.ActionTypeB);
}
else
{
if (SolverActions.Count >= Config.SynthHelperStepCount)
{
StopSolve();
return;
}
var tooltip = actionBase.GetTooltip(SolverSim, false);
var (_, state) = SolverSim.Execute(SolverLatestState, action);
SolverActions.Add((action, tooltip, state));
if (SolverActions.Count >= Config.SynthHelperStepCount)
StopSolve();
}
}
}
-81
View File
@@ -1,81 +0,0 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Interface.Windowing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Craftimizer.Plugin.Windows;
public sealed unsafe partial class Craft : Window, IDisposable
{
private ConcurrentQueue<ActionType> UsedActionQueue { get; set; } = new();
private IEnumerator<SimulationState>? StateTicker { get; set; }
private SimulationState? GetNextState()
{
if (RecipeUtils.IsCrafting && StateTicker == null)
StateTicker = TickState();
if (!RecipeUtils.IsCrafting && StateTicker != null)
StateTicker = null;
if (StateTicker == null)
return null;
StateTicker.MoveNext();
return StateTicker.Current;
}
private IEnumerator<SimulationState> TickState()
{
while (true)
{
SimulationState state;
// Dequeue used actions
var sim = new SimulatorNoRandom(new());
while (true)
{
state = GetAddonSimulationState();
var dequeued = false;
while (UsedActionQueue.TryDequeue(out var action))
{
dequeued = true;
(_, state) = sim.Execute(state, action);
ActionCount++;
ActionStates.MutateState(action.Base());
}
if (dequeued)
break;
// If nothing is dequeued and executed, just return the addon state
yield return state;
}
// Intermediate state, wait for addon change
var intermediateState = GetAddonSimulationState();
while (true)
{
yield return state;
var newState = GetAddonSimulationState();
if (!IsStateInIntermediate(newState, intermediateState))
break;
}
}
}
private static bool IsStateInIntermediate(SimulationState a, SimulationState b)
{
b.CP = a.CP;
b.ActiveEffects = a.ActiveEffects;
return a == b;
}
private void OnActionUsed(ActionType action)
{
if (!RecipeUtils.IsCrafting || RecipeUtils.AddonSynthesis == null)
return;
UsedActionQueue.Enqueue(action);
}
}
+79
View File
@@ -0,0 +1,79 @@
using Craftimizer.Plugin;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Linq;
namespace Craftimizer.Windows;
public sealed class MacroClipboard : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
private List<string> Macros { get; }
public MacroClipboard(IEnumerable<string> macros) : base("Macro Clipboard", WindowFlags)
{
Macros = new(macros);
IsOpen = true;
Service.WindowSystem.AddWindow(this);
}
public override void Draw()
{
var idx = 0;
foreach(var macro in Macros)
DrawMacro(idx++, macro);
}
private void DrawMacro(int idx, string macro)
{
using var id = ImRaii.PushId(idx);
using var panel = ImGuiUtils.GroupPanel($"Macro {idx + 1}", -1, out var availWidth);
var cursor = ImGui.GetCursorPos();
ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth);
var buttonCursor = ImGui.GetCursorPos();
ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight()));
var buttonHovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left];
var buttonClicked = buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left];
ImGui.SetCursorPos(buttonCursor);
{
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered);
ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(ImGui.GetFrameHeight()));
if (buttonClicked)
{
ImGui.SetClipboardText(macro);
Service.PluginInterface.UiBuilder.AddNotification($"Macro {idx + 1} copied to clipboard.", "Craftimizer Macro Copied", NotificationType.Success);
}
}
if (buttonHovered)
ImGui.SetTooltip("Copy to Clipboard");
ImGui.SetCursorPos(cursor);
{
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero);
var lineCount = macro.Count(c => c == '\n') + 1;
ImGui.InputTextMultiline("", ref macro, (uint)macro.Length + 1, new(availWidth, ImGui.GetTextLineHeight() * Math.Max(15, lineCount) + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll);
}
if (buttonHovered)
ImGui.SetMouseCursor(ImGuiMouseCursor.Arrow);
}
public void Dispose()
{
Service.WindowSystem.RemoveWindow(this);
}
}
File diff suppressed because it is too large Load Diff
+361
View File
@@ -0,0 +1,361 @@
using Craftimizer.Plugin;
using Craftimizer.Utils;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using System;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Sim = Craftimizer.Simulator.SimulatorNoRandom;
using Dalamud.Interface.Utility;
namespace Craftimizer.Windows;
public sealed class MacroList : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
public CharacterStats? CharacterStats { get; private set; }
public RecipeData? RecipeData { get; private set; }
private MacroEditor? EditorWindow { get; set; }
private IReadOnlyList<Macro> Macros => Service.Configuration.Macros;
private Dictionary<Macro, SimulationState> MacroStateCache { get; } = new();
public MacroList() : base("Craftimizer Macro List", WindowFlags, false)
{
RefreshSearch();
Macro.OnMacroChanged += OnMacroChanged;
Configuration.OnMacroListChanged += OnMacroListChanged;
CollapsedCondition = ImGuiCond.Appearing;
Collapsed = false;
SizeConstraints = new() { MinimumSize = new(500, 520), MaximumSize = new(float.PositiveInfinity) };
Service.WindowSystem.AddWindow(this);
}
public override bool DrawConditions()
{
return Service.ClientState.LocalPlayer != null;
}
public override void PreDraw()
{
var oldCharacterStats = CharacterStats;
var oldRecipeData = RecipeData;
EditorWindow = Service.Plugin.EditorWindow;
EditorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null;
RecipeData = EditorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData;
CharacterStats = EditorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats;
if (oldCharacterStats != CharacterStats || oldRecipeData != RecipeData)
RecalculateStats();
}
public override void Draw()
{
DrawSearchBar();
using var group = ImRaii.Child("macros", new(-1, -1));
if (sortedMacros.Count > 0)
{
var macros = new List<Macro>(sortedMacros);
foreach (var macro in macros)
DrawMacro(macro);
}
else
{
var text1 = "You have no macros! Create one by opening";
var text2 = "the Macro Editor here or from the Crafting Log.";
var text3 = "Open Crafting Log";
var text4 = "Open Macro Editor";
var buttonRowWidth = ImGui.CalcTextSize(text3).X + ImGui.CalcTextSize(text4).X + ImGui.GetStyle().ItemSpacing.X * 5;
var size = new Vector2(
Math.Max(
Math.Max(ImGui.CalcTextSize(text1).X, ImGui.CalcTextSize(text2).X),
buttonRowWidth
),
ImGui.GetTextLineHeightWithSpacing() * 2 + ImGui.GetFrameHeight()
);
ImGuiUtils.AlignMiddle(size);
using var child = ImRaii.Child("##macroMessage", size);
ImGuiUtils.TextCentered(text1);
ImGuiUtils.TextCentered(text2);
ImGuiUtils.AlignCentered(buttonRowWidth);
if (ImGui.Button(text3))
Service.Plugin.OpenCraftingLog();
ImGui.SameLine();
if (ImGui.Button(text4))
OpenEditor(null);
}
}
private string searchText = string.Empty;
private List<Macro> sortedMacros = null!;
private void DrawSearchBar()
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputTextWithHint("##search", "Search", ref searchText, 100))
RefreshSearch();
}
private void DrawMacro(Macro macro)
{
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
throw new InvalidOperationException("Combo actions should be sanitized away");
var stateNullable = GetMacroState(macro);
using var panel = ImGuiUtils.GroupPanel(macro.Name, -1, out var availWidth);
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
var spacing = ImGui.GetStyle().ItemSpacing.Y;
var miniRowHeight = (windowHeight - spacing) / 2f;
using var table = ImRaii.Table("table", stateNullable.HasValue ? 3 : 2, ImGuiTableFlags.BordersInnerV);
if (table)
{
if (stateNullable.HasValue)
ImGui.TableSetupColumn("stats", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch, 0);
ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight);
if (stateNullable is { } state)
{
ImGui.TableNextColumn();
if (Service.Configuration.ShowOptimalMacroStat)
{
var progressHeight = windowHeight;
if (state.Progress >= state.Input.Recipe.MaxProgress && state.Input.Recipe.MaxQuality > 0)
{
ImGuiUtils.ArcProgress(
(float)state.Quality / state.Input.Recipe.MaxQuality,
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}");
}
else
{
ImGuiUtils.ArcProgress(
(float)state.Progress / state.Input.Recipe.MaxProgress,
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}");
}
}
else
{
ImGuiUtils.ArcProgress(
(float)state.Progress / state.Input.Recipe.MaxProgress,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}");
ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress(
(float)state.Quality / state.Input.Recipe.MaxQuality,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}");
ImGuiUtils.ArcProgress((float)state.Durability / state.Input.Recipe.MaxDurability,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Durability));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}");
ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress(
(float)state.CP / state.Input.Stats.CP,
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.CP));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining CP: {state.CP} / {state.Input.Stats.CP}");
}
}
ImGui.TableNextColumn();
{
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight)))
Service.Plugin.CopyMacro(macro.Actions);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Copy to Clipboard");
ImGui.SameLine();
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, new(miniRowHeight)) && ImGui.GetIO().KeyShift)
Service.Configuration.RemoveMacro(macro);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Delete (Hold Shift)");
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.PencilAlt, new(miniRowHeight)))
ShowRenamePopup(macro);
DrawRenamePopup(macro);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Rename");
ImGui.SameLine();
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight)))
OpenEditor(macro);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Open in Simulator");
}
ImGui.TableNextColumn();
{
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
var itemCount = macro.Actions.Count;
for (var i = 0; i < itemsPerRow * 2; i++)
{
if (i % itemsPerRow != 0)
ImGui.SameLine(0, spacing);
if (i < itemCount)
{
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
if (!shouldShowMore)
{
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
}
else
{
var amtMore = itemCount - itemsPerRow * 2;
var pos = ImGui.GetCursorPos();
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
ImGui.SetCursorPos(pos);
ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f);
ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null);
}
}
else
ImGui.Dummy(new(miniRowHeight));
}
}
}
}
private string popupMacroName = string.Empty;
private Macro? popupMacro;
private void ShowRenamePopup(Macro macro)
{
ImGui.OpenPopup($"##renamePopup-{macro.GetHashCode()}");
popupMacro = macro;
popupMacroName = macro.Name;
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2));
}
private void DrawRenamePopup(Macro macro)
{
using var popup = ImRaii.Popup($"##renamePopup-{macro.GetHashCode()}");
if (popup)
{
if (ImGui.IsWindowAppearing())
ImGui.SetKeyboardFocusHere();
ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue))
{
if (!string.IsNullOrWhiteSpace(popupMacroName))
{
popupMacro!.Name = popupMacroName;
ImGui.CloseCurrentPopup();
}
}
}
}
private void RecalculateStats()
{
MacroStateCache.Clear();
}
private void RefreshSearch()
{
if (string.IsNullOrWhiteSpace(searchText))
{
sortedMacros = new(Macros);
return;
}
var matcher = new FuzzyMatcher(searchText.ToLowerInvariant(), MatchMode.FuzzyParts);
var query = Macros.AsParallel().Select(i => (Item: i, Score: matcher.Matches(i.Name.ToLowerInvariant())))
.Where(t => t.Score > 0)
.OrderByDescending(t => t.Score)
.Select(t => t.Item);
sortedMacros = query.ToList();
}
private void OpenEditor(Macro? macro)
{
var character = CharacterStats ?? new()
{
Craftsmanship = 100,
Control = 100,
CP = 200,
Level = 10,
CanUseManipulation = false,
HasSplendorousBuff = false,
IsSpecialist = false,
CLvl = 10,
};
var recipe = RecipeData ?? new(1023);
var buffs = EditorWindow?.Buffs ?? new(Service.Plugin.RecipeNoteWindow.CharacterStats != null ? Service.ClientState.LocalPlayer?.StatusList : null);
Service.Plugin.OpenMacroEditor(character, recipe, buffs, macro?.Actions ?? Enumerable.Empty<ActionType>(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null);
}
private void OnMacroChanged(Macro macro)
{
MacroStateCache.Remove(macro);
}
private void OnMacroListChanged()
{
RefreshSearch();
}
private SimulationState? GetMacroState(Macro macro)
{
if (CharacterStats == null || RecipeData == null)
return null;
if (MacroStateCache.TryGetValue(macro, out var state))
return state;
state = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo));
var sim = new Sim(state);
(_, state, _) = sim.ExecuteMultiple(state, macro.Actions);
return MacroStateCache[macro] = state;
}
public void Dispose()
{
Macro.OnMacroChanged -= OnMacroChanged;
Configuration.OnMacroListChanged -= OnMacroListChanged;
Service.WindowSystem.RemoveWindow(this);
}
}
+142 -84
View File
@@ -4,12 +4,14 @@ using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver;
using Craftimizer.Utils;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
@@ -59,6 +61,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
private CancellationTokenSource? BestMacroTokenSource { get; set; }
private Exception? BestMacroException { get; set; }
public (Macro, SimulationState)? BestSavedMacro { get; private set; }
public bool HasSavedMacro { get; private set; }
public SolverSolution? BestSuggestedMacro { get; private set; }
private IDalamudTextureWrap ExpertBadge { get; }
@@ -82,7 +85,21 @@ public sealed unsafe class RecipeNote : Window, IDisposable
IsOpen = true;
}
private bool wasOpen;
public override bool DrawConditions()
{
var isOpen = ShouldDraw();
if (isOpen != wasOpen)
{
if (wasOpen)
BestMacroTokenSource?.Cancel();
}
wasOpen = isOpen;
return isOpen;
}
private bool ShouldDraw()
{
if (Service.ClientState.LocalPlayer == null)
return false;
@@ -146,7 +163,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
statsChanged = true;
}
if (statsChanged && CraftStatus == CraftableStatus.OK)
if ((statsChanged || (BestMacroTokenSource?.IsCancellationRequested ?? false)) && CraftStatus == CraftableStatus.OK)
CalculateBestMacros();
return true;
@@ -172,16 +189,22 @@ public sealed unsafe class RecipeNote : Window, IDisposable
public override void Draw()
{
var availWidth = ImGui.GetContentRegionAvail().X;
using (var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedSame))
{
using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
if (table)
{
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableNextColumn();
DrawCharacterStats();
ImGui.TableNextColumn();
DrawRecipeStats();
// Ensure that we know the window should be the same size as this table. Any more and it'll grow slowly and won't shrink when it could
ImGui.SameLine(0, 0);
// The -1 is to account for the extra vertical separator on the right that ImGui draws for some reason
availWidth = ImGui.GetCursorPosX() - ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().CellPadding.X - 1;
}
}
@@ -190,35 +213,44 @@ public sealed unsafe class RecipeNote : Window, IDisposable
ImGui.Separator();
using (var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame))
{
using var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame);
if (table)
{
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered("Best Saved Macro");
if (BestSavedMacro is { } savedMacro)
availWidth -= ImGui.GetStyle().ItemSpacing.X * 2;
using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", availWidth, out _))
{
ImGuiUtils.TextCentered(savedMacro.Item1.Name);
DrawMacro("savedMacro", (savedMacro.Item1.Actions, savedMacro.Item2));
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
if (BestSavedMacro is { } savedMacro)
{
ImGuiUtils.TextCentered(savedMacro.Item1.Name, availWidth);
DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsAvailWidthOffset, true);
}
else
{
ImGui.Text("");
DrawMacro(null, null, stepsAvailWidthOffset, true);
}
}
else
using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", availWidth, out _))
{
ImGui.Text("");
DrawMacro("savedMacro", null);
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
if (BestSuggestedMacro is { } suggestedMacro)
DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsAvailWidthOffset, false);
else
DrawMacro(null, null, stepsAvailWidthOffset, false);
}
ImGui.Button("View Saved Macros", new(-1, 0));
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
ImGui.AlignTextToFramePadding();
ImGuiUtils.TextCentered("Suggested Macro");
if (BestSuggestedMacro is { } suggestedMacro)
DrawMacro("suggestedMacro", (suggestedMacro.Actions, suggestedMacro.State));
else
DrawMacro("suggestedMacro", null);
ImGui.Button("Open Simulator", new(-1, 0));
if (ImGui.Button("View Saved Macros", new(-1, 0)))
Service.Plugin.OpenMacroListWindow();
if (ImGui.Button("Open in Simulator", new(-1, 0)))
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty<ActionType>(), null);
}
}
}
@@ -237,7 +269,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
}
var levelText = string.Empty;
if (level != 0)
levelText = SqText.ToLevelString(level);
levelText = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(level);
var imageSize = ImGui.GetFrameHeight();
bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false;
if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob))
@@ -338,6 +370,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
}
else
ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets.");
ImGui.Dummy(default);
}
break;
case CraftableStatus.SpecialistRequired:
@@ -439,9 +472,9 @@ public sealed unsafe class RecipeNote : Window, IDisposable
var textStarsSize = Vector2.Zero;
if (!string.IsNullOrEmpty(textStars)) {
var layout = AxisFont.LayoutBuilder(textStars).Build();
textStarsSize = new(layout.Width + 3, layout.Height);
textStarsSize = new(layout.Width, layout.Height);
}
var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
var isExpert = RecipeData.RecipeInfo.IsExpert;
var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable;
var imageSize = ImGui.GetFrameHeight();
@@ -452,7 +485,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
ImGuiUtils.AlignCentered(
imageSize + 5 +
ImGui.CalcTextSize(textLevel).X +
textStarsSize.X +
(textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) +
(isCollectable ? badgeSize.X + 3 : 0) +
(isExpert ? badgeSize.X + 3 : 0)
);
@@ -516,16 +549,16 @@ public sealed unsafe class RecipeNote : Window, IDisposable
}
}
private void DrawMacro(string imGuiId, (List<ActionType> Actions, SimulationState State)? macroValue)
private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State)? macroValue, Action<IEnumerable<ActionType>>? setter, float stepsAvailWidthOffset, bool isSavedMacro)
{
//using var window = ImRaii.Child(imGuiId, new(-1, (name != null ? ImGui.GetTextLineHeightWithSpacing() : 0) + 2 * ImGui.GetFrameHeightWithSpacing()), false, ImGuiWindowFlags.AlwaysAutoResize);
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
if (macroValue == null)
if (macroValue is not { } macro)
{
if (BestMacroException == null)
ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X, windowHeight + 1 + ImGui.GetStyle().ItemSpacing.Y));
if (isSavedMacro && !HasSavedMacro)
ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
else if (BestMacroException == null)
ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
else
{
ImGui.AlignTextToFramePadding();
@@ -536,8 +569,8 @@ public sealed unsafe class RecipeNote : Window, IDisposable
}
return;
}
var macro = macroValue!.Value;
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
throw new InvalidOperationException("Combo actions should be sanitized away");
using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
if (table)
@@ -552,7 +585,6 @@ public sealed unsafe class RecipeNote : Window, IDisposable
var spacing = ImGui.GetStyle().ItemSpacing.Y;
var miniRowHeight = (windowHeight - spacing) / 2f;
//ImGui.Text($"{macro.Actions.Count}");
{
if (Service.Configuration.ShowOptimalMacroStat)
{
@@ -564,7 +596,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor));
ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
}
@@ -575,7 +607,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
progressHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor));
ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
}
@@ -587,7 +619,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor));
ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
@@ -597,7 +629,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor));
ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
@@ -605,7 +637,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.DurabilityColor));
ImGui.GetColorU32(Colors.Durability));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}");
@@ -615,7 +647,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f,
.5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.CPColor));
ImGui.GetColorU32(Colors.CP));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}");
}
@@ -623,31 +655,44 @@ public sealed unsafe class RecipeNote : Window, IDisposable
ImGui.TableNextColumn();
{
ImGuiUtils.TextMiddleNewLine($"{macro.Actions.Count}", new(miniRowHeight));
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight)))
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter);
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{macro.Actions.Count} Step{(macro.Actions.Count != 1 ? "s" : "")}");
using (var iconFont = ImRaii.PushFont(UiBuilder.IconFont))
if (ImGuiUtils.ButtonCentered(FontAwesomeIcon.Copy.ToIconString(), new(miniRowHeight)))
{
throw new NotImplementedException();
}
ImGui.SetTooltip("Open in Simulator");
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight)))
Service.Plugin.CopyMacro(macro.Actions);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Copy to Clipboard");
}
ImGui.TableNextColumn();
{
var itemsPerRow = (int)MathF.Ceiling((ImGui.GetContentRegionAvail().X + spacing) / (miniRowHeight + spacing));
var itemCount = Math.Min(macro.Actions.Count, itemsPerRow * 2);
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
var itemCount = macro.Actions.Count;
for (var i = 0; i < itemsPerRow * 2; i++)
{
if (i % itemsPerRow != 0)
ImGui.SameLine(0, spacing);
if (i < itemCount)
{
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
if (!shouldShowMore)
{
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
if (ImGui.IsItemHovered())
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
}
else
{
var amtMore = itemCount - itemsPerRow * 2;
var pos = ImGui.GetCursorPos();
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
ImGui.SetCursorPos(pos);
ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f);
ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null);
}
}
else
ImGui.Dummy(new(miniRowHeight));
@@ -758,25 +803,31 @@ public sealed unsafe class RecipeNote : Window, IDisposable
BestMacroTokenSource = new();
BestMacroException = null;
BestSavedMacro = null;
HasSavedMacro = false;
BestSuggestedMacro = null;
var token = BestMacroTokenSource.Token;
_ = Task.Run(() => CalculateBestMacrosTask(token), token)
.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;
var task = Task.Run(() => CalculateBestMacrosTask(token), token);
_ = task.ContinueWith(t =>
{
if (token == BestMacroTokenSource.Token)
BestMacroTokenSource = null;
});
_ = task.ContinueWith(t =>
{
if (token.IsCancellationRequested)
return;
try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (AggregateException e)
{
BestMacroException = e;
Log.Error(e, "Calculating macros failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
try
{
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
}
catch (AggregateException e)
{
BestMacroException = e;
Log.Error(e, "Calculating macros failed");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
private void CalculateBestMacrosTask(CancellationToken token)
@@ -790,26 +841,30 @@ public sealed unsafe class RecipeNote : Window, IDisposable
token.ThrowIfCancellationRequested();
var bestSaved = macros
.Select(macro =>
{
var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions);
outState.ActionCount = macro.Actions.Count;
var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0;
if (resp != ActionResponse.SimulationComplete)
HasSavedMacro = macros.Count > 0;
if (HasSavedMacro)
{
var bestSaved = macros
.Select(macro =>
{
if (failedIdx != -1)
score /= 2;
}
return (macro, outState, score);
})
.MaxBy(m => m.score);
var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions);
outState.ActionCount = macro.Actions.Count;
var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0;
if (resp != ActionResponse.SimulationComplete)
{
if (failedIdx != -1)
score /= 2;
}
return (macro, outState, score);
})
.MaxBy(m => m.score);
token.ThrowIfCancellationRequested();
token.ThrowIfCancellationRequested();
BestSavedMacro = (bestSaved.macro, bestSaved.outState);
BestSavedMacro = (bestSaved.macro, bestSaved.outState);
token.ThrowIfCancellationRequested();
token.ThrowIfCancellationRequested();
}
var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug;
@@ -819,10 +874,13 @@ public sealed unsafe class RecipeNote : Window, IDisposable
token.ThrowIfCancellationRequested();
BestSuggestedMacro = solution;
token.ThrowIfCancellationRequested();
}
public void Dispose()
{
Service.WindowSystem.RemoveWindow(this);
AxisFont?.Dispose();
}
}
+479 -289
View File
@@ -1,38 +1,34 @@
using Craftimizer.Solver;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
using System;
using System.Numerics;
namespace Craftimizer.Plugin.Windows;
public class Settings : Window
public sealed class Settings : Window, IDisposable
{
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
private static Configuration Config => Service.Configuration;
private const int OptionWidth = 200;
private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight());
public const string TabGeneral = "General";
public const string TabSimulator = "Simulator";
public const string TabSynthHelper = "Synthesis Helper";
public const string TabAbout = "About";
private string? SelectedTab { get; set; }
public Settings() : base("Craftimizer Settings")
public Settings() : base("Craftimizer Settings", WindowFlags)
{
Service.WindowSystem.AddWindow(this);
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(400, 400),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
MinimumSize = new(450, 400),
MaximumSize = new(float.PositiveInfinity)
};
Size = SizeConstraints.Value.MinimumSize;
SizeCondition = ImGuiCond.Appearing;
}
public void SelectTab(string label)
@@ -40,16 +36,16 @@ public class Settings : Window
SelectedTab = label;
}
private bool BeginTabItem(string label)
private ImRaii.IEndObject TabItem(string label)
{
var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal);
if (isSelected)
{
SelectedTab = null;
var open = true;
return ImGui.BeginTabItem(label, ref open, ImGuiTabItemFlags.SetSelected);
return ImRaii.TabItem(label, ref open, ImGuiTabItemFlags.SetSelected);
}
return ImGui.BeginTabItem(label);
return ImRaii.TabItem(label);
}
private static void DrawOption(string label, string tooltip, bool val, Action<bool> setter, ref bool isDirty)
@@ -63,25 +59,44 @@ public class Settings : Window
ImGui.SetTooltip(tooltip);
}
private static void DrawOption(string label, string tooltip, int val, Action<int> setter, ref bool isDirty)
private static void DrawOption<T>(string label, string tooltip, T value, T min, T max, Action<T> setter, ref bool isDirty) where T : struct, INumber<T>
{
ImGui.SetNextItemWidth(OptionWidth);
if (ImGui.InputInt(label, ref val))
var text = value.ToString();
if (ImGui.InputText(label, ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal))
{
setter(val);
isDirty = true;
if (T.TryParse(text, null, out var newValue))
{
newValue = T.Clamp(newValue, min, max);
if (value != newValue)
{
setter(newValue);
isDirty = true;
}
}
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip);
}
private static void DrawOption(string label, string tooltip, float val, Action<float> setter, ref bool isDirty)
private static void DrawOption<T>(string label, string tooltip, Func<T, string> getName, Func<T, string> getTooltip, T value, Action<T> setter, ref bool isDirty) where T : struct, Enum
{
ImGui.SetNextItemWidth(OptionWidth);
if (ImGui.InputFloat(label, ref val))
using (var combo = ImRaii.Combo(label, getName(value)))
{
setter(val);
isDirty = true;
if (combo)
{
foreach (var type in Enum.GetValues<T>())
{
if (ImGui.Selectable(getName(type), value.Equals(type)))
{
setter(type);
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(getTooltip(type));
}
}
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip);
@@ -112,14 +127,33 @@ public class Settings : Window
_ => "Unknown"
};
private static string GetCopyTypeName(MacroCopyConfiguration.CopyType type) =>
type switch
{
MacroCopyConfiguration.CopyType.OpenWindow => "Open a Window",
MacroCopyConfiguration.CopyType.CopyToMacro => "Copy to Macros",
MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to Clipboard",
_ => "Unknown",
};
private static string GetCopyTypeTooltip(MacroCopyConfiguration.CopyType type) =>
type switch
{
MacroCopyConfiguration.CopyType.OpenWindow => "Open a dedicated window with all macros being copied.\n" +
"Copy, view, and choose at your own leisure.",
MacroCopyConfiguration.CopyType.CopyToMacro => "Copy directly to the game's macro system.",
MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to your clipboard. Macros are separated by a blank line.",
_ => "Unknown"
};
public override void Draw()
{
if (ImGui.BeginTabBar("settingsTabBar"))
{
DrawTabGeneral();
DrawTabSimulator();
if (Config.EnableSynthHelper)
DrawTabSynthHelper();
//if (Config.EnableSynthHelper)
// DrawTabSynthHelper();
DrawTabAbout();
ImGui.EndTabBar();
@@ -128,31 +162,30 @@ public class Settings : Window
private void DrawTabGeneral()
{
if (!BeginTabItem("General"))
using var tab = TabItem("General");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
var isDirty = false;
DrawOption(
"Override Uncraftability Warning",
"Allow simulation for crafts that otherwise wouldn't\n" +
"be able to be crafted with your current gear",
Config.OverrideUncraftability,
v => Config.OverrideUncraftability = v,
ref isDirty
);
DrawOption(
"Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
"Extremely useful for expert recipes, where the condition can greatly affect\n" +
"which actions you take.",
Config.EnableSynthHelper,
v => Config.EnableSynthHelper = v,
ref isDirty
);
using (var g = ImRaii.Group())
{
using var d = ImRaii.Disabled();
DrawOption(
"Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
"Extremely useful for expert recipes, where the condition can greatly affect\n" +
"which actions you take.",
Config.EnableSynthHelper,
v => Config.EnableSynthHelper = v,
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
DrawOption(
"Show Only One Macro Stat",
@@ -164,10 +197,151 @@ public class Settings : Window
ref isDirty
);
ImGuiHelpers.ScaledDummy(5);
using (var panel = ImGuiUtils.GroupPanel("Copying Settings", -1, out _))
{
DrawOption(
"Macro Copy Method",
"The method to copy a macro with.",
GetCopyTypeName,
GetCopyTypeTooltip,
Config.MacroCopy.Type,
v => Config.MacroCopy.Type = v,
ref isDirty
);
if (Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro)
{
DrawOption(
"Copy Downwards",
"Copy subsequent macros downward (#1 -> #11) instead of to the right.",
Config.MacroCopy.CopyDown,
v => Config.MacroCopy.CopyDown = v,
ref isDirty
);
DrawOption(
"Copy to Shared Macros",
"Copy to the shared macros tab. Leaving this unchecked copies to the\n" +
"individual tab.",
Config.MacroCopy.SharedMacro,
v => Config.MacroCopy.SharedMacro = v,
ref isDirty
);
DrawOption(
"Macro Number",
"The # of the macro to being copying to. Subsequent macros will be\n" +
"copied relative to this macro.",
Config.MacroCopy.StartMacroIdx,
0, 99,
v => Config.MacroCopy.StartMacroIdx = v,
ref isDirty
);
DrawOption(
"Max Macro Copy Count",
"The maximum number of macros to be copied. Any more and a window is\n" +
"displayed with the rest of them.",
Config.MacroCopy.MaxMacroCount,
1, 99,
v => Config.MacroCopy.MaxMacroCount = v,
ref isDirty
);
}
DrawOption(
"Use MacroChain's /nextmacro",
"Replaces the last step with /nextmacro to run the next macro\n" +
"automatically. Overrides Add End Notification except for the\n" +
"last macro.",
Config.MacroCopy.UseNextMacro,
v => Config.MacroCopy.UseNextMacro = v,
ref isDirty
);
DrawOption(
"Add Macro Lock",
"Adds /mlock to the beginning of every macro. Prevents other\n" +
"macros from being run.",
Config.MacroCopy.UseMacroLock,
v => Config.MacroCopy.UseMacroLock = v,
ref isDirty
);
DrawOption(
"Add Notification",
"Replaces the last step of every macro with a /echo notification.",
Config.MacroCopy.AddNotification,
v => Config.MacroCopy.AddNotification = v,
ref isDirty
);
if (Config.MacroCopy.AddNotification)
{
DrawOption(
"Add Notification Sound",
"Adds a sound to the end of every macro.",
Config.MacroCopy.AddNotificationSound,
v => Config.MacroCopy.AddNotificationSound = v,
ref isDirty
);
if (Config.MacroCopy.AddNotificationSound)
{
DrawOption(
"Intermediate Notification Sound",
"Ending notification sound for an intermediary macro.\n" +
"Uses <se.#>",
Config.MacroCopy.IntermediateNotificationSound,
1, 16,
v =>
{
Config.MacroCopy.IntermediateNotificationSound = v;
UIModule.PlayChatSoundEffect((uint)v);
},
ref isDirty
);
DrawOption(
"Final Notification Sound",
"Ending notification sound for the final macro.\n" +
"Uses <se.#>",
Config.MacroCopy.EndNotificationSound,
1, 16,
v =>
{
Config.MacroCopy.EndNotificationSound = v;
UIModule.PlayChatSoundEffect((uint)v);
},
ref isDirty
);
}
}
if (Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacro)
{
DrawOption(
"Remove Wait Times",
"Remove <wait.#> at the end of every action. Useful for SomethingNeedDoing.",
Config.MacroCopy.RemoveWaitTimes,
v => Config.MacroCopy.RemoveWaitTimes = v,
ref isDirty
);
DrawOption(
"Combine Macro",
"Doesn't split the macro into smaller macros. Useful for SomethingNeedDoing.",
Config.MacroCopy.CombineMacro,
v => Config.MacroCopy.CombineMacro = v,
ref isDirty
);
}
}
if (isDirty)
Config.Save();
ImGui.EndTabItem();
}
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty)
@@ -176,228 +350,240 @@ public class Settings : Window
var config = configRef;
ImGuiUtils.BeginGroupPanel("General");
if (ImGui.Button("Reset to Default", OptionButtonSize))
using (var panel = ImGuiUtils.GroupPanel("General", -1, out _))
{
config = defaultConfig;
isDirty = true;
}
ImGui.SetNextItemWidth(OptionWidth);
if (ImGui.BeginCombo("Algorithm", GetAlgorithmName(config.Algorithm)))
{
foreach (var alg in Enum.GetValues<SolverAlgorithm>())
if (ImGui.Button("Reset to Default", OptionButtonSize))
{
if (ImGui.Selectable(GetAlgorithmName(alg), config.Algorithm == alg))
{
config = config with { Algorithm = alg };
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(GetAlgorithmTooltip(alg));
config = defaultConfig;
isDirty = true;
}
ImGui.EndCombo();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
DrawOption(
"Algorithm",
"The algorithm to use when solving for a macro. Different\n" +
"algorithms provide different pros and cons for using them.\n" +
"By far, the Stepwise Furcated algorithm provides the best\n" +
"results, especially for very difficult crafts."
"results, especially for very difficult crafts.",
GetAlgorithmName,
GetAlgorithmTooltip,
config.Algorithm,
v => config = config with { Algorithm = v },
ref isDirty
);
DrawOption(
"Iterations",
"The total number of iterations to run per crafting step.\n" +
"Higher values require more computational power. Higher values\n" +
"also may decrease variance, so other values should be tweaked\n" +
"as necessary to get a more favorable outcome.",
config.Iterations,
v => config = config with { Iterations = v },
ref isDirty
);
DrawOption(
"Iterations",
"The total number of iterations to run per crafting step.\n" +
"Higher values require more computational power. Higher values\n" +
"also may decrease variance, so other values should be tweaked\n" +
"as necessary to get a more favorable outcome.",
config.Iterations,
1000,
500000,
v => config = config with { Iterations = v },
ref isDirty
);
DrawOption(
"Max Step Count",
"The maximum number of crafting steps; this is generally the only\n" +
"setting you should change, and it should be set to around 5 steps\n" +
"more than what you'd expect. If this value is too low, the solver\n" +
"won't learn much per iteration; too high and it will waste time\n" +
"on useless extra steps.",
config.MaxStepCount,
v => config = config with { MaxStepCount = v },
ref isDirty
);
DrawOption(
"Max Step Count",
"The maximum number of crafting steps; this is generally the only\n" +
"setting you should change, and it should be set to around 5 steps\n" +
"more than what you'd expect. If this value is too low, the solver\n" +
"won't learn much per iteration; too high and it will waste time\n" +
"on useless extra steps.",
config.MaxStepCount,
1,
100,
v => config = config with { MaxStepCount = v },
ref isDirty
);
DrawOption(
"Exploration Constant",
"A constant that decides how often the solver will explore new,\n" +
"possibly good paths. If this value is too high,\n" +
"moves will mostly be decided at random.",
config.ExplorationConstant,
v => config = config with { ExplorationConstant = v },
ref isDirty
);
DrawOption(
"Exploration Constant",
"A constant that decides how often the solver will explore new,\n" +
"possibly good paths. If this value is too high,\n" +
"moves will mostly be decided at random.",
config.ExplorationConstant,
0,
10,
v => config = config with { ExplorationConstant = v },
ref isDirty
);
DrawOption(
"Score Weighting Constant",
"A constant ranging from 0 to 1 that configures how the solver\n" +
"scores and picks paths to travel to next. A value of 0 means\n" +
"actions will be chosen based on their average outcome, whereas\n" +
"1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant,
v => config = config with { MaxScoreWeightingConstant = v },
ref isDirty
);
DrawOption(
"Score Weighting Constant",
"A constant ranging from 0 to 1 that configures how the solver\n" +
"scores and picks paths to travel to next. A value of 0 means\n" +
"actions will be chosen based on their average outcome, whereas\n" +
"1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant,
0,
1,
v => config = config with { MaxScoreWeightingConstant = v },
ref isDirty
);
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated));
DrawOption(
"Max Core Count",
"The number of cores to use when solving. You should use as many\n" +
"as you can. If it's too high, it will have an effect on your gameplay\n" +
$"experience. A good estimate would be 1 or 2 cores less than your\n" +
$"system (FYI, you have {Environment.ProcessorCount} cores,) but\n" +
$"make sure to accomodate for any other tasks you have in the\n" +
$"background, if you have any.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.MaxThreadCount,
v => config = config with { MaxThreadCount = v },
ref isDirty
);
ImGui.EndDisabled();
using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
DrawOption(
"Max Core Count",
"The number of cores to use when solving. You should use as many\n" +
"as you can. If it's too high, it will have an effect on your gameplay\n" +
$"experience. A good estimate would be 1 or 2 cores less than your\n" +
$"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate\n" +
$"for any other tasks you have in the background, if you have any.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.MaxThreadCount,
1,
Environment.ProcessorCount,
v => config = config with { MaxThreadCount = v },
ref isDirty
);
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated));
DrawOption(
"Fork Count",
"Split the number of iterations across different solvers. In general,\n" +
"you should increase this value to at least the number of cores in\n" +
$"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" +
"The higher the number, the more chance you have of finding a\n" +
"better local maximum; this concept similar but not equivalent\n" +
"to the exploration constant.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.ForkCount,
v => config = config with { ForkCount = v },
ref isDirty
);
ImGui.EndDisabled();
using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
DrawOption(
"Fork Count",
"Split the number of iterations across different solvers. In general,\n" +
"you should increase this value to at least the number of cores in\n" +
$"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" +
"The higher the number, the more chance you have of finding a\n" +
"better local maximum; this concept similar but not equivalent\n" +
"to the exploration constant.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.ForkCount,
1,
500,
v => config = config with { ForkCount = v },
ref isDirty
);
ImGui.BeginDisabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated);
DrawOption(
"Furcated Action Count",
"On every craft step, pick this many top solutions and use them as\n" +
"the input for the next craft step. For best results, use Fork Count / 2\n" +
"and add about 1 or 2 more if needed.\n" +
"(Only used in the Stepwise Furcated algorithm)",
config.FurcatedActionCount,
v => config = config with { FurcatedActionCount = v },
ref isDirty
);
ImGui.EndDisabled();
ImGuiUtils.EndGroupPanel();
ImGuiUtils.BeginGroupPanel("Advanced");
DrawOption(
"Score Storage Threshold",
"If a craft achieves this certain arbitrary score, the solver will\n" +
"throw away all other possible combinations in favor of that one.\n" +
"Only change this value if you absolutely know what you're doing.",
config.ScoreStorageThreshold,
v => config = config with { ScoreStorageThreshold = v },
ref isDirty
);
DrawOption(
"Max Rollout Step Count",
"The maximum number of crafting steps every iteration can consider.\n" +
"Decreasing this value can have unintended side effects. Only change\n" +
"this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount,
v => config = config with { MaxRolloutStepCount = v },
ref isDirty
);
DrawOption(
"Strict Actions",
"When finding the next possible actions to execute, use a heuristic\n" +
"to restrict which actions to attempt taking. This results in a much\n" +
"better macro at the cost of not finding an extremely creative one.",
config.StrictActions,
v => config = config with { StrictActions = v },
ref isDirty
);
ImGuiUtils.EndGroupPanel();
ImGuiUtils.BeginGroupPanel("Score Weights (Advanced)");
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
ImGuiHelpers.ScaledDummy(10);
DrawOption(
"Progress",
"Amount of weight to give to the craft's progress.",
config.ScoreProgress,
v => config = config with { ScoreProgress = v },
ref isDirty
);
DrawOption(
"Quality",
"Amount of weight to give to the craft's quality.",
config.ScoreQuality,
v => config = config with { ScoreQuality = v },
ref isDirty
);
DrawOption(
"Durability",
"Amount of weight to give to the craft's remaining durability.",
config.ScoreDurability,
v => config = config with { ScoreDurability = v },
ref isDirty
);
DrawOption(
"CP",
"Amount of weight to give to the craft's remaining CP.",
config.ScoreCP,
v => config = config with { ScoreCP = v },
ref isDirty
);
DrawOption(
"Steps",
"Amount of weight to give to the craft's number of steps. The lower\n" +
"the step count, the higher the score.",
config.ScoreSteps,
v => config = config with { ScoreSteps = v },
ref isDirty
);
if (ImGui.Button("Normalize Weights", OptionButtonSize))
{
var total = config.ScoreProgress +
config.ScoreQuality +
config.ScoreDurability +
config.ScoreCP +
config.ScoreSteps;
config = config with
{
ScoreProgress = config.ScoreProgress / total,
ScoreQuality = config.ScoreQuality / total,
ScoreDurability = config.ScoreDurability / total,
ScoreCP = config.ScoreCP / total,
ScoreSteps = config.ScoreSteps / total
};
isDirty = true;
using (var d = ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated))
DrawOption(
"Furcated Action Count",
"On every craft step, pick this many top solutions and use them as\n" +
"the input for the next craft step. For best results, use Fork Count / 2\n" +
"and add about 1 or 2 more if needed.\n" +
"(Only used in the Stepwise Furcated algorithm)",
config.FurcatedActionCount,
1,
500,
v => config = config with { FurcatedActionCount = v },
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Normalize all weights to sum up to 1");
ImGuiUtils.EndGroupPanel();
using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _))
{
DrawOption(
"Score Storage Threshold",
"If a craft achieves this certain arbitrary score, the solver will\n" +
"throw away all other possible combinations in favor of that one.\n" +
"Only change this value if you absolutely know what you're doing.",
config.ScoreStorageThreshold,
0,
1,
v => config = config with { ScoreStorageThreshold = v },
ref isDirty
);
DrawOption(
"Max Rollout Step Count",
"The maximum number of crafting steps every iteration can consider.\n" +
"Decreasing this value can have unintended side effects. Only change\n" +
"this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount,
1,
50,
v => config = config with { MaxRolloutStepCount = v },
ref isDirty
);
DrawOption(
"Strict Actions",
"When finding the next possible actions to execute, use a heuristic\n" +
"to restrict which actions to attempt taking. This results in a much\n" +
"better macro at the cost of not finding an extremely creative one.",
config.StrictActions,
v => config = config with { StrictActions = v },
ref isDirty
);
}
using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _))
{
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
ImGuiHelpers.ScaledDummy(10);
DrawOption(
"Progress",
"Amount of weight to give to the craft's progress.",
config.ScoreProgress,
0,
1,
v => config = config with { ScoreProgress = v },
ref isDirty
);
DrawOption(
"Quality",
"Amount of weight to give to the craft's quality.",
config.ScoreQuality,
0,
1,
v => config = config with { ScoreQuality = v },
ref isDirty
);
DrawOption(
"Durability",
"Amount of weight to give to the craft's remaining durability.",
config.ScoreDurability,
0,
1,
v => config = config with { ScoreDurability = v },
ref isDirty
);
DrawOption(
"CP",
"Amount of weight to give to the craft's remaining CP.",
config.ScoreCP,
0,
1,
v => config = config with { ScoreCP = v },
ref isDirty
);
DrawOption(
"Steps",
"Amount of weight to give to the craft's number of steps. The lower\n" +
"the step count, the higher the score.",
config.ScoreSteps,
0,
1,
v => config = config with { ScoreSteps = v },
ref isDirty
);
if (ImGui.Button("Normalize Weights", OptionButtonSize))
{
var total = config.ScoreProgress +
config.ScoreQuality +
config.ScoreDurability +
config.ScoreCP +
config.ScoreSteps;
config = config with
{
ScoreProgress = config.ScoreProgress / total,
ScoreQuality = config.ScoreQuality / total,
ScoreDurability = config.ScoreDurability / total,
ScoreCP = config.ScoreCP / total,
ScoreSteps = config.ScoreSteps / total
};
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Normalize all weights to sum up to 1");
}
if (isDirty)
configRef = config;
@@ -405,30 +591,28 @@ public class Settings : Window
private void DrawTabSimulator()
{
if (!BeginTabItem("Simulator"))
using var tab = TabItem("Simulator");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
var isDirty = false;
DrawOption(
"Show Only Learned Actions",
"Don't show crafting actions that haven't been\n" +
"learned yet with your current job on the simulator sidebar",
Config.HideUnlearnedActions,
v => Config.HideUnlearnedActions = v,
ref isDirty
);
DrawOption(
"Condition Randomness",
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
"Turns off when generating a macro.",
Config.ConditionRandomness,
v => Config.ConditionRandomness = v,
ref isDirty
);
using (var g = ImRaii.Group())
{
using var d = ImRaii.Disabled();
DrawOption(
"Condition Randomness",
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
"Turns off when generating a macro.",
Config.ConditionRandomness,
v => Config.ConditionRandomness = v,
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
@@ -444,13 +628,12 @@ public class Settings : Window
if (isDirty)
Config.Save();
ImGui.EndTabItem();
}
private void DrawTabSynthHelper()
{
if (!BeginTabItem("Synthesis Helper"))
using var tab = TabItem("Synthesis Helper");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
@@ -461,6 +644,8 @@ public class Settings : Window
"Step Count",
"The number of future actions to solve for during an in-game craft.",
Config.SynthHelperStepCount,
1,
100,
v => Config.SynthHelperStepCount = v,
ref isDirty
);
@@ -479,13 +664,12 @@ public class Settings : Window
if (isDirty)
Config.Save();
ImGui.EndTabItem();
}
private void DrawTabAbout()
{
if (!BeginTabItem("About"))
using var tab = TabItem("About");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
@@ -493,28 +677,34 @@ public class Settings : Window
var plugin = Service.Plugin;
var icon = plugin.Icon;
ImGui.BeginTable("settingsAboutTable", 2);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
using (var table = ImRaii.Table("settingsAboutTable", 2))
{
if (table)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
ImGui.TableNextColumn();
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn();
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn();
ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}");
ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
ImGui.SameLine(0, 0);
ImGui.Text(")");
ImGui.EndTable();
ImGui.TableNextColumn();
ImGui.Text($"Craftimizer v{plugin.Version} {plugin.BuildConfiguration}");
ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
ImGui.SameLine(0, 0);
ImGui.Text(")");
}
}
ImGui.Text("Credit to altosock's ");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app");
ImGui.SameLine(0, 0);
ImGui.Text(" for the original solver algorithm");
}
ImGui.EndTabItem();
public void Dispose()
{
Service.WindowSystem.RemoveWindow(this);
}
}
+22
View File
@@ -1,3 +1,6 @@
using Craftimizer.Simulator.Actions;
using System.Collections.ObjectModel;
namespace Craftimizer.Simulator;
public enum ActionCategory
@@ -13,6 +16,25 @@ public enum ActionCategory
public static class ActionCategoryUtils
{
private static readonly ReadOnlyDictionary<ActionCategory, ActionType[]> SortedActions;
static ActionCategoryUtils()
{
SortedActions = new(
Enum.GetValues<ActionType>()
.Where(a => a.Category() != ActionCategory.Combo)
.GroupBy(a => a.Category())
.ToDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray()));
}
public static IReadOnlyList<ActionType> GetActions(this ActionCategory me)
{
if (SortedActions.TryGetValue(me, out var actions))
return actions;
throw new ArgumentException($"Unknown action category {me}", nameof(me));
}
public static string GetDisplayName(this ActionCategory category) =>
category switch
{
+2 -1
View File
@@ -48,7 +48,8 @@ public abstract class BaseAction
s.ActionStates.MutateState(this);
s.ActionCount++;
s.ActiveEffects.DecrementDuration();
if (IncreasesStepCount)
s.ActiveEffects.DecrementDuration();
}
public virtual void UseSuccess(Simulator s)
+4 -1
View File
@@ -14,10 +14,13 @@ internal abstract class BaseBuffAction : BaseAction
public override void UseSuccess(Simulator s) =>
s.AddEffect(Effect, Duration);
public sealed override string GetTooltip(Simulator s, bool addUsability)
public override string GetTooltip(Simulator s, bool addUsability)
{
var builder = new StringBuilder(base.GetTooltip(s, addUsability));
builder.AppendLine($"{Duration} Steps");
return builder.ToString();
}
protected string GetBaseTooltip(Simulator s, bool addUsability) =>
base.GetTooltip(s, addUsability);
}
+3
View File
@@ -15,4 +15,7 @@ internal sealed class CarefulObservation : BaseAction
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && 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";
}
+3
View File
@@ -14,4 +14,7 @@ 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 string GetTooltip(Simulator s, bool addUsability) =>
$"{GetBaseTooltip(s, addUsability)}Specialist Only";
}
+8 -6
View File
@@ -14,14 +14,16 @@ internal sealed class Manipulation : BaseBuffAction
public override void Use(Simulator s)
{
if (s.HasEffect(EffectType.Manipulation))
s.RestoreDurability(5);
s.ReduceCP(CPCost(s));
s.ReduceDurability(DurabilityCost);
UseSuccess(s);
s.ReduceCP(CPCost(s));
s.IncreaseStepCount();
s.ActionStates.MutateState(this);
s.ActionCount++;
if (IncreasesStepCount)
s.ActiveEffects.DecrementDuration();
}
}
+3
View File
@@ -18,4 +18,7 @@ internal sealed class TrainedEye : BaseAction
public override void UseSuccess(Simulator s) =>
s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality);
public override string GetTooltip(Simulator s, bool addUsability) =>
$"{base.GetTooltip(s, addUsability)}+{s.Input.Recipe.MaxQuality - s.Quality} Quality";
}
+5
View File
@@ -82,6 +82,11 @@ public record struct Effects
_ => 0
};
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsIndefinite(EffectType effect) =>
effect is EffectType.InnerQuiet or EffectType.HeartAndSoul;
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte GetStrength(EffectType effect) =>
+2
View File
@@ -25,6 +25,8 @@ public record struct SimulationState
74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100
};
public readonly int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.Recipe.MaxQuality * 100, 0, 100)];
public readonly int Collectability => Math.Max(Quality / 10, 1);
public readonly int MaxCollectability => Math.Max(Input.Recipe.MaxQuality / 10, 1);
public readonly bool IsFirstStep => StepCount == 0;
+2
View File
@@ -63,6 +63,8 @@ public class Simulator
return ActionResponse.ActionNotUnlocked;
if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation)
return ActionResponse.ActionNotUnlocked;
if (action is ActionType.CarefulObservation or ActionType.HeartAndSoul && !Input.Stats.IsSpecialist)
return ActionResponse.ActionNotUnlocked;
if (baseAction.CPCost(this) > CP)
return ActionResponse.NotEnoughCP;
return ActionResponse.CannotUseAction;
+20 -17
View File
@@ -35,9 +35,6 @@ public sealed class Solver : IDisposable
// Always called when a new step is generated.
public event NewActionDelegate? OnNewAction;
// Always called when the solver is fully complete.
public event SolutionDelegate? OnSolution;
public Solver(SolverConfig config, SimulationState state)
{
Config = config;
@@ -107,6 +104,12 @@ public sealed class Solver : IDisposable
CompletionTask?.Dispose();
}
private void InvokeNewAction(ActionType action)
{
foreach (var sanitizedAction in SolverSolution.SanitizeCombo(action))
OnNewAction?.Invoke(sanitizedAction);
}
private async Task<SolverSolution> SearchStepwiseFurcated()
{
var definiteActionCount = 0;
@@ -115,7 +118,7 @@ public sealed class Solver : IDisposable
var state = State;
var sim = new Simulator(state, Config.MaxStepCount);
var activeStates = new List<SolverSolution>() { new(new(), state) };
var activeStates = new List<SolverSolution>() { new(Array.Empty<ActionType>(), state) };
while (activeStates.Count != 0)
{
@@ -162,12 +165,12 @@ public sealed class Solver : IDisposable
if (bestAction.MaxScore >= Config.ScoreStorageThreshold)
{
var (_, furcatedActionIdx, solution) = bestAction;
var (activeActions, _) = activeStates[furcatedActionIdx];
(IEnumerable<ActionType> activeActions, _) = activeStates[furcatedActionIdx];
activeActions.AddRange(solution.Actions);
activeActions = activeActions.Concat(solution.Actions);
foreach (var action in activeActions.Skip(definiteActionCount))
OnNewAction?.Invoke(action);
return solution with { Actions = activeActions };
InvokeNewAction(action);
return solution with { ActionEnumerable = activeActions };
}
var newStates = new List<SolverSolution>(Config.FurcatedActionCount);
@@ -214,7 +217,7 @@ public sealed class Solver : IDisposable
if (definiteCount != equalCount)
{
foreach(var action in refActions.Take(equalCount).Skip(definiteCount))
OnNewAction?.Invoke(action);
InvokeNewAction(action);
definiteActionCount = equalCount;
}
@@ -224,11 +227,11 @@ public sealed class Solver : IDisposable
}
if (bestSims.Count == 0)
return new(new(), state);
return new(Array.Empty<ActionType>(), state);
var result = bestSims.MaxBy(s => s.Score).Result;
foreach (var action in result.Actions.Skip(definiteActionCount))
OnNewAction?.Invoke(action);
InvokeNewAction(action);
return result;
}
@@ -282,12 +285,12 @@ public sealed class Solver : IDisposable
{
actions.AddRange(solution.Actions);
foreach (var action in solution.Actions)
OnNewAction?.Invoke(action);
InvokeNewAction(action);
return solution with { Actions = actions };
}
var chosenAction = solution.Actions[0];
OnNewAction?.Invoke(chosenAction);
InvokeNewAction(chosenAction);
(_, state) = sim.Execute(state, chosenAction);
actions.Add(chosenAction);
@@ -321,12 +324,12 @@ public sealed class Solver : IDisposable
{
actions.AddRange(solution.Actions);
foreach (var action in solution.Actions)
OnNewAction?.Invoke(action);
InvokeNewAction(action);
return Task.FromResult(solution with { Actions = actions });
}
var chosenAction = solution.Actions[0];
OnNewAction?.Invoke(chosenAction);
InvokeNewAction(chosenAction);
(_, state) = sim.Execute(state, chosenAction);
actions.Add(chosenAction);
@@ -365,7 +368,7 @@ public sealed class Solver : IDisposable
var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution;
foreach (var action in solution.Actions)
OnNewAction?.Invoke(action);
InvokeNewAction(action);
return solution;
}
@@ -376,7 +379,7 @@ public sealed class Solver : IDisposable
solver.Search(Config.Iterations, Token);
var solution = solver.Solution();
foreach (var action in solution.Actions)
OnNewAction?.Invoke(action);
InvokeNewAction(action);
return Task.FromResult(solution);
}
+40 -1
View File
@@ -3,4 +3,43 @@ using Craftimizer.Simulator.Actions;
namespace Craftimizer.Solver;
public readonly record struct SolverSolution(List<ActionType> Actions, SimulationState State);
public readonly record struct SolverSolution {
private readonly List<ActionType> actions = null!;
public readonly IReadOnlyList<ActionType> Actions { get => actions; init => ActionEnumerable = value; }
public readonly IEnumerable<ActionType> ActionEnumerable { init => actions = SanitizeCombos(value).ToList(); }
public readonly SimulationState State { get; init; }
public SolverSolution(IEnumerable<ActionType> actions, SimulationState state)
{
ActionEnumerable = actions;
State = state;
}
public void Deconstruct(out IReadOnlyList<ActionType> actions, out SimulationState state)
{
actions = Actions;
state = State;
}
internal static IEnumerable<ActionType> SanitizeCombo(ActionType action)
{
if (action.Base() is BaseComboAction combo)
{
foreach (var a in SanitizeCombo(combo.ActionTypeA))
yield return a;
foreach (var b in SanitizeCombo(combo.ActionTypeB))
yield return b;
}
else
yield return action;
}
internal static IEnumerable<ActionType> SanitizeCombos(IEnumerable<ActionType> actions)
{
foreach (var action in actions)
{
foreach (var sanitizedAction in SanitizeCombo(action))
yield return sanitizedAction;
}
}
}