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.Simulator.Actions;
using Craftimizer.Solver; using Craftimizer.Solver;
using Dalamud.Configuration; using Dalamud.Configuration;
using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -10,8 +10,64 @@ namespace Craftimizer.Plugin;
[Serializable] [Serializable]
public class Macro public class Macro
{ {
public static event Action<Macro>? OnMacroChanged;
public string Name { get; set; } = string.Empty; 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] [Serializable]
@@ -19,9 +75,12 @@ public class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public bool OverrideUncraftability { get; set; } = true; public static event Action? OnMacroListChanged;
public bool HideUnlearnedActions { get; set; } = true;
public List<Macro> Macros { get; set; } = new(); [JsonProperty(PropertyName = "Macros")]
private List<Macro> macros { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<Macro> Macros => macros;
public bool ConditionRandomness { get; set; } = true; public bool ConditionRandomness { get; set; } = true;
public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault; public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault;
public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault; public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault;
@@ -29,10 +88,23 @@ public class Configuration : IPluginConfiguration
public bool ShowOptimalMacroStat { get; set; } = true; public bool ShowOptimalMacroStat { get; set; } = true;
public int SynthHelperStepCount { get; set; } = 5; public int SynthHelperStepCount { get; set; } = 5;
public Simulator.Simulator CreateSimulator(SimulationState state) => public MacroCopyConfiguration MacroCopy { get; set; } = new();
ConditionRandomness ?
new Simulator.Simulator(state) : public void AddMacro(Macro macro)
new SimulatorNoRandom(state); {
macros.Add(macro);
Save();
OnMacroListChanged?.Invoke();
}
public void RemoveMacro(Macro macro)
{
if (macros.Remove(macro))
{
Save();
OnMacroListChanged?.Invoke();
}
}
public void Save() => public void Save() =>
Service.PluginInterface.SavePluginConfig(this); Service.PluginInterface.SavePluginConfig(this);
+1 -1
View File
@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Authors>Asriel Camora</Authors> <Authors>Asriel Camora</Authors>
<Version>1.2.0.0</Version> <Version>1.9.0.0</Version>
<PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl> <PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl>
</PropertyGroup> </PropertyGroup>
+300 -63
View File
@@ -1,104 +1,93 @@
using Craftimizer.Utils;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
internal static class ImGuiUtils 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 // 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(); ImGui.BeginGroup();
var itemSpacing = ImGui.GetStyle().ItemSpacing; var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight(); 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);
// inner group
ImGui.BeginGroup(); ImGui.BeginGroup();
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0)); ImGui.Dummy(new Vector2(fullWidth, 0));
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
// label group
ImGui.BeginGroup(); ImGui.BeginGroup();
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y)); 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
// content group
ImGui.BeginGroup(); 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) return width;
{
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));
} }
public static void EndGroupPanel() public static void EndGroupPanel()
{ {
ImGui.PopItemWidth();
var itemSpacing = ImGui.GetStyle().ItemSpacing; var itemSpacing = ImGui.GetStyle().ItemSpacing;
{
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); // content group
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight();
ImGui.EndGroup(); ImGui.EndGroup();
// label group
ImGui.EndGroup(); ImGui.EndGroup();
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); // shifts full size by is (for rect placement)
ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y)); 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(); ImGui.EndGroup();
var itemMin = ImGui.GetItemRectMin();
var itemMax = ImGui.GetItemRectMax();
var labelRect = GroupPanelLabelStack.Pop(); var labelRect = GroupPanelLabelStack.Pop();
var innerMin = ImGui.GetItemRectMin() + new Vector2(0, labelRect.TopPadding);
var innerMax = ImGui.GetItemRectMax();
var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f; (Vector2 Min, Vector2 Max) frameRect = (innerMin, innerMax);
(Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0)); // add itemspacing padding on the label's sides
labelRect.Min.X -= itemSpacing.X; labelRect.Min.X -= itemSpacing.X / 2;
labelRect.Max.X += itemSpacing.X; labelRect.Max.X += itemSpacing.X / 2;
for (var i = 0; i < 4; ++i) for (var i = 0; i < 4; ++i)
{ {
var (minClip, maxClip) = i switch var (minClip, maxClip) = i switch
@@ -114,17 +103,47 @@ internal static class ImGuiUtils
ImGui.GetWindowDrawList().AddRect( ImGui.GetWindowDrawList().AddRect(
frameRect.Min, frameRect.Max, frameRect.Min, frameRect.Max,
ImGui.GetColorU32(ImGuiCol.Border), ImGui.GetColorU32(ImGuiCol.Border),
halfFrame.X); itemSpacing.X);
ImGui.PopClipRect(); ImGui.PopClipRect();
} }
ImGui.PopStyleVar(2);
ImGui.Dummy(Vector2.Zero); ImGui.Dummy(Vector2.Zero);
}
ImGui.EndGroup(); 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) private static Vector2 UnitCircle(float theta)
{ {
var (s, c) = MathF.SinCos(theta); var (s, c) = MathF.SinCos(theta);
@@ -156,7 +175,7 @@ internal static class ImGuiUtils
var offset = ImGui.GetCursorScreenPos() + new Vector2(radius); 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 incrementAngle = MathF.Tau / segments;
var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0; 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); 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) public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size)
{ {
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
@@ -254,6 +483,14 @@ internal static class ImGuiUtils
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); 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) public static void AlignMiddle(Vector2 size, Vector2 availSize = default)
{ {
if (availSize == default) if (availSize == default)
@@ -271,9 +508,9 @@ internal static class ImGuiUtils
ImGui.TextUnformatted(text); 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); ImGui.TextUnformatted(text);
} }
+65 -14
View File
@@ -1,21 +1,22 @@
using Craftimizer.Plugin.Utils;
using Craftimizer.Plugin.Windows; using Craftimizer.Plugin.Windows;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils; using Craftimizer.Utils;
using Craftimizer.Windows; using Craftimizer.Windows;
using Dalamud.Game.Command;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiScene; using System;
using Lumina.Excel.GeneratedSheets; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using ClassJob = Craftimizer.Simulator.ClassJob;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
public sealed class Plugin : IDalamudPlugin public sealed class Plugin : IDalamudPlugin
{ {
public string Name => "Craftimizer";
public string Version { get; } public string Version { get; }
public string Author { get; } public string Author { get; }
public string BuildConfiguration { get; } public string BuildConfiguration { get; }
@@ -23,23 +24,23 @@ public sealed class Plugin : IDalamudPlugin
public WindowSystem WindowSystem { get; } public WindowSystem WindowSystem { get; }
public Settings SettingsWindow { get; } public Settings SettingsWindow { get; }
public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; } public RecipeNote RecipeNoteWindow { get; }
public Craft SynthesisWindow { get; } public MacroList ListWindow { get; private set; }
public MacroEditor? EditorWindow { get; private set; }
public MacroClipboard? ClipboardWindow { get; private set; }
public Configuration Configuration { get; } public Configuration Configuration { get; }
public Hooks Hooks { get; } public Hooks Hooks { get; }
public Craftimizer.Utils.RecipeNote RecipeNote { get; }
public IconManager IconManager { get; } public IconManager IconManager { get; }
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
{ {
Service.Initialize(this, pluginInterface); Service.Initialize(this, pluginInterface);
WindowSystem = new("Craftimizer");
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new(); Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
Hooks = new(); Hooks = new();
RecipeNote = new();
IconManager = new(); IconManager = new();
WindowSystem = new(Name);
var assembly = Assembly.GetExecutingAssembly(); var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion; Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
@@ -49,15 +50,40 @@ public sealed class Plugin : IDalamudPlugin
SettingsWindow = new(); SettingsWindow = new();
RecipeNoteWindow = 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.Draw += WindowSystem.Draw;
Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow; 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() public void OpenSettingsWindow()
{ {
SettingsWindow.IsOpen = true; if (SettingsWindow.IsOpen ^= true)
SettingsWindow.BringToFront(); SettingsWindow.BringToFront();
} }
@@ -67,11 +93,36 @@ public sealed class Plugin : IDalamudPlugin
SettingsWindow.SelectTab(selectedTabLabel); 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() public void Dispose()
{ {
SimulatorWindow?.Dispose(); Service.CommandManager.RemoveHandler("/craftimizer");
SynthesisWindow.Dispose(); Service.CommandManager.RemoveHandler("/craftmacros");
RecipeNote.Dispose(); Service.CommandManager.RemoveHandler("/crafteditor");
SettingsWindow.Dispose();
RecipeNoteWindow.Dispose();
ListWindow.Dispose();
EditorWindow?.Dispose();
ClipboardWindow?.Dispose();
Hooks.Dispose(); Hooks.Dispose();
IconManager.Dispose(); IconManager.Dispose();
} }
-1
View File
@@ -1,6 +1,5 @@
using Craftimizer.Utils; using Craftimizer.Utils;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.IoC; using Dalamud.IoC;
+7 -3
View File
@@ -6,7 +6,6 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using System; using System;
using System.Globalization; 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) => public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) =>
ActionRows[(int)me, (int)classJob]; ActionRows[(int)me, (int)classJob];
@@ -309,11 +310,14 @@ internal static class EffectUtils
EffectType.FinalAppraisal => 2190, EffectType.FinalAppraisal => 2190,
EffectType.WasteNot2 => 257, EffectType.WasteNot2 => 257,
EffectType.MuscleMemory => 2191, EffectType.MuscleMemory => 2191,
EffectType.Manipulation => 258, EffectType.Manipulation => 1164,
EffectType.HeartAndSoul => 2665, 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) => public static Status Status(this EffectType me) =>
LuminaSheets.StatusSheet.GetRow(me.StatusId())!; LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
+15 -14
View File
@@ -1,6 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -8,7 +9,7 @@ using System.Text;
namespace Craftimizer.Plugin.Utils; namespace Craftimizer.Plugin.Utils;
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17 // https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
public static class Chat public static unsafe class Chat
{ {
private static class Signatures 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"; 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 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() static Chat()
{ {
@@ -33,7 +34,7 @@ public static class Chat
{ {
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr)) 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"); 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); using var payload = new ChatPayload(message);
var mem1 = Marshal.AllocHGlobal(400); 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> /// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public static unsafe string SanitiseText(string text) public static unsafe string SanitiseText(string text)
{ {
if (_sanitiseString == null) if (SanitiseString == null)
{ {
throw new InvalidOperationException("Could not find signature for chat sanitisation"); throw new InvalidOperationException("Could not find signature for chat sanitisation");
} }
var uText = Utf8String.FromString(text); var uText = Utf8String.FromString(text);
_sanitiseString(uText, 0x27F, IntPtr.Zero); SanitiseString(uText, 0x27F, IntPtr.Zero);
var sanitised = uText->ToString(); var sanitised = uText->ToString();
uText->Dtor(); uText->Dtor();
@@ -151,19 +152,19 @@ public static class Chat
internal ChatPayload(byte[] stringBytes) internal ChatPayload(byte[] stringBytes)
{ {
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length); Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0); Marshal.WriteByte(textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1); textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64; unk1 = 64;
this.unk2 = 0; unk2 = 0;
} }
public void Dispose() 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 ParamCraftsmanship = 70;
public const int ParamControl = 71; 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) public static GearsetItem[] GetGearsetItems(InventoryContainer* container)
{ {
var items = new GearsetItem[(int)container->Size]; var items = new GearsetItem[(int)container->Size];
@@ -128,10 +146,10 @@ public static unsafe class Gearsets
public static bool IsSplendorousTool(GearsetItem item) => 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); 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) => public static int CalculateCLvl(int level) =>
characterLevel <= 80 (level > 0 && level <= 90) ?
? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel LevelToCLvlLUT[level - 1] :
: (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; 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 // 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) private static int CalculateParamCap(Item item, int paramId)
+16 -1
View File
@@ -1,6 +1,6 @@
using Craftimizer.Plugin; using Craftimizer.Plugin;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using ImGuiScene; using Dalamud.Plugin.Services;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -11,6 +11,7 @@ namespace Craftimizer.Utils;
public sealed class IconManager : IDisposable public sealed class IconManager : IDisposable
{ {
private readonly Dictionary<uint, IDalamudTextureWrap> iconCache = new(); 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> textureCache = new();
private readonly Dictionary<string, IDalamudTextureWrap> assemblyCache = new(); private readonly Dictionary<string, IDalamudTextureWrap> assemblyCache = new();
@@ -22,6 +23,16 @@ public sealed class IconManager : IDisposable
return ret; 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) public IDalamudTextureWrap GetTexture(string path)
{ {
if (!textureCache.TryGetValue(path, out var ret)) if (!textureCache.TryGetValue(path, out var ret))
@@ -55,6 +66,10 @@ public sealed class IconManager : IDisposable
image.Dispose(); image.Dispose();
iconCache.Clear(); iconCache.Clear();
foreach (var image in hqIconCache.Values)
image.Dispose();
hqIconCache.Clear();
foreach (var image in textureCache.Values) foreach (var image in textureCache.Values)
image.Dispose(); image.Dispose();
textureCache.Clear(); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
public static class SqText 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, ['0'] = SeIconChar.Number0,
['1'] = SeIconChar.Number1, ['1'] = SeIconChar.Number1,
@@ -26,8 +27,15 @@ public static class SqText
public static string ToLevelString<T>(T value) where T : IBinaryInteger<T> public static string ToLevelString<T>(T value) where T : IBinaryInteger<T>
{ {
var str = value.ToString() ?? throw new FormatException("Failed to format value"); 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()); 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);
}
}
+106 -48
View File
@@ -4,12 +4,14 @@ using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using Craftimizer.Solver; using Craftimizer.Solver;
using Craftimizer.Utils; using Craftimizer.Utils;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Utility; using Dalamud.Utility;
@@ -59,6 +61,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
private CancellationTokenSource? BestMacroTokenSource { get; set; } private CancellationTokenSource? BestMacroTokenSource { get; set; }
private Exception? BestMacroException { get; set; } private Exception? BestMacroException { get; set; }
public (Macro, SimulationState)? BestSavedMacro { get; private set; } public (Macro, SimulationState)? BestSavedMacro { get; private set; }
public bool HasSavedMacro { get; private set; }
public SolverSolution? BestSuggestedMacro { get; private set; } public SolverSolution? BestSuggestedMacro { get; private set; }
private IDalamudTextureWrap ExpertBadge { get; } private IDalamudTextureWrap ExpertBadge { get; }
@@ -82,7 +85,21 @@ public sealed unsafe class RecipeNote : Window, IDisposable
IsOpen = true; IsOpen = true;
} }
private bool wasOpen;
public override bool DrawConditions() 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) if (Service.ClientState.LocalPlayer == null)
return false; return false;
@@ -146,7 +163,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
statsChanged = true; statsChanged = true;
} }
if (statsChanged && CraftStatus == CraftableStatus.OK) if ((statsChanged || (BestMacroTokenSource?.IsCancellationRequested ?? false)) && CraftStatus == CraftableStatus.OK)
CalculateBestMacros(); CalculateBestMacros();
return true; return true;
@@ -172,16 +189,22 @@ public sealed unsafe class RecipeNote : Window, IDisposable
public override void Draw() 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) if (table)
{ {
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthFixed, 0);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawCharacterStats(); DrawCharacterStats();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawRecipeStats(); 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(); ImGui.Separator();
using (var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame))
{ {
using var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame);
if (table) if (table)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); availWidth -= ImGui.GetStyle().ItemSpacing.X * 2;
ImGuiUtils.TextCentered("Best Saved Macro"); using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", availWidth, out _))
{
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
if (BestSavedMacro is { } savedMacro) if (BestSavedMacro is { } savedMacro)
{ {
ImGuiUtils.TextCentered(savedMacro.Item1.Name); ImGuiUtils.TextCentered(savedMacro.Item1.Name, availWidth);
DrawMacro("savedMacro", (savedMacro.Item1.Actions, savedMacro.Item2)); DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsAvailWidthOffset, true);
} }
else else
{ {
ImGui.Text(""); ImGui.Text("");
DrawMacro("savedMacro", null); DrawMacro(null, null, stepsAvailWidthOffset, true);
}
} }
ImGui.Button("View Saved Macros", new(-1, 0));
ImGui.Separator(); using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", availWidth, out _))
{
ImGui.AlignTextToFramePadding(); var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
ImGuiUtils.TextCentered("Suggested Macro");
if (BestSuggestedMacro is { } suggestedMacro) if (BestSuggestedMacro is { } suggestedMacro)
DrawMacro("suggestedMacro", (suggestedMacro.Actions, suggestedMacro.State)); DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsAvailWidthOffset, false);
else else
DrawMacro("suggestedMacro", null); DrawMacro(null, null, stepsAvailWidthOffset, false);
ImGui.Button("Open Simulator", new(-1, 0)); }
ImGuiHelpers.ScaledDummy(5);
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; var levelText = string.Empty;
if (level != 0) if (level != 0)
levelText = SqText.ToLevelString(level); levelText = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(level);
var imageSize = ImGui.GetFrameHeight(); var imageSize = ImGui.GetFrameHeight();
bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false;
if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob))
@@ -338,6 +370,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
} }
else else
ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets."); ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets.");
ImGui.Dummy(default);
} }
break; break;
case CraftableStatus.SpecialistRequired: case CraftableStatus.SpecialistRequired:
@@ -439,9 +472,9 @@ public sealed unsafe class RecipeNote : Window, IDisposable
var textStarsSize = Vector2.Zero; var textStarsSize = Vector2.Zero;
if (!string.IsNullOrEmpty(textStars)) { if (!string.IsNullOrEmpty(textStars)) {
var layout = AxisFont.LayoutBuilder(textStars).Build(); 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 isExpert = RecipeData.RecipeInfo.IsExpert;
var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable;
var imageSize = ImGui.GetFrameHeight(); var imageSize = ImGui.GetFrameHeight();
@@ -452,7 +485,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
ImGuiUtils.AlignCentered( ImGuiUtils.AlignCentered(
imageSize + 5 + imageSize + 5 +
ImGui.CalcTextSize(textLevel).X + ImGui.CalcTextSize(textLevel).X +
textStarsSize.X + (textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) +
(isCollectable ? badgeSize.X + 3 : 0) + (isCollectable ? badgeSize.X + 3 : 0) +
(isExpert ? 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(); var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
if (macroValue == null) if (macroValue is not { } macro)
{ {
if (BestMacroException == null) if (isSavedMacro && !HasSavedMacro)
ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X, windowHeight + 1 + ImGui.GetStyle().ItemSpacing.Y)); 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 else
{ {
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
@@ -536,8 +569,8 @@ public sealed unsafe class RecipeNote : Window, IDisposable
} }
return; return;
} }
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
var macro = macroValue!.Value; throw new InvalidOperationException("Combo actions should be sanitized away");
using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
if (table) if (table)
@@ -552,7 +585,6 @@ public sealed unsafe class RecipeNote : Window, IDisposable
var spacing = ImGui.GetStyle().ItemSpacing.Y; var spacing = ImGui.GetStyle().ItemSpacing.Y;
var miniRowHeight = (windowHeight - spacing) / 2f; var miniRowHeight = (windowHeight - spacing) / 2f;
//ImGui.Text($"{macro.Actions.Count}");
{ {
if (Service.Configuration.ShowOptimalMacroStat) if (Service.Configuration.ShowOptimalMacroStat)
{ {
@@ -564,7 +596,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
progressHeight / 2f, progressHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
} }
@@ -575,7 +607,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
progressHeight / 2f, progressHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
} }
@@ -587,7 +619,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); ImGui.GetColorU32(Colors.Progress));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
@@ -597,7 +629,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); ImGui.GetColorU32(Colors.Quality));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
@@ -605,7 +637,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.DurabilityColor)); ImGui.GetColorU32(Colors.Durability));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}"); 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, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Plugin.Windows.Simulator.CPColor)); ImGui.GetColorU32(Colors.CP));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}"); ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}");
} }
@@ -623,32 +655,45 @@ public sealed unsafe class RecipeNote : Window, IDisposable
ImGui.TableNextColumn(); 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()) if (ImGui.IsItemHovered())
ImGui.SetTooltip($"{macro.Actions.Count} Step{(macro.Actions.Count != 1 ? "s" : "")}"); ImGui.SetTooltip("Open in Simulator");
using (var iconFont = ImRaii.PushFont(UiBuilder.IconFont)) if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight)))
if (ImGuiUtils.ButtonCentered(FontAwesomeIcon.Copy.ToIconString(), new(miniRowHeight))) Service.Plugin.CopyMacro(macro.Actions);
{
throw new NotImplementedException();
}
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Copy to Clipboard"); ImGui.SetTooltip("Copy to Clipboard");
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
{ {
var itemsPerRow = (int)MathF.Ceiling((ImGui.GetContentRegionAvail().X + spacing) / (miniRowHeight + spacing)); var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
var itemCount = Math.Min(macro.Actions.Count, itemsPerRow * 2); var itemCount = macro.Actions.Count;
for (var i = 0; i < itemsPerRow * 2; i++) for (var i = 0; i < itemsPerRow * 2; i++)
{ {
if (i % itemsPerRow != 0) if (i % itemsPerRow != 0)
ImGui.SameLine(0, spacing); ImGui.SameLine(0, spacing);
if (i < itemCount) 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)); ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); 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 else
ImGui.Dummy(new(miniRowHeight)); ImGui.Dummy(new(miniRowHeight));
} }
@@ -758,11 +803,17 @@ public sealed unsafe class RecipeNote : Window, IDisposable
BestMacroTokenSource = new(); BestMacroTokenSource = new();
BestMacroException = null; BestMacroException = null;
BestSavedMacro = null; BestSavedMacro = null;
HasSavedMacro = false;
BestSuggestedMacro = null; BestSuggestedMacro = null;
var token = BestMacroTokenSource.Token; var token = BestMacroTokenSource.Token;
_ = Task.Run(() => CalculateBestMacrosTask(token), token) var task = Task.Run(() => CalculateBestMacrosTask(token), token);
.ContinueWith(t => _ = task.ContinueWith(t =>
{
if (token == BestMacroTokenSource.Token)
BestMacroTokenSource = null;
});
_ = task.ContinueWith(t =>
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
return; return;
@@ -790,6 +841,9 @@ public sealed unsafe class RecipeNote : Window, IDisposable
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
HasSavedMacro = macros.Count > 0;
if (HasSavedMacro)
{
var bestSaved = macros var bestSaved = macros
.Select(macro => .Select(macro =>
{ {
@@ -810,6 +864,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
BestSavedMacro = (bestSaved.macro, bestSaved.outState); BestSavedMacro = (bestSaved.macro, bestSaved.outState);
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
}
var solver = new Solver.Solver(config, state) { Token = token }; var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug; solver.OnLog += Log.Debug;
@@ -819,10 +874,13 @@ public sealed unsafe class RecipeNote : Window, IDisposable
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
BestSuggestedMacro = solution; BestSuggestedMacro = solution;
token.ThrowIfCancellationRequested();
} }
public void Dispose() public void Dispose()
{ {
Service.WindowSystem.RemoveWindow(this);
AxisFont?.Dispose(); AxisFont?.Dispose();
} }
} }
+283 -93
View File
@@ -1,38 +1,34 @@
using Craftimizer.Solver; using Craftimizer.Solver;
using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET; using ImGuiNET;
using System; using System;
using System.Numerics; using System.Numerics;
namespace Craftimizer.Plugin.Windows; 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 static Configuration Config => Service.Configuration;
private const int OptionWidth = 200; private const int OptionWidth = 200;
private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight()); 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; } private string? SelectedTab { get; set; }
public Settings() : base("Craftimizer Settings") public Settings() : base("Craftimizer Settings", WindowFlags)
{ {
Service.WindowSystem.AddWindow(this); Service.WindowSystem.AddWindow(this);
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
MinimumSize = new Vector2(400, 400), MinimumSize = new(450, 400),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue) MaximumSize = new(float.PositiveInfinity)
}; };
Size = SizeConstraints.Value.MinimumSize;
SizeCondition = ImGuiCond.Appearing;
} }
public void SelectTab(string label) public void SelectTab(string label)
@@ -40,16 +36,16 @@ public class Settings : Window
SelectedTab = label; SelectedTab = label;
} }
private bool BeginTabItem(string label) private ImRaii.IEndObject TabItem(string label)
{ {
var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal); var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal);
if (isSelected) if (isSelected)
{ {
SelectedTab = null; SelectedTab = null;
var open = true; 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) private static void DrawOption(string label, string tooltip, bool val, Action<bool> setter, ref bool isDirty)
@@ -63,26 +59,45 @@ public class Settings : Window
ImGui.SetTooltip(tooltip); 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); 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); if (T.TryParse(text, null, out var newValue))
{
newValue = T.Clamp(newValue, min, max);
if (value != newValue)
{
setter(newValue);
isDirty = true; isDirty = true;
} }
}
}
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip); 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); ImGui.SetNextItemWidth(OptionWidth);
if (ImGui.InputFloat(label, ref val)) using (var combo = ImRaii.Combo(label, getName(value)))
{ {
setter(val); if (combo)
{
foreach (var type in Enum.GetValues<T>())
{
if (ImGui.Selectable(getName(type), value.Equals(type)))
{
setter(type);
isDirty = true; isDirty = true;
} }
if (ImGui.IsItemHovered())
ImGui.SetTooltip(getTooltip(type));
}
}
}
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip); ImGui.SetTooltip(tooltip);
} }
@@ -112,14 +127,33 @@ public class Settings : Window
_ => "Unknown" _ => "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() public override void Draw()
{ {
if (ImGui.BeginTabBar("settingsTabBar")) if (ImGui.BeginTabBar("settingsTabBar"))
{ {
DrawTabGeneral(); DrawTabGeneral();
DrawTabSimulator(); DrawTabSimulator();
if (Config.EnableSynthHelper) //if (Config.EnableSynthHelper)
DrawTabSynthHelper(); // DrawTabSynthHelper();
DrawTabAbout(); DrawTabAbout();
ImGui.EndTabBar(); ImGui.EndTabBar();
@@ -128,22 +162,18 @@ public class Settings : Window
private void DrawTabGeneral() private void DrawTabGeneral()
{ {
if (!BeginTabItem("General")) using var tab = TabItem("General");
if (!tab)
return; return;
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var isDirty = false; 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
);
using (var g = ImRaii.Group())
{
using var d = ImRaii.Disabled();
DrawOption( DrawOption(
"Enable Synthesis Helper", "Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft.\n" + "Adds a helper next to your synthesis window to help solve for the best craft.\n" +
@@ -153,6 +183,9 @@ public class Settings : Window
v => Config.EnableSynthHelper = v, v => Config.EnableSynthHelper = v,
ref isDirty ref isDirty
); );
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
DrawOption( DrawOption(
"Show Only One Macro Stat", "Show Only One Macro Stat",
@@ -164,10 +197,151 @@ public class Settings : Window
ref isDirty 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) if (isDirty)
Config.Save(); Config.Save();
ImGui.EndTabItem();
} }
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty) private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty)
@@ -176,35 +350,25 @@ public class Settings : Window
var config = configRef; var config = configRef;
ImGuiUtils.BeginGroupPanel("General"); using (var panel = ImGuiUtils.GroupPanel("General", -1, out _))
{
if (ImGui.Button("Reset to Default", OptionButtonSize)) if (ImGui.Button("Reset to Default", OptionButtonSize))
{ {
config = defaultConfig; config = defaultConfig;
isDirty = true; isDirty = true;
} }
ImGui.SetNextItemWidth(OptionWidth); DrawOption(
if (ImGui.BeginCombo("Algorithm", GetAlgorithmName(config.Algorithm))) "Algorithm",
{
foreach (var alg in Enum.GetValues<SolverAlgorithm>())
{
if (ImGui.Selectable(GetAlgorithmName(alg), config.Algorithm == alg))
{
config = config with { Algorithm = alg };
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(GetAlgorithmTooltip(alg));
}
ImGui.EndCombo();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
"The algorithm to use when solving for a macro. Different\n" + "The algorithm to use when solving for a macro. Different\n" +
"algorithms provide different pros and cons for using them.\n" + "algorithms provide different pros and cons for using them.\n" +
"By far, the Stepwise Furcated algorithm provides the best\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( DrawOption(
@@ -214,6 +378,8 @@ public class Settings : Window
"also may decrease variance, so other values should be tweaked\n" + "also may decrease variance, so other values should be tweaked\n" +
"as necessary to get a more favorable outcome.", "as necessary to get a more favorable outcome.",
config.Iterations, config.Iterations,
1000,
500000,
v => config = config with { Iterations = v }, v => config = config with { Iterations = v },
ref isDirty ref isDirty
); );
@@ -226,6 +392,8 @@ public class Settings : Window
"won't learn much per iteration; too high and it will waste time\n" + "won't learn much per iteration; too high and it will waste time\n" +
"on useless extra steps.", "on useless extra steps.",
config.MaxStepCount, config.MaxStepCount,
1,
100,
v => config = config with { MaxStepCount = v }, v => config = config with { MaxStepCount = v },
ref isDirty ref isDirty
); );
@@ -236,6 +404,8 @@ public class Settings : Window
"possibly good paths. If this value is too high,\n" + "possibly good paths. If this value is too high,\n" +
"moves will mostly be decided at random.", "moves will mostly be decided at random.",
config.ExplorationConstant, config.ExplorationConstant,
0,
10,
v => config = config with { ExplorationConstant = v }, v => config = config with { ExplorationConstant = v },
ref isDirty ref isDirty
); );
@@ -247,27 +417,29 @@ public class Settings : Window
"actions will be chosen based on their average outcome, whereas\n" + "actions will be chosen based on their average outcome, whereas\n" +
"1 uses their best outcome achieved so far.", "1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant, config.MaxScoreWeightingConstant,
0,
1,
v => config = config with { MaxScoreWeightingConstant = v }, v => config = config with { MaxScoreWeightingConstant = v },
ref isDirty ref isDirty
); );
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
DrawOption( DrawOption(
"Max Core Count", "Max Core Count",
"The number of cores to use when solving. You should use as many\n" + "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" + "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" + $"experience. A good estimate would be 1 or 2 cores less than your\n" +
$"system (FYI, you have {Environment.ProcessorCount} cores,) but\n" + $"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate\n" +
$"make sure to accomodate for any other tasks you have in the\n" + $"for any other tasks you have in the background, if you have any.\n" +
$"background, if you have any.\n" +
"(Only used in the Forked and Furcated algorithms)", "(Only used in the Forked and Furcated algorithms)",
config.MaxThreadCount, config.MaxThreadCount,
1,
Environment.ProcessorCount,
v => config = config with { MaxThreadCount = v }, v => config = config with { MaxThreadCount = v },
ref isDirty ref isDirty
); );
ImGui.EndDisabled();
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
DrawOption( DrawOption(
"Fork Count", "Fork Count",
"Split the number of iterations across different solvers. In general,\n" + "Split the number of iterations across different solvers. In general,\n" +
@@ -278,12 +450,13 @@ public class Settings : Window
"to the exploration constant.\n" + "to the exploration constant.\n" +
"(Only used in the Forked and Furcated algorithms)", "(Only used in the Forked and Furcated algorithms)",
config.ForkCount, config.ForkCount,
1,
500,
v => config = config with { ForkCount = v }, v => config = config with { ForkCount = v },
ref isDirty ref isDirty
); );
ImGui.EndDisabled();
ImGui.BeginDisabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated); using (var d = ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated))
DrawOption( DrawOption(
"Furcated Action Count", "Furcated Action Count",
"On every craft step, pick this many top solutions and use them as\n" + "On every craft step, pick this many top solutions and use them as\n" +
@@ -291,21 +464,23 @@ public class Settings : Window
"and add about 1 or 2 more if needed.\n" + "and add about 1 or 2 more if needed.\n" +
"(Only used in the Stepwise Furcated algorithm)", "(Only used in the Stepwise Furcated algorithm)",
config.FurcatedActionCount, config.FurcatedActionCount,
1,
500,
v => config = config with { FurcatedActionCount = v }, v => config = config with { FurcatedActionCount = v },
ref isDirty ref isDirty
); );
ImGui.EndDisabled(); }
ImGuiUtils.EndGroupPanel();
ImGuiUtils.BeginGroupPanel("Advanced");
using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _))
{
DrawOption( DrawOption(
"Score Storage Threshold", "Score Storage Threshold",
"If a craft achieves this certain arbitrary score, the solver will\n" + "If a craft achieves this certain arbitrary score, the solver will\n" +
"throw away all other possible combinations in favor of that one.\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.", "Only change this value if you absolutely know what you're doing.",
config.ScoreStorageThreshold, config.ScoreStorageThreshold,
0,
1,
v => config = config with { ScoreStorageThreshold = v }, v => config = config with { ScoreStorageThreshold = v },
ref isDirty ref isDirty
); );
@@ -316,6 +491,8 @@ public class Settings : Window
"Decreasing this value can have unintended side effects. Only change\n" + "Decreasing this value can have unintended side effects. Only change\n" +
"this value if you absolutely know what you're doing.", "this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount, config.MaxRolloutStepCount,
1,
50,
v => config = config with { MaxRolloutStepCount = v }, v => config = config with { MaxRolloutStepCount = v },
ref isDirty ref isDirty
); );
@@ -329,10 +506,10 @@ public class Settings : Window
v => config = config with { StrictActions = v }, v => config = config with { StrictActions = v },
ref isDirty ref isDirty
); );
}
ImGuiUtils.EndGroupPanel(); using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _))
{
ImGuiUtils.BeginGroupPanel("Score Weights (Advanced)");
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed."); ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
ImGuiHelpers.ScaledDummy(10); ImGuiHelpers.ScaledDummy(10);
@@ -340,6 +517,8 @@ public class Settings : Window
"Progress", "Progress",
"Amount of weight to give to the craft's progress.", "Amount of weight to give to the craft's progress.",
config.ScoreProgress, config.ScoreProgress,
0,
1,
v => config = config with { ScoreProgress = v }, v => config = config with { ScoreProgress = v },
ref isDirty ref isDirty
); );
@@ -348,6 +527,8 @@ public class Settings : Window
"Quality", "Quality",
"Amount of weight to give to the craft's quality.", "Amount of weight to give to the craft's quality.",
config.ScoreQuality, config.ScoreQuality,
0,
1,
v => config = config with { ScoreQuality = v }, v => config = config with { ScoreQuality = v },
ref isDirty ref isDirty
); );
@@ -356,6 +537,8 @@ public class Settings : Window
"Durability", "Durability",
"Amount of weight to give to the craft's remaining durability.", "Amount of weight to give to the craft's remaining durability.",
config.ScoreDurability, config.ScoreDurability,
0,
1,
v => config = config with { ScoreDurability = v }, v => config = config with { ScoreDurability = v },
ref isDirty ref isDirty
); );
@@ -364,6 +547,8 @@ public class Settings : Window
"CP", "CP",
"Amount of weight to give to the craft's remaining CP.", "Amount of weight to give to the craft's remaining CP.",
config.ScoreCP, config.ScoreCP,
0,
1,
v => config = config with { ScoreCP = v }, v => config = config with { ScoreCP = v },
ref isDirty ref isDirty
); );
@@ -373,6 +558,8 @@ public class Settings : Window
"Amount of weight to give to the craft's number of steps. The lower\n" + "Amount of weight to give to the craft's number of steps. The lower\n" +
"the step count, the higher the score.", "the step count, the higher the score.",
config.ScoreSteps, config.ScoreSteps,
0,
1,
v => config = config with { ScoreSteps = v }, v => config = config with { ScoreSteps = v },
ref isDirty ref isDirty
); );
@@ -396,8 +583,7 @@ public class Settings : Window
} }
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Normalize all weights to sum up to 1"); ImGui.SetTooltip("Normalize all weights to sum up to 1");
}
ImGuiUtils.EndGroupPanel();
if (isDirty) if (isDirty)
configRef = config; configRef = config;
@@ -405,22 +591,17 @@ public class Settings : Window
private void DrawTabSimulator() private void DrawTabSimulator()
{ {
if (!BeginTabItem("Simulator")) using var tab = TabItem("Simulator");
if (!tab)
return; return;
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var isDirty = false; var isDirty = false;
DrawOption( using (var g = ImRaii.Group())
"Show Only Learned Actions", {
"Don't show crafting actions that haven't been\n" + using var d = ImRaii.Disabled();
"learned yet with your current job on the simulator sidebar",
Config.HideUnlearnedActions,
v => Config.HideUnlearnedActions = v,
ref isDirty
);
DrawOption( DrawOption(
"Condition Randomness", "Condition Randomness",
"Allows the simulator condition to fluctuate randomly like a real craft.\n" + "Allows the simulator condition to fluctuate randomly like a real craft.\n" +
@@ -429,6 +610,9 @@ public class Settings : Window
v => Config.ConditionRandomness = v, v => Config.ConditionRandomness = v,
ref isDirty ref isDirty
); );
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.Separator(); ImGui.Separator();
@@ -444,13 +628,12 @@ public class Settings : Window
if (isDirty) if (isDirty)
Config.Save(); Config.Save();
ImGui.EndTabItem();
} }
private void DrawTabSynthHelper() private void DrawTabSynthHelper()
{ {
if (!BeginTabItem("Synthesis Helper")) using var tab = TabItem("Synthesis Helper");
if (!tab)
return; return;
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
@@ -461,6 +644,8 @@ public class Settings : Window
"Step Count", "Step Count",
"The number of future actions to solve for during an in-game craft.", "The number of future actions to solve for during an in-game craft.",
Config.SynthHelperStepCount, Config.SynthHelperStepCount,
1,
100,
v => Config.SynthHelperStepCount = v, v => Config.SynthHelperStepCount = v,
ref isDirty ref isDirty
); );
@@ -479,13 +664,12 @@ public class Settings : Window
if (isDirty) if (isDirty)
Config.Save(); Config.Save();
ImGui.EndTabItem();
} }
private void DrawTabAbout() private void DrawTabAbout()
{ {
if (!BeginTabItem("About")) using var tab = TabItem("About");
if (!tab)
return; return;
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
@@ -493,28 +677,34 @@ public class Settings : Window
var plugin = Service.Plugin; var plugin = Service.Plugin;
var icon = plugin.Icon; var icon = plugin.Icon;
ImGui.BeginTable("settingsAboutTable", 2); using (var table = ImRaii.Table("settingsAboutTable", 2))
{
if (table)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width); ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height)); ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}"); ImGui.Text($"Craftimizer v{plugin.Version} {plugin.BuildConfiguration}");
ImGui.Text($"By {plugin.Author} ("); ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGui.Text(")"); ImGui.Text(")");
}
ImGui.EndTable(); }
ImGui.Text("Credit to altosock's "); ImGui.Text("Credit to altosock's ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app"); ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGui.Text(" for the original solver algorithm"); 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; namespace Craftimizer.Simulator;
public enum ActionCategory public enum ActionCategory
@@ -13,6 +16,25 @@ public enum ActionCategory
public static class ActionCategoryUtils 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) => public static string GetDisplayName(this ActionCategory category) =>
category switch category switch
{ {
+1
View File
@@ -48,6 +48,7 @@ public abstract class BaseAction
s.ActionStates.MutateState(this); s.ActionStates.MutateState(this);
s.ActionCount++; s.ActionCount++;
if (IncreasesStepCount)
s.ActiveEffects.DecrementDuration(); s.ActiveEffects.DecrementDuration();
} }
+4 -1
View File
@@ -14,10 +14,13 @@ internal abstract class BaseBuffAction : BaseAction
public override void UseSuccess(Simulator s) => public override void UseSuccess(Simulator s) =>
s.AddEffect(Effect, Duration); 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)); var builder = new StringBuilder(base.GetTooltip(s, addUsability));
builder.AppendLine($"{Duration} Steps"); builder.AppendLine($"{Duration} Steps");
return builder.ToString(); 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 bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3;
public override void UseSuccess(Simulator s) => s.StepCondition(); 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 int CPCost(Simulator s) => 0;
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; 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) public override void Use(Simulator s)
{ {
if (s.HasEffect(EffectType.Manipulation))
s.RestoreDurability(5);
s.ReduceCP(CPCost(s));
s.ReduceDurability(DurabilityCost);
UseSuccess(s); UseSuccess(s);
s.ReduceCP(CPCost(s));
s.IncreaseStepCount(); 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) => public override void UseSuccess(Simulator s) =>
s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality); 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 _ => 0
}; };
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsIndefinite(EffectType effect) =>
effect is EffectType.InnerQuiet or EffectType.HeartAndSoul;
[Pure] [Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly byte GetStrength(EffectType effect) => 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 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 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; public readonly bool IsFirstStep => StepCount == 0;
+2
View File
@@ -63,6 +63,8 @@ public class Simulator
return ActionResponse.ActionNotUnlocked; return ActionResponse.ActionNotUnlocked;
if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation) if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation)
return ActionResponse.ActionNotUnlocked; return ActionResponse.ActionNotUnlocked;
if (action is ActionType.CarefulObservation or ActionType.HeartAndSoul && !Input.Stats.IsSpecialist)
return ActionResponse.ActionNotUnlocked;
if (baseAction.CPCost(this) > CP) if (baseAction.CPCost(this) > CP)
return ActionResponse.NotEnoughCP; return ActionResponse.NotEnoughCP;
return ActionResponse.CannotUseAction; return ActionResponse.CannotUseAction;
+20 -17
View File
@@ -35,9 +35,6 @@ public sealed class Solver : IDisposable
// Always called when a new step is generated. // Always called when a new step is generated.
public event NewActionDelegate? OnNewAction; public event NewActionDelegate? OnNewAction;
// Always called when the solver is fully complete.
public event SolutionDelegate? OnSolution;
public Solver(SolverConfig config, SimulationState state) public Solver(SolverConfig config, SimulationState state)
{ {
Config = config; Config = config;
@@ -107,6 +104,12 @@ public sealed class Solver : IDisposable
CompletionTask?.Dispose(); CompletionTask?.Dispose();
} }
private void InvokeNewAction(ActionType action)
{
foreach (var sanitizedAction in SolverSolution.SanitizeCombo(action))
OnNewAction?.Invoke(sanitizedAction);
}
private async Task<SolverSolution> SearchStepwiseFurcated() private async Task<SolverSolution> SearchStepwiseFurcated()
{ {
var definiteActionCount = 0; var definiteActionCount = 0;
@@ -115,7 +118,7 @@ public sealed class Solver : IDisposable
var state = State; var state = State;
var sim = new Simulator(state, Config.MaxStepCount); 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) while (activeStates.Count != 0)
{ {
@@ -162,12 +165,12 @@ public sealed class Solver : IDisposable
if (bestAction.MaxScore >= Config.ScoreStorageThreshold) if (bestAction.MaxScore >= Config.ScoreStorageThreshold)
{ {
var (_, furcatedActionIdx, solution) = bestAction; 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)) foreach (var action in activeActions.Skip(definiteActionCount))
OnNewAction?.Invoke(action); InvokeNewAction(action);
return solution with { Actions = activeActions }; return solution with { ActionEnumerable = activeActions };
} }
var newStates = new List<SolverSolution>(Config.FurcatedActionCount); var newStates = new List<SolverSolution>(Config.FurcatedActionCount);
@@ -214,7 +217,7 @@ public sealed class Solver : IDisposable
if (definiteCount != equalCount) if (definiteCount != equalCount)
{ {
foreach(var action in refActions.Take(equalCount).Skip(definiteCount)) foreach(var action in refActions.Take(equalCount).Skip(definiteCount))
OnNewAction?.Invoke(action); InvokeNewAction(action);
definiteActionCount = equalCount; definiteActionCount = equalCount;
} }
@@ -224,11 +227,11 @@ public sealed class Solver : IDisposable
} }
if (bestSims.Count == 0) if (bestSims.Count == 0)
return new(new(), state); return new(Array.Empty<ActionType>(), state);
var result = bestSims.MaxBy(s => s.Score).Result; var result = bestSims.MaxBy(s => s.Score).Result;
foreach (var action in result.Actions.Skip(definiteActionCount)) foreach (var action in result.Actions.Skip(definiteActionCount))
OnNewAction?.Invoke(action); InvokeNewAction(action);
return result; return result;
} }
@@ -282,12 +285,12 @@ public sealed class Solver : IDisposable
{ {
actions.AddRange(solution.Actions); actions.AddRange(solution.Actions);
foreach (var action in solution.Actions) foreach (var action in solution.Actions)
OnNewAction?.Invoke(action); InvokeNewAction(action);
return solution with { Actions = actions }; return solution with { Actions = actions };
} }
var chosenAction = solution.Actions[0]; var chosenAction = solution.Actions[0];
OnNewAction?.Invoke(chosenAction); InvokeNewAction(chosenAction);
(_, state) = sim.Execute(state, chosenAction); (_, state) = sim.Execute(state, chosenAction);
actions.Add(chosenAction); actions.Add(chosenAction);
@@ -321,12 +324,12 @@ public sealed class Solver : IDisposable
{ {
actions.AddRange(solution.Actions); actions.AddRange(solution.Actions);
foreach (var action in solution.Actions) foreach (var action in solution.Actions)
OnNewAction?.Invoke(action); InvokeNewAction(action);
return Task.FromResult(solution with { Actions = actions }); return Task.FromResult(solution with { Actions = actions });
} }
var chosenAction = solution.Actions[0]; var chosenAction = solution.Actions[0];
OnNewAction?.Invoke(chosenAction); InvokeNewAction(chosenAction);
(_, state) = sim.Execute(state, chosenAction); (_, state) = sim.Execute(state, chosenAction);
actions.Add(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; var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution;
foreach (var action in solution.Actions) foreach (var action in solution.Actions)
OnNewAction?.Invoke(action); InvokeNewAction(action);
return solution; return solution;
} }
@@ -376,7 +379,7 @@ public sealed class Solver : IDisposable
solver.Search(Config.Iterations, Token); solver.Search(Config.Iterations, Token);
var solution = solver.Solution(); var solution = solver.Solution();
foreach (var action in solution.Actions) foreach (var action in solution.Actions)
OnNewAction?.Invoke(action); InvokeNewAction(action);
return Task.FromResult(solution); return Task.FromResult(solution);
} }
+40 -1
View File
@@ -3,4 +3,43 @@ using Craftimizer.Simulator.Actions;
namespace Craftimizer.Solver; 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;
}
}
}