forked from liza/Workshoppa
🎉 Initial rework complete
This commit is contained in:
commit
8b8245bf0a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.idea
|
||||
*.user
|
16
Workshoppa.sln
Normal file
16
Workshoppa.sln
Normal file
@ -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
|
3
Workshoppa/.gitignore
vendored
Normal file
3
Workshoppa/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/obj
|
||||
/bin
|
161
Workshoppa/Callback.cs
Normal file
161
Workshoppa/Callback.cs
Normal file
@ -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<AtkUnitBase_FireCallbackDelegate>(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<string> 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<string>();
|
||||
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();
|
||||
}
|
||||
}
|
25
Workshoppa/Configuration.cs
Normal file
25
Workshoppa/Configuration.cs
Normal file
@ -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<QueuedItem> 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; }
|
||||
}
|
||||
}
|
114
Workshoppa/External/DalamudReflector.cs
vendored
Normal file
114
Workshoppa/External/DalamudReflector.cs
vendored
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Originally part of ECommons by NightmareXIV.
|
||||
///
|
||||
/// https://github.com/NightmareXIV/ECommons/blob/master/ECommons/Reflection/DalamudReflector.cs
|
||||
/// </summary>
|
||||
internal sealed class DalamudReflector : IDisposable
|
||||
{
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly Framework _framework;
|
||||
private readonly Dictionary<string, IDalamudPlugin> _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<object>(), 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;
|
||||
}
|
||||
}
|
53
Workshoppa/External/YesAlreadyIpc.cs
vendored
Normal file
53
Workshoppa/External/YesAlreadyIpc.cs
vendored
Normal file
@ -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);
|
||||
}
|
||||
}
|
17
Workshoppa/GameData/CraftItem.cs
Normal file
17
Workshoppa/GameData/CraftItem.cs
Normal file
@ -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; }
|
||||
}
|
21
Workshoppa/GameData/CraftState.cs
Normal file
21
Workshoppa/GameData/CraftState.cs
Normal file
@ -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<CraftItem> Items { get; init; }
|
||||
|
||||
public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal);
|
||||
|
||||
public bool IsCraftComplete() => StepsComplete == StepsTotal - 1 && IsPhaseComplete();
|
||||
}
|
73
Workshoppa/GameData/WorkshopCache.cs
Normal file
73
Workshoppa/GameData/WorkshopCache.cs
Normal file
@ -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<ushort, Item> itemMapping = dataManager.GetExcelSheet<CompanyCraftSupplyItem>()!
|
||||
.Where(x => x.RowId > 0)
|
||||
.ToDictionary(x => (ushort)x.RowId, x => x.Item.Value!);
|
||||
|
||||
Crafts = dataManager.GetExcelSheet<CompanyCraftSequence>()!
|
||||
.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" <maxwait.30>
|
||||
/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<WorkshopCraft> Crafts { get; private set; } = new List<WorkshopCraft>();
|
||||
}
|
13
Workshoppa/GameData/WorkshopCraft.cs
Normal file
13
Workshoppa/GameData/WorkshopCraft.cs
Normal file
@ -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<WorkshopCraftPhase> Phases { get; init; }
|
||||
}
|
8
Workshoppa/GameData/WorkshopCraftCategory.cs
Normal file
8
Workshoppa/GameData/WorkshopCraftCategory.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Workshoppa.GameData;
|
||||
|
||||
public enum WorkshopCraftCategory
|
||||
{
|
||||
AetherialWheels = 0,
|
||||
AirshipsSubmersibles = 1,
|
||||
Housing = 2,
|
||||
}
|
10
Workshoppa/GameData/WorkshopCraftItem.cs
Normal file
10
Workshoppa/GameData/WorkshopCraftItem.cs
Normal file
@ -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;
|
||||
}
|
9
Workshoppa/GameData/WorkshopCraftPhase.cs
Normal file
9
Workshoppa/GameData/WorkshopCraftPhase.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Workshoppa.GameData;
|
||||
|
||||
internal sealed class WorkshopCraftPhase
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required IReadOnlyList<WorkshopCraftItem> Items { get; init; }
|
||||
}
|
21
Workshoppa/Stage.cs
Normal file
21
Workshoppa/Stage.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace Workshoppa;
|
||||
|
||||
public enum Stage
|
||||
{
|
||||
TakeItemFromQueue,
|
||||
TargetFabricationStation,
|
||||
|
||||
OpenCraftingLog,
|
||||
SelectCraftCategory,
|
||||
SelectCraft,
|
||||
ConfirmCraft,
|
||||
|
||||
SelectCraftBranch,
|
||||
ContributeMaterials,
|
||||
ConfirmMaterialDelivery,
|
||||
|
||||
ConfirmCollectProduct,
|
||||
|
||||
RequestStop,
|
||||
Stopped,
|
||||
}
|
163
Workshoppa/Windows/MainWindow.cs
Normal file
163
Workshoppa/Windows/MainWindow.cs
Normal file
@ -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,
|
||||
}
|
||||
}
|
122
Workshoppa/WorkshopPlugin.Craft.cs
Normal file
122
Workshoppa/WorkshopPlugin.Craft.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
133
Workshoppa/WorkshopPlugin.CraftingLog.cs
Normal file
133
Workshoppa/WorkshopPlugin.CraftingLog.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
266
Workshoppa/WorkshopPlugin.GameFunctions.cs
Normal file
266
Workshoppa/WorkshopPlugin.GameFunctions.cs
Normal file
@ -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<T>(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<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) && IsAddonReady(addon))
|
||||
return addon;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This actually has different addons depending on the craft, e.g. SubmarinePartsMenu.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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<string> predicate)
|
||||
{
|
||||
if (TryGetAddonByName<AddonSelectString>("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<string> predicate)
|
||||
{
|
||||
if (TryGetAddonByName<AddonSelectYesno>("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<CraftItem> 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;
|
||||
}
|
||||
}
|
232
Workshoppa/WorkshopPlugin.cs
Normal file
232
Workshoppa/WorkshopPlugin.cs
Normal file
@ -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<ushort> _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);
|
||||
}
|
||||
}
|
69
Workshoppa/Workshoppa.csproj
Normal file
69
Workshoppa/Workshoppa.csproj
Normal file
@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Version>1.0</Version>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>dist</OutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<DebugType>portable</DebugType>
|
||||
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<DebugType>portable</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
||||
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.11"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGui.NET">
|
||||
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
|
||||
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
|
||||
</Target>
|
||||
</Project>
|
7
Workshoppa/Workshoppa.json
Normal file
7
Workshoppa/Workshoppa.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Name": "Workshop Turn-In",
|
||||
"Author": "Liza Carvelli",
|
||||
"Punchline": "",
|
||||
"Description": "",
|
||||
"RepoUrl": "https://git.carvel.li/liza/Workshoppa"
|
||||
}
|
13
Workshoppa/packages.lock.json
Normal file
13
Workshoppa/packages.lock.json
Normal file
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
global.json
Normal file
7
global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "7.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user