Compare commits

...

39 Commits
v2.1 ... master

Author SHA1 Message Date
3eda727219
API 11 2024-11-19 16:32:37 +01:00
e93819da5b
Use shop base from LLib for ceruleum tank/dark matter windows 2024-11-09 12:26:37 +01:00
23c606dfc8
Update LLib 2024-09-22 12:51:11 +02:00
50b975ae20
Exclude crystals, shards and clusters from generated material list 2024-07-28 22:07:24 +02:00
3a0bb492fa
Fix 'Export Material List to Clipboard' 2024-07-12 18:33:10 +02:00
ccfb6b7423
API 10 2024-07-04 12:04:50 +02:00
2e52ac8784
Fix commented out code 2024-05-11 14:08:28 +02:00
3e750c3d15
Add code to save dalamud's window options (pinned/clickthrough/alpha) 2024-05-11 14:02:25 +02:00
1a72a2dbe0
Add config window size constraints 2024-05-11 14:02:08 +02:00
af964b0723
Add commands to buy certain quantities of ceruleum tanks 2024-05-11 13:56:39 +02:00
9ae2785fae
Currently crafted item can now be cancelled even when not near a fabrication station 2024-03-28 14:37:11 +01:00
6c040389ac
Fix UI scaling issues 2024-03-27 19:21:41 +01:00
ec5e3968ca
Allow workshop window to be minimized if not currently crafting 2024-03-22 22:32:49 +01:00
b61de1f52c
NET 8 2024-03-20 19:52:54 +01:00
7d5cbd8563
Fix typo 2024-02-04 09:43:03 +01:00
c712fdb11f
Remove redundant log bits 2024-01-25 08:36:43 +01:00
451317f3d2
Clipboard export of craftable ingredient list/venture list 2024-01-25 08:33:32 +01:00
9bf055b78c
Change Import/export format to a teamcraft-style list
This means that your new clipboard export would look like:

1x Modified Shark-class Pressure Hull
1x Modified Shark-class Stern
1x Modified Unkiu-class Bow

instead of the previous base64 string.
2024-01-21 06:46:11 +01:00
2b5fb62842
Save queue as/load queue from presets 2024-01-19 09:41:04 +01:00
aa635c8620
Clipboard Import/Export of queue 2024-01-19 08:50:53 +01:00
5ef278a73b
Update icon URL 2024-01-16 06:52:25 +01:00
ee2b8bae27
Fix inventory slots not being filled to 999 if no free inventory slots available 2023-11-17 15:13:13 +01:00
c4a02ac2d9
Bump version/recompile 2023-11-14 20:20:04 +01:00
0c3d7c74ea
Update header icon logic for Dalamud changes 2023-11-09 11:46:26 +01:00
1021220064
IPC for YesAlready 1.4.x.x 2023-11-07 20:07:42 +01:00
60e3e4bdbc
Show icons for items 2023-11-01 10:51:53 +01:00
309f07c3e9
Show errors if turn-in isn't possible (instead of only printing to log) 2023-10-30 01:44:42 +01:00
f3e0d8f17c
Experimental Ceruleum Tank calculator 2023-10-25 00:19:42 +02:00
b94be6a77c
Revert changes to opening the crafting log 2023-10-19 21:18:51 +02:00
8a29673e0d
Update manifest 2023-10-19 20:49:43 +02:00
804959319e
Rework turn in handling 2023-10-19 20:47:02 +02:00
bcb19355f2
Show estimated Grade 6 Dark Matter cost 2023-10-19 09:22:21 +02:00
375546b4ac
Don't show window if between areas 2023-10-14 16:26:06 +02:00
a0e7687b58
Update submodule URL 2023-10-13 23:59:04 +02:00
f3a9ebba1a
Experimental Repair Kit calculator 2023-10-13 22:08:22 +02:00
2418750b06
Fix exception when entering/leaving workshop 2023-10-13 00:59:24 +02:00
0bf6e65bfe
Automatically open Workshop window when near a Fabrication Station 2023-10-11 11:05:16 +02:00
c270050c3c
Add LLib (fixes an API 9 issue in GetAddonById) 2023-10-11 10:52:48 +02:00
4f8d5e99cd
Remove random comment 2023-10-06 09:35:07 +02:00
32 changed files with 2841 additions and 492 deletions

1017
.editorconfig Normal file

File diff suppressed because it is too large Load Diff

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "LLib"]
path = LLib
url = https://git.carvel.li/liza/LLib.git

1
LLib Submodule

@ -0,0 +1 @@
Subproject commit e4bbc05ede6f6f01e7028b24614ed8cb333e909c

View File

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workshoppa", "Workshoppa\Workshoppa.csproj", "{4C2E2AD7-D897-4476-A17A-838932D95223}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workshoppa", "Workshoppa\Workshoppa.csproj", "{4C2E2AD7-D897-4476-A17A-838932D95223}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{43BFEA08-94E9-491B-9A54-5795B9AFB38E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,9 @@ Global
{4C2E2AD7-D897-4476-A17A-838932D95223}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{4C2E2AD7-D897-4476-A17A-838932D95223}.Release|Any CPU.Build.0 = Release|Any CPU {4C2E2AD7-D897-4476-A17A-838932D95223}.Release|Any CPU.Build.0 = Release|Any CPU
{43BFEA08-94E9-491B-9A54-5795B9AFB38E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43BFEA08-94E9-491B-9A54-5795B9AFB38E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43BFEA08-94E9-491B-9A54-5795B9AFB38E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43BFEA08-94E9-491B-9A54-5795B9AFB38E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Amalj_0027aa/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ceruleum/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workshoppa/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Yesno/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Z_0027ranmaia/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Configuration; using Dalamud.Configuration;
using LLib.ImGui;
using Workshoppa.GameData; using Workshoppa.GameData;
namespace Workshoppa; namespace Workshoppa;
@ -10,8 +11,14 @@ internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public CurrentItem? CurrentlyCraftedItem = null; public CurrentItem? CurrentlyCraftedItem { get; set; }
public List<QueuedItem> ItemQueue = new(); public List<QueuedItem> ItemQueue { get; set; } = new();
public bool EnableRepairKitCalculator { get; set; } = true;
public bool EnableCeruleumTankCalculator { get; set; } = true;
public List<Preset> Presets { get; set; } = new();
public WindowConfig MainWindowConfig { get; } = new();
public WindowConfig ConfigWindowConfig { get; } = new();
internal sealed class QueuedItem internal sealed class QueuedItem
{ {
@ -24,7 +31,7 @@ internal sealed class Configuration : IPluginConfiguration
public uint WorkshopItemId { get; set; } public uint WorkshopItemId { get; set; }
public bool StartedCrafting { get; set; } public bool StartedCrafting { get; set; }
public uint PhasesComplete { get; set; } = 0; public uint PhasesComplete { get; set; }
public List<PhaseItem> ContributedItemsInCurrentPhase { get; set; } = new(); public List<PhaseItem> ContributedItemsInCurrentPhase { get; set; } = new();
public bool UpdateFromCraftState(CraftState craftState) public bool UpdateFromCraftState(CraftState craftState)
@ -74,4 +81,11 @@ internal sealed class Configuration : IPluginConfiguration
public uint ItemId { get; set; } public uint ItemId { get; set; }
public uint QuantityComplete { get; set; } public uint QuantityComplete { get; set; }
} }
internal sealed class Preset
{
public required Guid Id { get; set; }
public required string Name { get; set; }
public List<QueuedItem> ItemQueue { get; set; } = new();
}
} }

View File

@ -1,113 +0,0 @@
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Reflection;
using Dalamud.Plugin.Services;
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 IFramework _framework;
private readonly IPluginLog _pluginLog;
private readonly Dictionary<string, IDalamudPlugin> _pluginCache = new();
private bool _pluginsChanged = false;
public DalamudReflector(DalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_framework = framework;
_pluginLog = pluginLog;
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(IFramework 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;
}
}

View File

@ -0,0 +1,107 @@
using System.Collections.Generic;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace Workshoppa.External;
internal sealed class ExternalPluginHandler
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IPluginLog _pluginLog;
private readonly PandoraIpc _pandoraIpc;
private bool? _pandoraState;
public ExternalPluginHandler(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_pluginLog = pluginLog;
_pandoraIpc = new PandoraIpc(pluginInterface, pluginLog);
}
public bool Saved { get; private set; }
public void Save()
{
if (Saved)
{
_pluginLog.Information("Not overwriting external plugin state");
return;
}
_pluginLog.Information("Saving external plugin state...");
SaveYesAlreadyState();
SavePandoraState();
Saved = true;
}
private void SaveYesAlreadyState()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
!data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Disabling YesAlready");
data.Add(nameof(Workshoppa));
}
}
private void SavePandoraState()
{
_pandoraState = _pandoraIpc.DisableIfNecessary();
_pluginLog.Information($"Previous pandora feature state: {_pandoraState}");
}
/// <summary>
/// Unlike Pandora/YesAlready, we only disable TextAdvance during the item turn-in so that the cutscene skip
/// still works (if enabled).
/// </summary>
public void SaveTextAdvance()
{
if (_pluginInterface.TryGetData<HashSet<string>>("TextAdvance.StopRequests", out var data) &&
!data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Disabling textadvance");
data.Add(nameof(Workshoppa));
}
}
public void Restore()
{
if (Saved)
{
RestoreYesAlready();
RestorePandora();
}
Saved = false;
_pandoraState = null;
}
private void RestoreYesAlready()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Restoring YesAlready");
data.Remove(nameof(Workshoppa));
}
}
private void RestorePandora()
{
_pluginLog.Information($"Restoring previous pandora state: {_pandoraState}");
if (_pandoraState == true)
_pandoraIpc.Enable();
}
public void RestoreTextAdvance()
{
if (_pluginInterface.TryGetData<HashSet<string>>("TextAdvance.StopRequests", out var data) &&
data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Restoring textadvance");
data.Remove(nameof(Workshoppa));
}
}
}

52
Workshoppa/External/PandoraIpc.cs vendored Normal file
View File

@ -0,0 +1,52 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
namespace Workshoppa.External;
internal sealed class PandoraIpc
{
private const string AutoTurnInFeature = "Auto-select Turn-ins";
private readonly IPluginLog _pluginLog;
private readonly ICallGateSubscriber<string, bool?> _getEnabled;
private readonly ICallGateSubscriber<string, bool, object?> _setEnabled;
public PandoraIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled");
_setEnabled = pluginInterface.GetIpcSubscriber<string, bool, object?>("PandorasBox.SetFeatureEnabled");
}
public bool? DisableIfNecessary()
{
try
{
bool? enabled = _getEnabled.InvokeFunc(AutoTurnInFeature);
_pluginLog.Information($"Pandora's {AutoTurnInFeature} is {enabled?.ToString() ?? "null"}");
if (enabled == true)
_setEnabled.InvokeAction(AutoTurnInFeature, false);
return enabled;
}
catch (IpcNotReadyError e)
{
_pluginLog.Information(e, "Unable to read pandora state");
return null;
}
}
public void Enable()
{
try
{
_setEnabled.InvokeAction(AutoTurnInFeature, true);
}
catch (IpcNotReadyError e)
{
_pluginLog.Error(e, "Unable to restore pandora state");
}
}
}

View File

@ -1,52 +0,0 @@
using System.Reflection;
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

