Compare commits

..

15 Commits
v4.2 ... master

25 changed files with 1497 additions and 518 deletions

1017
.editorconfig Normal file

File diff suppressed because it is too large Load Diff

2
LLib

@ -1 +1 @@
Subproject commit 865a6080319f8ccbcd5fd5b0004404822b6e60d4 Subproject commit e4bbc05ede6f6f01e7028b24614ed8cb333e909c

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,12 +11,15 @@ internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public CurrentItem? CurrentlyCraftedItem { get; set; } = null; public CurrentItem? CurrentlyCraftedItem { get; set; }
public List<QueuedItem> ItemQueue { get; set; } = new(); public List<QueuedItem> ItemQueue { get; set; } = new();
public bool EnableRepairKitCalculator { get; set; } = true; public bool EnableRepairKitCalculator { get; set; } = true;
public bool EnableCeruleumTankCalculator { get; set; } = true; public bool EnableCeruleumTankCalculator { get; set; } = true;
public List<Preset> Presets { get; set; } = new(); 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
{ {
public uint WorkshopItemId { get; set; } public uint WorkshopItemId { get; set; }
@ -27,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)

View File

@ -6,13 +6,13 @@ namespace Workshoppa.External;
internal sealed class ExternalPluginHandler internal sealed class ExternalPluginHandler
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly PandoraIpc _pandoraIpc; private readonly PandoraIpc _pandoraIpc;
private bool? _pandoraState; private bool? _pandoraState;
public ExternalPluginHandler(DalamudPluginInterface pluginInterface, IPluginLog pluginLog) public ExternalPluginHandler(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_pluginLog = pluginLog; _pluginLog = pluginLog;

View File

@ -13,7 +13,7 @@ internal sealed class PandoraIpc
private readonly ICallGateSubscriber<string, bool?> _getEnabled; private readonly ICallGateSubscriber<string, bool?> _getEnabled;
private readonly ICallGateSubscriber<string, bool, object?> _setEnabled; private readonly ICallGateSubscriber<string, bool, object?> _setEnabled;
public PandoraIpc(DalamudPluginInterface pluginInterface, IPluginLog pluginLog) public PandoraIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{ {
_pluginLog = pluginLog; _pluginLog = pluginLog;
_getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled"); _getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled");

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

@ -1,10 +1,11 @@
using System; using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib; using LLib;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.CustomSheets; using Lumina.Excel.Sheets;
using Lumina.Excel.GeneratedSheets; using Lumina.Text.ReadOnly;
namespace Workshoppa.GameData; namespace Workshoppa.GameData;
@ -13,19 +14,19 @@ internal sealed class GameStrings
public GameStrings(IDataManager dataManager, IPluginLog pluginLog) public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{ {
PurchaseItemForGil = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog) PurchaseItemForGil = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForGil)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForGil)}");
PurchaseItemForCompanyCredits = dataManager.GetRegex<Addon>(3473, addon => addon.Text, pluginLog) PurchaseItemForCompanyCredits = dataManager.GetRegex<Addon>(3473, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}");
ViewCraftingLog = ViewCraftingLog =
dataManager.GetString<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE", dataManager.GetString<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE",
pluginLog) ?? throw new Exception($"Unable to resolve {nameof(ViewCraftingLog)}"); pluginLog) ?? throw new ConstraintException($"Unable to resolve {nameof(ViewCraftingLog)}");
TurnInHighQualityItem = dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog) TurnInHighQualityItem = dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(TurnInHighQualityItem)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(TurnInHighQualityItem)}");
ContributeItems = dataManager.GetRegex<Addon>(6652, addon => addon.Text, pluginLog) ContributeItems = dataManager.GetRegex<Addon>(6652, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(ContributeItems)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(ContributeItems)}");
RetrieveFinishedItem = RetrieveFinishedItem =
dataManager.GetRegex<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_FINISH_CONF", pluginLog) dataManager.GetRegex<WorkshopDialogue>("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_FINISH_CONF", pluginLog)
?? throw new Exception($"Unable to resolve {nameof(RetrieveFinishedItem)}"); ?? throw new ConstraintException($"Unable to resolve {nameof(RetrieveFinishedItem)}");
} }
public Regex PurchaseItemForGil { get; } public Regex PurchaseItemForGil { get; }
@ -36,7 +37,17 @@ internal sealed class GameStrings
public Regex RetrieveFinishedItem { get; } public Regex RetrieveFinishedItem { get; }
[Sheet("custom/001/CmnDefCompanyManufactory_00150")] [Sheet("custom/001/CmnDefCompanyManufactory_00150")]
private class WorkshopDialogue : QuestDialogueText [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

@ -2,18 +2,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
namespace Workshoppa.GameData; namespace Workshoppa.GameData;
public sealed class RecipeTree internal sealed class RecipeTree
{ {
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IPluginLog _pluginLog;
private readonly IReadOnlyList<uint> _shopItemsOnly; private readonly IReadOnlyList<uint> _shopItemsOnly;
public RecipeTree(IDataManager dataManager) public RecipeTree(IDataManager dataManager, IPluginLog pluginLog)
{ {
_dataManager = dataManager; _dataManager = dataManager;
_pluginLog = pluginLog;
// probably incomplete, e.g. different housing districts have different shop types // probably incomplete, e.g. different housing districts have different shop types
var shopVendorIds = new uint[] var shopVendorIds = new uint[]
@ -29,16 +31,17 @@ public sealed class RecipeTree
262211, // Z'ranmaia, upper decks 262211, // Z'ranmaia, upper decks
}; };
_shopItemsOnly = _dataManager.GetExcelSheet<GilShopItem>()! _shopItemsOnly = _dataManager.GetSubrowExcelSheet<GilShopItem>()
.Flatten()
.Where(x => shopVendorIds.Contains(x.RowId)) .Where(x => shopVendorIds.Contains(x.RowId))
.Select(x => x.Item.Row) .Select(x => x.Item.RowId)
.Where(x => x > 0) .Where(x => x > 0)
.Distinct() .Distinct()
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
} }
public List<Ingredient> ResolveRecipes(List<Ingredient> materials) public IReadOnlyList<Ingredient> ResolveRecipes(IReadOnlyList<Ingredient> materials)
{ {
// look up recipes recursively // look up recipes recursively
int limit = 10; int limit = 10;
@ -62,6 +65,9 @@ public sealed class RecipeTree
AmountCrafted = x.First().AmountCrafted, AmountCrafted = x.First().AmountCrafted,
}) })
.ToList(); .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 // 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 })) foreach (var ingredient in completeList.Where(x => x is { AmountCrafted: > 1 }))
@ -72,7 +78,8 @@ public sealed class RecipeTree
//_pluginLog.Information($" → {part.Name}"); //_pluginLog.Information($" → {part.Name}");
int unmodifiedQuantity = part.TotalQuantity; int unmodifiedQuantity = part.TotalQuantity;
int roundedQuantity = (int)((unmodifiedQuantity + ingredient.AmountCrafted - 1) / ingredient.AmountCrafted); int roundedQuantity =
(int)((unmodifiedQuantity + ingredient.AmountCrafted - 1) / ingredient.AmountCrafted);
part.TotalQuantity = part.TotalQuantity - unmodifiedQuantity + roundedQuantity; part.TotalQuantity = part.TotalQuantity - unmodifiedQuantity + roundedQuantity;
} }
} }
@ -83,13 +90,20 @@ public sealed class RecipeTree
List<RecipeInfo> sortedList = new List<RecipeInfo>(); List<RecipeInfo> sortedList = new List<RecipeInfo>();
while (sortedList.Count < completeList.Count) while (sortedList.Count < completeList.Count)
{ {
var craftable = completeList.Where(x => _pluginLog.Verbose("Sort round");
var canBeCrafted = completeList.Where(x =>
!sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId))) !sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId)))
.ToList(); .ToList();
if (craftable.Count == 0) foreach (var item in canBeCrafted)
throw new Exception("Unable to sort items"); _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(craftable.OrderBy(x => x.Name)); sortedList.AddRange(canBeCrafted.OrderBy(x => x.Name));
} }
return sortedList.Cast<Ingredient>().ToList(); return sortedList.Cast<Ingredient>().ToList();
@ -106,33 +120,34 @@ public sealed class RecipeTree
if (recipe == null) if (recipe == null)
continue; continue;
foreach (var ingredient in recipe.UnkData5.Take(8)) for (int i = 0; i < 8; ++ i)
{ {
if (ingredient == null || ingredient.ItemIngredient == 0) var ingredient = recipe.Value.Ingredient[i];
if (!ingredient.IsValid || ingredient.RowId == 0)
continue; continue;
Item? item = _dataManager.GetExcelSheet<Item>()!.GetRow((uint)ingredient.ItemIngredient); Item item = ingredient.Value;
if (item == null) if (!IsValidItem(item.RowId))
continue; continue;
Recipe? ingredientRecipe = GetFirstRecipeForItem((uint)ingredient.ItemIngredient); Recipe? ingredientRecipe = GetFirstRecipeForItem(ingredient.RowId);
//_pluginLog.Information($"Adding {item.Name}"); //_pluginLog.Information($"Adding {item.Name}");
ingredients.Add(new RecipeInfo ingredients.Add(new RecipeInfo
{ {
ItemId = (uint)ingredient.ItemIngredient, ItemId = ingredient.RowId,
Name = item.Name, Name = item.Name.ToString(),
TotalQuantity = material.TotalQuantity * ingredient.AmountIngredient, TotalQuantity = material.TotalQuantity * recipe.Value.AmountIngredient[i],
Type = Type =
_shopItemsOnly.Contains((uint)ingredient.ItemIngredient) ? Ingredient.EType.ShopItem : _shopItemsOnly.Contains(ingredient.RowId) ? Ingredient.EType.ShopItem :
ingredientRecipe != null ? Ingredient.EType.Craftable : ingredientRecipe != null ? Ingredient.EType.Craftable :
GetGatheringItem((uint)ingredient.ItemIngredient) != null ? Ingredient.EType.Gatherable : GetGatheringItem(ingredient.RowId) != null ? Ingredient.EType.Gatherable :
GetVentureItem((uint)ingredient.ItemIngredient) != null ? Ingredient.EType.Gatherable : GetVentureItem(ingredient.RowId) != null ? Ingredient.EType.Gatherable :
Ingredient.EType.Other, Ingredient.EType.Other,
AmountCrafted = ingredientRecipe?.AmountResult ?? 1, AmountCrafted = ingredientRecipe?.AmountResult ?? 1,
DependsOn = ingredientRecipe?.UnkData5.Take(8).Where(x => x != null && x.ItemIngredient != 0) DependsOn = ingredientRecipe?.Ingredient.Where(x => x.IsValid && IsValidItem(x.RowId))
.Select(x => (uint)x.ItemIngredient) .Select(x => x.RowId)
.ToList() .ToList()
?? new(), ?? new(),
}); });
@ -142,7 +157,7 @@ public sealed class RecipeTree
return ingredients; return ingredients;
} }
private List<RecipeInfo> ExtendWithAmountCrafted(List<Ingredient> materials) private List<RecipeInfo> ExtendWithAmountCrafted(IEnumerable<Ingredient> materials)
{ {
return materials.Select(x => new return materials.Select(x => new
{ {
@ -156,28 +171,33 @@ public sealed class RecipeTree
Name = x.Ingredient.Name, Name = x.Ingredient.Name,
TotalQuantity = x.Ingredient.TotalQuantity, TotalQuantity = x.Ingredient.TotalQuantity,
Type = _shopItemsOnly.Contains(x.Ingredient.ItemId) ? Ingredient.EType.ShopItem : x.Ingredient.Type, Type = _shopItemsOnly.Contains(x.Ingredient.ItemId) ? Ingredient.EType.ShopItem : x.Ingredient.Type,
AmountCrafted = x.Recipe!.AmountResult, AmountCrafted = x.Recipe!.Value.AmountResult,
DependsOn = x.Recipe.UnkData5.Take(8).Where(y => y != null && y.ItemIngredient != 0) DependsOn = x.Recipe.Value.Ingredient.Where(y => y.IsValid && IsValidItem(y.RowId))
.Select(y => (uint)y.ItemIngredient) .Select(y => y.RowId)
.ToList(), .ToList(),
}) })
.ToList(); .ToList();
} }
public Recipe? GetFirstRecipeForItem(uint itemId) private Recipe? GetFirstRecipeForItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<Recipe>()!.FirstOrDefault(x => x.RowId > 0 && x.ItemResult.Row == itemId); return _dataManager.GetExcelSheet<Recipe>().FirstOrDefault(x => x.RowId > 0 && x.ItemResult.RowId == itemId);
} }
public GatheringItem? GetGatheringItem(uint itemId) private GatheringItem? GetGatheringItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<GatheringItem>()!.FirstOrDefault(x => x.RowId > 0 && (uint)x.Item == itemId); return _dataManager.GetExcelSheet<GatheringItem>().FirstOrDefault(x => x.RowId > 0 && x.Item.RowId == itemId);
} }
public RetainerTaskNormal? GetVentureItem(uint itemId) private RetainerTaskNormal? GetVentureItem(uint itemId)
{ {
return _dataManager.GetExcelSheet<RetainerTaskNormal>()! return _dataManager.GetExcelSheet<RetainerTaskNormal>()
.FirstOrDefault(x => x.RowId > 0 && x.Item.Row == itemId); .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 private sealed class RecipeInfo : Ingredient

View File

@ -1,10 +0,0 @@
namespace Workshoppa.GameData.Shops;
internal sealed class ItemForSale
{
public required int Position { get; init; }
public required uint ItemId { get; init; }
public required string? ItemName { get; init; }
public required uint Price { get; init; }
public required uint OwnedItems { get; init; }
}

View File

@ -1,19 +0,0 @@
using System;
namespace Workshoppa.GameData.Shops;
internal sealed class PurchaseState
{
public PurchaseState(int desiredItems, int ownedItems)
{
DesiredItems = desiredItems;
OwnedItems = ownedItems;
}
public int DesiredItems { get; }
public int OwnedItems { get; set; }
public int ItemsLeftToBuy => Math.Max(0, DesiredItems - OwnedItems);
public bool IsComplete => ItemsLeftToBuy == 0;
public bool IsAwaitingYesNo { get; set; }
public DateTime NextStep { get; set; } = DateTime.MinValue;
}

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,41 +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(),
IconId = x.ResultItem.Value!.Icon, IconId = x.ResultItem.Value.Icon,
Category = (WorkshopCraftCategory)x.CompanyCraftDraftCategory.Row, Category = (WorkshopCraftCategory)x.CompanyCraftDraftCategory.RowId,
Type = x.CompanyCraftType.Row, Type = x.CompanyCraftType.RowId,
Phases = x.CompanyCraftPart.Where(part => part.Row != 0) 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].Icon, 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(),
}) })

