🎉 Initial rework complete

This commit is contained in:
Liza 2023-10-01 22:50:21 +02:00
commit 8b8245bf0a
Signed by: liza
GPG Key ID: 7199F8D727D55F67
24 changed files with 1558 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
*.user

16
Workshoppa.sln Normal file
View 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
View File

@ -0,0 +1,3 @@
/dist
/obj
/bin

161
Workshoppa/Callback.cs Normal file
View 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();
}
}

View 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
View 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
View 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);
}
}

View 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; }
}

View 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();
}

View 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>();
}

View 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; }
}

View File

@ -0,0 +1,8 @@
namespace Workshoppa.GameData;
public enum WorkshopCraftCategory
{
AetherialWheels = 0,
AirshipsSubmersibles = 1,
Housing = 2,
}

View 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;
}

View 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
View File

@ -0,0 +1,21 @@
namespace Workshoppa;
public enum Stage
{
TakeItemFromQueue,
TargetFabricationStation,
OpenCraftingLog,
SelectCraftCategory,
SelectCraft,
ConfirmCraft,
SelectCraftBranch,
ContributeMaterials,
ConfirmMaterialDelivery,
ConfirmCollectProduct,
RequestStop,
Stopped,
}

View 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,
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}

View 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);
}
}

View 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>

View File

@ -0,0 +1,7 @@
{
"Name": "Workshop Turn-In",
"Author": "Liza Carvelli",
"Punchline": "",
"Description": "",
"RepoUrl": "https://git.carvel.li/liza/Workshoppa"
}

View 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
View File

@ -0,0 +1,7 @@
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}