@ -8,7 +8,7 @@ public sealed class CraftState
public required uint ResultItem { get; init; } public required uint ResultItem { get; init; }
public required uint StepsComplete { get; init; } public required uint StepsComplete { get; init; }
public required uint StepsTotal { get; init; } public required uint StepsTotal { get; init; }
public required List<CraftItem> Items { get; init; } public required IReadOnlyList<CraftItem> Items { get; init; }
public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal); public bool IsPhaseComplete() => Items.All(x => x.Finished || x.StepsComplete == x.StepsTotal);

View File

@ -0,0 +1,53 @@
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using LLib;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace Workshoppa.GameData;
internal sealed class GameStrings
{
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
PurchaseItemForGil = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForGil)}");
PurchaseItemForCompanyCredits = dataManager.GetRegex<Addon>(3473, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}");
ViewCraftingLog =
dataManager.GetString<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE",
pluginLog) ?? throw new ConstraintException($"Unable to resolve {nameof(ViewCraftingLog)}");
TurnInHighQualityItem = dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(TurnInHighQualityItem)}");
ContributeItems = dataManager.GetRegex<Addon>(6652, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(ContributeItems)}");
RetrieveFinishedItem =
dataManager.GetRegex<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_FINISH_CONF", pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(RetrieveFinishedItem)}");
}
public Regex PurchaseItemForGil { get; }
public Regex PurchaseItemForCompanyCredits { get; }
public string ViewCraftingLog { get; }
public string TurnInHighQualityItem { get; }
public Regex ContributeItems { get; }
public Regex RetrieveFinishedItem { get; }
[Sheet("custom/001/CmnDefCompanyManufactory_00150")]
[SuppressMessage("Performance", "CA1812")]
private readonly struct WorkshopDialogue(ExcelPage page, uint offset, uint row)
: IQuestDialogueText, IExcelRow<WorkshopDialogue>
{
public uint RowId => row;
public ReadOnlySeString Key => page.ReadString(offset, offset);
public ReadOnlySeString Value => page.ReadString(offset + 4, offset);
static WorkshopDialogue IExcelRow<WorkshopDialogue>.Create(ExcelPage page, uint offset,
uint row) =>
new(page, offset, row);
}
}

View File

@ -0,0 +1,18 @@
namespace Workshoppa.GameData;
public class Ingredient
{
public required uint ItemId { get; init; }
public uint IconId { get; init; }
public required string Name { get; init; }
public required int TotalQuantity { get; set; }
public required EType Type { get; init; }
public enum EType
{
Craftable,
Gatherable,
Other,
ShopItem,
}
}

View File