View File

@ -5,10 +5,9 @@ using Dalamud.Interface.Components;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using LLib;
using LLib.GameUI; using LLib.GameUI;
using LLib.Shop.Model;
using Workshoppa.External; using Workshoppa.External;
using Workshoppa.GameData.Shops;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
@ -17,33 +16,39 @@ internal sealed class CeruleumTankWindow : ShopWindow
{ {
private const int CeruleumTankItemId = 10155; private const int CeruleumTankItemId = 10155;
private readonly WorkshopPlugin _plugin;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IChatGui _chatGui;
private int _companyCredits; private int _companyCredits;
private int _buyStackCount; private int _buyStackCount;
private bool _buyPartialStacks = true; private bool _buyPartialStacks = true;
public CeruleumTankWindow(WorkshopPlugin plugin, IPluginLog pluginLog, public CeruleumTankWindow(
IGameGui gameGui, IAddonLifecycle addonLifecycle, Configuration configuration, IPluginLog pluginLog,
ExternalPluginHandler externalPluginHandler) IGameGui gameGui,
: base("Ceruleum Tanks###WorkshoppaCeruleumTankWindow", "FreeCompanyCreditShop", plugin, pluginLog, gameGui, addonLifecycle, externalPluginHandler) IAddonLifecycle addonLifecycle,
Configuration configuration,
ExternalPluginHandler externalPluginHandler,
IChatGui chatGui)
: base("Ceruleum Tanks###WorkshoppaCeruleumTankWindow", "FreeCompanyCreditShop", pluginLog, gameGui,
addonLifecycle, externalPluginHandler)
{ {
_plugin = plugin;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_chatGui = chatGui;
_configuration = configuration; _configuration = configuration;
} }
protected override bool Enabled => _configuration.EnableCeruleumTankCalculator; public override bool IsEnabled => _configuration.EnableCeruleumTankCalculator;
protected override unsafe void UpdateShopStock(AtkUnitBase* addon) public override unsafe void UpdateShopStock(AtkUnitBase* addon)
{ {
if (addon->AtkValuesCount != 170) if (addon->AtkValuesCount != 170)
{ {
_pluginLog.Error($"Unexpected amount of atkvalues for FreeCompanyCreditShop addon ({addon->AtkValuesCount})"); _pluginLog.Error(
$"Unexpected amount of atkvalues for FreeCompanyCreditShop addon ({addon->AtkValuesCount})");
_companyCredits = 0; _companyCredits = 0;
ItemForSale = null; Shop.ItemForSale = null;
return; return;
} }
@ -53,10 +58,11 @@ internal sealed class CeruleumTankWindow : ShopWindow
uint itemCount = atkValues[9].UInt; uint itemCount = atkValues[9].UInt;
if (itemCount == 0) if (itemCount == 0)
{ {
ItemForSale = null; Shop.ItemForSale = null;
return; return;
} }
ItemForSale = Enumerable.Range(0, (int)itemCount)
Shop.ItemForSale = Enumerable.Range(0, (int)itemCount)
.Select(i => new ItemForSale .Select(i => new ItemForSale
{ {
Position = i, Position = i,
@ -68,18 +74,18 @@ internal sealed class CeruleumTankWindow : ShopWindow
.FirstOrDefault(x => x.ItemId == CeruleumTankItemId); .FirstOrDefault(x => x.ItemId == CeruleumTankItemId);
} }
protected override int GetCurrencyCount() => _companyCredits; public override int GetCurrencyCount() => _companyCredits;
public override void Draw() public override void Draw()
{ {
if (ItemForSale == null) if (Shop.ItemForSale == null)
{ {
IsOpen = false; IsOpen = false;
return; return;
} }
int ceruleumTanks = GetItemCount(CeruleumTankItemId); int ceruleumTanks = Shop.GetItemCount(CeruleumTankItemId);
int freeInventorySlots = _plugin.GetFreeInventorySlots(); int freeInventorySlots = Shop.CountFreeInventorySlots();
ImGui.Text("Inventory"); ImGui.Text("Inventory");
ImGui.Indent(); ImGui.Indent();
@ -89,7 +95,7 @@ internal sealed class CeruleumTankWindow : ShopWindow
ImGui.Separator(); ImGui.Separator();
if (PurchaseState == null) if (Shop.PurchaseState == null)
{ {
ImGui.SetNextItemWidth(100); ImGui.SetNextItemWidth(100);
ImGui.InputInt("Stacks to Buy", ref _buyStackCount); ImGui.InputInt("Stacks to Buy", ref _buyStackCount);
@ -103,33 +109,33 @@ internal sealed class CeruleumTankWindow : ShopWindow
if (_buyPartialStacks && ceruleumTanks % 999 > 0) if (_buyPartialStacks && ceruleumTanks % 999 > 0)
missingItems += (999 - ceruleumTanks % 999); missingItems += (999 - ceruleumTanks % 999);
if (PurchaseState != null) if (Shop.PurchaseState != null)
{ {
HandleNextPurchaseStep(); Shop.HandleNextPurchaseStep();
if (PurchaseState != null) if (Shop.PurchaseState != null)
{ {
ImGui.Text($"Buying {FormatStackCount(PurchaseState.ItemsLeftToBuy)}..."); ImGui.Text($"Buying {FormatStackCount(Shop.PurchaseState.ItemsLeftToBuy)}...");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
CancelAutoPurchase(); Shop.CancelAutoPurchase();
} }
} }
else else
{ {
int toPurchase = Math.Min(GetMaxItemsToPurchase(), missingItems); int toPurchase = Math.Min(Shop.GetMaxItemsToPurchase(), missingItems);
if (toPurchase > 0) if (toPurchase > 0)
{ {
ImGui.Spacing(); ImGui.Spacing();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign, if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign,
$"Auto-Buy {FormatStackCount(toPurchase)} for {ItemForSale.Price * toPurchase:N0} CC")) $"Auto-Buy {FormatStackCount(toPurchase)} for {Shop.ItemForSale.Price * toPurchase:N0} CC"))
{ {
StartAutoPurchase(toPurchase); Shop.StartAutoPurchase(toPurchase);
HandleNextPurchaseStep(); Shop.HandleNextPurchaseStep();
} }
} }
} }
} }
private string FormatStackCount(int ceruleumTanks) private static string FormatStackCount(int ceruleumTanks)
{ {
int fullStacks = ceruleumTanks / 999; int fullStacks = ceruleumTanks / 999;
int partials = ceruleumTanks % 999; int partials = ceruleumTanks % 999;
@ -139,14 +145,71 @@ internal sealed class CeruleumTankWindow : ShopWindow
return $"{fullStacks:N0} {stacks}"; return $"{fullStacks:N0} {stacks}";
} }
protected override unsafe void FirePurchaseCallback(AtkUnitBase* addonShop, int buyNow) public override unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
{ {
var buyItem = stackalloc AtkValue[] var buyItem = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 0 }, new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.UInt, UInt = (uint)ItemForSale!.Position }, new() { Type = ValueType.UInt, UInt = (uint)Shop.ItemForSale!.Position },
new() { Type = ValueType.UInt, UInt = (uint)buyNow }, new() { Type = ValueType.UInt, UInt = (uint)buyNow },
}; };
addonShop->FireCallback(3, buyItem); 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

@ -1,17 +1,16 @@
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib.ImGui;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
internal sealed class ConfigWindow : LImGui.LWindow internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration) public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration)
: base("Workshoppa - Configuration###WorkshoppaConfigWindow") : base("Workshoppa - Configuration###WorkshoppaConfigWindow")
{ {
@ -21,8 +20,15 @@ internal sealed class ConfigWindow : LImGui.LWindow
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver; PositionCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.AlwaysAutoResize; Flags = ImGuiWindowFlags.AlwaysAutoResize;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(270, 50),
};
} }
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw() public override void Draw()
{ {
bool enableRepairKitCalculator = _configuration.EnableRepairKitCalculator; bool enableRepairKitCalculator = _configuration.EnableRepairKitCalculator;
@ -39,4 +45,6 @@ internal sealed class ConfigWindow : LImGui.LWindow
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
} }
} }
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
} }

