From 8b8245bf0a210d9f37289107b9207fffc96f86b6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 1 Oct 2023 22:50:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Initial=20rework=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Workshoppa.sln | 16 ++ Workshoppa/.gitignore | 3 + Workshoppa/Callback.cs | 161 +++++++++++ Workshoppa/Configuration.cs | 25 ++ Workshoppa/External/DalamudReflector.cs | 114 ++++++++ Workshoppa/External/YesAlreadyIpc.cs | 53 ++++ Workshoppa/GameData/CraftItem.cs | 17 ++ Workshoppa/GameData/CraftState.cs | 21 ++ Workshoppa/GameData/WorkshopCache.cs | 73 +++++ Workshoppa/GameData/WorkshopCraft.cs | 13 + Workshoppa/GameData/WorkshopCraftCategory.cs | 8 + Workshoppa/GameData/WorkshopCraftItem.cs | 10 + Workshoppa/GameData/WorkshopCraftPhase.cs | 9 + Workshoppa/Stage.cs | 21 ++ Workshoppa/Windows/MainWindow.cs | 163 ++++++++++++ Workshoppa/WorkshopPlugin.Craft.cs | 122 +++++++++ Workshoppa/WorkshopPlugin.CraftingLog.cs | 133 ++++++++++ Workshoppa/WorkshopPlugin.GameFunctions.cs | 266 +++++++++++++++++++ Workshoppa/WorkshopPlugin.cs | 232 ++++++++++++++++ Workshoppa/Workshoppa.csproj | 69 +++++ Workshoppa/Workshoppa.json | 7 + Workshoppa/packages.lock.json | 13 + global.json | 7 + 24 files changed, 1558 insertions(+) create mode 100644 .gitignore create mode 100644 Workshoppa.sln create mode 100644 Workshoppa/.gitignore create mode 100644 Workshoppa/Callback.cs create mode 100644 Workshoppa/Configuration.cs create mode 100644 Workshoppa/External/DalamudReflector.cs create mode 100644 Workshoppa/External/YesAlreadyIpc.cs create mode 100644 Workshoppa/GameData/CraftItem.cs create mode 100644 Workshoppa/GameData/CraftState.cs create mode 100644 Workshoppa/GameData/WorkshopCache.cs create mode 100644 Workshoppa/GameData/WorkshopCraft.cs create mode 100644 Workshoppa/GameData/WorkshopCraftCategory.cs create mode 100644 Workshoppa/GameData/WorkshopCraftItem.cs create mode 100644 Workshoppa/GameData/WorkshopCraftPhase.cs create mode 100644 Workshoppa/Stage.cs create mode 100644 Workshoppa/Windows/MainWindow.cs create mode 100644 Workshoppa/WorkshopPlugin.Craft.cs create mode 100644 Workshoppa/WorkshopPlugin.CraftingLog.cs create mode 100644 Workshoppa/WorkshopPlugin.GameFunctions.cs create mode 100644 Workshoppa/WorkshopPlugin.cs create mode 100644 Workshoppa/Workshoppa.csproj create mode 100644 Workshoppa/Workshoppa.json create mode 100644 Workshoppa/packages.lock.json create mode 100644 global.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05dc549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +*.user diff --git a/Workshoppa.sln b/Workshoppa.sln new file mode 100644 index 0000000..3332f95 --- /dev/null +++ b/Workshoppa.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workshoppa", "Workshoppa\Workshoppa.csproj", "{4C2E2AD7-D897-4476-A17A-838932D95223}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4C2E2AD7-D897-4476-A17A-838932D95223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C2E2AD7-D897-4476-A17A-838932D95223}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C2E2AD7-D897-4476-A17A-838932D95223}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C2E2AD7-D897-4476-A17A-838932D95223}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Workshoppa/.gitignore b/Workshoppa/.gitignore new file mode 100644 index 0000000..958518b --- /dev/null +++ b/Workshoppa/.gitignore @@ -0,0 +1,3 @@ +/dist +/obj +/bin diff --git a/Workshoppa/Callback.cs b/Workshoppa/Callback.cs new file mode 100644 index 0000000..69bc8a7 --- /dev/null +++ b/Workshoppa/Callback.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using Dalamud.Game; +using Dalamud.Logging; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Workshoppa; + +public sealed unsafe class Callback +{ + private delegate byte AtkUnitBase_FireCallbackDelegate(AtkUnitBase* @base, int valueCount, AtkValue* values, + byte updateState); + + private readonly AtkUnitBase_FireCallbackDelegate FireCallback; + + public static readonly AtkValue ZeroAtkValue = new() { Type = 0, Int = 0 }; + + public Callback(SigScanner sigScanner) + { + var ptr = sigScanner.ScanText("E8 ?? ?? ?? ?? 8B 4C 24 20 0F B6 D8"); + FireCallback = Marshal.GetDelegateForFunctionPointer(ptr); + PluginLog.Information($"Initialized Callback module, FireCallback = 0x{ptr:X16}"); + } + + public void FireRaw(AtkUnitBase* @base, int valueCount, AtkValue* values, byte updateState = 0) + { + FireCallback(@base, valueCount, values, updateState); + } + + public void Fire(AtkUnitBase* @base, bool updateState, params object[] values) + { + if (@base == null) throw new Exception("Null UnitBase"); + var atkValues = (AtkValue*)Marshal.AllocHGlobal(values.Length * sizeof(AtkValue)); + if (atkValues == null) return; + try + { + for (var i = 0; i < values.Length; i++) + { + var v = values[i]; + switch (v) + { + case uint uintValue: + atkValues[i].Type = ValueType.UInt; + atkValues[i].UInt = uintValue; + break; + case int intValue: + atkValues[i].Type = ValueType.Int; + atkValues[i].Int = intValue; + break; + case float floatValue: + atkValues[i].Type = ValueType.Float; + atkValues[i].Float = floatValue; + break; + case bool boolValue: + atkValues[i].Type = ValueType.Bool; + atkValues[i].Byte = (byte)(boolValue ? 1 : 0); + break; + case string stringValue: + { + atkValues[i].Type = ValueType.String; + var stringBytes = Encoding.UTF8.GetBytes(stringValue); + var stringAlloc = Marshal.AllocHGlobal(stringBytes.Length + 1); + Marshal.Copy(stringBytes, 0, stringAlloc, stringBytes.Length); + Marshal.WriteByte(stringAlloc, stringBytes.Length, 0); + atkValues[i].String = (byte*)stringAlloc; + break; + } + case AtkValue rawValue: + { + atkValues[i] = rawValue; + break; + } + default: + throw new ArgumentException($"Unable to convert type {v.GetType()} to AtkValue"); + } + } + +#if false + List callbackValues = new(); + for (var i = 0; i < values.Length; i++) + { + callbackValues.Add( + $" Value {i}: [input: {values[i]}/{values[i]?.GetType().Name}] -> {DecodeValue(atkValues[i])})"); + } +#endif + + PluginLog.Verbose( + $"Firing callback: {MemoryHelper.ReadStringNullTerminated((nint)@base->Name)}, valueCount = {values.Length}, updateStatte = {updateState}, values:\n"); + FireRaw(@base, values.Length, atkValues, (byte)(updateState ? 1 : 0)); + } + finally + { + for (var i = 0; i < values.Length; i++) + { + if (atkValues[i].Type == ValueType.String) + { + Marshal.FreeHGlobal(new IntPtr(atkValues[i].String)); + } + } + + Marshal.FreeHGlobal(new IntPtr(atkValues)); + } + } + + public static string DecodeValues(int cnt, AtkValue* values) + { + var atkValueList = new List(); + try + { + for (var i = 0; i < cnt; i++) + { + atkValueList.Add(DecodeValue(values[i])); + } + } + catch (Exception e) + { + PluginLog.Error("Could not decode values", e); + } + + return string.Join("\n", atkValueList); + } + + public static string DecodeValue(AtkValue a) + { + var str = new StringBuilder(a.Type.ToString()).Append(": "); + switch (a.Type) + { + case ValueType.Int: + { + str.Append(a.Int); + break; + } + case ValueType.String: + { + str.Append(Marshal.PtrToStringUTF8(new IntPtr(a.String))); + break; + } + case ValueType.UInt: + { + str.Append(a.UInt); + break; + } + case ValueType.Bool: + { + str.Append(a.Byte != 0); + break; + } + default: + { + str.Append($"Unknown Type: {a.Int}"); + break; + } + } + + return str.ToString(); + } +} diff --git a/Workshoppa/Configuration.cs b/Workshoppa/Configuration.cs new file mode 100644 index 0000000..9913a4c --- /dev/null +++ b/Workshoppa/Configuration.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Dalamud.Configuration; + +namespace Workshoppa; + +internal sealed class Configuration : IPluginConfiguration +{ + public int Version { get; set; } = 1; + + public CurrentItem? CurrentlyCraftedItem = null; + public List ItemQueue = new(); + + internal sealed class QueuedItem + { + public uint WorkshopItemId { get; set; } + public int Quantity { get; set; } + } + + internal sealed class CurrentItem + { + public uint WorkshopItemId { get; set; } + public bool StartedCrafting { get; set; } + public bool FinishedCrafting { get; set; } + } +} diff --git a/Workshoppa/External/DalamudReflector.cs b/Workshoppa/External/DalamudReflector.cs new file mode 100644 index 0000000..1bb2895 --- /dev/null +++ b/Workshoppa/External/DalamudReflector.cs @@ -0,0 +1,114 @@ +using Dalamud.Game; +using Dalamud.Logging; + + +using Dalamud.Plugin; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Workshoppa.External; + +/// +/// Originally part of ECommons by NightmareXIV. +/// +/// https://github.com/NightmareXIV/ECommons/blob/master/ECommons/Reflection/DalamudReflector.cs +/// +internal sealed class DalamudReflector : IDisposable +{ + private readonly DalamudPluginInterface _pluginInterface; + private readonly Framework _framework; + private readonly Dictionary _pluginCache = new(); + private bool _pluginsChanged = false; + + public DalamudReflector(DalamudPluginInterface pluginInterface, Framework framework) + { + _pluginInterface = pluginInterface; + _framework = framework; + var pm = GetPluginManager(); + pm.GetType().GetEvent("OnInstalledPluginsChanged")!.AddEventHandler(pm, OnInstalledPluginsChanged); + + _framework.Update += FrameworkUpdate; + } + + public void Dispose() + { + _framework.Update -= FrameworkUpdate; + + var pm = GetPluginManager(); + pm.GetType().GetEvent("OnInstalledPluginsChanged")!.RemoveEventHandler(pm, OnInstalledPluginsChanged); + } + + private void FrameworkUpdate(Framework framework) + { + if (_pluginsChanged) + { + _pluginsChanged = false; + _pluginCache.Clear(); + } + } + + private object GetPluginManager() + { + return _pluginInterface.GetType().Assembly.GetType("Dalamud.Service`1", true)! + .MakeGenericType( + _pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!) + .GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty(), null)!; + } + + public bool TryGetDalamudPlugin(string internalName, out IDalamudPlugin? instance, bool suppressErrors = false, + bool ignoreCache = false) + { + if (!ignoreCache && _pluginCache.TryGetValue(internalName, out instance)) + { + return true; + } + + try + { + var pluginManager = GetPluginManager(); + var installedPlugins = + (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue( + pluginManager)!; + + foreach (var t in installedPlugins) + { + if ((string?)t.GetType().GetProperty("Name")!.GetValue(t) == internalName) + { + var type = t.GetType().Name == "LocalDevPlugin" ? t.GetType().BaseType : t.GetType(); + var plugin = (IDalamudPlugin?)type! + .GetField("instance", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(t); + if (plugin == null) + { + PluginLog.Warning($"[DalamudReflector] Found requested plugin {internalName} but it was null"); + } + else + { + instance = plugin; + _pluginCache[internalName] = plugin; + return true; + } + } + } + + instance = null; + return false; + } + catch (Exception e) + { + if (!suppressErrors) + { + PluginLog.Error(e, $"Can't find {internalName} plugin: {e.Message}"); + } + + instance = null; + return false; + } + } + + private void OnInstalledPluginsChanged() + { + PluginLog.Verbose("Installed plugins changed event fired"); + _pluginsChanged = true; + } +} diff --git a/Workshoppa/External/YesAlreadyIpc.cs b/Workshoppa/External/YesAlreadyIpc.cs new file mode 100644 index 0000000..884179d --- /dev/null +++ b/Workshoppa/External/YesAlreadyIpc.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using Dalamud.Logging; + +namespace Workshoppa.External; + +internal sealed class YesAlreadyIpc +{ + private readonly DalamudReflector _dalamudReflector; + + public YesAlreadyIpc(DalamudReflector dalamudReflector) + { + _dalamudReflector = dalamudReflector; + } + + private object? GetConfiguration() + { + if (_dalamudReflector.TryGetDalamudPlugin("Yes Already", out var plugin)) + { + var pluginService = plugin!.GetType().Assembly.GetType("YesAlready.Service"); + return pluginService!.GetProperty("Configuration", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null); + } + + return null; + } + + public bool? DisableIfNecessary() + { + object? configuration = GetConfiguration(); + if (configuration == null) + return null; + + var property = configuration.GetType().GetProperty("Enabled")!; + bool enabled = (bool)property.GetValue(configuration)!; + if (enabled) + { + property.SetValue(configuration, false); + return true; + } + + return false; + } + + public void Enable() + { + object? configuration = GetConfiguration(); + if (configuration == null) + return; + + + var property = configuration.GetType().GetProperty("Enabled")!; + property.SetValue(configuration, true); + } +} diff --git a/Workshoppa/GameData/CraftItem.cs b/Workshoppa/GameData/CraftItem.cs new file mode 100644 index 0000000..230f553 --- /dev/null +++ b/Workshoppa/GameData/CraftItem.cs @@ -0,0 +1,17 @@ +namespace Workshoppa.GameData; + +public class CraftItem +{ + public uint ItemId { get; set; } + public uint IconId { get; set; } + public string? ItemName { get; set; } + public int CrafterIconId { get; set; } + public uint ItemCountPerStep { get; set; } + public uint ItemCountNQ { get; set; } + public uint ItemCountHQ { get; set; } + public uint Experience { get; set; } + public uint StepsComplete { get; set; } + public uint StepsTotal { get; set; } + public bool Finished { get; set; } + public uint CrafterMinimumLevel { get; set; } +} diff --git a/Workshoppa/GameData/CraftState.cs b/Workshoppa/GameData/CraftState.cs new file mode 100644 index 0000000..334edac --- /dev/null +++ b/Workshoppa/GameData/CraftState.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Workshoppa.GameData; + +public sealed class CraftState +{ + public required uint ResultItem { get; init; } + public required uint StepsComplete { get; init; } + public required uint StepsTotal { get; init; } + public required List Items { get; init; } + + public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal); + + public bool IsCraftComplete() => StepsComplete == StepsTotal - 1 && IsPhaseComplete(); +} diff --git a/Workshoppa/GameData/WorkshopCache.cs b/Workshoppa/GameData/WorkshopCache.cs new file mode 100644 index 0000000..0887d9e --- /dev/null +++ b/Workshoppa/GameData/WorkshopCache.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Data; +using Dalamud.Logging; +using Lumina.Excel.GeneratedSheets; + +namespace Workshoppa.GameData; + +internal sealed class WorkshopCache +{ + public WorkshopCache(DataManager dataManager) + { + Task.Run(() => + { + try + { + Dictionary itemMapping = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .ToDictionary(x => (ushort)x.RowId, x => x.Item.Value!); + + Crafts = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0) + .Select(x => new WorkshopCraft + { + WorkshopItemId = x.RowId, + ResultItem = x.ResultItem.Row, + Name = x.ResultItem.Value!.Name.ToString(), + Category = (WorkshopCraftCategory)x.CompanyCraftDraftCategory.Row, + Type = x.CompanyCraftType.Row, + Phases = x.CompanyCraftPart.Where(part => part.Row != 0) + .SelectMany(part => + part.Value!.CompanyCraftProcess + .Where(y => y.Value!.UnkData0.Any(z => z.SupplyItem > 0)) + .Select(y => (Type: part.Value!.CompanyCraftType.Value, Process: y))) + .Select(y => new WorkshopCraftPhase + { + Name = y.Type!.Name.ToString(), + Items = y.Process.Value!.UnkData0 + .Where(item => item.SupplyItem > 0) + .Select(item => new WorkshopCraftItem + { + ItemId = itemMapping[item.SupplyItem].RowId, + Name = itemMapping[item.SupplyItem].Name.ToString(), + SetQuantity = item.SetQuantity, + SetsRequired = item.SetsRequired, + }) + .ToList() + .AsReadOnly(), + }) + .ToList() + .AsReadOnly(), + }) + .ToList() + .AsReadOnly(); + } + catch (Exception e) + { + PluginLog.Error(e, "Unable to load cached items"); + } + }); + } + + /* + /waitaddon "CompanyCraftRecipeNoteBook" + /pcall CompanyCraftRecipeNoteBook false 2 0 1u 16u 548u 1505u 715u 0 + /wait 0.3 + /pcall CompanyCraftRecipeNoteBook false 1 0 0 0 548u 0 0 0 + */ + + public IReadOnlyList Crafts { get; private set; } = new List(); +} diff --git a/Workshoppa/GameData/WorkshopCraft.cs b/Workshoppa/GameData/WorkshopCraft.cs new file mode 100644 index 0000000..0b1d8cb --- /dev/null +++ b/Workshoppa/GameData/WorkshopCraft.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Workshoppa.GameData; + +internal sealed class WorkshopCraft +{ + public required uint WorkshopItemId { get; init; } + public required uint ResultItem { get; init; } + public required string Name { get; init; } + public required WorkshopCraftCategory Category { get; init; } + public required uint Type { get; init; } + public required IReadOnlyList Phases { get; init; } +} diff --git a/Workshoppa/GameData/WorkshopCraftCategory.cs b/Workshoppa/GameData/WorkshopCraftCategory.cs new file mode 100644 index 0000000..dfb6f4f --- /dev/null +++ b/Workshoppa/GameData/WorkshopCraftCategory.cs @@ -0,0 +1,8 @@ +namespace Workshoppa.GameData; + +public enum WorkshopCraftCategory +{ + AetherialWheels = 0, + AirshipsSubmersibles = 1, + Housing = 2, +} diff --git a/Workshoppa/GameData/WorkshopCraftItem.cs b/Workshoppa/GameData/WorkshopCraftItem.cs new file mode 100644 index 0000000..2cce343 --- /dev/null +++ b/Workshoppa/GameData/WorkshopCraftItem.cs @@ -0,0 +1,10 @@ +namespace Workshoppa.GameData; + +internal sealed class WorkshopCraftItem +{ + public required uint ItemId { get; init; } + public required string Name { get; init; } + public required int SetQuantity { get; init; } + public required int SetsRequired { get; init; } + public int TotalQuantity => SetQuantity * SetsRequired; +} diff --git a/Workshoppa/GameData/WorkshopCraftPhase.cs b/Workshoppa/GameData/WorkshopCraftPhase.cs new file mode 100644 index 0000000..4d2d71b --- /dev/null +++ b/Workshoppa/GameData/WorkshopCraftPhase.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Workshoppa.GameData; + +internal sealed class WorkshopCraftPhase +{ + public required string Name { get; init; } + public required IReadOnlyList Items { get; init; } +} diff --git a/Workshoppa/Stage.cs b/Workshoppa/Stage.cs new file mode 100644 index 0000000..1dcb03b --- /dev/null +++ b/Workshoppa/Stage.cs @@ -0,0 +1,21 @@ +namespace Workshoppa; + +public enum Stage +{ + TakeItemFromQueue, + TargetFabricationStation, + + OpenCraftingLog, + SelectCraftCategory, + SelectCraft, + ConfirmCraft, + + SelectCraftBranch, + ContributeMaterials, + ConfirmMaterialDelivery, + + ConfirmCollectProduct, + + RequestStop, + Stopped, +} diff --git a/Workshoppa/Windows/MainWindow.cs b/Workshoppa/Windows/MainWindow.cs new file mode 100644 index 0000000..f1aedfe --- /dev/null +++ b/Workshoppa/Windows/MainWindow.cs @@ -0,0 +1,163 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using ImGuiNET; +using Workshoppa.GameData; + +namespace Workshoppa.Windows; + +internal sealed class MainWindow : Window +{ + private readonly WorkshopPlugin _plugin; + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _configuration; + private readonly WorkshopCache _workshopCache; + + private string _searchString = string.Empty; + + public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration, WorkshopCache workshopCache) + : base("Workshoppa###WorkshoppaMainWindow") + { + _plugin = plugin; + _pluginInterface = pluginInterface; + _configuration = configuration; + _workshopCache = workshopCache; + + Position = new Vector2(100, 100); + PositionCondition = ImGuiCond.FirstUseEver; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(350, 50), + MaximumSize = new Vector2(500, 500), + }; + + Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse; + } + + public bool NearFabricationStation { get; set; } = false; + public ButtonState State { get; set; } = ButtonState.None; + + public override void Draw() + { + var currentItem = _configuration.CurrentlyCraftedItem; + if (currentItem != null) + { + var currentCraft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == currentItem.WorkshopItemId); + ImGui.Text($"Currently Crafting: {currentCraft.Name}"); + + ImGui.BeginDisabled(!NearFabricationStation); + if (_plugin.CurrentStage == Stage.Stopped) + { + if (currentItem.StartedCrafting) + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Resume")) + State = ButtonState.Resume; + } + else + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting")) + State = ButtonState.Start; + } + + ImGui.SameLine(); + ImGui.BeginDisabled(!ImGui.GetIO().KeyCtrl); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel")) + { + State = ButtonState.Pause; + _configuration.CurrentlyCraftedItem = null; + + Save(); + } + ImGui.EndDisabled(); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && !ImGui.GetIO().KeyCtrl) + ImGui.SetTooltip( + $"Hold CTRL to remove this as craft. You have to manually use the fabrication station to cancel or finish this craft before you can continue using the queue."); + } + else + { + ImGui.BeginDisabled(_plugin.CurrentStage == Stage.RequestStop); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Pause, "Pause")) + State = ButtonState.Pause; + + ImGui.EndDisabled(); + } + ImGui.EndDisabled(); + } + else + { + ImGui.Text("Currently Crafting: ---"); + + ImGui.BeginDisabled(!NearFabricationStation || _configuration.ItemQueue.Sum(x => x.Quantity) == 0 || _plugin.CurrentStage != Stage.Stopped); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting")) + State = ButtonState.Start; + ImGui.EndDisabled(); + } + + ImGui.Separator(); + ImGui.Text("Queue:"); + //ImGui.BeginDisabled(); + for (int i = 0; i < _configuration.ItemQueue.Count; ++ i) + { + ImGui.PushID($"ItemQueue{i}"); + var item = _configuration.ItemQueue[i]; + var craft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == item.WorkshopItemId); + + ImGui.SetNextItemWidth(100); + int quantity = item.Quantity; + if (ImGui.InputInt(craft.Name, ref quantity)) + { + item.Quantity = Math.Max(0, quantity); + Save(); + } + + ImGui.PopID(); + } + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo("##CraftSelection", "Add Craft...")) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256); + + foreach (var craft in _workshopCache.Crafts + .Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) + .OrderBy(x => x.WorkshopItemId)) + { + if (ImGui.Selectable($"{craft.Name}##SelectCraft{craft.WorkshopItemId}")) + { + _configuration.ItemQueue.Add(new Configuration.QueuedItem + { + WorkshopItemId = craft.WorkshopItemId, + Quantity = 1, + }); + Save(); + } + } + + ImGui.EndCombo(); + } + //ImGui.EndDisabled(); + + ImGui.Separator(); + ImGui.Text($"Stage: {_plugin.CurrentStage}"); + } + + private void Save() + { + _pluginInterface.SavePluginConfig(_configuration); + } + + public enum ButtonState + { + None, + Start, + Resume, + Pause, + Stop, + } +} diff --git a/Workshoppa/WorkshopPlugin.Craft.cs b/Workshoppa/WorkshopPlugin.Craft.cs new file mode 100644 index 0000000..1485063 --- /dev/null +++ b/Workshoppa/WorkshopPlugin.Craft.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Workshoppa.GameData; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Workshoppa; + +partial class WorkshopPlugin +{ + private uint? _contributingItemId; + + private void SelectCraftBranch() + { + if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials."))) + CurrentStage = Stage.ContributeMaterials; + else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production."))) + { + PluginLog.Information("Phase is complete"); + CurrentStage = Stage.TargetFabricationStation; + _continueAt = DateTime.Now.AddSeconds(3); + } + else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of"))) + { + PluginLog.Information("Item is almost complete, confirming last cutscene"); + CurrentStage = Stage.TargetFabricationStation; + _continueAt = DateTime.Now.AddSeconds(3); + } + else if (SelectSelectString("collect", 0, s => s == "Collect finished product.")) + { + PluginLog.Information("Item is complete"); + CurrentStage = Stage.ConfirmCollectProduct; + _continueAt = DateTime.Now.AddSeconds(0.25); + } + } + + private unsafe void ContributeMaterials() + { + AtkUnitBase* addonMaterialDelivery = GetMaterialDeliveryAddon(); + if (addonMaterialDelivery == null) + return; + + CraftState? craftState = ReadCraftState(addonMaterialDelivery); + if (craftState == null || craftState.ResultItem == 0) + { + PluginLog.Warning("Could not parse craft state"); + _continueAt = DateTime.Now.AddSeconds(1); + return; + } + + for (int i = 0; i < craftState.Items.Count; ++i) + { + var item = craftState.Items[i]; + if (item.Finished) + continue; + + if (!HasItemInSingleSlot(item.ItemId, item.ItemCountPerStep)) + { + PluginLog.Error($"Can't contribute item {item.ItemId} to craft, couldn't find {item.ItemCountPerStep}x in a single inventory slot"); + CurrentStage = Stage.RequestStop; + break; + } + + PluginLog.Information($"Contributing {item.ItemCountPerStep}x {item.ItemName}"); + _contributingItemId = item.ItemId; + var contributeMaterial = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.UInt, Int = i }, + new() { Type = ValueType.UInt, UInt = item.ItemCountPerStep }, + new() { Type = 0, Int = 0 } + }; + addonMaterialDelivery->FireCallback(4, contributeMaterial); + CurrentStage = Stage.ConfirmMaterialDelivery; + break; + } + } + + private unsafe void ConfirmMaterialDelivery() + { + AtkUnitBase* addonMaterialDelivery = GetMaterialDeliveryAddon(); + if (addonMaterialDelivery == null) + return; + + CraftState? craftState = ReadCraftState(addonMaterialDelivery); + if (craftState == null || craftState.ResultItem == 0) + { + PluginLog.Warning("Could not parse craft state"); + _continueAt = DateTime.Now.AddSeconds(1); + return; + } + + if (SelectSelectYesno(0, s => s.StartsWith("Contribute") && s.EndsWith("to the company project?"))) + { + var item = craftState.Items.Single(x => x.ItemId == _contributingItemId); + item.StepsComplete++; + if (craftState.IsPhaseComplete()) + { + CurrentStage = Stage.TargetFabricationStation; + _continueAt = DateTime.Now.AddSeconds(0.5); + } + else + { + CurrentStage = Stage.ContributeMaterials; + _continueAt = DateTime.Now.AddSeconds(1); + } + } + } + + private void ConfirmCollectProduct() + { + if (SelectSelectYesno(0, s => s.StartsWith("Retrieve"))) + { + _configuration.CurrentlyCraftedItem = null; + _pluginInterface.SavePluginConfig(_configuration); + + CurrentStage = Stage.TakeItemFromQueue; + _continueAt = DateTime.Now.AddSeconds(0.5); + } + } +} diff --git a/Workshoppa/WorkshopPlugin.CraftingLog.cs b/Workshoppa/WorkshopPlugin.CraftingLog.cs new file mode 100644 index 0000000..7150402 --- /dev/null +++ b/Workshoppa/WorkshopPlugin.CraftingLog.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Workshoppa; + +partial class WorkshopPlugin +{ + private bool InteractWithFabricationStation(GameObject fabricationStation) + { + InteractWithTarget(fabricationStation); + return true; + } + + private void TakeItemFromQueue() + { + if (_configuration.CurrentlyCraftedItem == null) + { + while (_configuration.ItemQueue.Count > 0 && _configuration.CurrentlyCraftedItem == null) + { + var firstItem = _configuration.ItemQueue[0]; + if (firstItem.Quantity > 0) + { + _configuration.CurrentlyCraftedItem = new Configuration.CurrentItem + { + WorkshopItemId = firstItem.WorkshopItemId, + }; + + if (firstItem.Quantity > 1) + firstItem.Quantity--; + else + _configuration.ItemQueue.Remove(firstItem); + } + else + _configuration.ItemQueue.Remove(firstItem); + } + + _pluginInterface.SavePluginConfig(_configuration); + if (_configuration.CurrentlyCraftedItem != null) + CurrentStage = Stage.TargetFabricationStation; + else + CurrentStage = Stage.RequestStop; + } + else + CurrentStage = Stage.TargetFabricationStation; + } + + private void OpenCraftingLog() + { + if (SelectSelectString("craftlog", 0, s => s == "View company crafting log.")) + CurrentStage = Stage.SelectCraftCategory; + } + + private unsafe void SelectCraftCategory() + { + AtkUnitBase* addonCraftingLog = GetCompanyCraftingLogAddon(); + if (addonCraftingLog == null) + return; + + var craft = GetCurrentCraft(); + PluginLog.Information($"Selecting category {craft.Category} and type {craft.Type}"); + var selectCategory = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 2 }, + new() { Type = 0, Int = 0 }, + new() { Type = ValueType.UInt, UInt = (uint)craft.Category }, + new() { Type = ValueType.UInt, UInt = craft.Type }, + new() { Type = ValueType.UInt, Int = 0 }, + new() { Type = ValueType.UInt, Int = 0 }, + new() { Type = ValueType.UInt, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonCraftingLog->FireCallback(8, selectCategory); + CurrentStage = Stage.SelectCraft; + _continueAt = DateTime.Now.AddSeconds(0.1); + } + + private unsafe void SelectCraft() + { + AtkUnitBase* addonCraftingLog = GetCompanyCraftingLogAddon(); + if (addonCraftingLog == null) + return; + + var craft = GetCurrentCraft(); + var atkValues = addonCraftingLog->AtkValues; + + uint shownItemCount = atkValues[13].UInt; + var visibleItems = Enumerable.Range(0, (int)shownItemCount) + .Select(i => new + { + WorkshopItemId = atkValues[14 + 4 * i].UInt, + Name = ReadAtkString(atkValues[17 + 4 * i]), + }) + .ToList(); + + if (visibleItems.All(x => x.WorkshopItemId != craft.WorkshopItemId)) + { + PluginLog.Error($"Could not find {craft.Name} in current list, is it unlocked?"); + CurrentStage = Stage.RequestStop; + return; + } + + PluginLog.Information($"Selecting craft {craft.WorkshopItemId}"); + var selectCraft = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 1 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = ValueType.UInt, UInt = craft.WorkshopItemId }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonCraftingLog->FireCallback(8, selectCraft); + CurrentStage = Stage.ConfirmCraft; + _continueAt = DateTime.Now.AddSeconds(0.1); + } + + private void ConfirmCraft() + { + if (SelectSelectYesno(0, s => s.StartsWith("Craft "))) + { + _configuration.CurrentlyCraftedItem!.StartedCrafting = true; + _pluginInterface.SavePluginConfig(_configuration); + + CurrentStage = Stage.TargetFabricationStation; + } + } +} diff --git a/Workshoppa/WorkshopPlugin.GameFunctions.cs b/Workshoppa/WorkshopPlugin.GameFunctions.cs new file mode 100644 index 0000000..d2e0a82 --- /dev/null +++ b/Workshoppa/WorkshopPlugin.GameFunctions.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Workshoppa.GameData; + +namespace Workshoppa; + +partial class WorkshopPlugin +{ + private unsafe void InteractWithTarget(GameObject obj) + { + PluginLog.Information($"Setting target to {obj}"); + /* + if (_targetManager.Target == null || _targetManager.Target != obj) + { + _targetManager.Target = obj; + } +*/ + TargetSystem.Instance()->InteractWithObject( + (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false); + } + + private float GetDistanceToEventObject(int npcId, out GameObject? o) + { + foreach (var obj in _objectTable) + { + if (obj.ObjectKind == ObjectKind.EventObj) + { + if (GetNpcId(obj) == npcId) + { + o = obj; + return Vector3.Distance(_clientState.LocalPlayer!.Position, obj.Position); + } + } + } + + o = null; + return float.MaxValue; + } + + private int GetNpcId(GameObject obj) + { + return Marshal.ReadInt32(obj.Address + 128); + } + + private unsafe bool TryGetAddonByName(string addonName, out T* addonPtr) + where T : unmanaged + { + var a = _gameGui.GetAddonByName(addonName); + if (a != IntPtr.Zero) + { + addonPtr = (T*)a; + return true; + } + else + { + addonPtr = null; + return false; + } + } + + private unsafe bool IsAddonReady(AtkUnitBase* addon) + { + return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; + } + + private unsafe AtkUnitBase* GetCompanyCraftingLogAddon() + { + if (TryGetAddonByName("CompanyCraftRecipeNoteBook", out var addon) && IsAddonReady(addon)) + return addon; + + return null; + } + + /// + /// This actually has different addons depending on the craft, e.g. SubmarinePartsMenu. + /// + /// + private unsafe AtkUnitBase* GetMaterialDeliveryAddon() + { + var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.CompanyCraftMaterial); + if (agentInterface != null && agentInterface->IsAgentActive()) + { + var addonId = agentInterface->GetAddonID(); + if (addonId == 0) + return null; + + AtkUnitBase* addon = GetAddonById(addonId); + if (IsAddonReady(addon)) + return addon; + } + + return null; + } + + private unsafe AtkUnitBase* GetAddonById(uint id) + { + var unitManagers = &AtkStage.GetSingleton()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; + for (var i = 0; i < 18; i++) + { + var unitManager = &unitManagers[i]; + var unitBaseArray = &(unitManager->AtkUnitEntries); + for (var j = 0; j < unitManager->Count; j++) + { + var unitBase = unitBaseArray[j]; + if (unitBase->ID == id) + { + return unitBase; + } + } + } + + return null; + } + + private unsafe bool SelectSelectString(string marker, int choice, Predicate predicate) + { + if (TryGetAddonByName("SelectString", out var addonSelectString) && + IsAddonReady(&addonSelectString->AtkUnitBase)) + { + int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount; + if (entries < choice) + return false; + + var textPointer = addonSelectString->PopupMenu.PopupMenu.EntryNames[choice]; + if (textPointer == null) + return false; + + var text = MemoryHelper.ReadSeStringNullTerminated((nint)textPointer).ToString(); + PluginLog.Information($"SelectSelectString for {marker}, Choice would be '{text}'"); + if (predicate(text)) + { + addonSelectString->AtkUnitBase.FireCallbackInt(choice); + return true; + } + } + + return false; + } + + private unsafe bool SelectSelectYesno(int choice, Predicate predicate) + { + if (TryGetAddonByName("SelectYesno", out var addonSelectYesno) && + IsAddonReady(&addonSelectYesno->AtkUnitBase)) + { + var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString(); + text = text.Replace("\n", "").Replace("\r", ""); + if (predicate(text)) + { + PluginLog.Information($"Selecting choice {choice} for '{text}'"); + addonSelectYesno->AtkUnitBase.FireCallbackInt(choice); + return true; + } + else + { + PluginLog.Warning($"Text {text} does not match"); + } + } + + return false; + } + + private unsafe string? ReadAtkString(AtkValue atkValue) + { + if (atkValue.String != null) + return MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue.String)).ToString(); + return null; + } + + private unsafe CraftState? ReadCraftState(AtkUnitBase* addonMaterialDelivery) + { + try + { + var atkValues = addonMaterialDelivery->AtkValues; + if (addonMaterialDelivery->AtkValuesCount == 157 && atkValues != null) + { + uint resultItem = atkValues[0].UInt; + uint stepsComplete = atkValues[6].UInt; + uint stepsTotal = atkValues[7].UInt; + uint listItemCount = atkValues[11].UInt; + List items = Enumerable.Range(0, (int)listItemCount) + .Select(i => new CraftItem + { + ItemId = atkValues[12 + i].UInt, + IconId = atkValues[24 + i].UInt, + ItemName = ReadAtkString(atkValues[36 + i]), + CrafterIconId = atkValues[48 + i].Int, + ItemCountPerStep = atkValues[60 + i].UInt, + ItemCountNQ = atkValues[72 + i].UInt, + ItemCountHQ = ParseAtkItemCountHq(atkValues[84 + i]), + Experience = atkValues[96 + i].UInt, + StepsComplete = atkValues[108 + i].UInt, + StepsTotal = atkValues[120 + i].UInt, + Finished = atkValues[132 + i].UInt > 0, + CrafterMinimumLevel = atkValues[144 + i].UInt, + }) + .ToList(); + + return new CraftState + { + ResultItem = resultItem, + StepsComplete = stepsComplete, + StepsTotal = stepsTotal, + Items = items, + }; + } + } + catch (Exception e) + { + PluginLog.Warning(e, "Could not parse CompanyCraftMaterial info"); + } + + return null; + } + + private uint ParseAtkItemCountHq(AtkValue atkValue) + { + // NQ / HQ string + // I have no clue, but it doesn't seme like the available HQ item count is strored anywhere in the atkvalues?? + string? s = ReadAtkString(atkValue); + if (s != null) + { + var parts = s.Replace("\ue03c", "").Split('/'); + if (parts.Length > 1) + { + return uint.Parse(parts[1].Replace(",", "").Replace(".", "").Trim()); + } + } + + return 0; + } + + private unsafe bool HasItemInSingleSlot(uint itemId, uint count) + { + var inventoryManger = InventoryManager.Instance(); + if (inventoryManger == null) + return false; + + for (InventoryType t = InventoryType.Inventory1; t <= InventoryType.Inventory4; ++t) + { + var container = inventoryManger->GetInventoryContainer(t); + for (int i = 0; i < container->Size; ++i) + { + var item = container->GetInventorySlot(i); + if (item == null) + continue; + + if (item->ItemID == itemId && item->Quantity >= count) + return true; + } + } + + return false; + } +} diff --git a/Workshoppa/WorkshopPlugin.cs b/Workshoppa/WorkshopPlugin.cs new file mode 100644 index 0000000..9f22431 --- /dev/null +++ b/Workshoppa/WorkshopPlugin.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Memory; +using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Workshoppa.External; +using Workshoppa.GameData; +using Workshoppa.Windows; + +namespace Workshoppa; + +[SuppressMessage("ReSharper", "UnusedType.Global")] +public sealed partial class WorkshopPlugin : IDalamudPlugin +{ + private const int FabricationStationId = 0x1E98F4; + private readonly IReadOnlyList _workshopTerritories = new ushort[] { 423, 424, 425, 653, 984 }.AsReadOnly(); + private readonly WindowSystem _windowSystem = new WindowSystem(nameof(WorkshopPlugin)); + + private readonly DalamudPluginInterface _pluginInterface; + private readonly GameGui _gameGui; + private readonly Framework _framework; + private readonly Condition _condition; + private readonly ClientState _clientState; + private readonly ObjectTable _objectTable; + private readonly CommandManager _commandManager; + + private readonly Configuration _configuration; + private readonly YesAlreadyIpc _yesAlreadyIpc; + private readonly WorkshopCache _workshopCache; + private readonly MainWindow _mainWindow; + + private Stage _currentStageInternal = Stage.Stopped; + private DateTime _continueAt = DateTime.MinValue; + private (bool Saved, bool? PreviousState) _yesAlreadyState = (false, null); + + public WorkshopPlugin(DalamudPluginInterface pluginInterface, GameGui gameGui, Framework framework, + Condition condition, ClientState clientState, ObjectTable objectTable, DataManager dataManager, + CommandManager commandManager) + { + _pluginInterface = pluginInterface; + _gameGui = gameGui; + _framework = framework; + _condition = condition; + _clientState = clientState; + _objectTable = objectTable; + _commandManager = commandManager; + + var dalamudReflector = new DalamudReflector(_pluginInterface, _framework); + _yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector); + _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); + _workshopCache = new WorkshopCache(dataManager); + + _mainWindow = new(this, _pluginInterface, _configuration, _workshopCache) { IsOpen = true }; + _windowSystem.AddWindow(_mainWindow); + + _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; + _pluginInterface.UiBuilder.OpenMainUi += _mainWindow.Toggle; + _framework.Update += FrameworkUpdate; + _commandManager.AddHandler("/ws", new CommandInfo(ProcessCommand)); + } + + public string Name => "Workshop Plugin"; + + internal Stage CurrentStage + { + get => _currentStageInternal; + private set + { + if (_currentStageInternal != value) + { + PluginLog.Information($"Changing stage from {_currentStageInternal} to {value}"); + _currentStageInternal = value; + } + } + } + + private void FrameworkUpdate(Framework framework) + { + if (!_clientState.IsLoggedIn || + !_workshopTerritories.Contains(_clientState.TerritoryType) || + _condition[ConditionFlag.BoundByDuty] || + GetDistanceToEventObject(FabricationStationId, out var fabricationStation) >= 5f) + { + _mainWindow.NearFabricationStation = false; + } + else if (DateTime.Now >= _continueAt) + { + _mainWindow.NearFabricationStation = true; + + if (_mainWindow.State is MainWindow.ButtonState.Pause or MainWindow.ButtonState.Stop) + { + _mainWindow.State = MainWindow.ButtonState.None; + if (CurrentStage != Stage.Stopped) + { + RestoreYesAlready(); + CurrentStage = Stage.Stopped; + } + + return; + } + else if (_mainWindow.State is MainWindow.ButtonState.Start or MainWindow.ButtonState.Resume && CurrentStage == Stage.Stopped) + { + _mainWindow.State = MainWindow.ButtonState.None; + CurrentStage = Stage.TakeItemFromQueue; + } + + if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_yesAlreadyState.Saved) + SaveYesAlready(); + + switch (CurrentStage) + { + case Stage.TakeItemFromQueue: + TakeItemFromQueue(); + break; + + case Stage.TargetFabricationStation: + if (InteractWithFabricationStation(fabricationStation!)) + { + if (_configuration.CurrentlyCraftedItem is { StartedCrafting: true }) + CurrentStage = Stage.SelectCraftBranch; + else + CurrentStage = Stage.OpenCraftingLog; + } + + break; + + case Stage.OpenCraftingLog: + OpenCraftingLog(); + break; + + case Stage.SelectCraftCategory: + SelectCraftCategory(); + break; + + case Stage.SelectCraft: + SelectCraft(); + break; + + case Stage.ConfirmCraft: + ConfirmCraft(); + break; + + case Stage.RequestStop: + RestoreYesAlready(); + CurrentStage = Stage.Stopped; + break; + + case Stage.SelectCraftBranch: + SelectCraftBranch(); + break; + + case Stage.ContributeMaterials: + ContributeMaterials(); + break; + + case Stage.ConfirmMaterialDelivery: + ConfirmMaterialDelivery(); + break; + + case Stage.ConfirmCollectProduct: + ConfirmCollectProduct(); + break; + + case Stage.Stopped: + break; + + default: + PluginLog.Warning($"Unknown stage {CurrentStage}"); + break; + } + } + } + + private WorkshopCraft GetCurrentCraft() + { + return _workshopCache.Crafts.Single(x => x.WorkshopItemId == _configuration.CurrentlyCraftedItem!.WorkshopItemId); + } + + private void ProcessCommand(string command, string arguments) => _mainWindow.Toggle(); + + public void Dispose() + { + _commandManager.RemoveHandler("/ws"); + _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + _pluginInterface.UiBuilder.OpenMainUi -= _mainWindow.Toggle; + _framework.Update -= FrameworkUpdate; + + RestoreYesAlready(); + } + + private void SaveYesAlready() + { + if (_yesAlreadyState.Saved) + { + PluginLog.Information("Not overwriting yesalready state"); + return; + } + + _yesAlreadyState = (true, _yesAlreadyIpc.DisableIfNecessary()); + PluginLog.Information($"Previous yesalready state: {_yesAlreadyState.PreviousState}"); + } + + private void RestoreYesAlready() + { + if (_yesAlreadyState.Saved) + { + PluginLog.Information($"Restoring previous yesalready state: {_yesAlreadyState.PreviousState}"); + if (_yesAlreadyState.PreviousState == true) + _yesAlreadyIpc.Enable(); + } + + _yesAlreadyState = (false, null); + } +} diff --git a/Workshoppa/Workshoppa.csproj b/Workshoppa/Workshoppa.csproj new file mode 100644 index 0000000..64d5a11 --- /dev/null +++ b/Workshoppa/Workshoppa.csproj @@ -0,0 +1,69 @@ + + + net7.0-windows + 1.0 + 11.0 + enable + true + false + false + dist + true + portable + $(SolutionDir)=X:\ + true + portable + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + + + + $(DALAMUD_HOME)/ + + + + + + + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + + + + + + diff --git a/Workshoppa/Workshoppa.json b/Workshoppa/Workshoppa.json new file mode 100644 index 0000000..ae7a124 --- /dev/null +++ b/Workshoppa/Workshoppa.json @@ -0,0 +1,7 @@ +{ + "Name": "Workshop Turn-In", + "Author": "Liza Carvelli", + "Punchline": "", + "Description": "", + "RepoUrl": "https://git.carvel.li/liza/Workshoppa" +} diff --git a/Workshoppa/packages.lock.json b/Workshoppa/packages.lock.json new file mode 100644 index 0000000..467f0f2 --- /dev/null +++ b/Workshoppa/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net7.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.11, )", + "resolved": "2.1.11", + "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + } + } + } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..aaac9e0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file