Clipboard export of craftable ingredient list/venture list

This commit is contained in:
Liza 2024-01-25 08:33:32 +01:00
parent 9bf055b78c
commit 451317f3d2
Signed by: liza
GPG Key ID: 7199F8D727D55F67
5 changed files with 262 additions and 30 deletions

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,190 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
namespace Workshoppa.GameData;
public 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.GetExcelSheet<GilShopItem>()!
.Where(x => shopVendorIds.Contains(x.RowId))
.Select(x => x.Item.Row)
.Where(x => x > 0)
.Distinct()
.ToList()
.AsReadOnly();
}
public List<Ingredient> ResolveRecipes(List<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();
// 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)
{
var craftable = completeList.Where(x =>
!sortedList.Contains(x) && x.DependsOn.All(y => sortedList.Any(z => y == z.ItemId)))
.ToList();
if (craftable.Count == 0)
throw new Exception("Unable to sort items");
sortedList.AddRange(craftable.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;
foreach (var ingredient in recipe.UnkData5.Take(8))
{
if (ingredient == null || ingredient.ItemIngredient == 0)
continue;
Item? item = _dataManager.GetExcelSheet<Item>()!.GetRow((uint)ingredient.ItemIngredient);
if (item == null)
continue;
Recipe? ingredientRecipe = GetFirstRecipeForItem((uint)ingredient.ItemIngredient);
_pluginLog.Information($"Adding {item.Name}");
ingredients.Add(new RecipeInfo
{
ItemId = (uint)ingredient.ItemIngredient,
Name = item.Name,
TotalQuantity = material.TotalQuantity * ingredient.AmountIngredient,
Type =
_shopItemsOnly.Contains((uint)ingredient.ItemIngredient) ? Ingredient.EType.ShopItem :
ingredientRecipe != null ? Ingredient.EType.Craftable :
GetGatheringItem((uint)ingredient.ItemIngredient) != null ? Ingredient.EType.Gatherable :
GetVentureItem((uint)ingredient.ItemIngredient) != null ? Ingredient.EType.Gatherable :
Ingredient.EType.Other,
AmountCrafted = ingredientRecipe?.AmountResult ?? 1,
DependsOn = ingredientRecipe?.UnkData5.Take(8).Where(x => x != null && x.ItemIngredient != 0)
.Select(x => (uint)x.ItemIngredient)
.ToList()
?? new(),
});
}
}
return ingredients;
}
private List<RecipeInfo> ExtendWithAmountCrafted(List<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!.AmountResult,
DependsOn = x.Recipe.UnkData5.Take(8).Where(y => y != null && y.ItemIngredient != 0)
.Select(y => (uint)y.ItemIngredient)
.ToList(),
})
.ToList();
}
public Recipe? GetFirstRecipeForItem(uint itemId)
{
return _dataManager.GetExcelSheet<Recipe>()!.FirstOrDefault(x => x.RowId > 0 && x.ItemResult.Row == itemId);
}
public GatheringItem? GetGatheringItem(uint itemId)
{
return _dataManager.GetExcelSheet<GatheringItem>()!.FirstOrDefault(x => x.RowId > 0 && (uint)x.Item == itemId);
}
public RetainerTaskNormal? GetVentureItem(uint itemId)
{
return _dataManager.GetExcelSheet<RetainerTaskNormal>()!
.FirstOrDefault(x => x.RowId > 0 && x.Item.Row == itemId);
}
private sealed class RecipeInfo : Ingredient
{
public required uint AmountCrafted { get; init; }
public required List<uint> DependsOn { get; init; }
}
}

View File

@ -29,6 +29,7 @@ internal sealed class MainWindow : LImGui.LWindow
private readonly WorkshopCache _workshopCache; private readonly WorkshopCache _workshopCache;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly RecipeTree _recipeTree;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private string _searchString = string.Empty; private string _searchString = string.Empty;
@ -37,7 +38,7 @@ internal sealed class MainWindow : LImGui.LWindow
public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IClientState clientState, public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IClientState clientState,
Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui, Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui,
IPluginLog pluginLog) RecipeTree recipeTree, IPluginLog pluginLog)
: base("Workshoppa###WorkshoppaMainWindow") : base("Workshoppa###WorkshoppaMainWindow")
{ {
_plugin = plugin; _plugin = plugin;
@ -47,6 +48,7 @@ internal sealed class MainWindow : LImGui.LWindow
_workshopCache = workshopCache; _workshopCache = workshopCache;
_iconCache = iconCache; _iconCache = iconCache;
_chatGui = chatGui; _chatGui = chatGui;
_recipeTree = recipeTree;
_pluginLog = pluginLog; _pluginLog = pluginLog;
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
@ -459,6 +461,22 @@ internal sealed class MainWindow : LImGui.LWindow
_chatGui.Print("Copied queue content to clipboard."); _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.EndDisabled();
ImGui.EndMenu(); ImGui.EndMenu();
@ -504,7 +522,32 @@ internal sealed class MainWindow : LImGui.LWindow
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(23, 23));
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
}
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();
@ -529,41 +572,22 @@ internal sealed class MainWindow : LImGui.LWindow
} }
} }
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, x.IconId }) .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.IconId, IconId = x.Key.IconId,
x.Key.Name, 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);
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine(0, 3);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
}
ImGui.TextColored(inInventory >= item.TotalQuantity ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
$"{item.Name} ({inInventory} / {item.TotalQuantity})");
}
ImGui.Unindent(20);
} }
private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity) private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity)

View File

@ -71,7 +71,7 @@ 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, _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);

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<Version>4.1</Version> <Version>4.2</Version>
<LangVersion>11.0</LangVersion> <LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>