View File

@ -7,23 +7,24 @@ 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.Internal; 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;
using LLib.ImGui;
using Workshoppa.GameData; using Workshoppa.GameData;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
// FIXME The close button doesn't work near the workshop, either hide it or make it work // FIXME The close button doesn't work near the workshop, either hide it or make it work
internal sealed class MainWindow : LImGui.LWindow internal sealed class MainWindow : LWindow, IPersistableWindowConfig
{ {
private static readonly Regex CountAndName = new(@"^(\d{1,5})x?\s+(.*)$", RegexOptions.Compiled); 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;
@ -36,7 +37,7 @@ internal sealed class MainWindow : LImGui.LWindow
private bool _checkInventory; private bool _checkInventory;
private string _newPresetName = string.Empty; private string _newPresetName = string.Empty;
public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IClientState clientState, public MainWindow(WorkshopPlugin plugin, IDalamudPluginInterface pluginInterface, IClientState clientState,
Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui, Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui,
RecipeTree recipeTree, IPluginLog pluginLog) RecipeTree recipeTree, IPluginLog pluginLog)
: base("Workshoppa###WorkshoppaMainWindow") : base("Workshoppa###WorkshoppaMainWindow")
@ -60,7 +61,7 @@ internal sealed class MainWindow : LImGui.LWindow
MaximumSize = new Vector2(500, 9999), MaximumSize = new Vector2(500, 9999),
}; };
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.MenuBar; Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.MenuBar;
AllowClickthrough = false; AllowClickthrough = false;
} }
@ -69,7 +70,9 @@ internal sealed class MainWindow : LImGui.LWindow
public ButtonState State { get; set; } = ButtonState.None; public ButtonState State { get; set; } = ButtonState.None;
private 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()
{ {
@ -92,9 +95,9 @@ internal sealed class MainWindow : LImGui.LWindow
IDalamudTextureWrap? icon = _iconCache.GetIcon(currentCraft.IconId); IDalamudTextureWrap? icon = _iconCache.GetIcon(currentCraft.IconId);
if (icon != null) if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, 3); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() - ImGui.GetTextLineHeight()) / 2);
} }
ImGui.TextUnformatted($"{currentCraft.Name}"); ImGui.TextUnformatted($"{currentCraft.Name}");
@ -106,8 +109,7 @@ internal sealed class MainWindow : LImGui.LWindow
_checkInventory = !_checkInventory; _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"))
@ -128,7 +130,9 @@ internal sealed class MainWindow : LImGui.LWindow
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;
@ -138,10 +142,9 @@ internal sealed class MainWindow : LImGui.LWindow
} }
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();
} }
@ -194,11 +197,11 @@ internal sealed class MainWindow : LImGui.LWindow
IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId); IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId);
if (icon != null) if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, 3); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
} }
ImGui.SetNextItemWidth(100); 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))
{ {
@ -231,18 +234,18 @@ internal sealed class MainWindow : LImGui.LWindow
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))
{ {
IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId); IDalamudTextureWrap? icon = _iconCache.GetIcon(craft.IconId);
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
if (icon != null) if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X, ImGui.GetStyle().ItemSpacing.Y / 2));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
} }
if (ImGui.Selectable($"{craft.Name}##SelectCraft{craft.WorkshopItemId}")) if (ImGui.Selectable($"{craft.Name}##SelectCraft{craft.WorkshopItemId}", false, ImGuiSelectableFlags.SpanAllColumns))
{ {
_configuration.ItemQueue.Add(new Configuration.QueuedItem _configuration.ItemQueue.Add(new Configuration.QueuedItem
{ {
@ -251,6 +254,13 @@ internal sealed class MainWindow : LImGui.LWindow
}); });
Save(); Save();
} }
if (icon != null)
{
ImGui.SameLine(0, 0);
ImGui.SetCursorPos(pos);
ImGui.Image(icon.ImGuiHandle, iconSize);
}
} }
ImGui.EndCombo(); ImGui.EndCombo();
@ -319,7 +329,7 @@ internal sealed class MainWindow : LImGui.LWindow
ImGui.InputTextWithHint("", "Preset Name...", ref _newPresetName, 64); ImGui.InputTextWithHint("", "Preset Name...", ref _newPresetName, 64);
ImGui.BeginDisabled(_configuration.Presets.Any(x => ImGui.BeginDisabled(_configuration.Presets.Any(x =>
x.Name.Equals(_newPresetName, StringComparison.CurrentCultureIgnoreCase))); x.Name.Equals(_newPresetName, StringComparison.OrdinalIgnoreCase)));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save"))
{ {
_configuration.Presets.Add(new Configuration.Preset _configuration.Presets.Add(new Configuration.Preset
@ -400,7 +410,7 @@ internal sealed class MainWindow : LImGui.LWindow
continue; continue;
var craft = _workshopCache.Crafts.FirstOrDefault(x => var craft = _workshopCache.Crafts.FirstOrDefault(x =>
x.Name.Equals(match.Groups[2].Value, StringComparison.CurrentCultureIgnoreCase)); x.Name.Equals(match.Groups[2].Value, StringComparison.OrdinalIgnoreCase));
if (craft != null && int.TryParse(match.Groups[1].Value, out int quantity)) if (craft != null && int.TryParse(match.Groups[1].Value, out int quantity))
{ {
fromClipboardItems.Add(new Configuration.QueuedItem fromClipboardItems.Add(new Configuration.QueuedItem
@ -534,9 +544,11 @@ internal sealed class MainWindow : LImGui.LWindow
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId); IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId);
if (icon != null) if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, 3); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() - ImGui.GetTextLineHeight()) / 2);
icon.Dispose();
} }
ImGui.TextColored(inInventory >= item.TotalQuantity ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed, ImGui.TextColored(inInventory >= item.TotalQuantity ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
@ -590,7 +602,7 @@ internal sealed class MainWindow : LImGui.LWindow
.ToList(); .ToList();
} }
private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity) private static 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;
@ -603,12 +615,14 @@ internal sealed class MainWindow : LImGui.LWindow
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,

