forked from liza/Workshoppa
Clipboard export of craftable ingredient list/venture list
This commit is contained in:
parent
9bf055b78c
commit
451317f3d2
18
Workshoppa/GameData/Ingredient.cs
Normal file
18
Workshoppa/GameData/Ingredient.cs
Normal 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,
|
||||
}
|
||||
}
|
190
Workshoppa/GameData/RecipeTree.cs
Normal file
190
Workshoppa/GameData/RecipeTree.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ internal sealed class MainWindow : LImGui.LWindow
|
||||
private readonly WorkshopCache _workshopCache;
|
||||
private readonly IconCache _iconCache;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly RecipeTree _recipeTree;
|
||||
private readonly IPluginLog _pluginLog;
|
||||
|
||||
private string _searchString = string.Empty;
|
||||
@ -37,7 +38,7 @@ internal sealed class MainWindow : LImGui.LWindow
|
||||
|
||||
public MainWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IClientState clientState,
|
||||
Configuration configuration, WorkshopCache workshopCache, IconCache iconCache, IChatGui chatGui,
|
||||
IPluginLog pluginLog)
|
||||
RecipeTree recipeTree, IPluginLog pluginLog)
|
||||
: base("Workshoppa###WorkshoppaMainWindow")
|
||||
{
|
||||
_plugin = plugin;
|
||||
@ -47,6 +48,7 @@ internal sealed class MainWindow : LImGui.LWindow
|
||||
_workshopCache = workshopCache;
|
||||
_iconCache = iconCache;
|
||||
_chatGui = chatGui;
|
||||
_recipeTree = recipeTree;
|
||||
_pluginLog = pluginLog;
|
||||
|
||||
Position = new Vector2(100, 100);
|
||||
@ -459,6 +461,22 @@ internal sealed class MainWindow : LImGui.LWindow
|
||||
_chatGui.Print("Copied queue content to clipboard.");
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Export Material List to Clipboard"))
|
||||
{
|
||||
var toClipboardItems = _recipeTree.ResolveRecipes(GetMaterialList()).Where(x => x.Type == Ingredient.EType.Craftable);
|
||||
ImGui.SetClipboardText(string.Join(Environment.NewLine, toClipboardItems.Select(x => $"{x.TotalQuantity}x {x.Name}")));
|
||||
|
||||
_chatGui.Print("Copied material list to clipboard.");
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Export Gathered/Venture materials to Clipboard"))
|
||||
{
|
||||
var toClipboardItems = _recipeTree.ResolveRecipes(GetMaterialList()).Where(x => x.Type == Ingredient.EType.Gatherable);
|
||||
ImGui.SetClipboardText(string.Join(Environment.NewLine, toClipboardItems.Select(x => $"{x.TotalQuantity}x {x.Name}")));
|
||||
|
||||
_chatGui.Print("Copied material list to clipboard.");
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.EndMenu();
|
||||
@ -504,7 +522,32 @@ internal sealed class MainWindow : LImGui.LWindow
|
||||
private unsafe void CheckMaterial()
|
||||
{
|
||||
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
|
||||
.SelectMany(x => Enumerable.Range(0, x.Quantity).Select(_ => x.WorkshopItemId))
|
||||
.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.Items)
|
||||
.GroupBy(x => new { x.Name, x.ItemId, x.IconId })
|
||||
.OrderBy(x => x.Key.Name)
|
||||
.Select(x => new
|
||||
.Select(x => new Ingredient
|
||||
{
|
||||
x.Key.ItemId,
|
||||
x.Key.IconId,
|
||||
x.Key.Name,
|
||||
ItemId = x.Key.ItemId,
|
||||
IconId = x.Key.IconId,
|
||||
Name = x.Key.Name,
|
||||
TotalQuantity = completedForCurrentCraft.TryGetValue(x.Key.ItemId, out var completed)
|
||||
? x.Sum(y => y.TotalQuantity) - completed
|
||||
: x.Sum(y => y.TotalQuantity),
|
||||
});
|
||||
|
||||
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);
|
||||
Type = Ingredient.EType.Craftable,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void AddMaterial(Dictionary<uint, int> completedForCurrentCraft, uint itemId, int quantity)
|
||||
|
@ -71,7 +71,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
|
||||
_gameStrings = new(dataManager, _pluginLog);
|
||||
|
||||
_mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache,
|
||||
new IconCache(textureProvider), _chatGui, _pluginLog);
|
||||
new IconCache(textureProvider), _chatGui, new RecipeTree(dataManager, _pluginLog), _pluginLog);
|
||||
_windowSystem.AddWindow(_mainWindow);
|
||||
_configWindow = new(_pluginInterface, _configuration);
|
||||
_windowSystem.AddWindow(_configWindow);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Version>4.1</Version>
|
||||
<Version>4.2</Version>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
|
Loading…
Reference in New Issue
Block a user