@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
namespace Workshoppa.GameData;
internal sealed class RecipeTree
{
private readonly IDataManager _dataManager;
private readonly IPluginLog _pluginLog;
private readonly IReadOnlyList<uint> _shopItemsOnly;
public RecipeTree(IDataManager dataManager, IPluginLog pluginLog)
{
_dataManager = dataManager;
_pluginLog = pluginLog;
// probably incomplete, e.g. different housing districts have different shop types
var shopVendorIds = new uint[]
{
262461, // Purchase Items (Lumber, Metal, Stone, Bone, Leather)
262462, // Purchase Items (Cloth, Reagents)
262463, // Purchase Items (Gardening, Dyes)
262471, // Purchase Items (Catalysts)
262472, // Purchase (Cooking Ingredients)
262692, // Amalj'aa
262422, // Housing District Merchant
262211, // Z'ranmaia, upper decks
};
_shopItemsOnly = _dataManager.GetSubrowExcelSheet<GilShopItem>()
.Flatten()
.Where(x => shopVendorIds.Contains(x.RowId))
.Select(x => x.Item.RowId)
.Where(x => x > 0)
.Distinct()
.ToList()
.AsReadOnly();
}
public IReadOnlyList<Ingredient> ResolveRecipes(IReadOnlyList<Ingredient> materials)
{
// look up recipes recursively
int limit = 10;
List<RecipeInfo> nextStep = ExtendWithAmountCrafted(materials);
List<RecipeInfo> completeList = new(nextStep);
while (--limit > 0 && nextStep.Any(x => x.Type == Ingredient.EType.Craftable))
{
nextStep = GetIngredients(nextStep);
completeList.AddRange(nextStep);
}
// sum up all recipes
completeList = completeList.GroupBy(x => x.ItemId)
.Select(x => new RecipeInfo
{
ItemId = x.Key,
Name = x.First().Name,
TotalQuantity = x.Sum(y => y.TotalQuantity),
Type = x.First().Type,
DependsOn = x.First().DependsOn,
AmountCrafted = x.First().AmountCrafted,
})
.ToList();
_pluginLog.Verbose("Complete craft list:");
foreach (var item in completeList)
_pluginLog.Verbose($" {item.TotalQuantity}x {item.Name}");
// if a recipe has a specific amount crafted, divide the gathered amount by it
foreach (var ingredient in completeList.Where(x => x is { AmountCrafted: > 1 }))
{
//_pluginLog.Information($"Fudging {ingredient.Name}");
foreach (var part in completeList.Where(x => ingredient.DependsOn.Contains(x.ItemId)))
{
//_pluginLog.Information($" → {part.Name}");
int unmodifiedQuantity = part.TotalQuantity;
int roundedQuantity =
(int)((unmodifiedQuantity + ingredient.AmountCrafted - 1) / ingredient.AmountCrafted);
part.TotalQuantity = part.TotalQuantity - unmodifiedQuantity + roundedQuantity;
}
}
// figure out the correct order for items to be crafted
foreach (var item in completeList.Where(x => x.Type == Ingredient.EType.ShopItem))
item.DependsOn.Clear();
List<RecipeInfo> sortedList = new List<RecipeInfo>();
while (sortedList.Count < completeList.Count)
{
_pluginLog.Verbose("Sort round");
var canBeCrafted = completeList.Where(x =>
!sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId)))
.ToList();
foreach (var item in canBeCrafted)
_pluginLog.Verbose($" can craft: {item.TotalQuantity}x {item.Name}");
if (canBeCrafted.Count == 0)
{
foreach (var item in completeList.Where(x => !sortedList.Contains(x)))
_pluginLog.Warning($" can't craft: {item.TotalQuantity}x {item.Name} → ({string.Join(", ", item.DependsOn.Where(y => sortedList.All(z => y != z.ItemId)))})");
throw new InvalidOperationException("Unable to sort items");
}
sortedList.AddRange(canBeCrafted.OrderBy(x => x.Name));
}
return sortedList.Cast<Ingredient>().ToList();
}
private List<RecipeInfo> GetIngredients(List<RecipeInfo> materials)
{
List<RecipeInfo> ingredients = new();
foreach (var material in materials.Where(x => x.Type == Ingredient.EType.Craftable))
{
//_pluginLog.Information($"Looking up recipe for {material.Name}");
var recipe = GetFirstRecipeForItem(material.ItemId);
if (recipe == null)
continue;
for (int i = 0; i < 8; ++ i)
{
var ingredient = recipe.Value.Ingredient[i];
if (!ingredient.IsValid || ingredient.RowId == 0)
continue;
Item item = ingredient.Value;
if (!IsValidItem(item.RowId))
continue;
Recipe? ingredientRecipe = GetFirstRecipeForItem(ingredient.RowId);
//_pluginLog.Information($"Adding {item.Name}");
ingredients.Add(new RecipeInfo
{
ItemId = ingredient.RowId,
Name = item.Name.ToString(),
TotalQuantity = material.TotalQuantity * recipe.Value.AmountIngredient[i],
Type =
_shopItemsOnly.Contains(ingredient.RowId) ? Ingredient.EType.ShopItem :
ingredientRecipe != null ? Ingredient.EType.Craftable :
GetGatheringItem(ingredient.RowId) != null ? Ingredient.EType.Gatherable :
GetVentureItem(ingredient.RowId) != null ? Ingredient.EType.Gatherable :
Ingredient.EType.Other,
AmountCrafted = ingredientRecipe?.AmountResult ?? 1,
DependsOn = ingredientRecipe?.Ingredient.Where(x => x.IsValid && IsValidItem(x.RowId))
.Select(x => x.RowId)
.ToList()
?? new(),
});
}
}
return ingredients;
}
private List<RecipeInfo> ExtendWithAmountCrafted(IEnumerable<Ingredient> materials)
{
return materials.Select(x => new
{
Ingredient = x,
Recipe = GetFirstRecipeForItem(x.ItemId)
})
.Where(x => x.Recipe != null)
.Select(x => new RecipeInfo
{
ItemId = x.Ingredient.ItemId,
Name = x.Ingredient.Name,
TotalQuantity = x.Ingredient.TotalQuantity,
Type = _shopItemsOnly.Contains(x.Ingredient.ItemId) ? Ingredient.EType.ShopItem : x.Ingredient.Type,
AmountCrafted = x.Recipe!.Value.AmountResult,
DependsOn = x.Recipe.Value.Ingredient.Where(y => y.IsValid && IsValidItem(y.RowId))
.Select(y => y.RowId)
.ToList(),
})
.ToList();
}
private Recipe? GetFirstRecipeForItem(uint itemId)
{
return _dataManager.GetExcelSheet<Recipe>().FirstOrDefault(x => x.RowId > 0 && x.ItemResult.RowId == itemId);
}
private GatheringItem? GetGatheringItem(uint itemId)
{
return _dataManager.GetExcelSheet<GatheringItem>().FirstOrDefault(x => x.RowId > 0 && x.Item.RowId == itemId);
}
private RetainerTaskNormal? GetVentureItem(uint itemId)
{
return _dataManager.GetExcelSheet<RetainerTaskNormal>()
.FirstOrDefault(x => x.RowId > 0 && x.Item.RowId == itemId);
}
private static bool IsValidItem(uint itemId)
{
return itemId > 19 && itemId != uint.MaxValue;
}
private sealed class RecipeInfo : Ingredient
{
public required uint AmountCrafted { get; init; }
public required List<uint> DependsOn { get; init; }
}
}

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
namespace Workshoppa.GameData; namespace Workshoppa.GameData;
@ -15,39 +16,45 @@ internal sealed class WorkshopCache
{ {
try try
{ {
Dictionary<ushort, Item> itemMapping = dataManager.GetExcelSheet<CompanyCraftSupplyItem>()! Dictionary<uint, Item> itemMapping = dataManager.GetExcelSheet<CompanyCraftSupplyItem>()
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.ToDictionary(x => (ushort)x.RowId, x => x.Item.Value!); .ToDictionary(x => x.RowId, x => x.Item.Value);
Crafts = dataManager.GetExcelSheet<CompanyCraftSequence>()! Crafts = dataManager.GetExcelSheet<CompanyCraftSequence>()
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.Select(x => new WorkshopCraft .Select(x => new WorkshopCraft
{ {
WorkshopItemId = x.RowId, WorkshopItemId = x.RowId,
ResultItem = x.ResultItem.Row, ResultItem = x.ResultItem.RowId,
Name = x.ResultItem.Value!.Name.ToString(), Name = x.ResultItem.Value.Name.ToString(),
Category = (WorkshopCraftCategory)x.CompanyCraftDraftCategory.Row, IconId = x.ResultItem.Value.Icon,
Type = x.CompanyCraftType.Row, Category = (WorkshopCraftCategory)x.CompanyCraftDraftCategory.RowId,
Phases = x.CompanyCraftPart.Where(part => part.Row != 0) Type = x.CompanyCraftType.RowId,
Phases = x.CompanyCraftPart.Where(part => part.RowId != 0)
.SelectMany(part => .SelectMany(part =>
part.Value!.CompanyCraftProcess 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 .Select(y => new WorkshopCraftPhase
{ {
Name = y.Type!.Name.ToString(), Name = part.Value.CompanyCraftType.Value.Name.ToString(),
Items = y.Process.Value!.UnkData0 Items = Enumerable.Range(0, y.Value.SupplyItem.Count)
.Where(item => item.SupplyItem > 0) .Select(i => new
{
SupplyItem = y.Value.SupplyItem[i],
SetsRequired = y.Value.SetsRequired[i],
SetQuantity = y.Value.SetQuantity[i],
})
.Where(item => item.SupplyItem.RowId > 0)
.Select(item => new WorkshopCraftItem .Select(item => new WorkshopCraftItem
{ {
ItemId = itemMapping[item.SupplyItem].RowId, ItemId = itemMapping[item.SupplyItem.RowId].RowId,
Name = itemMapping[item.SupplyItem].Name.ToString(), Name = itemMapping[item.SupplyItem.RowId].Name.ToString(),
IconId = itemMapping[item.SupplyItem.RowId].Icon,
SetQuantity = item.SetQuantity, SetQuantity = item.SetQuantity,
SetsRequired = item.SetsRequired, SetsRequired = item.SetsRequired,
}) })
.ToList() .ToList()
.AsReadOnly(), .AsReadOnly(),
}) }))
.ToList() .ToList()
.AsReadOnly(), .AsReadOnly(),
}) })
@ -61,12 +68,5 @@ internal sealed class WorkshopCache
}); });
} }
/*
/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>(); public IReadOnlyList<WorkshopCraft> Crafts { get; private set; } = new List<WorkshopCraft>();
} }

View File

@ -7,6 +7,7 @@ internal sealed class WorkshopCraft
public required uint WorkshopItemId { get; init; } public required uint WorkshopItemId { get; init; }
public required uint ResultItem { get; init; } public required uint ResultItem { get; init; }
public required string Name { get; init; } public required string Name { get; init; }
public required ushort IconId { get; init; }
public required WorkshopCraftCategory Category { get; init; } public required WorkshopCraftCategory Category { get; init; }
public required uint Type { get; init; } public required uint Type { get; init; }
public required IReadOnlyList<WorkshopCraftPhase> Phases { get; init; } public required IReadOnlyList<WorkshopCraftPhase> Phases { get; init; }

View File

@ -4,6 +4,7 @@ internal sealed class WorkshopCraftItem
{ {
public required uint ItemId { get; init; } public required uint ItemId { get; init; }
public required string Name { get; init; } public required string Name { get; init; }
public required ushort IconId { get; init; }
public required int SetQuantity { get; init; } public required int SetQuantity { get; init; }
public required int SetsRequired { get; init; } public required int SetsRequired { get; init; }
public int TotalQuantity => SetQuantity * SetsRequired; public int TotalQuantity => SetQuantity * SetsRequired;

View File

@ -12,6 +12,9 @@ public enum Stage
SelectCraftBranch, SelectCraftBranch,
ContributeMaterials, ContributeMaterials,
OpenRequestItemWindow,
OpenRequestItemSelect,
ConfirmRequestItemWindow,
ConfirmMaterialDelivery, ConfirmMaterialDelivery,
ConfirmCollectProduct, ConfirmCollectProduct,

View File

@ -0,0 +1,215 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using LLib.GameUI;
using LLib.Shop.Model;
using Workshoppa.External;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa.Windows;
internal sealed class CeruleumTankWindow : ShopWindow
{
private const int CeruleumTankItemId = 10155;
private readonly IPluginLog _pluginLog;
private readonly Configuration _configuration;
private readonly IChatGui _chatGui;
private int _companyCredits;
private int _buyStackCount;
private bool _buyPartialStacks = true;
public CeruleumTankWindow(
IPluginLog pluginLog,
IGameGui gameGui,
IAddonLifecycle addonLifecycle,
Configuration configuration,
ExternalPluginHandler externalPluginHandler,
IChatGui chatGui)
: base("Ceruleum Tanks###WorkshoppaCeruleumTankWindow", "FreeCompanyCreditShop", pluginLog, gameGui,
addonLifecycle, externalPluginHandler)
{
_pluginLog = pluginLog;
_chatGui = chatGui;
_configuration = configuration;
}
public override bool IsEnabled => _configuration.EnableCeruleumTankCalculator;
public override unsafe void UpdateShopStock(AtkUnitBase* addon)
{
if (addon->AtkValuesCount != 170)
{
_pluginLog.Error(
$"Unexpected amount of atkvalues for FreeCompanyCreditShop addon ({addon->AtkValuesCount})");
_companyCredits = 0;
Shop.ItemForSale = null;
return;
}
var atkValues = addon->AtkValues;
_companyCredits = (int)atkValues[3].UInt;
uint itemCount = atkValues[9].UInt;
if (itemCount == 0)
{
Shop.ItemForSale = null;
return;
}
Shop.ItemForSale = Enumerable.Range(0, (int)itemCount)
.Select(i => new ItemForSale
{
Position = i,
ItemName = atkValues[10 + i].ReadAtkString(),
Price = atkValues[130 + i].UInt,
OwnedItems = atkValues[90 + i].UInt,
ItemId = atkValues[30 + i].UInt,
})
.FirstOrDefault(x => x.ItemId == CeruleumTankItemId);
}
public override int GetCurrencyCount() => _companyCredits;
public override void Draw()
{
if (Shop.ItemForSale == null)
{
IsOpen = false;
return;
}
int ceruleumTanks = Shop.GetItemCount(CeruleumTankItemId);
int freeInventorySlots = Shop.CountFreeInventorySlots();
ImGui.Text("Inventory");
ImGui.Indent();
ImGui.Text($"Ceruleum Tanks: {FormatStackCount(ceruleumTanks)}");
ImGui.Text($"Free Slots: {freeInventorySlots}");
ImGui.Unindent();
ImGui.Separator();
if (Shop.PurchaseState == null)
{
ImGui.SetNextItemWidth(100);
ImGui.InputInt("Stacks to Buy", ref _buyStackCount);
_buyStackCount = Math.Min(freeInventorySlots, Math.Max(0, _buyStackCount));
if (ceruleumTanks % 999 > 0)
ImGui.Checkbox($"Fill Partial Stacks (+{999 - ceruleumTanks % 999})", ref _buyPartialStacks);
}
int missingItems = _buyStackCount * 999;
if (_buyPartialStacks && ceruleumTanks % 999 > 0)
missingItems += (999 - ceruleumTanks % 999);
if (Shop.PurchaseState != null)
{
Shop.HandleNextPurchaseStep();
if (Shop.PurchaseState != null)
{
ImGui.Text($"Buying {FormatStackCount(Shop.PurchaseState.ItemsLeftToBuy)}...");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
Shop.CancelAutoPurchase();
}
}
else
{
int toPurchase = Math.Min(Shop.GetMaxItemsToPurchase(), missingItems);
if (toPurchase > 0)
{
ImGui.Spacing();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign,
$"Auto-Buy {FormatStackCount(toPurchase)} for {Shop.ItemForSale.Price * toPurchase:N0} CC"))
{
Shop.StartAutoPurchase(toPurchase);
Shop.HandleNextPurchaseStep();
}
}
}
}
private static string FormatStackCount(int ceruleumTanks)
{
int fullStacks = ceruleumTanks / 999;
int partials = ceruleumTanks % 999;
string stacks = fullStacks == 1 ? "stack" : "stacks";
if (partials > 0)
return $"{fullStacks:N0} {stacks} + {partials}";
return $"{fullStacks:N0} {stacks}";
}
public override unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
{
var buyItem = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.UInt, UInt = (uint)Shop.ItemForSale!.Position },
new() { Type = ValueType.UInt, UInt = (uint)buyNow },
};
addonShop->FireCallback(3, buyItem);
}
public bool TryParseBuyRequest(string arguments, out int missingQuantity)
{
if (!int.TryParse(arguments, out int stackCount) || stackCount <= 0)
{
missingQuantity = 0;
return false;
}
int freeInventorySlots = Shop.CountFreeInventorySlots();
stackCount = Math.Min(freeInventorySlots, stackCount);
missingQuantity = Math.Min(Shop.GetMaxItemsToPurchase(), stackCount * 999);
return true;
}
public bool TryParseFillRequest(string arguments, out int missingQuantity)
{
if (!int.TryParse(arguments, out int stackCount) || stackCount < 0)
{
missingQuantity = 0;
return false;
}
int freeInventorySlots = Shop.CountFreeInventorySlots();
int partialStacks = Shop.CountInventorySlotsWithCondition(CeruleumTankItemId, q => q < 999);
int fullStacks = Shop.CountInventorySlotsWithCondition(CeruleumTankItemId, q => q == 999);
int tanks = Math.Min((fullStacks + partialStacks + freeInventorySlots) * 999,
Math.Max(stackCount * 999, (fullStacks + partialStacks) * 999));
_pluginLog.Information("T: " + tanks);
int owned = Shop.GetItemCount(CeruleumTankItemId);
if (tanks <= owned)
missingQuantity = 0;
else
missingQuantity = Math.Min(Shop.GetMaxItemsToPurchase(), tanks - owned);
return true;
}
public void StartPurchase(int quantity)
{
if (!IsOpen || Shop.ItemForSale == null)
{
_chatGui.PrintError("Could not start purchase, shop window is not open.");
return;
}
if (quantity <= 0)
{
_chatGui.Print("Not buying ceruleum tanks, you already have enough.");
return;
}
_chatGui.Print($"Starting purchase of {FormatStackCount(quantity)} ceruleum tanks.");
Shop.StartAutoPurchase(quantity);
Shop.HandleNextPurchaseStep();
}
}

View File

@ -0,0 +1,50 @@
using System.Numerics;
using Dalamud.Plugin;
using ImGuiNET;
using LLib.ImGui;
namespace Workshoppa.Windows;
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration)
: base("Workshoppa - Configuration###WorkshoppaConfigWindow")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.AlwaysAutoResize;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(270, 50),
};
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw()
{
bool enableRepairKitCalculator = _configuration.EnableRepairKitCalculator;
if (ImGui.Checkbox("Enable Repair Kit Calculator", ref enableRepairKitCalculator))
{
_configuration.EnableRepairKitCalculator = enableRepairKitCalculator;
_pluginInterface.SavePluginConfig(_configuration);
}
bool enableCeruleumTankCalculator = _configuration.EnableCeruleumTankCalculator;
if (ImGui.Checkbox("Enable Ceruleum Tank Calculator", ref enableCeruleumTankCalculator))
{
_configuration.EnableCeruleumTankCalculator = enableCeruleumTankCalculator;
_pluginInterface.SavePluginConfig(_configuration);
}
}
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
}

View File

@ -2,29 +2,44 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET; using ImGuiNET;
using LLib;
using LLib.ImGui;
using Workshoppa.GameData; using Workshoppa.GameData;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
internal sealed class MainWindow : Window // FIXME The close button doesn't work near the workshop, either hide it or make it work
internal sealed class MainWindow : LWindow, IPersistableWindowConfig
{ {
private static readonly Regex CountAndName = new(@"^(\d{1,5})x?\s+(.*)$", RegexOptions.Compiled);
private readonly WorkshopPlugin _plugin; private readonly WorkshopPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly WorkshopCache _workshopCache; private readonly WorkshopCache _workshopCache;
private readonly IconCache _iconCache;
private readonly IChatGui _chatGui;
private readonly RecipeTree _recipeTree;
private readonly IPluginLog _pluginLog;
private string _searchString = string.Empty; private string _searchString = string.Empty;
private bool _checkInventory;
private string _newPresetName = string.Empty;
public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IClientState clientState, Configuration configuration, WorkshopCache workshopCache) public MainWindow(WorkshopPlugin plugin, IDalamudPluginInterface pluginInterface, IClientState clientState,
Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui,
RecipeTree recipeTree, IPluginLog pluginLog)
: base("Workshoppa###WorkshoppaMainWindow") : base("Workshoppa###WorkshoppaMainWindow")
{ {
_plugin = plugin; _plugin = plugin;
@ -32,6 +47,10 @@ internal sealed class MainWindow : Window
_clientState = clientState; _clientState = clientState;
_configuration = configuration; _configuration = configuration;
_workshopCache = workshopCache; _workshopCache = workshopCache;
_iconCache = iconCache;
_chatGui = chatGui;
_recipeTree = recipeTree;
_pluginLog = pluginLog;
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver; PositionCondition = ImGuiCond.FirstUseEver;
@ -39,48 +58,81 @@ internal sealed class MainWindow : Window
SizeConstraints = new WindowSizeConstraints SizeConstraints = new WindowSizeConstraints
{ {
MinimumSize = new Vector2(350, 50), MinimumSize = new Vector2(350, 50),
MaximumSize = new Vector2(500, 500), MaximumSize = new Vector2(500, 9999),
}; };
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse; Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.MenuBar;
AllowClickthrough = false;
} }
public EOpenReason OpenReason { get; set; } = EOpenReason.None;
public bool NearFabricationStation { get; set; } public bool NearFabricationStation { get; set; }
public ButtonState State { get; set; } = ButtonState.None; public ButtonState State { get; set; } = ButtonState.None;
public bool IsDiscipleOfHand => private bool IsDiscipleOfHand =>
_clientState.LocalPlayer != null && _clientState.LocalPlayer.ClassJob.Id is >= 8 and <= 15; _clientState.LocalPlayer != null && _clientState.LocalPlayer.ClassJob.RowId is >= 8 and <= 15;
public WindowConfig WindowConfig => _configuration.MainWindowConfig;
public override void Draw() public override void Draw()
{ {
if (ImGui.BeginMenuBar())
{
ImGui.BeginDisabled(_plugin.CurrentStage != Stage.Stopped);
DrawPresetsMenu();
DrawClipboardMenu();
ImGui.EndDisabled();
ImGui.EndMenuBar();
}
var currentItem = _configuration.CurrentlyCraftedItem; var currentItem = _configuration.CurrentlyCraftedItem;
if (currentItem != null) if (currentItem != null)
{ {
var currentCraft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == currentItem.WorkshopItemId); var currentCraft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == currentItem.WorkshopItemId);
ImGui.Text($"Currently Crafting: {currentCraft.Name}"); ImGui.Text($"Currently Crafting:");
IDalamudTextureWrap? icon = _iconCache.GetIcon(currentCraft.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() - ImGui.GetTextLineHeight()) / 2);
}
ImGui.TextUnformatted($"{currentCraft.Name}");
ImGui.Spacing();
if (_plugin.CurrentStage == Stage.Stopped) if (_plugin.CurrentStage == Stage.Stopped)
{ {
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Search, "Check Inventory")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Search, "Check Inventory"))
ImGui.OpenPopup(nameof(CheckMaterial)); _checkInventory = !_checkInventory;
ImGui.SameLine(); ImGui.SameLine();
ImGui.BeginDisabled(!NearFabricationStation); ImGui.BeginDisabled(!NearFabricationStation || !IsDiscipleOfHand);
ImGui.BeginDisabled(!IsDiscipleOfHand);
if (currentItem.StartedCrafting) if (currentItem.StartedCrafting)
{ {
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Resume")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Resume"))
{
State = ButtonState.Resume; State = ButtonState.Resume;
_checkInventory = false;
}
} }
else else
{ {
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting"))
{
State = ButtonState.Start; State = ButtonState.Start;
_checkInventory = false;
} }
}
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.SameLine(); ImGui.SameLine();
ImGui.BeginDisabled(!ImGui.GetIO().KeyCtrl);
bool keysHeld = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift;
ImGui.BeginDisabled(!keysHeld);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel"))
{ {
State = ButtonState.Pause; State = ButtonState.Pause;
@ -88,11 +140,11 @@ internal sealed class MainWindow : Window
Save(); Save();
} }
ImGui.EndDisabled(); ImGui.EndDisabled();
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && !ImGui.GetIO().KeyCtrl) if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && !keysHeld)
ImGui.SetTooltip( 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."); $"Hold CTRL+SHIFT to remove this as craft. You have to manually use the fabrication station to cancel or finish the workshop project before you can continue using the queue.");
ImGui.EndDisabled();
ShowErrorConditions(); ShowErrorConditions();
} }
@ -110,34 +162,46 @@ internal sealed class MainWindow : Window
ImGui.Text("Currently Crafting: ---"); ImGui.Text("Currently Crafting: ---");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Search, "Check Inventory")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Search, "Check Inventory"))
ImGui.OpenPopup(nameof(CheckMaterial)); _checkInventory = !_checkInventory;
ImGui.SameLine(); ImGui.SameLine();
ImGui.BeginDisabled(!NearFabricationStation || _configuration.ItemQueue.Sum(x => x.Quantity) == 0 || _plugin.CurrentStage != Stage.Stopped || !IsDiscipleOfHand); ImGui.BeginDisabled(!NearFabricationStation || _configuration.ItemQueue.Sum(x => x.Quantity) == 0 ||
_plugin.CurrentStage != Stage.Stopped || !IsDiscipleOfHand);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Play, "Start Crafting"))
{
State = ButtonState.Start; State = ButtonState.Start;
_checkInventory = false;
}
ImGui.EndDisabled(); ImGui.EndDisabled();
ShowErrorConditions(); ShowErrorConditions();
} }
if (ImGui.BeginPopup(nameof(CheckMaterial))) if (_checkInventory)
{ {
ImGui.Separator();
CheckMaterial(); CheckMaterial();
ImGui.EndPopup();
} }
ImGui.Separator(); ImGui.Separator();
ImGui.Text("Queue:"); ImGui.Text("Queue:");
ImGui.BeginDisabled(_plugin.CurrentStage != Stage.Stopped); ImGui.BeginDisabled(_plugin.CurrentStage != Stage.Stopped);
Configuration.QueuedItem? itemToRemove = null; Configuration.QueuedItem? itemToRemove = null;
for (int i = 0; i < _configuration.ItemQueue.Count; ++ i) for (int i = 0; i < _configuration.ItemQueue.Count; ++i)
{ {
ImGui.PushID($"ItemQueue{i}"); ImGui.PushID($"ItemQueue{i}");
var item = _configuration.ItemQueue[i]; var item = _configuration.ItemQueue[i];
var craft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == item.WorkshopItemId); var craft = _workshopCache.Crafts.Single(x => x.WorkshopItemId == item.WorkshopItemId);
ImGui.SetNextItemWidth(100); IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
}
ImGui.SetNextItemWidth(Math.Max(100 * ImGui.GetIO().FontGlobalScale, 4 * (ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X)));
int quantity = item.Quantity; int quantity = item.Quantity;
if (ImGui.InputInt(craft.Name, ref quantity)) if (ImGui.InputInt(craft.Name, ref quantity))
{ {
@ -164,16 +228,24 @@ internal sealed class MainWindow : Window
} }
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.BeginCombo("##CraftSelection", "Add Craft...")) if (ImGui.BeginCombo("##CraftSelection", "Add Craft...", ImGuiComboFlags.HeightLarge))
{ {
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256); ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256);
foreach (var craft in _workshopCache.Crafts foreach (var craft in _workshopCache.Crafts
.Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) .Where(x => x.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.WorkshopItemId)) .OrderBy(x => x.WorkshopItemId))
{ {
if (ImGui.Selectable($"{craft.Name}##SelectCraft{craft.WorkshopItemId}")) IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId);
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
if (icon != null)
{
ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X, ImGui.GetStyle().ItemSpacing.Y / 2));
}
if (ImGui.Selectable($"{craft.Name}##SelectCraft{craft.WorkshopItemId}", false, ImGuiSelectableFlags.SpanAllColumns))
{ {
_configuration.ItemQueue.Add(new Configuration.QueuedItem _configuration.ItemQueue.Add(new Configuration.QueuedItem
{ {
@ -182,25 +254,312 @@ internal sealed class MainWindow : Window
}); });
Save(); Save();
} }
if (icon != null)
{
ImGui.SameLine(0, 0);
ImGui.SetCursorPos(pos);
ImGui.Image(icon.ImGuiHandle, iconSize);
}
} }
ImGui.EndCombo(); ImGui.EndCombo();
} }
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.Separator(); ImGui.Separator();
ImGui.Text($"Debug (Stage): {_plugin.CurrentStage}"); ImGui.Text($"Debug (Stage): {_plugin.CurrentStage}");
} }
private void DrawPresetsMenu()
{
if (ImGui.BeginMenu("Presets"))
{
if (_configuration.Presets.Count == 0)
{
ImGui.BeginDisabled();
ImGui.MenuItem("Import Queue from Preset");
ImGui.EndDisabled();
}
else if (ImGui.BeginMenu("Import Queue from Preset"))
{
if (_configuration.Presets.Count == 0)
ImGui.MenuItem("You have no presets.");
foreach (var preset in _configuration.Presets)
{
ImGui.PushID($"Preset{preset.Id}");
if (ImGui.MenuItem(preset.Name))
{
foreach (var item in preset.ItemQueue)
{
var queuedItem =
_configuration.ItemQueue.FirstOrDefault(x => x.WorkshopItemId == item.WorkshopItemId);
if (queuedItem != null)
queuedItem.Quantity += item.Quantity;
else
{
_configuration.ItemQueue.Add(new Configuration.QueuedItem
{
WorkshopItemId = item.WorkshopItemId,
Quantity = item.Quantity,
});
}
}
Save();
_chatGui.Print($"Imported {preset.ItemQueue.Count} items from preset.");
}
ImGui.PopID();
}
ImGui.EndMenu();
}
if (_configuration.ItemQueue.Count == 0)
{
ImGui.BeginDisabled();
ImGui.MenuItem("Export Queue to Preset");
ImGui.EndDisabled();
}
else if (ImGui.BeginMenu("Export Queue to Preset"))
{
ImGui.InputTextWithHint("", "Preset Name...", ref _newPresetName, 64);
ImGui.BeginDisabled(_configuration.Presets.Any(x =>
x.Name.Equals(_newPresetName, StringComparison.OrdinalIgnoreCase)));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save"))
{
_configuration.Presets.Add(new Configuration.Preset
{
Id = Guid.NewGuid(),
Name = _newPresetName,
ItemQueue = _configuration.ItemQueue.Select(x => new Configuration.QueuedItem
{
WorkshopItemId = x.WorkshopItemId,
Quantity = x.Quantity
}).ToList()
});
Save();
_chatGui.Print($"Saved queue as preset '{_newPresetName}'.");
_newPresetName = string.Empty;
}
ImGui.EndDisabled();
ImGui.EndMenu();
}
if (_configuration.Presets.Count == 0)
{
ImGui.BeginDisabled();
ImGui.MenuItem("Delete Preset");
ImGui.EndDisabled();
}
else if (ImGui.BeginMenu("Delete Preset"))
{
if (_configuration.Presets.Count == 0)
ImGui.MenuItem("You have no presets.");
Guid? presetToRemove = null;
foreach (var preset in _configuration.Presets)
{
ImGui.PushID($"Preset{preset.Id}");
if (ImGui.MenuItem(preset.Name))
{
presetToRemove = preset.Id;
}
ImGui.PopID();
}
if (presetToRemove != null)
{
var preset = _configuration.Presets.First(x => x.Id == presetToRemove);
_configuration.Presets.Remove(preset);
Save();
_chatGui.Print($"Deleted preset '{preset.Name}'.");
}
ImGui.EndMenu();
}
ImGui.EndMenu();
}
}
private void DrawClipboardMenu()
{
if (ImGui.BeginMenu("Clipboard"))
{
List<Configuration.QueuedItem> fromClipboardItems = new();
try
{
string? clipboardText = GetClipboardText();
if (!string.IsNullOrWhiteSpace(clipboardText))
{
foreach (var clipboardLine in clipboardText.ReplaceLineEndings().Split(Environment.NewLine))
{
var match = CountAndName.Match(clipboardLine);
if (!match.Success)
continue;
var craft = _workshopCache.Crafts.FirstOrDefault(x =>
x.Name.Equals(match.Groups[2].Value, StringComparison.OrdinalIgnoreCase));
if (craft != null && int.TryParse(match.Groups[1].Value, out int quantity))
{
fromClipboardItems.Add(new Configuration.QueuedItem
{
WorkshopItemId = craft.WorkshopItemId,
Quantity = quantity,
});
}
}
}
}
catch (Exception)
{
//_pluginLog.Warning(e, "Unable to extract clipboard text");
}
ImGui.BeginDisabled(fromClipboardItems.Count == 0);
if (ImGui.MenuItem("Import Queue from Clipboard"))
{
_pluginLog.Information($"Importing {fromClipboardItems.Count} items...");
int count = 0;
foreach (var item in fromClipboardItems)
{
var queuedItem =
_configuration.ItemQueue.FirstOrDefault(x => x.WorkshopItemId == item.WorkshopItemId);
if (queuedItem != null)
queuedItem.Quantity += item.Quantity;
else
{
_configuration.ItemQueue.Add(new Configuration.QueuedItem
{
WorkshopItemId = item.WorkshopItemId,
Quantity = item.Quantity,
});
}
++count;
}
Save();
_chatGui.Print($"Imported {count} items from clipboard.");
}
ImGui.EndDisabled();
ImGui.BeginDisabled(_configuration.ItemQueue.Count == 0);
if (ImGui.MenuItem("Export Queue to Clipboard"))
{
var toClipboardItems = _configuration.ItemQueue.Select(x =>
new
{
_workshopCache.Crafts.Single(y => x.WorkshopItemId == y.WorkshopItemId).Name,
x.Quantity
})
.Select(x => $"{x.Quantity}x {x.Name}");
ImGui.SetClipboardText(string.Join(Environment.NewLine, toClipboardItems));
_chatGui.Print("Copied queue content to clipboard.");
}
if (ImGui.MenuItem("Export Material List to Clipboard"))
{
var toClipboardItems = _recipeTree.ResolveRecipes(GetMaterialList()).Where(x => x.Type == Ingredient.EType.Craftable);
ImGui.SetClipboardText(string.Join(Environment.NewLine, toClipboardItems.Select(x => $"{x.TotalQuantity}x {x.Name}")));
_chatGui.Print("Copied material list to clipboard.");
}
if (ImGui.MenuItem("Export Gathered/Venture materials to Clipboard"))
{
var toClipboardItems = _recipeTree.ResolveRecipes(GetMaterialList()).Where(x => x.Type == Ingredient.EType.Gatherable);
ImGui.SetClipboardText(string.Join(Environment.NewLine, toClipboardItems.Select(x => $"{x.TotalQuantity}x {x.Name}")));
_chatGui.Print("Copied material list to clipboard.");
}
ImGui.EndDisabled();
ImGui.EndMenu();
}
}
/// <summary>
/// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
/// </summary>
private unsafe string? GetClipboardText()
{
byte* ptr = ImGuiNative.igGetClipboardText();
if (ptr == null)
return null;
int byteCount = 0;
while (ptr[byteCount] != 0)
++byteCount;
return Encoding.UTF8.GetString(ptr, byteCount);
}
private void Save() private void Save()
{ {
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
} }
public void Toggle(EOpenReason reason)
{
if (!IsOpen)
{
IsOpen = true;
OpenReason = reason;
}
else
IsOpen = false;
}
public override void OnClose()
{
OpenReason = EOpenReason.None;
}
private unsafe void CheckMaterial() private unsafe void CheckMaterial()
{ {
ImGui.Text("Items needed for all crafts in queue:"); ImGui.Text("Items needed for all crafts in queue:");
var items = GetMaterialList();
ImGui.Indent(20);
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var item in items)
{
int inInventory = inventoryManager->GetInventoryItemCount(item.ItemId, true, false, false) +
inventoryManager->GetInventoryItemCount(item.ItemId, false, false, false);
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() - ImGui.GetTextLineHeight()) / 2);
icon.Dispose();
}
ImGui.TextColored(inInventory >= item.TotalQuantity ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
$"{item.Name} ({inInventory} / {item.TotalQuantity})");
}
ImGui.Unindent(20);
}
private List<Ingredient> GetMaterialList()
{
List<uint> workshopItemIds = _configuration.ItemQueue List<uint> workshopItemIds = _configuration.ItemQueue
.SelectMany(x => Enumerable.Range(0, x.Quantity).Select(_ => x.WorkshopItemId)) .SelectMany(x => Enumerable.Range(0, x.Quantity).Select(_ => x.WorkshopItemId))
.ToList(); .ToList();
@ -225,34 +584,25 @@ internal sealed class MainWindow : Window
} }
} }
var items = workshopItemIds.Select(x => _workshopCache.Crafts.Single(y => y.WorkshopItemId == x)) return workshopItemIds.Select(x => _workshopCache.Crafts.Single(y => y.WorkshopItemId == x))
.SelectMany(x => x.Phases) .SelectMany(x => x.Phases)
.SelectMany(x => x.Items) .SelectMany(x => x.Items)
.GroupBy(x => new { x.Name, x.ItemId }) .GroupBy(x => new { x.Name, x.ItemId, x.IconId })
.OrderBy(x => x.Key.Name) .OrderBy(x => x.Key.Name)
.Select(x => new .Select(x => new Ingredient
{ {
x.Key.ItemId, ItemId = x.Key.ItemId,
x.Key.Name, IconId = x.Key.IconId,
Name = x.Key.Name,
TotalQuantity = completedForCurrentCraft.TryGetValue(x.Key.ItemId, out var completed) TotalQuantity = completedForCurrentCraft.TryGetValue(x.Key.ItemId, out var completed)
? x.Sum(y => y.TotalQuantity) - completed ? x.Sum(y => y.TotalQuantity) - completed
: x.Sum(y => y.TotalQuantity), : x.Sum(y => y.TotalQuantity),
}); Type = Ingredient.EType.Craftable,
})
ImGui.Indent(20); .ToList();
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var item in items)
{
int inInventory = inventoryManager->GetInventoryItemCount(item.ItemId, true, false, false) +
inventoryManager->GetInventoryItemCount(item.ItemId, false, false, false);
ImGui.TextColored(inInventory >= item.TotalQuantity ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
$"{item.Name} ({inInventory} / {item.TotalQuantity})");
} }
ImGui.Unindent(20); private static void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity)
}
private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity)
{ {
if (completedForCurrentCraft.TryGetValue(itemId, out var existingQuantity)) if (completedForCurrentCraft.TryGetValue(itemId, out var existingQuantity))
completedForCurrentCraft[itemId] = quantity + existingQuantity; completedForCurrentCraft[itemId] = quantity + existingQuantity;
@ -262,16 +612,17 @@ internal sealed class MainWindow : Window
private void ShowErrorConditions() private void ShowErrorConditions()
{ {
if (!_plugin.WorkshopTerritories.Contains(_clientState.TerritoryType)) if (!_plugin.WorkshopTerritories.Contains(_clientState.TerritoryType))
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not in the Company Workshop."); ImGui.TextColored(ImGuiColors.DalamudRed, "You are not in the Company Workshop.");
else if (!NearFabricationStation) else if (!NearFabricationStation)
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not near a Farbrication Station."); ImGui.TextColored(ImGuiColors.DalamudRed, "You are not near a Fabrication Station.");
if (!IsDiscipleOfHand) if (!IsDiscipleOfHand)
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to be a Disciple of the Hand to start crafting."); ImGui.TextColored(ImGuiColors.DalamudRed, "You need to be a Disciple of the Hand to start crafting.");
} }
public void SaveWindowConfig() => Save();
public enum ButtonState public enum ButtonState
{ {
None, None,
@ -280,4 +631,12 @@ internal sealed class MainWindow : Window
Pause, Pause,
Stop, Stop,
} }
public enum EOpenReason
{
None,
Command,
NearFabricationStation,
PluginInstaller,
}
} }

View File

@ -0,0 +1,139 @@
using System;
using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using LLib.GameUI;
using LLib.Shop.Model;
using Workshoppa.External;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa.Windows;
internal sealed class RepairKitWindow : ShopWindow
{
private const int DarkMatterCluster6ItemId = 10386;
private readonly IPluginLog _pluginLog;
private readonly Configuration _configuration;
public RepairKitWindow(
IPluginLog pluginLog,
IGameGui gameGui,
IAddonLifecycle addonLifecycle,
Configuration configuration,
ExternalPluginHandler externalPluginHandler)
: base("Repair Kits###WorkshoppaRepairKitWindow", "Shop", pluginLog, gameGui, addonLifecycle, externalPluginHandler)
{
_pluginLog = pluginLog;
_configuration = configuration;
}
public override bool IsEnabled => _configuration.EnableRepairKitCalculator;
public override unsafe void UpdateShopStock(AtkUnitBase* addon)
{
if (GetDarkMatterClusterCount() == 0)
{
Shop.ItemForSale = null;
return;
}
if (addon->AtkValuesCount != 625)
{
_pluginLog.Error($"Unexpected amount of atkvalues for Shop addon ({addon->AtkValuesCount})");
Shop.ItemForSale = null;
return;
}
var atkValues = addon->AtkValues;
// Check if on 'Current Stock' tab?
if (atkValues[0].UInt != 0)
{
Shop.ItemForSale = null;
return;
}
uint itemCount = atkValues[2].UInt;
if (itemCount == 0)
{
Shop.ItemForSale = null;
return;
}
Shop.ItemForSale = Enumerable.Range(0, (int)itemCount)
.Select(i => new ItemForSale
{
Position = i,
ItemName = atkValues[14 + i].ReadAtkString(),
Price = atkValues[75 + i].UInt,
OwnedItems = atkValues[136 + i].UInt,
ItemId = atkValues[441 + i].UInt,
})
.FirstOrDefault(x => x.ItemId == DarkMatterCluster6ItemId);
}
private int GetDarkMatterClusterCount() => Shop.GetItemCount(10335);
public override int GetCurrencyCount() => Shop.GetItemCount(1);
public override void Draw()
{
int darkMatterClusters = GetDarkMatterClusterCount();
if (Shop.ItemForSale == null || darkMatterClusters == 0)
{
IsOpen = false;
return;
}
ImGui.Text("Inventory");
ImGui.Indent();
ImGui.Text($"Dark Matter Clusters: {darkMatterClusters:N0}");
ImGui.Text($"Grade 6 Dark Matter: {Shop.ItemForSale.OwnedItems:N0}");
ImGui.Unindent();
int missingItems = Math.Max(0, darkMatterClusters * 5 - (int)Shop.ItemForSale.OwnedItems);
ImGui.TextColored(missingItems == 0 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
$"Missing Grade 6 Dark Matter: {missingItems:N0}");
if (Shop.PurchaseState != null)
{
Shop.HandleNextPurchaseStep();
if (Shop.PurchaseState != null)
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
Shop.CancelAutoPurchase();
}
}
else
{
int toPurchase = Math.Min(Shop.GetMaxItemsToPurchase(), missingItems);
if (toPurchase > 0)
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign,
$"Auto-Buy missing Dark Matter for {Shop.ItemForSale.Price * toPurchase:N0}{SeIconChar.Gil.ToIconString()}"))
{
Shop.StartAutoPurchase(toPurchase);
Shop.HandleNextPurchaseStep();
}
}
}
}
public override unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
{
var buyItem = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.Int, Int = Shop.ItemForSale!.Position },
new() { Type = ValueType.Int, Int = buyNow },
new() { Type = 0, Int = 0 }
};
addonShop->FireCallback(4, buyItem);
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Numerics;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using LLib.ImGui;
using LLib.Shop;
using Workshoppa.External;
namespace Workshoppa.Windows;
internal abstract class ShopWindow : LWindow, IShopWindow, IDisposable
{
private readonly ExternalPluginHandler _externalPluginHandler;
protected ShopWindow(
string windowName,
string addonName,
IPluginLog pluginLog,
IGameGui gameGui,
IAddonLifecycle addonLifecycle,
ExternalPluginHandler externalPluginHandler)
: base(windowName)
{
_externalPluginHandler = externalPluginHandler;
Shop = new RegularShopBase(this, addonName, pluginLog, gameGui, addonLifecycle);
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse;
}
public void Dispose() => Shop.Dispose();
public bool AutoBuyEnabled => Shop.AutoBuyEnabled;
public bool IsAwaitingYesNo
{
get { return Shop.IsAwaitingYesNo; }
set { Shop.IsAwaitingYesNo = value; }
}
protected RegularShopBase Shop { get; }
public abstract bool IsEnabled { get; }
public abstract int GetCurrencyCount();
public abstract unsafe void UpdateShopStock(AtkUnitBase* addon);
public abstract unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow);
public void SaveExternalPluginState() => _externalPluginHandler.Save();
public void RestoreExternalPluginState() => _externalPluginHandler.Restore();
}

View File

@ -1,5 +1,9 @@
using System; using System;
using System.Linq; using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Workshoppa.GameData; using Workshoppa.GameData;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -47,12 +51,12 @@ partial class WorkshopPlugin
private void SelectCraftBranch() private void SelectCraftBranch()
{ {
if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials."))) if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials.", StringComparison.Ordinal)))
{ {
CurrentStage = Stage.ContributeMaterials; CurrentStage = Stage.ContributeMaterials;
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
} }
else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production."))) else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production.", StringComparison.Ordinal)))
{ {
_pluginLog.Information("Phase is complete"); _pluginLog.Information("Phase is complete");
@ -63,7 +67,7 @@ partial class WorkshopPlugin
CurrentStage = Stage.TargetFabricationStation; CurrentStage = Stage.TargetFabricationStation;
_continueAt = DateTime.Now.AddSeconds(3); _continueAt = DateTime.Now.AddSeconds(3);
} }
else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of"))) else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of", StringComparison.Ordinal)))
{ {
_pluginLog.Information("Item is almost complete, confirming last cutscene"); _pluginLog.Information("Item is almost complete, confirming last cutscene");
CurrentStage = Stage.TargetFabricationStation; CurrentStage = Stage.TargetFabricationStation;
@ -107,10 +111,28 @@ partial class WorkshopPlugin
{ {
_pluginLog.Error( _pluginLog.Error(
$"Can't contribute item {item.ItemId} to craft, couldn't find {item.ItemCountPerStep}x in a single inventory slot"); $"Can't contribute item {item.ItemId} to craft, couldn't find {item.ItemCountPerStep}x in a single inventory slot");
InventoryManager* inventoryManager = InventoryManager.Instance();
int itemCount = 0;
if (inventoryManager != null)
{
itemCount = inventoryManager->GetInventoryItemCount(item.ItemId, true, false, false) +
inventoryManager->GetInventoryItemCount(item.ItemId, false, false, false);
}
if (itemCount < item.ItemCountPerStep)
_chatGui.PrintError(
$"[Workshoppa] You don't have the needed {item.ItemCountPerStep}x {item.ItemName} to continue.");
else
_chatGui.PrintError(
$"[Workshoppa] You don't have {item.ItemCountPerStep}x {item.ItemName} in a single stack, you need to merge the items in your inventory manually to continue.");
CurrentStage = Stage.RequestStop; CurrentStage = Stage.RequestStop;
break; break;
} }
_externalPluginHandler.SaveTextAdvance();
_pluginLog.Information($"Contributing {item.ItemCountPerStep}x {item.ItemName}"); _pluginLog.Information($"Contributing {item.ItemCountPerStep}x {item.ItemName}");
_contributingItemId = item.ItemId; _contributingItemId = item.ItemId;
var contributeMaterial = stackalloc AtkValue[] var contributeMaterial = stackalloc AtkValue[]
@ -121,13 +143,75 @@ partial class WorkshopPlugin
new() { Type = 0, Int = 0 } new() { Type = 0, Int = 0 }
}; };
addonMaterialDelivery->FireCallback(4, contributeMaterial); addonMaterialDelivery->FireCallback(4, contributeMaterial);
CurrentStage = Stage.ConfirmMaterialDelivery; _fallbackAt = DateTime.Now.AddSeconds(0.2);
_continueAt = DateTime.Now.AddSeconds(0.5); CurrentStage = Stage.OpenRequestItemWindow;
break; break;
} }
} }
private unsafe void ConfirmMaterialDelivery() private unsafe void RequestPostSetup(AddonEvent type, AddonArgs addon)
{
var addonRequest = (AddonRequest*)addon.Addon;
_pluginLog.Verbose($"{nameof(RequestPostSetup)}: {CurrentStage}, {addonRequest->EntryCount}");
if (CurrentStage != Stage.OpenRequestItemWindow)
return;
if (addonRequest->EntryCount != 1)
return;
_fallbackAt = DateTime.MaxValue;
CurrentStage = Stage.OpenRequestItemSelect;
var contributeMaterial = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 2 },
new() { Type = ValueType.UInt, Int = 0 },
new() { Type = ValueType.UInt, UInt = 44 },
new() { Type = ValueType.UInt, UInt = 0 }
};
addonRequest->AtkUnitBase.FireCallback(4, contributeMaterial);
}
private unsafe void ContextIconMenuPostReceiveEvent(AddonEvent type, AddonArgs addon)
{
if (CurrentStage != Stage.OpenRequestItemSelect)
return;
CurrentStage = Stage.ConfirmRequestItemWindow;
var selectSlot = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.Int, Int = 0 /* slot */ },
new() { Type = ValueType.UInt, UInt = 20802 /* probably the item's icon */ },
new() { Type = ValueType.UInt, UInt = 0 },
new() { Type = 0, Int = 0 },
};
((AddonContextIconMenu*)addon.Addon)->AtkUnitBase.FireCallback(5, selectSlot);
}
private unsafe void RequestPostRefresh(AddonEvent type, AddonArgs addon)
{
_pluginLog.Verbose($"{nameof(RequestPostRefresh)}: {CurrentStage}");
if (CurrentStage != Stage.ConfirmRequestItemWindow)
return;
var addonRequest = (AddonRequest*)addon.Addon;
if (addonRequest->EntryCount != 1)
return;
CurrentStage = Stage.ConfirmMaterialDelivery;
var closeWindow = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.UInt, UInt = 0 },
new() { Type = ValueType.UInt, UInt = 0 },
new() { Type = ValueType.UInt, UInt = 0 }
};
addonRequest->AtkUnitBase.FireCallback(4, closeWindow);
addonRequest->AtkUnitBase.Close(false);
_externalPluginHandler.RestoreTextAdvance();
}
private unsafe void ConfirmMaterialDeliveryFollowUp()
{ {
AtkUnitBase* addonMaterialDelivery = GetMaterialDeliveryAddon(); AtkUnitBase* addonMaterialDelivery = GetMaterialDeliveryAddon();
if (addonMaterialDelivery == null) if (addonMaterialDelivery == null)
@ -141,16 +225,6 @@ partial class WorkshopPlugin
return; return;
} }
if (SelectSelectYesno(0, s => s == "Do you really want to trade a high-quality item?"))
{
_pluginLog.Information("Confirming HQ item turn in");
CurrentStage = Stage.ConfirmMaterialDelivery;
_continueAt = DateTime.Now.AddSeconds(0.1);
return;
}
if (SelectSelectYesno(0, s => s.StartsWith("Contribute") && s.EndsWith("to the company project?")))
{
var item = craftState.Items.Single(x => x.ItemId == _contributingItemId); var item = craftState.Items.Single(x => x.ItemId == _contributingItemId);
item.StepsComplete++; item.StepsComplete++;
if (craftState.IsPhaseComplete()) if (craftState.IsPhaseComplete())
@ -169,22 +243,4 @@ partial class WorkshopPlugin
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
} }
} }
else if (DateTime.Now > _continueAt.AddSeconds(20))
{
_pluginLog.Warning("No confirmation dialog, falling back to previous stage");
CurrentStage = Stage.ContributeMaterials;
}
}
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

@ -2,17 +2,15 @@
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa; namespace Workshoppa;
partial class WorkshopPlugin partial class WorkshopPlugin
{ {
private bool InteractWithFabricationStation(GameObject fabricationStation) private void InteractWithFabricationStation(IGameObject fabricationStation)
{ => InteractWithTarget(fabricationStation);
InteractWithTarget(fabricationStation);
return true;
}
private void TakeItemFromQueue() private void TakeItemFromQueue()
{ {
@ -49,7 +47,7 @@ partial class WorkshopPlugin
private void OpenCraftingLog() private void OpenCraftingLog()
{ {
if (SelectSelectString("craftlog", 0, s => s == "View company crafting log.")) if (SelectSelectString("craftlog", 0, s => s == _gameStrings.ViewCraftingLog))
CurrentStage = Stage.SelectCraftCategory; CurrentStage = Stage.SelectCraftCategory;
} }
@ -91,7 +89,7 @@ partial class WorkshopPlugin
.Select(i => new .Select(i => new
{ {
WorkshopItemId = atkValues[14 + 4 * i].UInt, WorkshopItemId = atkValues[14 + 4 * i].UInt,
Name = ReadAtkString(atkValues[17 + 4 * i]), Name = atkValues[17 + 4 * i].ReadAtkString(),
}) })
.ToList(); .ToList();
@ -121,7 +119,7 @@ partial class WorkshopPlugin
private void ConfirmCraft() private void ConfirmCraft()
{ {
if (SelectSelectYesno(0, s => s.StartsWith("Craft "))) if (SelectSelectYesno(0, s => s.StartsWith("Craft ", StringComparison.Ordinal)))
{ {
_configuration.CurrentlyCraftedItem!.StartedCrafting = true; _configuration.CurrentlyCraftedItem!.StartedCrafting = true;
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
@ -10,13 +11,14 @@ using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Workshoppa.GameData; using Workshoppa.GameData;
namespace Workshoppa; namespace Workshoppa;
partial class WorkshopPlugin partial class WorkshopPlugin
{ {
private unsafe void InteractWithTarget(GameObject obj) private unsafe void InteractWithTarget(IGameObject obj)
{ {
_pluginLog.Information($"Setting target to {obj}"); _pluginLog.Information($"Setting target to {obj}");
/* /*
@ -29,16 +31,23 @@ partial class WorkshopPlugin
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false); (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false);
} }
private float GetDistanceToEventObject(IReadOnlyList<uint> npcIds, out GameObject? o) private float GetDistanceToEventObject(IReadOnlyList<uint> npcIds, out IGameObject? o)
{
Vector3? localPlayerPosition = _clientState.LocalPlayer?.Position;
if (localPlayerPosition != null)
{ {
foreach (var obj in _objectTable) foreach (var obj in _objectTable)
{ {
if (obj.ObjectKind == ObjectKind.EventObj) if (obj.ObjectKind == ObjectKind.EventObj)
{ {
if (npcIds.Contains(GetNpcId(obj))) if (npcIds.Contains(obj.DataId))
{ {
o = obj; o = obj;
return Vector3.Distance(_clientState.LocalPlayer!.Position, obj.Position); float distance = Vector3.Distance(localPlayerPosition.Value,
obj.Position + new Vector3(0, -2, 0));
if (distance > 0.01)
return distance;
}
} }
} }
} }
@ -47,35 +56,10 @@ partial class WorkshopPlugin
return float.MaxValue; return float.MaxValue;
} }
private unsafe uint GetNpcId(GameObject obj)
{
return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address)->GetNpcID();
}
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() private unsafe AtkUnitBase* GetCompanyCraftingLogAddon()
{ {
if (TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) && IsAddonReady(addon)) if (_gameGui.TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) &&
LAddon.IsAddonReady(addon))
return addon; return addon;
return null; return null;
@ -90,39 +74,22 @@ partial class WorkshopPlugin
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.CompanyCraftMaterial); var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.CompanyCraftMaterial);
if (agentInterface != null && agentInterface->IsAgentActive()) if (agentInterface != null && agentInterface->IsAgentActive())
{ {
var addonId = agentInterface->GetAddonID(); var addonId = agentInterface->GetAddonId();
if (addonId == 0) if (addonId == 0)
return null; return null;
AtkUnitBase* addon = GetAddonById(addonId); AtkUnitBase* addon = LAddon.GetAddonById(addonId);
if (IsAddonReady(addon)) if (LAddon.IsAddonReady(addon))
return addon; return addon;
} }
return null; return null;
} }
private unsafe AtkUnitBase* GetAddonById(uint id)
{
var unitManagers = &AtkStage.GetSingleton()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList;
for (var i = 0; i < 18; i++)
{
foreach (AtkUnitBase* unitBase in unitManagers[i].EntriesSpan)
{
if (unitBase != null && unitBase->ID == id)
{
return unitBase;
}
}
}
return null;
}
private unsafe bool SelectSelectString(string marker, int choice, Predicate<string> predicate) private unsafe bool SelectSelectString(string marker, int choice, Predicate<string> predicate)
{ {
if (TryGetAddonByName<AddonSelectString>("SelectString", out var addonSelectString) && if (_gameGui.TryGetAddonByName<AddonSelectString>("SelectString", out var addonSelectString) &&
IsAddonReady(&addonSelectString->AtkUnitBase)) LAddon.IsAddonReady(&addonSelectString->AtkUnitBase))
{ {
int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount; int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount;
if (entries < choice) if (entries < choice)
@ -146,11 +113,13 @@ partial class WorkshopPlugin
private unsafe bool SelectSelectYesno(int choice, Predicate<string> predicate) private unsafe bool SelectSelectYesno(int choice, Predicate<string> predicate)
{ {
if (TryGetAddonByName<AddonSelectYesno>("SelectYesno", out var addonSelectYesno) && if (_gameGui.TryGetAddonByName<AddonSelectYesno>("SelectYesno", out var addonSelectYesno) &&
IsAddonReady(&addonSelectYesno->AtkUnitBase)) LAddon.IsAddonReady(&addonSelectYesno->AtkUnitBase))
{ {
var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString(); var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString();
text = text.Replace("\n", "").Replace("\r", ""); text = text
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
if (predicate(text)) if (predicate(text))
{ {
_pluginLog.Information($"Selecting choice {choice} for '{text}'"); _pluginLog.Information($"Selecting choice {choice} for '{text}'");
@ -166,13 +135,6 @@ partial class WorkshopPlugin
return false; 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) private unsafe CraftState? ReadCraftState(AtkUnitBase* addonMaterialDelivery)
{ {
try try
@ -189,7 +151,7 @@ partial class WorkshopPlugin
{ {
ItemId = atkValues[12 + i].UInt, ItemId = atkValues[12 + i].UInt,
IconId = atkValues[24 + i].UInt, IconId = atkValues[24 + i].UInt,
ItemName = ReadAtkString(atkValues[36 + i]), ItemName = atkValues[36 + i].ReadAtkString(),
CrafterIconId = atkValues[48 + i].Int, CrafterIconId = atkValues[48 + i].Int,
ItemCountPerStep = atkValues[60 + i].UInt, ItemCountPerStep = atkValues[60 + i].UInt,
ItemCountNQ = atkValues[72 + i].UInt, ItemCountNQ = atkValues[72 + i].UInt,
@ -219,17 +181,22 @@ partial class WorkshopPlugin
return null; return null;
} }
private uint ParseAtkItemCountHq(AtkValue atkValue) private static uint ParseAtkItemCountHq(AtkValue atkValue)
{ {
// NQ / HQ string // NQ / HQ string
// I have no clue, but it doesn't seme like the available HQ item count is strored anywhere in the atkvalues?? // 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); string? s = atkValue.ReadAtkString();
if (s != null) if (s != null)
{ {
var parts = s.Replace("\ue03c", "").Split('/'); var parts = s.Replace("\ue03c", "", StringComparison.Ordinal).Split('/');
if (parts.Length > 1) if (parts.Length > 1)
{ {
return uint.Parse(parts[1].Replace(",", "").Replace(".", "").Trim()); return uint.Parse(
parts[1]
.Replace(",", "", StringComparison.Ordinal)
.Replace(".", "", StringComparison.Ordinal)
.Trim(),
CultureInfo.InvariantCulture);
} }
} }
@ -251,7 +218,7 @@ partial class WorkshopPlugin
if (item == null) if (item == null)
continue; continue;
if (item->ItemID == itemId && item->Quantity >= count) if (item->ItemId == itemId && item->Quantity >= count)
return true; return true;
} }
} }

View File

@ -0,0 +1,81 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Workshoppa;
partial class WorkshopPlugin
{
private unsafe void SelectYesNoPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString()
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
_pluginLog.Verbose($"YesNo prompt: '{text}'");
if (_repairKitWindow.IsOpen)
{
_pluginLog.Verbose($"Checking for Repair Kit YesNo ({_repairKitWindow.AutoBuyEnabled}, {_repairKitWindow.IsAwaitingYesNo})");
if (_repairKitWindow.AutoBuyEnabled && _repairKitWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForGil.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
_repairKitWindow.IsAwaitingYesNo = false;
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
else
{
_pluginLog.Verbose("Not a purchase confirmation match");
}
}
else if (_ceruleumTankWindow.IsOpen)
{
_pluginLog.Verbose($"Checking for Ceruleum Tank YesNo ({_ceruleumTankWindow.AutoBuyEnabled}, {_ceruleumTankWindow.IsAwaitingYesNo})");
if (_ceruleumTankWindow.AutoBuyEnabled && _ceruleumTankWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForCompanyCredits.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
_ceruleumTankWindow.IsAwaitingYesNo = false;
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
else
{
_pluginLog.Verbose("Not a purchase confirmation match");
}
}
else if (CurrentStage != Stage.Stopped)
{
if (CurrentStage == Stage.ConfirmMaterialDelivery && _gameStrings.TurnInHighQualityItem == text)
{
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
else if (CurrentStage == Stage.ConfirmMaterialDelivery && _gameStrings.ContributeItems.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
ConfirmMaterialDeliveryFollowUp();
}
else if (CurrentStage == Stage.ConfirmCollectProduct && _gameStrings.RetrieveFinishedItem.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
ConfirmCollectProductFollowUp();
}
}
}
private void ConfirmCollectProductFollowUp()
{
_configuration.CurrentlyCraftedItem = null;
_pluginInterface.SavePluginConfig(_configuration);
CurrentStage = Stage.TakeItemFromQueue;
_continueAt = DateTime.Now.AddSeconds(0.5);
}
}

View File

@ -2,25 +2,30 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiNET;
using LLib;
using Workshoppa.External; using Workshoppa.External;
using Workshoppa.GameData; using Workshoppa.GameData;
using Workshoppa.Windows; using Workshoppa.Windows;
namespace Workshoppa; namespace Workshoppa;
[SuppressMessage("ReSharper", "UnusedType.Global")] [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public sealed partial class WorkshopPlugin : IDalamudPlugin public sealed partial class WorkshopPlugin : IDalamudPlugin
{ {
private readonly IReadOnlyList<uint> FabricationStationIds = new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly(); private readonly IReadOnlyList<uint> _fabricationStationIds =
new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly();
internal readonly IReadOnlyList<ushort> WorkshopTerritories = new ushort[] { 423, 424, 425, 653, 984 }.AsReadOnly(); internal readonly IReadOnlyList<ushort> WorkshopTerritories = new ushort[] { 423, 424, 425, 653, 984 }.AsReadOnly();
private readonly WindowSystem _windowSystem = new WindowSystem(nameof(WorkshopPlugin)); private readonly WindowSystem _windowSystem = new WindowSystem(nameof(WorkshopPlugin));
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly ICondition _condition; private readonly ICondition _condition;
@ -28,19 +33,27 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IChatGui _chatGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly ExternalPluginHandler _externalPluginHandler;
private readonly WorkshopCache _workshopCache; private readonly WorkshopCache _workshopCache;
private readonly GameStrings _gameStrings;
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
private readonly ConfigWindow _configWindow;
private readonly RepairKitWindow _repairKitWindow;
private readonly CeruleumTankWindow _ceruleumTankWindow;
private Stage _currentStageInternal = Stage.Stopped; private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
private (bool Saved, bool? PreviousState) _yesAlreadyState = (false, null); private DateTime _fallbackAt = DateTime.MaxValue;
public WorkshopPlugin(DalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework, public WorkshopPlugin(IDalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework,
ICondition condition, IClientState clientState, IObjectTable objectTable, IDataManager dataManager, ICondition condition, IClientState clientState, IObjectTable objectTable, IDataManager dataManager,
ICommandManager commandManager, IPluginLog pluginLog) ICommandManager commandManager, IPluginLog pluginLog, IAddonLifecycle addonLifecycle, IChatGui chatGui,
ITextureProvider textureProvider)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_gameGui = gameGui; _gameGui = gameGui;
@ -50,22 +63,51 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_objectTable = objectTable; _objectTable = objectTable;
_commandManager = commandManager; _commandManager = commandManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_chatGui = chatGui;
var dalamudReflector = new DalamudReflector(_pluginInterface, _framework, _pluginLog); _externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _pluginLog);
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_workshopCache = new WorkshopCache(dataManager, _pluginLog); _workshopCache = new WorkshopCache(dataManager, _pluginLog);
_gameStrings = new(dataManager, _pluginLog);
_mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache); _mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache,
new IconCache(textureProvider), _chatGui, new RecipeTree(dataManager, _pluginLog), _pluginLog);
_windowSystem.AddWindow(_mainWindow); _windowSystem.AddWindow(_mainWindow);
_configWindow = new(_pluginInterface, _configuration);
_windowSystem.AddWindow(_configWindow);
_repairKitWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler);
_windowSystem.AddWindow(_repairKitWindow);
_ceruleumTankWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler, _chatGui);
_windowSystem.AddWindow(_ceruleumTankWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += _mainWindow.Toggle; _pluginInterface.UiBuilder.OpenMainUi += OpenMainUi;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_framework.Update += FrameworkUpdate; _framework.Update += FrameworkUpdate;
_commandManager.AddHandler("/ws", new CommandInfo(ProcessCommand) _commandManager.AddHandler("/ws", new CommandInfo(ProcessCommand)
{ {
HelpMessage = "Open UI" HelpMessage = "Open UI"
}); });
_commandManager.AddHandler("/workshoppa", new CommandInfo(ProcessCommand)
{
ShowInHelp = false,
});
_commandManager.AddHandler("/buy-tanks", new CommandInfo(ProcessBuyCommand)
{
HelpMessage = "Buy a given number of ceruleum tank stacks.",
});
_commandManager.AddHandler("/fill-tanks", new CommandInfo(ProcessFillCommand)
{
HelpMessage = "Fill your inventory with a given number of ceruleum tank stacks.",
});
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
} }
internal Stage CurrentStage internal Stage CurrentStage
@ -75,9 +117,14 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
{ {
if (_currentStageInternal != value) if (_currentStageInternal != value)
{ {
_pluginLog.Information($"Changing stage from {_currentStageInternal} to {value}"); _pluginLog.Debug($"Changing stage from {_currentStageInternal} to {value}");
_currentStageInternal = value; _currentStageInternal = value;
} }
if (value != Stage.Stopped)
_mainWindow.Flags |= ImGuiWindowFlags.NoCollapse;
else
_mainWindow.Flags &= ~ImGuiWindowFlags.NoCollapse;
} }
} }
@ -86,34 +133,51 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
if (!_clientState.IsLoggedIn || if (!_clientState.IsLoggedIn ||
!WorkshopTerritories.Contains(_clientState.TerritoryType) || !WorkshopTerritories.Contains(_clientState.TerritoryType) ||
_condition[ConditionFlag.BoundByDuty] || _condition[ConditionFlag.BoundByDuty] ||
GetDistanceToEventObject(FabricationStationIds, out var fabricationStation) >= 5f) _condition[ConditionFlag.BetweenAreas] ||
_condition[ConditionFlag.BetweenAreas51] ||
GetDistanceToEventObject(_fabricationStationIds, out var fabricationStation) >= 3f)
{ {
_mainWindow.NearFabricationStation = false; _mainWindow.NearFabricationStation = false;
if (_mainWindow.IsOpen &&
_mainWindow.OpenReason == MainWindow.EOpenReason.NearFabricationStation &&
_configuration.CurrentlyCraftedItem == null &&
_configuration.ItemQueue.Count == 0)
{
_mainWindow.IsOpen = false;
}
} }
else if (DateTime.Now >= _continueAt) else if (DateTime.Now >= _continueAt)
{ {
_mainWindow.NearFabricationStation = true; _mainWindow.NearFabricationStation = true;
if (!_mainWindow.IsOpen)
{
_mainWindow.IsOpen = true;
_mainWindow.OpenReason = MainWindow.EOpenReason.NearFabricationStation;
}
if (_mainWindow.State is MainWindow.ButtonState.Pause or MainWindow.ButtonState.Stop) if (_mainWindow.State is MainWindow.ButtonState.Pause or MainWindow.ButtonState.Stop)
{ {
_mainWindow.State = MainWindow.ButtonState.None; _mainWindow.State = MainWindow.ButtonState.None;
if (CurrentStage != Stage.Stopped) if (CurrentStage != Stage.Stopped)
{ {
RestoreYesAlready(); _externalPluginHandler.Restore();
CurrentStage = Stage.Stopped; CurrentStage = Stage.Stopped;
} }
return; return;
} }
else if (_mainWindow.State is MainWindow.ButtonState.Start or MainWindow.ButtonState.Resume && CurrentStage == Stage.Stopped) else if (_mainWindow.State is MainWindow.ButtonState.Start or MainWindow.ButtonState.Resume &&
CurrentStage == Stage.Stopped)
{ {
// TODO Error checking, we should ensure the player has the required job level for *all* crafting parts // TODO Error checking, we should ensure the player has the required job level for *all* crafting parts
_mainWindow.State = MainWindow.ButtonState.None; _mainWindow.State = MainWindow.ButtonState.None;
CurrentStage = Stage.TakeItemFromQueue; CurrentStage = Stage.TakeItemFromQueue;
} }
if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_yesAlreadyState.Saved) if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_externalPluginHandler.Saved)
SaveYesAlready(); _externalPluginHandler.Save();
switch (CurrentStage) switch (CurrentStage)
{ {
@ -125,13 +189,12 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
break; break;
case Stage.TargetFabricationStation: case Stage.TargetFabricationStation:
if (InteractWithFabricationStation(fabricationStation!))
{
if (_configuration.CurrentlyCraftedItem is { StartedCrafting: true }) if (_configuration.CurrentlyCraftedItem is { StartedCrafting: true })
CurrentStage = Stage.SelectCraftBranch; CurrentStage = Stage.SelectCraftBranch;
else else
CurrentStage = Stage.OpenCraftingLog; CurrentStage = Stage.OpenCraftingLog;
}
InteractWithFabricationStation(fabricationStation!);
break; break;
@ -152,7 +215,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
break; break;
case Stage.RequestStop: case Stage.RequestStop:
RestoreYesAlready(); _externalPluginHandler.Restore();
CurrentStage = Stage.Stopped; CurrentStage = Stage.Stopped;
break; break;
@ -164,12 +227,24 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
ContributeMaterials(); ContributeMaterials();
break; break;
case Stage.OpenRequestItemWindow:
// see RequestPostSetup and related
if (DateTime.Now > _fallbackAt)
goto case Stage.ContributeMaterials;
break;
case Stage.OpenRequestItemSelect:
case Stage.ConfirmRequestItemWindow:
// see RequestPostSetup and related
break;
case Stage.ConfirmMaterialDelivery: case Stage.ConfirmMaterialDelivery:
ConfirmMaterialDelivery(); // see SelectYesNoPostSetup
break; break;
case Stage.ConfirmCollectProduct: case Stage.ConfirmCollectProduct:
ConfirmCollectProduct(); // see SelectYesNoPostSetup
break; break;
case Stage.Stopped: case Stage.Stopped:
@ -184,42 +259,56 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private WorkshopCraft GetCurrentCraft() private WorkshopCraft GetCurrentCraft()
{ {
return _workshopCache.Crafts.Single(x => x.WorkshopItemId == _configuration.CurrentlyCraftedItem!.WorkshopItemId); return _workshopCache.Crafts.Single(
x => x.WorkshopItemId == _configuration.CurrentlyCraftedItem!.WorkshopItemId);
} }
private void ProcessCommand(string command, string arguments) => _mainWindow.Toggle(); private void ProcessCommand(string command, string arguments)
{
if (arguments is "c" or "config")
_configWindow.Toggle();
else
_mainWindow.Toggle(MainWindow.EOpenReason.Command);
}
private void ProcessBuyCommand(string command, string arguments)
{
if (_ceruleumTankWindow.TryParseBuyRequest(arguments, out int missingQuantity))
_ceruleumTankWindow.StartPurchase(missingQuantity);
else
_chatGui.PrintError($"Usage: {command} <stacks>");
}
private void ProcessFillCommand(string command, string arguments)
{
if (_ceruleumTankWindow.TryParseFillRequest(arguments, out int missingQuantity))
_ceruleumTankWindow.StartPurchase(missingQuantity);
else
_chatGui.PrintError($"Usage: {command} <stacks>");
}
private void OpenMainUi()
=> _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller);
public void Dispose() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
_addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_commandManager.RemoveHandler("/fill-tanks");
_commandManager.RemoveHandler("/buy-tanks");
_commandManager.RemoveHandler("/workshoppa");
_commandManager.RemoveHandler("/ws"); _commandManager.RemoveHandler("/ws");
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi -= _mainWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.OpenMainUi -= OpenMainUi;
_framework.Update -= FrameworkUpdate; _framework.Update -= FrameworkUpdate;
RestoreYesAlready(); _ceruleumTankWindow.Dispose();
} _repairKitWindow.Dispose();
private void SaveYesAlready() _externalPluginHandler.RestoreTextAdvance();
{ _externalPluginHandler.Restore();
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

@ -1,66 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Dalamud.NET.Sdk/11.0.0">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <Version>7.0</Version>
<Version>2.1</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <Import Project="..\LLib\LLib.targets"/>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath> <Import Project="..\LLib\RenameZip.targets"/>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/> <ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup> </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="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" Condition="'$(Configuration)' == 'Release'">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project> </Project>

View File

@ -2,7 +2,7 @@
"Name": "Workshoppa", "Name": "Workshoppa",
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Better Company Workshop Turn-In", "Punchline": "Better Company Workshop Turn-In",
"Description": "Requires Pandora's Box (or a similar plugin) with 'Auto-select Turn-ins' enabled", "Description": "",
"RepoUrl": "https://git.carvel.li/liza/Workshoppa", "RepoUrl": "https://git.carvel.li/liza/Workshoppa",
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/Workshoppa.png" "IconUrl": "https://plugins.carvel.li/icons/Workshoppa.png"
} }

View File

@ -1,12 +1,86 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": { "net8.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.12, )", "requested": "[11.0.0, )",
"resolved": "2.1.12", "resolved": "11.0.0",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" "contentHash": "bjT7XUlhIJSmsE/O76b7weUX+evvGQctbQB8aKXt94o+oPWxHpCepxAGMs7Thow3AzCyqWs7cOpp9/2wcgRRQA=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.SourceLink.AzureRepos.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"llib": {
"type": "Project",
"dependencies": {
"DalamudPackager": "[11.0.0, )"
}
} }
} }
} }

View File

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