View File

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

View File

@ -1,198 +1,49 @@
using System; using System;
using System.Numerics; using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib.ImGui;
using LLib.GameUI; using LLib.Shop;
using Workshoppa.External; using Workshoppa.External;
using Workshoppa.GameData.Shops;
namespace Workshoppa.Windows; namespace Workshoppa.Windows;
internal abstract class ShopWindow : LImGui.LWindow, IDisposable internal abstract class ShopWindow : LWindow, IShopWindow, IDisposable
{ {
private readonly string _addonName;
private readonly WorkshopPlugin _plugin;
private readonly IPluginLog _pluginLog;
private readonly IGameGui _gameGui;
private readonly IAddonLifecycle _addonLifecycle;
private readonly ExternalPluginHandler _externalPluginHandler; private readonly ExternalPluginHandler _externalPluginHandler;
protected ItemForSale? ItemForSale; protected ShopWindow(
protected PurchaseState? PurchaseState; string windowName,
string addonName,
protected ShopWindow(string name, string addonName, WorkshopPlugin plugin, IPluginLog pluginLog, IPluginLog pluginLog,
IGameGui gameGui, IAddonLifecycle addonLifecycle, ExternalPluginHandler externalPluginHandler) IGameGui gameGui,
: base(name) IAddonLifecycle addonLifecycle,
ExternalPluginHandler externalPluginHandler)
: base(windowName)
{ {
_addonName = addonName;
_plugin = plugin;
_pluginLog = pluginLog;
_gameGui = gameGui;
_addonLifecycle = addonLifecycle;
_externalPluginHandler = externalPluginHandler; _externalPluginHandler = externalPluginHandler;
Shop = new RegularShopBase(this, addonName, pluginLog, gameGui, addonLifecycle);
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always; PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse; Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
} }
public bool AutoBuyEnabled => PurchaseState != null; public void Dispose() => Shop.Dispose();
protected abstract bool Enabled { get; }
public bool AutoBuyEnabled => Shop.AutoBuyEnabled;
public bool IsAwaitingYesNo public bool IsAwaitingYesNo
{ {
get => PurchaseState?.IsAwaitingYesNo ?? false; get { return Shop.IsAwaitingYesNo; }
set => PurchaseState!.IsAwaitingYesNo = value; set { Shop.IsAwaitingYesNo = value; }
} }
private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args) protected RegularShopBase Shop { get; }
{ public abstract bool IsEnabled { get; }
if (!Enabled) public abstract int GetCurrencyCount();
{ public abstract unsafe void UpdateShopStock(AtkUnitBase* addon);
ItemForSale = null; public abstract unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow);
IsOpen = false; public void SaveExternalPluginState() => _externalPluginHandler.Save();
return; public void RestoreExternalPluginState() => _externalPluginHandler.Restore();
}
UpdateShopStock((AtkUnitBase*)args.Addon);
PostUpdateShopStock();
if (ItemForSale != null)
IsOpen = true;
}
private void ShopPreFinalize(AddonEvent type, AddonArgs args)
{
PurchaseState = null;
_externalPluginHandler.Restore();
IsOpen = false;
}
private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args)
{
if (!Enabled)
{
ItemForSale = null;
IsOpen = false;
return;
}
UpdateShopStock((AtkUnitBase*)args.Addon);
PostUpdateShopStock();
if (ItemForSale != null)
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
short x = 0, y = 0;
addon->GetPosition(&x, &y);
short width = 0, height = 0;
addon->GetSize(&width, &height, true);
x += width;
if ((short)Position!.Value.X != x || (short)Position!.Value.Y != y)
Position = new Vector2(x, y);
IsOpen = true;
}
else
IsOpen = false;
}
protected abstract unsafe void UpdateShopStock(AtkUnitBase* addon);
private void PostUpdateShopStock()
{
if (ItemForSale != null && PurchaseState != null)
{
int ownedItems = (int)ItemForSale.OwnedItems;
if (PurchaseState.OwnedItems != ownedItems)
{
PurchaseState.OwnedItems = ownedItems;
PurchaseState.NextStep = DateTime.Now.AddSeconds(0.25);
}
}
}
protected unsafe int GetItemCount(uint itemId)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
return inventoryManager->GetInventoryItemCount(itemId, checkEquipped: false, checkArmory: false);
}
protected abstract int GetCurrencyCount();
protected int GetMaxItemsToPurchase()
{
if (ItemForSale == null)
return 0;
int currency = GetCurrencyCount();
return (int)(currency / ItemForSale!.Price);
}
protected void CancelAutoPurchase()
{
PurchaseState = null;
_externalPluginHandler.Restore();
}
protected void StartAutoPurchase(int toPurchase)
{
PurchaseState = new((int)ItemForSale!.OwnedItems + toPurchase, (int)ItemForSale.OwnedItems);
_externalPluginHandler.Save();
}
protected unsafe void HandleNextPurchaseStep()
{
if (ItemForSale == null || PurchaseState == null)
return;
int maxStackSize = _plugin.DetermineMaxStackSize(ItemForSale.ItemId);
if (maxStackSize == 0 && !_plugin.HasFreeInventorySlot())
{
_pluginLog.Warning($"No free inventory slots, can't buy more {ItemForSale.ItemName}");
PurchaseState = null;
_externalPluginHandler.Restore();
}
else if (!PurchaseState.IsComplete)
{
if (PurchaseState.NextStep <= DateTime.Now &&
_gameGui.TryGetAddonByName(_addonName, out AtkUnitBase* addonShop))
{
int buyNow = Math.Min(PurchaseState.ItemsLeftToBuy, maxStackSize);
_pluginLog.Information($"Buying {buyNow}x {ItemForSale.ItemName}");
FirePurchaseCallback(addonShop, buyNow);
PurchaseState.NextStep = DateTime.MaxValue;
PurchaseState.IsAwaitingYesNo = true;
}
}
else
{
_pluginLog.Information(
$"Stopping item purchase (desired = {PurchaseState.DesiredItems}, owned = {PurchaseState.OwnedItems})");
PurchaseState = null;
_externalPluginHandler.Restore();
}
}
protected abstract unsafe void FirePurchaseCallback(AtkUnitBase* addonShop, int buyNow);
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
}
} }

View File

@ -51,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");
@ -67,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;

View File

@ -9,7 +9,7 @@ namespace Workshoppa;
partial class WorkshopPlugin partial class WorkshopPlugin
{ {
private void InteractWithFabricationStation(GameObject fabricationStation) private void InteractWithFabricationStation(IGameObject fabricationStation)
=> InteractWithTarget(fabricationStation); => InteractWithTarget(fabricationStation);
private void TakeItemFromQueue() private void TakeItemFromQueue()
@ -119,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;
@ -17,7 +18,7 @@ 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}");
/* /*
@ -30,7 +31,7 @@ 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; Vector3? localPlayerPosition = _clientState.LocalPlayer?.Position;
if (localPlayerPosition != null) if (localPlayerPosition != null)
@ -39,7 +40,7 @@ partial class WorkshopPlugin
{ {
if (obj.ObjectKind == ObjectKind.EventObj) if (obj.ObjectKind == ObjectKind.EventObj)
{ {
if (npcIds.Contains(GetNpcId(obj))) if (npcIds.Contains(obj.DataId))
{ {
o = obj; o = obj;
float distance = Vector3.Distance(localPlayerPosition.Value, float distance = Vector3.Distance(localPlayerPosition.Value,
@ -55,12 +56,6 @@ 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 AtkUnitBase* GetCompanyCraftingLogAddon() private unsafe AtkUnitBase* GetCompanyCraftingLogAddon()
{ {
if (_gameGui.TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) && if (_gameGui.TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) &&
@ -79,7 +74,7 @@ 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;
@ -122,7 +117,9 @@ partial class WorkshopPlugin
LAddon.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}'");
@ -184,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 = atkValue.ReadAtkString(); 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);
} }
} }
@ -216,62 +218,11 @@ 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;
} }
} }
return false; return false;
} }
public bool HasFreeInventorySlot() => GetFreeInventorySlots() > 0;
public unsafe int GetFreeInventorySlots()
{
var inventoryManger = InventoryManager.Instance();
if (inventoryManger == null)
return 0;
int count = 0;
for (InventoryType t = InventoryType.Inventory1; t <= InventoryType.Inventory4; ++t)
{
var container = inventoryManger->GetInventoryContainer(t);
for (int i = 0; i < container->Size; ++i)
{
var item = container->GetInventorySlot(i);
if (item == null || item->ItemID == 0)
++count;
}
}
return count;
}
public unsafe int DetermineMaxStackSize(uint itemId)
{
var inventoryManger = InventoryManager.Instance();
if (inventoryManger == null)
return 0;
int max = 0;
for (InventoryType t = InventoryType.Inventory1; t <= InventoryType.Inventory4; ++t)
{
var container = inventoryManger->GetInventoryContainer(t);
for (int i = 0; i < container->Size; ++i)
{
var item = container->GetInventorySlot(i);
if (item == null || item->ItemID == 0)
return 99;
if (item->ItemID == itemId)
{
max += (999 - (int)item->Quantity);
if (max >= 99)
break;
}
}
}
return Math.Min(99, max);
}
} }

View File

@ -13,7 +13,9 @@ partial class WorkshopPlugin
_pluginLog.Verbose("SelectYesNo post-setup"); _pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon; AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", ""); string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString()
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
_pluginLog.Verbose($"YesNo prompt: '{text}'"); _pluginLog.Verbose($"YesNo prompt: '{text}'");
if (_repairKitWindow.IsOpen) if (_repairKitWindow.IsOpen)

View File

@ -8,6 +8,7 @@ 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 LLib;
using Workshoppa.External; using Workshoppa.External;
using Workshoppa.GameData; using Workshoppa.GameData;
@ -15,7 +16,7 @@ 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 = private readonly IReadOnlyList<uint> _fabricationStationIds =
@ -24,7 +25,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
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;
@ -49,7 +50,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
private DateTime _fallbackAt = DateTime.MaxValue; 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, IAddonLifecycle addonLifecycle, IChatGui chatGui, ICommandManager commandManager, IPluginLog pluginLog, IAddonLifecycle addonLifecycle, IChatGui chatGui,
ITextureProvider textureProvider) ITextureProvider textureProvider)
@ -71,15 +72,15 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_gameStrings = new(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); new IconCache(textureProvider), _chatGui, new RecipeTree(dataManager, _pluginLog), _pluginLog);
_windowSystem.AddWindow(_mainWindow); _windowSystem.AddWindow(_mainWindow);
_configWindow = new(_pluginInterface, _configuration); _configWindow = new(_pluginInterface, _configuration);
_windowSystem.AddWindow(_configWindow); _windowSystem.AddWindow(_configWindow);
_repairKitWindow = new(this, _pluginLog, _gameGui, addonLifecycle, _configuration, _repairKitWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler); _externalPluginHandler);
_windowSystem.AddWindow(_repairKitWindow); _windowSystem.AddWindow(_repairKitWindow);
_ceruleumTankWindow = new(this, _pluginLog, _gameGui, addonLifecycle, _configuration, _ceruleumTankWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler); _externalPluginHandler, _chatGui);
_windowSystem.AddWindow(_ceruleumTankWindow); _windowSystem.AddWindow(_ceruleumTankWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
@ -90,6 +91,18 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
{ {
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, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
@ -107,6 +120,11 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_pluginLog.Debug($"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;
} }
} }
@ -253,6 +271,22 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_mainWindow.Toggle(MainWindow.EOpenReason.Command); _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() private void OpenMainUi()
=> _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller); => _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller);
@ -262,6 +296,9 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh); _addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup); _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.OpenConfigUi -= _configWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;

View File

@ -1,68 +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>4.2</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>
<ProjectReference Include="..\LLib\LLib.csproj" /> <ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
</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

@ -1,15 +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": { "llib": {
"type": "Project" "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
} }