diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj
index 1b2e168..c021083 100644
--- a/Craftimizer/Craftimizer.csproj
+++ b/Craftimizer/Craftimizer.csproj
@@ -28,6 +28,12 @@
+
+
+
+
+
+
@@ -50,6 +56,10 @@
$(DalamudLibPath)Dalamud.dll
false
+
+ $(DalamudLibPath)Dalamud.Interface.dll
+ false
+
$(DalamudLibPath)ImGui.NET.dll
false
diff --git a/Craftimizer/Graphics/collectible_badge.png b/Craftimizer/Graphics/collectible_badge.png
new file mode 100644
index 0000000..5b3b890
Binary files /dev/null and b/Craftimizer/Graphics/collectible_badge.png differ
diff --git a/Craftimizer/Graphics/expert.png b/Craftimizer/Graphics/expert.png
new file mode 100644
index 0000000..db26b79
Binary files /dev/null and b/Craftimizer/Graphics/expert.png differ
diff --git a/Craftimizer/Graphics/expert_badge.png b/Craftimizer/Graphics/expert_badge.png
new file mode 100644
index 0000000..5186387
Binary files /dev/null and b/Craftimizer/Graphics/expert_badge.png differ
diff --git a/Craftimizer/Graphics/no_manip.png b/Craftimizer/Graphics/no_manip.png
new file mode 100644
index 0000000..d1ef9f9
Binary files /dev/null and b/Craftimizer/Graphics/no_manip.png differ
diff --git a/Craftimizer/Graphics/specialist.png b/Craftimizer/Graphics/specialist.png
new file mode 100644
index 0000000..2eafa00
Binary files /dev/null and b/Craftimizer/Graphics/specialist.png differ
diff --git a/Craftimizer/Graphics/splendorous.png b/Craftimizer/Graphics/splendorous.png
new file mode 100644
index 0000000..9817b87
Binary files /dev/null and b/Craftimizer/Graphics/splendorous.png differ
diff --git a/Craftimizer/Icons.cs b/Craftimizer/Icons.cs
deleted file mode 100644
index 37ebeb0..0000000
--- a/Craftimizer/Icons.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using ImGuiScene;
-using System.Collections.Generic;
-
-namespace Craftimizer.Plugin;
-
-internal static class Icons
-{
- private static readonly Dictionary Cache = new();
-
- public static TextureWrap GetIconFromPath(string path)
- {
- if (!Cache.TryGetValue(path, out var ret))
- Cache.Add(path, ret = Service.DataManager.GetImGuiTexture(path)!);
- return ret;
- }
-
- public static TextureWrap GetIconFromId(ushort id) =>
- GetIconFromPath($"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex");
-}
diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs
index c895934..0e34dc6 100644
--- a/Craftimizer/ImGuiUtils.cs
+++ b/Craftimizer/ImGuiUtils.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
-
namespace Craftimizer.Plugin;
internal static class ImGuiUtils
@@ -158,4 +157,24 @@ internal static class ImGuiUtils
ImGui.SetTooltip("Open in Browser");
}
}
+
+ public static void AlignCentered(float width)
+ {
+ var availWidth = ImGui.GetContentRegionAvail().X;
+ if (availWidth > width)
+ ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2);
+ }
+
+ // https://stackoverflow.com/a/67855985
+ public static void TextCentered(string text)
+ {
+ AlignCentered(ImGui.CalcTextSize(text).X);
+ ImGui.TextUnformatted(text);
+ }
+
+ public static bool ButtonCentered(string text)
+ {
+ AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2);
+ return ImGui.Button(text);
+ }
}
diff --git a/Craftimizer/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs
index 67ba2e9..18301f2 100644
--- a/Craftimizer/LuminaSheets.cs
+++ b/Craftimizer/LuminaSheets.cs
@@ -16,6 +16,9 @@ public static class LuminaSheets
public static readonly ExcelSheet ClassJobSheet = Service.DataManager.GetExcelSheet()!;
public static readonly ExcelSheet- ItemSheet = Service.DataManager.GetExcelSheet
- ()!;
public static readonly ExcelSheet
- ItemSheetEnglish = Service.DataManager.GetExcelSheet
- (ClientLanguage.English)!;
+ public static readonly ExcelSheet ENpcResidentSheet = Service.DataManager.GetExcelSheet()!;
+ public static readonly ExcelSheet LevelSheet = Service.DataManager.GetExcelSheet()!;
+ public static readonly ExcelSheet QuestSheet = Service.DataManager.GetExcelSheet()!;
public static readonly ExcelSheet MateriaSheet = Service.DataManager.GetExcelSheet()!;
public static readonly ExcelSheet BaseParamSheet = Service.DataManager.GetExcelSheet()!;
public static readonly ExcelSheet ItemFoodSheet = Service.DataManager.GetExcelSheet()!;
diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs
index cb31ff4..c62db6e 100644
--- a/Craftimizer/Plugin.cs
+++ b/Craftimizer/Plugin.cs
@@ -1,6 +1,7 @@
using Craftimizer.Plugin.Windows;
using Craftimizer.Simulator;
using Craftimizer.Utils;
+using Craftimizer.Windows;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
@@ -16,39 +17,35 @@ public sealed class Plugin : IDalamudPlugin
public string Name => "Craftimizer";
public string Version { get; }
public string Author { get; }
- public string Configuration { get; }
+ public string BuildConfiguration { get; }
public TextureWrap Icon { get; }
public WindowSystem WindowSystem { get; }
public Settings SettingsWindow { get; }
- public CraftingLog RecipeNoteWindow { get; }
+ public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; }
public Craft SynthesisWindow { get; }
public Windows.Simulator? SimulatorWindow { get; set; }
+ public Configuration Configuration { get; }
public Hooks Hooks { get; }
- public RecipeNote RecipeNote { get; }
+ public Craftimizer.Utils.RecipeNote RecipeNote { get; }
+ public IconManager IconManager { get; }
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
{
- Service.Plugin = this;
- pluginInterface.Create();
- Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
+ Service.Initialize(this, pluginInterface);
+
+ Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
+ Hooks = new();
+ RecipeNote = new();
+ IconManager = new();
+ WindowSystem = new(Name);
var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetCustomAttribute()!.InformationalVersion;
Author = assembly.GetCustomAttribute()!.Company;
- Configuration = assembly.GetCustomAttribute()!.Configuration;
- byte[] iconData;
- using (var stream = assembly.GetManifestResourceStream("Craftimizer.icon.png")!)
- {
- iconData = new byte[stream.Length];
- _ = stream.Read(iconData);
- }
- Icon = Service.PluginInterface.UiBuilder.LoadImage(iconData);
-
- Hooks = new();
- RecipeNote = new();
- WindowSystem = new(Name);
+ BuildConfiguration = assembly.GetCustomAttribute()!.Configuration;
+ Icon = IconManager.GetAssemblyTexture("icon.png");
SettingsWindow = new();
RecipeNoteWindow = new();
@@ -86,5 +83,6 @@ public sealed class Plugin : IDalamudPlugin
SynthesisWindow.Dispose();
RecipeNote.Dispose();
Hooks.Dispose();
+ IconManager.Dispose();
}
}
diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs
index 0c02ad0..e09304e 100644
--- a/Craftimizer/Service.cs
+++ b/Craftimizer/Service.cs
@@ -1,13 +1,11 @@
-using Dalamud.Data;
+using Craftimizer.Utils;
using Dalamud.Game;
-using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
-using Dalamud.Game.Command;
-using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
namespace Craftimizer.Plugin;
@@ -15,19 +13,26 @@ public sealed class Service
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
[PluginService] public static DalamudPluginInterface PluginInterface { get; private set; }
- [PluginService] public static CommandManager CommandManager { get; private set; }
- [PluginService] public static ObjectTable Objects { get; private set; }
- [PluginService] public static SigScanner SigScanner { get; private set; }
- [PluginService] public static GameGui GameGui { get; private set; }
- [PluginService] public static ClientState ClientState { get; private set; }
- [PluginService] public static DataManager DataManager { get; private set; }
- [PluginService] public static TargetManager TargetManager { get; private set; }
+ [PluginService] public static ICommandManager CommandManager { get; private set; }
+ [PluginService] public static IObjectTable Objects { get; private set; }
+ [PluginService] public static ISigScanner SigScanner { get; private set; }
+ [PluginService] public static IGameGui GameGui { get; private set; }
+ [PluginService] public static IClientState ClientState { get; private set; }
+ [PluginService] public static IDataManager DataManager { get; private set; }
+ [PluginService] public static ITextureProvider TextureProvider { get; private set; }
+ [PluginService] public static ITargetManager TargetManager { get; private set; }
[PluginService] public static Condition Condition { get; private set; }
[PluginService] public static Framework Framework { get; private set; }
- public static Plugin Plugin { get; internal set; }
- public static Configuration Configuration { get; internal set; }
+ public static Plugin Plugin { get; private set; }
+ public static Configuration Configuration => Plugin.Configuration;
public static WindowSystem WindowSystem => Plugin.WindowSystem;
+ public static IconManager IconManager => Plugin.IconManager;
#pragma warning restore CS8618
+ internal static void Initialize(Plugin plugin, DalamudPluginInterface iface)
+ {
+ Plugin = plugin;
+ iface.Create();
+ }
}
diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs
index 27eeb87..4d30def 100644
--- a/Craftimizer/SimulatorUtils.cs
+++ b/Craftimizer/SimulatorUtils.cs
@@ -2,6 +2,9 @@ using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
using System;
@@ -10,8 +13,10 @@ using System.Linq;
using System.Numerics;
using System.Text;
using Action = Lumina.Excel.GeneratedSheets.Action;
+using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob;
using Condition = Craftimizer.Simulator.Condition;
+using Status = Lumina.Excel.GeneratedSheets.Status;
namespace Craftimizer.Plugin;
@@ -85,11 +90,11 @@ internal static class ActionUtils
{
var (craftAction, action) = GetActionRow(me, classJob);
if (craftAction != null)
- return Icons.GetIconFromId(craftAction.Icon);
+ return Service.IconManager.GetIcon(craftAction.Icon);
if (action != null)
- return Icons.GetIconFromId(action.Icon);
+ return Service.IconManager.GetIcon(action.Icon);
// Old "Steady Hand" action icon
- return Icons.GetIconFromId(1953);
+ return Service.IconManager.GetIcon(1953);
}
public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction)
@@ -145,12 +150,24 @@ internal static class ClassJobUtils
public static sbyte GetExpArrayIdx(this ClassJob me) =>
LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex;
- public static string GetName(this ClassJob classJob)
+ public static unsafe short GetPlayerLevel(this ClassJob me) =>
+ PlayerState.Instance()->ClassJobLevelArray[me.GetExpArrayIdx()];
+
+ public static unsafe bool CanPlayerUseManipulation(this ClassJob me) =>
+ ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(me), (GameObject*)Service.ClientState.LocalPlayer!.Address);
+
+ public static string GetName(this ClassJob me)
{
- var job = LuminaSheets.ClassJobSheet.GetRow(classJob.GetClassJobIndex())!;
+ var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!;
return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue);
}
+ public static Quest GetUnlockQuest(this ClassJob me) =>
+ LuminaSheets.QuestSheet.GetRow(65720 + (uint)me) ?? throw new ArgumentException($"Could not get unlock quest for {me}", nameof(me));
+
+ public static ushort GetIconId(this ClassJob me) =>
+ (ushort)(62100 + me.GetClassJobIndex());
+
public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) =>
classJob switch
{
@@ -303,7 +320,7 @@ internal static class EffectUtils
}
public static TextureWrap GetIcon(this EffectType me, int strength) =>
- Icons.GetIconFromId(me.GetIconId(strength));
+ Service.IconManager.GetIcon(me.GetIconId(strength));
public static string GetTooltip(this EffectType me, int strength, int duration)
{
diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs
index 86e16c5..0a6dd19 100644
--- a/Craftimizer/Utils/Gearsets.cs
+++ b/Craftimizer/Utils/Gearsets.cs
@@ -8,7 +8,7 @@ using System;
using System.Linq;
namespace Craftimizer.Plugin.Utils;
-internal static unsafe class Gearsets
+public static unsafe class Gearsets
{
public record struct GearsetStats(int CP, int Craftsmanship, int Control);
public record struct GearsetMateria(ushort Type, ushort Grade);
@@ -117,9 +117,12 @@ internal static unsafe class Gearsets
public static bool IsSpecialistSoulCrystal(GearsetItem item)
{
+ if (item.itemId == 0)
+ return false;
+
var luminaItem = LuminaSheets.ItemSheet.GetRow(item.itemId)!;
- // Soul Crystal ItemUICategory DoH Category
- return luminaItem.ItemUICategory.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
+ // Soul Crystal ItemUICategory DoH Category
+ return luminaItem.ItemUICategory.Row == 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
}
public static bool IsSplendorousTool(GearsetItem item) =>
diff --git a/Craftimizer/Utils/IconManager.cs b/Craftimizer/Utils/IconManager.cs
new file mode 100644
index 0000000..abefac2
--- /dev/null
+++ b/Craftimizer/Utils/IconManager.cs
@@ -0,0 +1,66 @@
+using Craftimizer.Plugin;
+using Dalamud.Interface.Internal;
+using ImGuiScene;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace Craftimizer.Utils;
+
+public sealed class IconManager : IDisposable
+{
+ private readonly Dictionary iconCache = new();
+ private readonly Dictionary textureCache = new();
+ private readonly Dictionary assemblyCache = new();
+
+ public IDalamudTextureWrap GetIcon(uint id)
+ {
+ if (!iconCache.TryGetValue(id, out var ret))
+ iconCache.Add(id, ret = Service.TextureProvider.GetIcon(id) ??
+ throw new ArgumentException($"Invalid icon id {id}", nameof(id)));
+ return ret;
+ }
+
+ public IDalamudTextureWrap GetTexture(string path)
+ {
+ if (!textureCache.TryGetValue(path, out var ret))
+ textureCache.Add(path, ret = Service.TextureProvider.GetTextureFromGame(path) ??
+ throw new ArgumentException($"Invalid texture {path}", nameof(path)));
+ return ret;
+ }
+
+ public TextureWrap GetAssemblyTexture(string filename)
+ {
+ if (!assemblyCache.TryGetValue(filename, out var ret))
+ assemblyCache.Add(filename, ret = GetAssemblyTextureInternal(filename));
+ return ret;
+ }
+
+ private static TextureWrap GetAssemblyTextureInternal(string filename)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ byte[] iconData;
+ using (var stream = assembly.GetManifestResourceStream($"Craftimizer.{filename}") ?? throw new InvalidDataException($"Could not load resource {filename}"))
+ {
+ iconData = new byte[stream.Length];
+ _ = stream.Read(iconData);
+ }
+ return Service.PluginInterface.UiBuilder.LoadImage(iconData);
+ }
+
+ public void Dispose()
+ {
+ foreach (var image in iconCache.Values)
+ image.Dispose();
+ iconCache.Clear();
+
+ foreach (var image in textureCache.Values)
+ image.Dispose();
+ textureCache.Clear();
+
+ foreach (var image in assemblyCache.Values)
+ image.Dispose();
+ assemblyCache.Clear();
+ }
+}
diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs
index 9178682..d27154b 100644
--- a/Craftimizer/Utils/RecipeNote.cs
+++ b/Craftimizer/Utils/RecipeNote.cs
@@ -16,6 +16,52 @@ 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 = (int)Table.Quality * Recipe.QualityFactor / 100,
+ 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; }
diff --git a/Craftimizer/Utils/SqText.cs b/Craftimizer/Utils/SqText.cs
new file mode 100644
index 0000000..b8ab740
--- /dev/null
+++ b/Craftimizer/Utils/SqText.cs
@@ -0,0 +1,33 @@
+using Dalamud.Game.Text;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Craftimizer.Utils;
+
+public static class SqText
+{
+ private static ReadOnlyDictionary levelNumReplacements = new(new Dictionary
+ {
+ ['0'] = SeIconChar.Number0,
+ ['1'] = SeIconChar.Number1,
+ ['2'] = SeIconChar.Number2,
+ ['3'] = SeIconChar.Number3,
+ ['4'] = SeIconChar.Number4,
+ ['5'] = SeIconChar.Number5,
+ ['6'] = SeIconChar.Number6,
+ ['7'] = SeIconChar.Number7,
+ ['8'] = SeIconChar.Number8,
+ ['9'] = SeIconChar.Number9,
+ });
+
+ public static string ToLevelString(T value) where T : IBinaryInteger
+ {
+ var str = value.ToString() ?? throw new FormatException("Failed to format value");
+ foreach(var (k, v) in levelNumReplacements)
+ str = str.Replace(k, v.ToIconChar());
+ return SeIconChar.LevelEn.ToIconChar() + str;
+ }
+}
diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs
new file mode 100644
index 0000000..4685ded
--- /dev/null
+++ b/Craftimizer/Windows/RecipeNote.cs
@@ -0,0 +1,542 @@
+using Craftimizer.Plugin;
+using Craftimizer.Plugin.Utils;
+using Craftimizer.Simulator;
+using Craftimizer.Simulator.Actions;
+using Craftimizer.Utils;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Interface;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.GameFonts;
+using Dalamud.Interface.Raii;
+using Dalamud.Interface.Windowing;
+using Dalamud.Logging;
+using Dalamud.Utility;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+using ImGuiScene;
+using System;
+using System.Linq;
+using System.Numerics;
+using ActionType = Craftimizer.Simulator.Actions.ActionType;
+using ClassJob = Craftimizer.Simulator.ClassJob;
+using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote;
+
+namespace Craftimizer.Windows;
+
+public sealed unsafe class RecipeNote : Window, IDisposable
+{
+ private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
+ | ImGuiWindowFlags.AlwaysAutoResize
+ | ImGuiWindowFlags.NoSavedSettings
+ | ImGuiWindowFlags.NoFocusOnAppearing
+ | ImGuiWindowFlags.NoNavFocus;
+
+ public enum CraftableStatus
+ {
+ OK,
+ LockedClassJob,
+ WrongClassJob,
+ SpecialistRequired,
+ RequiredItem,
+ RequiredStatus,
+ CraftsmanshipTooLow,
+ ControlTooLow,
+ }
+
+ public AddonRecipeNote* Addon { get; private set; }
+ public RecipeData? RecipeData { get; private set; }
+ public CharacterStats? CharacterStats { get; private set; }
+ public CraftableStatus CraftStatus { get; private set; }
+
+ private TextureWrap ExpertBadge { get; }
+ private TextureWrap CollectibleBadge { get; }
+ private TextureWrap SplendorousBadge { get; }
+ private TextureWrap SpecialistBadge { get; }
+ private TextureWrap NoManipulationBadge { get; }
+ private GameFontHandle AxisFont { get; }
+
+ public RecipeNote() : base("Craftimizer RecipeNode", WindowFlags, false)
+ {
+ ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png");
+ CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png");
+ SplendorousBadge = Service.IconManager.GetAssemblyTexture("Graphics.splendorous.png");
+ SpecialistBadge = Service.IconManager.GetAssemblyTexture("Graphics.specialist.png");
+ NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png");
+ AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14));
+
+ Service.WindowSystem.AddWindow(this);
+
+ IsOpen = true;
+ }
+
+ public override bool DrawConditions()
+ {
+ if (Service.ClientState.LocalPlayer == null)
+ return false;
+
+ {
+ Addon = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote");
+ if (Addon == null)
+ return false;
+
+ // Check if RecipeNote addon is visible
+ if (Addon->AtkUnitBase.WindowNode == null)
+ return false;
+
+ // Check if RecipeNote has a visible selected recipe
+ if (!Addon->Unk258->IsVisible)
+ return false;
+ }
+
+ {
+ var instance = CSRecipeNote.Instance();
+
+ var list = instance->RecipeList;
+ if (list == null)
+ return false;
+
+ var recipeEntry = list->SelectedRecipe;
+ if (recipeEntry == null)
+ return false;
+
+ var recipeId = recipeEntry->RecipeId;
+ RecipeData = new(recipeId);
+ }
+
+ Gearsets.GearsetItem[] gearItems;
+ {
+ var gearStats = Gearsets.CalculateGearsetCurrentStats();
+
+ var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
+ if (container == null)
+ return false;
+
+ gearItems = Gearsets.GetGearsetItems(container);
+
+ CharacterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation());
+ }
+
+ CraftStatus = CalculateCraftStatus(gearItems);
+
+ return true;
+ }
+
+ public override void PreDraw()
+ {
+ 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 = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59);
+ var nodeParent = Addon->Unk258; // unit.GetNodeById(57);
+
+ Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale);
+ SizeConstraints = new WindowSizeConstraints
+ {
+ MinimumSize = new(-1),
+ MaximumSize = new(10000, 10000)
+ };
+ }
+
+ public override void Draw()
+ {
+ using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
+ if (table)
+ {
+ ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch);
+ ImGui.TableNextColumn();
+ DrawCharacterStats();
+ ImGui.TableNextColumn();
+ DrawRecipeStats();
+ }
+ }
+
+ private void DrawCharacterStats()
+ {
+ ImGuiUtils.TextCentered("Crafter");
+
+ var level = RecipeData!.ClassJob.GetPlayerLevel();
+ {
+ var className = RecipeData.ClassJob.GetName();
+ var levelText = string.Empty;
+ if (level != 0)
+ levelText = SqText.ToLevelString(level);
+ var imageSize = ImGuiUtils.ButtonHeight;
+ bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false;
+ if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob))
+ {
+ hasSplendorous = CharacterStats!.HasSplendorousBuff;
+ hasSpecialist = CharacterStats!.IsSpecialist;
+ shouldHaveManip = !CharacterStats.CanUseManipulation && CharacterStats.Level >= ActionType.Manipulation.Level();
+ }
+
+ ImGuiUtils.AlignCentered(
+ imageSize + 5 +
+ ImGui.CalcTextSize(className).X +
+ (level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X)) +
+ (hasSplendorous ? (3 + imageSize) : 0) +
+ (hasSpecialist ? (3 + imageSize) : 0) +
+ (shouldHaveManip ? (3 + imageSize) : 0)
+ );
+ ImGui.AlignTextToFramePadding();
+
+ ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize));
+ ImGui.SameLine(0, 5);
+
+ if (level != 0)
+ {
+ ImGui.Text(levelText);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(level)}");
+ ImGui.SameLine(0, 3);
+ }
+
+ ImGui.Text(className);
+
+ if (hasSplendorous)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.Image(SplendorousBadge.ImGuiHandle, new Vector2(imageSize));
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"Splendorous Tool");
+ }
+
+ if (hasSpecialist)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.Image(SpecialistBadge.ImGuiHandle, new Vector2(imageSize), Vector2.Zero, Vector2.One, new(0.99f, 0.97f, 0.62f, 1f));
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"Specialist");
+ }
+
+ if (shouldHaveManip)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.Image(NoManipulationBadge.ImGuiHandle, new Vector2(imageSize));
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"No Manipulation (Missing Job Quest)");
+ }
+ }
+
+ ImGui.Separator();
+
+ switch (CraftStatus)
+ {
+ case CraftableStatus.LockedClassJob:
+ {
+ ImGuiUtils.TextCentered($"You do not have {RecipeData.ClassJob.GetName().ToLowerInvariant()} unlocked.");
+ ImGui.Separator();
+ var unlockQuest = RecipeData.ClassJob.GetUnlockQuest();
+ var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row);
+
+ var unlockText = $"Unlock it from {questGiver}";
+ ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight);
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text(unlockText);
+ ImGui.SameLine(0, 5);
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
+ Service.GameGui.OpenMapWithMapLink(mapPayload);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip("Open in map");
+
+ ImGuiUtils.TextCentered($"{questTerritory} ({questLocation.X:0.0}, {questLocation.Y:0.0})");
+ }
+ break;
+ case CraftableStatus.WrongClassJob:
+ {
+ ImGuiUtils.TextCentered($"You are not a {RecipeData.ClassJob.GetName().ToLowerInvariant()}.");
+ var gearsetId = GetGearsetForJob(RecipeData.ClassJob);
+ if (gearsetId.HasValue)
+ {
+ if (ImGuiUtils.ButtonCentered("Switch Job"))
+ Chat.SendMessage($"/gearset change {gearsetId + 1}");
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}");
+ }
+ else
+ ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets.");
+ }
+ break;
+ case CraftableStatus.SpecialistRequired:
+ {
+ ImGuiUtils.TextCentered($"You need to be a specialist to craft this recipe.");
+
+ var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399);
+
+ var unlockText = $"Trade a Soul of the Crafter to {vendorName}";
+ ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight);
+ ImGui.AlignTextToFramePadding();
+ ImGui.Text(unlockText);
+ ImGui.SameLine(0, 5);
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
+ Service.GameGui.OpenMapWithMapLink(mapPayload);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip("Open in map");
+
+ ImGuiUtils.TextCentered($"{vendorTerritory} ({vendorLoation.X:0.0}, {vendorLoation.Y:0.0})");
+ }
+ break;
+ case CraftableStatus.RequiredItem:
+ {
+ var item = RecipeData.Recipe.ItemRequired.Value!;
+ var itemName = item.Name.ToDalamudString().ToString();
+ var imageSize = ImGuiUtils.ButtonHeight;
+
+ ImGuiUtils.TextCentered($"You are missing the required equipment.");
+ ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X);
+ ImGui.AlignTextToFramePadding();
+ ImGui.Image(Service.IconManager.GetIcon(item.Icon).ImGuiHandle, new(imageSize));
+ ImGui.SameLine(0, 5);
+ ImGui.Text(itemName);
+ }
+ break;
+ case CraftableStatus.RequiredStatus:
+ {
+ var status = RecipeData.Recipe.StatusRequired.Value!;
+ var statusName = status.Name.ToDalamudString().ToString();
+ var statusIcon = Service.IconManager.GetIcon(status.Icon);
+ var imageSize = new Vector2(ImGuiUtils.ButtonHeight * statusIcon.Width / statusIcon.Height, ImGuiUtils.ButtonHeight);
+
+ ImGuiUtils.TextCentered($"You are missing the required status effect.");
+ ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X);
+ ImGui.AlignTextToFramePadding();
+ ImGui.Image(statusIcon.ImGuiHandle, imageSize);
+ ImGui.SameLine(0, 5);
+ ImGui.Text(statusName);
+ }
+ break;
+ case CraftableStatus.CraftsmanshipTooLow:
+ {
+ ImGuiUtils.TextCentered("Your Craftsmanship is too low.");
+
+ DrawRequiredStatsTable(CharacterStats!.Craftsmanship, RecipeData.Recipe.RequiredCraftsmanship);
+ }
+ break;
+ case CraftableStatus.ControlTooLow:
+ {
+ ImGuiUtils.TextCentered("Your Control is too low.");
+
+ DrawRequiredStatsTable(CharacterStats!.Control, RecipeData.Recipe.RequiredControl);
+ }
+ break;
+ case CraftableStatus.OK:
+ {
+ using var table = ImRaii.Table("characterStats", 2, ImGuiTableFlags.NoHostExtendX);
+ if (table)
+ {
+ ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100);
+ ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch);
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Craftsmanship");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{CharacterStats!.Craftsmanship}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Control");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{CharacterStats.Control}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("CP");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{CharacterStats.CP}");
+ }
+ }
+ break;
+ }
+ }
+
+ private void DrawRecipeStats()
+ {
+ ImGuiUtils.TextCentered("Recipe");
+
+ {
+ var textStars = new string('★', RecipeData!.Table.Stars);
+ var textStarsSize = Vector2.Zero;
+ if (!string.IsNullOrEmpty(textStars)) {
+ var layout = AxisFont.LayoutBuilder(textStars).Build();
+ textStarsSize = new(layout.Width + 3, layout.Height);
+ }
+ var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
+ var isExpert = RecipeData.RecipeInfo.IsExpert;
+ var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable;
+ var imageSize = ImGuiUtils.ButtonHeight;
+ var textSize = ImGui.CalcTextSize("A").Y;
+ var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize);
+ var badgeOffset = (imageSize - badgeSize.Y) / 2;
+
+ ImGuiUtils.AlignCentered(
+ imageSize + 5 +
+ ImGui.CalcTextSize(textLevel).X +
+ textStarsSize.X +
+ (isCollectable ? badgeSize.X + 3 : 0) +
+ (isExpert ? badgeSize.X + 3 : 0)
+ );
+ ImGui.AlignTextToFramePadding();
+
+ ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize));
+
+ ImGui.SameLine(0, 5);
+ ImGui.Text(textLevel);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}");
+
+ if (textStarsSize != Vector2.Zero)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (textStarsSize.Y - textSize) / 2);
+ AxisFont.Text(textStars);
+ }
+
+ if (isCollectable)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
+ ImGui.Image(CollectibleBadge.ImGuiHandle, badgeSize);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"Collectible");
+ }
+
+ if (isExpert)
+ {
+ ImGui.SameLine(0, 3);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
+ ImGui.Image(ExpertBadge.ImGuiHandle, badgeSize);
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip($"Expert Recipe");
+ }
+ }
+
+ using var table = ImRaii.Table("recipeStats", 2);
+ if (table)
+ {
+ ImGui.TableSetupColumn("rcol1", ImGuiTableColumnFlags.WidthFixed, 100);
+ ImGui.TableSetupColumn("rcol2", ImGuiTableColumnFlags.WidthStretch);
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Progress");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{RecipeData.RecipeInfo.MaxProgress}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Quality");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{RecipeData.RecipeInfo.MaxQuality}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Durability");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{RecipeData.RecipeInfo.MaxDurability}");
+ }
+ }
+
+ private static void DrawRequiredStatsTable(int current, int required)
+ {
+ if (current >= required)
+ throw new ArgumentOutOfRangeException(nameof(current));
+
+ using var table = ImRaii.Table("requiredStats", 2, ImGuiTableFlags.NoHostExtendX);
+ if (table)
+ {
+ ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100);
+ ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch);
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Current");
+ ImGui.TableNextColumn();
+ ImGui.TextColored(new(0, 1, 0, 1), $"{current}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("Required");
+ ImGui.TableNextColumn();
+ ImGui.TextColored(new(1, 0, 0, 1), $"{required}");
+
+ ImGui.TableNextColumn();
+ ImGui.Text("You need");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{required - current}");
+ }
+ }
+
+ private CraftableStatus CalculateCraftStatus(Gearsets.GearsetItem[] gearItems)
+ {
+ if (RecipeData!.ClassJob.GetPlayerLevel() == 0)
+ return CraftableStatus.LockedClassJob;
+
+ if (PlayerState.Instance()->CurrentClassJobId != RecipeData.ClassJob.GetClassJobIndex())
+ return CraftableStatus.WrongClassJob;
+
+ if (RecipeData.Recipe.IsSpecializationRequired && !(CharacterStats!.IsSpecialist))
+ return CraftableStatus.SpecialistRequired;
+
+ var itemRequired = RecipeData.Recipe.ItemRequired;
+ if (itemRequired.Row != 0 && itemRequired.Value != null)
+ {
+ if (!gearItems.Any(i => Gearsets.IsItem(i, itemRequired.Row)))
+ return CraftableStatus.RequiredItem;
+ }
+
+ var statusRequired = RecipeData.Recipe.StatusRequired;
+ if (statusRequired.Row != 0 && statusRequired.Value != null)
+ {
+ if (!Service.ClientState.LocalPlayer!.StatusList.Any(s => s.StatusId == statusRequired.Row))
+ return CraftableStatus.RequiredStatus;
+ }
+
+ if (RecipeData.Recipe.RequiredCraftsmanship > CharacterStats!.Craftsmanship)
+ return CraftableStatus.CraftsmanshipTooLow;
+
+ if (RecipeData.Recipe.RequiredControl > CharacterStats.Control)
+ return CraftableStatus.ControlTooLow;
+
+ return CraftableStatus.OK;
+ }
+
+ private static (string NpcName, string Territory, Vector2 MapLocation, MapLinkPayload Payload) ResolveLevelData(uint levelRowId)
+ {
+ var level = LuminaSheets.LevelSheet.GetRow(levelRowId) ??
+ throw new ArgumentNullException(nameof(levelRowId), $"Invalid level row {levelRowId}");
+ var territory = level.Territory.Value!.PlaceName.Value!.Name.ToDalamudString().ToString();
+ var location = MapUtil.WorldToMap(new(level.X, level.Z), level.Map.Value!);
+
+ return (ResolveNpcResidentName(level.Object), territory, location, new(level.Territory.Row, level.Map.Row, location.X, location.Y));
+ }
+
+ private static string ResolveNpcResidentName(uint npcRowId)
+ {
+ var resident = LuminaSheets.ENpcResidentSheet.GetRow(npcRowId) ??
+ throw new ArgumentNullException(nameof(npcRowId), $"Invalid npc row {npcRowId}");
+ return resident.Singular.ToDalamudString().ToString();
+ }
+
+ private static int? GetGearsetForJob(ClassJob job)
+ {
+ var gearsetModule = RaptureGearsetModule.Instance();
+ for (var i = 0; i < 100; i++)
+ {
+ var gearset = gearsetModule->Gearset[i];
+ if (gearset == null)
+ continue;
+ if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists))
+ continue;
+ if (gearset->ID != i)
+ continue;
+ if (gearset->ClassJob != job.GetClassJobIndex())
+ continue;
+ return i;
+ }
+ return null;
+ }
+
+ public void Dispose()
+ {
+ AxisFont?.Dispose();
+ }
+}
diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs
index e55ada3..1088b3a 100644
--- a/Craftimizer/Windows/Settings.cs
+++ b/Craftimizer/Windows/Settings.cs
@@ -473,7 +473,7 @@ public class Settings : Window
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn();
- ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.Configuration}");
+ ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}");
ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs
index 9ad426e..acc0d57 100644
--- a/Craftimizer/Windows/SimulatorDrawer.cs
+++ b/Craftimizer/Windows/SimulatorDrawer.cs
@@ -133,7 +133,7 @@ public sealed partial class Simulator : Window, IDisposable
{
var imageSize = new Vector2(ImGui.GetFontSize() * 2.25f);
- ImGui.Image(Icons.GetIconFromId(Item.Icon).ImGuiHandle, imageSize);
+ ImGui.Image(Service.IconManager.GetIcon(Item.Icon).ImGuiHandle, imageSize);
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f);
ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString());