Compare commits

..

No commits in common. "master" and "v3.0" have entirely different histories.
master ... v3.0

29 changed files with 557 additions and 2252 deletions

File diff suppressed because it is too large Load Diff

2
LLib

@ -1 +1 @@
Subproject commit e4bbc05ede6f6f01e7028b24614ed8cb333e909c
Subproject commit 7649b0d51b35c993839b918805718f046f06ae9b

View File

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

View File

@ -1,22 +1,27 @@
using System.Collections.Generic;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LLib;
namespace Workshoppa.External;
internal sealed class ExternalPluginHandler
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IPluginLog _pluginLog;
private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly PandoraIpc _pandoraIpc;
private bool? _yesAlreadyState;
private bool? _pandoraState;
public ExternalPluginHandler(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
public ExternalPluginHandler(DalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_pluginLog = pluginLog;
var dalamudReflector = new DalamudReflector(pluginInterface, framework, pluginLog);
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
_pandoraIpc = new PandoraIpc(pluginInterface, pluginLog);
}
@ -38,12 +43,8 @@ internal sealed class ExternalPluginHandler
private void SaveYesAlreadyState()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
!data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Disabling YesAlready");
data.Add(nameof(Workshoppa));
}
_yesAlreadyState = _yesAlreadyIpc.DisableIfNecessary();
_pluginLog.Information($"Previous yesalready state: {_yesAlreadyState}");
}
private void SavePandoraState()
@ -75,17 +76,15 @@ internal sealed class ExternalPluginHandler
}
Saved = false;
_yesAlreadyState = null;
_pandoraState = null;
}
private void RestoreYesAlready()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
data.Contains(nameof(Workshoppa)))
{
_pluginLog.Debug("Restoring YesAlready");
data.Remove(nameof(Workshoppa));
}
_pluginLog.Information($"Restoring previous yesalready state: {_yesAlreadyState}");
if (_yesAlreadyState == true)
_yesAlreadyIpc.Enable();
}
private void RestorePandora()

View File

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

53
Workshoppa/External/YesAlreadyIpc.cs vendored Normal file
View File

@ -0,0 +1,53 @@
using System.Reflection;
using LLib;
namespace Workshoppa.External;
internal sealed class YesAlreadyIpc
{
private readonly DalamudReflector _dalamudReflector;
public YesAlreadyIpc(DalamudReflector dalamudReflector)
{
_dalamudReflector = dalamudReflector;
}
private object? GetConfiguration()
{
if (_dalamudReflector.TryGetDalamudPlugin("Yes Already", out var plugin))
{
var pluginService = plugin!.GetType().Assembly.GetType("YesAlready.Service");
return pluginService!.GetProperty("Configuration", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null);
}
return null;
}
public bool? DisableIfNecessary()
{
object? configuration = GetConfiguration();
if (configuration == null)
return null;
var property = configuration.GetType().GetProperty("Enabled")!;
bool enabled = (bool)property.GetValue(configuration)!;
if (enabled)
{
property.SetValue(configuration, false);
return true;
}
return false;
}
public void Enable()
{
object? configuration = GetConfiguration();
if (configuration == null)
return;
var property = configuration.GetType().GetProperty("Enabled")!;
property.SetValue(configuration, true);
}
}

View File

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

View File

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

@ -1,18 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +1,133 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using LLib;
using LLib.GameUI;
using LLib.Shop.Model;
using Workshoppa.External;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa.Windows;
internal sealed class RepairKitWindow : ShopWindow
internal sealed class RepairKitWindow : Window, IDisposable
{
private const int DarkMatterCluster6ItemId = 10386;
private readonly WorkshopPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IPluginLog _pluginLog;
private readonly IGameGui _gameGui;
private readonly IAddonLifecycle _addonLifecycle;
private readonly Configuration _configuration;
private readonly ExternalPluginHandler _externalPluginHandler;
public RepairKitWindow(
IPluginLog pluginLog,
IGameGui gameGui,
IAddonLifecycle addonLifecycle,
Configuration configuration,
private ItemForSale? _itemForSale;
private PurchaseState? _purchaseState;
public RepairKitWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IPluginLog pluginLog,
IGameGui gameGui, IAddonLifecycle addonLifecycle, Configuration configuration,
ExternalPluginHandler externalPluginHandler)
: base("Repair Kits###WorkshoppaRepairKitWindow", "Shop", pluginLog, gameGui, addonLifecycle, externalPluginHandler)
: base("Repair Kits###WorkshoppaRepairKitWindow")
{
_plugin = plugin;
_pluginInterface = pluginInterface;
_pluginLog = pluginLog;
_gameGui = gameGui;
_addonLifecycle = addonLifecycle;
_configuration = configuration;
_externalPluginHandler = externalPluginHandler;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Shop", ShopPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "Shop", ShopPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "Shop", ShopPostUpdate);
}
public override bool IsEnabled => _configuration.EnableRepairKitCalculator;
public bool AutoBuyEnabled => _purchaseState != null;
public override unsafe void UpdateShopStock(AtkUnitBase* addon)
public bool IsAwaitingYesNo
{
get => _purchaseState?.IsAwaitingYesNo ?? false;
set => _purchaseState!.IsAwaitingYesNo = value;
}
private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args)
{
if (!_configuration.EnableRepairKitCalculator)
{
_itemForSale = null;
IsOpen = false;
return;
}
UpdateShopStock((AtkUnitBase*)args.Addon);
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 (!_configuration.EnableRepairKitCalculator)
{
_itemForSale = null;
IsOpen = false;
return;
}
UpdateShopStock((AtkUnitBase*)args.Addon);
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;
}
private unsafe void UpdateShopStock(AtkUnitBase* addon)
{
if (GetDarkMatterClusterCount() == 0)
{
Shop.ItemForSale = null;
_itemForSale = null;
return;
}
if (addon->AtkValuesCount != 625)
{
_pluginLog.Error($"Unexpected amount of atkvalues for Shop addon ({addon->AtkValuesCount})");
Shop.ItemForSale = null;
_itemForSale = null;
return;
}
@ -55,18 +136,18 @@ internal sealed class RepairKitWindow : ShopWindow
// Check if on 'Current Stock' tab?
if (atkValues[0].UInt != 0)
{
Shop.ItemForSale = null;
_itemForSale = null;
return;
}
uint itemCount = atkValues[2].UInt;
if (itemCount == 0)
{
Shop.ItemForSale = null;
_itemForSale = null;
return;
}
Shop.ItemForSale = Enumerable.Range(0, (int)itemCount)
_itemForSale = Enumerable.Range(0, (int)itemCount)
.Select(i => new ItemForSale
{
Position = i,
@ -76,64 +157,154 @@ internal sealed class RepairKitWindow : ShopWindow
ItemId = atkValues[441 + i].UInt,
})
.FirstOrDefault(x => x.ItemId == DarkMatterCluster6ItemId);
if (_itemForSale != null && _purchaseState != null)
{
int ownedItems = (int)_itemForSale.OwnedItems;
if (_purchaseState.OwnedItems != ownedItems)
{
_purchaseState.OwnedItems = ownedItems;
_purchaseState.NextStep = DateTime.Now.AddSeconds(0.25);
}
}
}
private int GetDarkMatterClusterCount() => Shop.GetItemCount(10335);
private int GetDarkMatterClusterCount() => GetItemCount(10335);
public override int GetCurrencyCount() => Shop.GetItemCount(1);
private int GetGil() => GetItemCount(1);
private unsafe int GetItemCount(uint itemId)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
return inventoryManager->GetInventoryItemCount(itemId, checkEquipped: false, checkArmory: false);
}
private int GetMaxItemsToPurchase()
{
if (_itemForSale == null)
return 0;
int gil = GetGil();
return (int)(gil / _itemForSale!.Price);
}
public override void Draw()
{
int darkMatterClusters = GetDarkMatterClusterCount();
if (Shop.ItemForSale == null || darkMatterClusters == 0)
if (_itemForSale == null || darkMatterClusters == 0)
{
IsOpen = false;
return;
}
LImGui.AddPatreonIcon(_pluginInterface);
ImGui.Text("Inventory");
ImGui.Indent();
ImGui.Text($"Dark Matter Clusters: {darkMatterClusters:N0}");
ImGui.Text($"Grade 6 Dark Matter: {Shop.ItemForSale.OwnedItems:N0}");
ImGui.Text($"Grade 6 Dark Matter: {_itemForSale.OwnedItems:N0}");
ImGui.Unindent();
int missingItems = Math.Max(0, darkMatterClusters * 5 - (int)Shop.ItemForSale.OwnedItems);
int missingItems = Math.Max(0, darkMatterClusters * 5 - (int)_itemForSale.OwnedItems);
ImGui.TextColored(missingItems == 0 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed,
$"Missing Grade 6 Dark Matter: {missingItems:N0}");
if (Shop.PurchaseState != null)
if (_purchaseState != null)
{
Shop.HandleNextPurchaseStep();
if (Shop.PurchaseState != null)
HandleNextPurchaseStep();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
Shop.CancelAutoPurchase();
_purchaseState = null;
_externalPluginHandler.Restore();
}
}
else
{
int toPurchase = Math.Min(Shop.GetMaxItemsToPurchase(), missingItems);
int toPurchase = Math.Min(GetMaxItemsToPurchase(), missingItems);
if (toPurchase > 0)
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign,
$"Auto-Buy missing Dark Matter for {Shop.ItemForSale.Price * toPurchase:N0}{SeIconChar.Gil.ToIconString()}"))
$"Auto-Buy missing Dark Matter for {_itemForSale.Price * toPurchase:N0}{SeIconChar.Gil.ToIconString()}"))
{
Shop.StartAutoPurchase(toPurchase);
Shop.HandleNextPurchaseStep();
_purchaseState = new((int)_itemForSale.OwnedItems + toPurchase, (int)_itemForSale.OwnedItems);
_externalPluginHandler.Save();
HandleNextPurchaseStep();
}
}
}
}
public override unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
private unsafe void HandleNextPurchaseStep()
{
var buyItem = stackalloc AtkValue[]
if (_itemForSale == null || _purchaseState == null)
return;
if (!_plugin.HasFreeInventorySlot())
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.Int, Int = Shop.ItemForSale!.Position },
new() { Type = ValueType.Int, Int = buyNow },
new() { Type = 0, Int = 0 }
};
addonShop->FireCallback(4, buyItem);
_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("Shop", out AtkUnitBase* addonShop))
{
int buyNow = Math.Min(_purchaseState.ItemsLeftToBuy, 99);
_pluginLog.Information($"Buying {buyNow}x {_itemForSale.ItemName}");
var buyItem = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
new() { Type = ValueType.Int, Int = _itemForSale.Position },
new() { Type = ValueType.Int, Int = buyNow },
new() { Type = 0, Int = 0 }
};
addonShop->FireCallback(4, buyItem);
_purchaseState.NextStep = DateTime.MaxValue;
_purchaseState.IsAwaitingYesNo = true;
}
}
else
{
_pluginLog.Information(
$"Stopping item purchase (desired = {_purchaseState.DesiredItems}, owned = {_purchaseState.OwnedItems})");
_purchaseState = null;
_externalPluginHandler.Restore();
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Shop", ShopPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "Shop", ShopPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "PostUpdate", ShopPostUpdate);
}
private 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; }
}
private 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,49 +0,0 @@
using System;
using System.Numerics;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using LLib.ImGui;
using LLib.Shop;
using Workshoppa.External;
namespace Workshoppa.Windows;
internal abstract class ShopWindow : LWindow, IShopWindow, IDisposable
{
private readonly ExternalPluginHandler _externalPluginHandler;
protected ShopWindow(
string windowName,
string addonName,
IPluginLog pluginLog,
IGameGui gameGui,
IAddonLifecycle addonLifecycle,
ExternalPluginHandler externalPluginHandler)
: base(windowName)
{
_externalPluginHandler = externalPluginHandler;
Shop = new RegularShopBase(this, addonName, pluginLog, gameGui, addonLifecycle);
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse;
}
public void Dispose() => Shop.Dispose();
public bool AutoBuyEnabled => Shop.AutoBuyEnabled;
public bool IsAwaitingYesNo
{
get { return Shop.IsAwaitingYesNo; }
set { Shop.IsAwaitingYesNo = value; }
}
protected RegularShopBase Shop { get; }
public abstract bool IsEnabled { get; }
public abstract int GetCurrencyCount();
public abstract unsafe void UpdateShopStock(AtkUnitBase* addon);
public abstract unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow);
public void SaveExternalPluginState() => _externalPluginHandler.Save();
public void RestoreExternalPluginState() => _externalPluginHandler.Restore();
}

View File

@ -2,7 +2,6 @@
using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Workshoppa.GameData;
@ -51,12 +50,12 @@ partial class WorkshopPlugin
private void SelectCraftBranch()
{
if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials.", StringComparison.Ordinal)))
if (SelectSelectString("contrib", 0, s => s.StartsWith("Contribute materials.")))
{
CurrentStage = Stage.ContributeMaterials;
_continueAt = DateTime.Now.AddSeconds(1);
}
else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production.", StringComparison.Ordinal)))
else if (SelectSelectString("advance", 0, s => s.StartsWith("Advance to the next phase of production.")))
{
_pluginLog.Information("Phase is complete");
@ -67,7 +66,7 @@ partial class WorkshopPlugin
CurrentStage = Stage.TargetFabricationStation;
_continueAt = DateTime.Now.AddSeconds(3);
}
else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of", StringComparison.Ordinal)))
else if (SelectSelectString("complete", 0, s => s.StartsWith("Complete the construction of")))
{
_pluginLog.Information("Item is almost complete, confirming last cutscene");
CurrentStage = Stage.TargetFabricationStation;
@ -111,22 +110,6 @@ partial class WorkshopPlugin
{
_pluginLog.Error(
$"Can't contribute item {item.ItemId} to craft, couldn't find {item.ItemCountPerStep}x in a single inventory slot");
InventoryManager* inventoryManager = InventoryManager.Instance();
int itemCount = 0;
if (inventoryManager != null)
{
itemCount = inventoryManager->GetInventoryItemCount(item.ItemId, true, false, false) +
inventoryManager->GetInventoryItemCount(item.ItemId, false, false, false);
}
if (itemCount < item.ItemCountPerStep)
_chatGui.PrintError(
$"[Workshoppa] You don't have the needed {item.ItemCountPerStep}x {item.ItemName} to continue.");
else
_chatGui.PrintError(
$"[Workshoppa] You don't have {item.ItemCountPerStep}x {item.ItemName} in a single stack, you need to merge the items in your inventory manually to continue.");
CurrentStage = Stage.RequestStop;
break;
}

View File

@ -9,7 +9,7 @@ namespace Workshoppa;
partial class WorkshopPlugin
{
private void InteractWithFabricationStation(IGameObject fabricationStation)
private void InteractWithFabricationStation(GameObject fabricationStation)
=> InteractWithTarget(fabricationStation);
private void TakeItemFromQueue()
@ -45,12 +45,6 @@ partial class WorkshopPlugin
CurrentStage = Stage.TargetFabricationStation;
}
private void OpenCraftingLog()
{
if (SelectSelectString("craftlog", 0, s => s == _gameStrings.ViewCraftingLog))
CurrentStage = Stage.SelectCraftCategory;
}
private unsafe void SelectCraftCategory()
{
AtkUnitBase* addonCraftingLog = GetCompanyCraftingLogAddon();
@ -119,7 +113,7 @@ partial class WorkshopPlugin
private void ConfirmCraft()
{
if (SelectSelectYesno(0, s => s.StartsWith("Craft ", StringComparison.Ordinal)))
if (SelectSelectYesno(0, s => s.StartsWith("Craft ")))
{
_configuration.CurrentlyCraftedItem!.StartedCrafting = true;
_pluginInterface.SavePluginConfig(_configuration);

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
@ -18,7 +17,7 @@ namespace Workshoppa;
partial class WorkshopPlugin
{
private unsafe void InteractWithTarget(IGameObject obj)
private unsafe void InteractWithTarget(GameObject obj)
{
_pluginLog.Information($"Setting target to {obj}");
/*
@ -31,7 +30,7 @@ partial class WorkshopPlugin
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false);
}
private float GetDistanceToEventObject(IReadOnlyList<uint> npcIds, out IGameObject? o)
private float GetDistanceToEventObject(IReadOnlyList<uint> npcIds, out GameObject? o)
{
Vector3? localPlayerPosition = _clientState.LocalPlayer?.Position;
if (localPlayerPosition != null)
@ -40,11 +39,10 @@ partial class WorkshopPlugin
{
if (obj.ObjectKind == ObjectKind.EventObj)
{
if (npcIds.Contains(obj.DataId))
if (npcIds.Contains(GetNpcId(obj)))
{
o = obj;
float distance = Vector3.Distance(localPlayerPosition.Value,
obj.Position + new Vector3(0, -2, 0));
float distance = Vector3.Distance(localPlayerPosition.Value, obj.Position + new Vector3(0, -2, 0));
if (distance > 0.01)
return distance;
}
@ -56,10 +54,15 @@ partial class WorkshopPlugin
return float.MaxValue;
}
private unsafe uint GetNpcId(GameObject obj)
{
return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address)->GetNpcID();
}
private unsafe AtkUnitBase* GetCompanyCraftingLogAddon()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) &&
LAddon.IsAddonReady(addon))
if (_gameGui.TryGetAddonByName<AtkUnitBase>("CompanyCraftRecipeNoteBook", out var addon) && LAddon.IsAddonReady(addon))
return addon;
return null;
@ -74,7 +77,7 @@ partial class WorkshopPlugin
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.CompanyCraftMaterial);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonId();
var addonId = agentInterface->GetAddonID();
if (addonId == 0)
return null;
@ -117,9 +120,7 @@ partial class WorkshopPlugin
LAddon.IsAddonReady(&addonSelectYesno->AtkUnitBase))
{
var text = MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString();
text = text
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
text = text.Replace("\n", "").Replace("\r", "");
if (predicate(text))
{
_pluginLog.Information($"Selecting choice {choice} for '{text}'");
@ -181,22 +182,17 @@ partial class WorkshopPlugin
return null;
}
private static uint ParseAtkItemCountHq(AtkValue atkValue)
private uint ParseAtkItemCountHq(AtkValue atkValue)
{
// NQ / HQ string
// I have no clue, but it doesn't seme like the available HQ item count is strored anywhere in the atkvalues??
string? s = atkValue.ReadAtkString();
if (s != null)
{
var parts = s.Replace("\ue03c", "", StringComparison.Ordinal).Split('/');
var parts = s.Replace("\ue03c", "").Split('/');
if (parts.Length > 1)
{
return uint.Parse(
parts[1]
.Replace(",", "", StringComparison.Ordinal)
.Replace(".", "", StringComparison.Ordinal)
.Trim(),
CultureInfo.InvariantCulture);
return uint.Parse(parts[1].Replace(",", "").Replace(".", "").Trim());
}
}
@ -218,7 +214,27 @@ partial class WorkshopPlugin
if (item == null)
continue;
if (item->ItemId == itemId && item->Quantity >= count)
if (item->ItemID == itemId && item->Quantity >= count)
return true;
}
}
return false;
}
public unsafe bool HasFreeInventorySlot()
{
var inventoryManger = InventoryManager.Instance();
if (inventoryManger == null)
return false;
for (InventoryType t = InventoryType.Inventory1; t <= InventoryType.Inventory4; ++t)
{
var container = inventoryManger->GetInventoryContainer(t);
for (int i = 0; i < container->Size; ++i)
{
var item = container->GetInventorySlot(i);
if (item == null || item->ItemID == 0)
return true;
}
}

View File

@ -0,0 +1,63 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Workshoppa;
partial class WorkshopPlugin
{
private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Verbose("SelectString post-setup");
string desiredText;
Action followUp;
if (CurrentStage == Stage.OpenCraftingLog)
{
desiredText = _gameStrings.ViewCraftingLog;
followUp = OpenCraftingLogFollowUp;
}
else
return;
_pluginLog.Verbose($"Looking for '{desiredText}' in prompt");
AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount;
for (int i = 0; i < entries; ++i)
{
var textPointer = addonSelectString->PopupMenu.PopupMenu.EntryNames[i];
if (textPointer == null)
continue;
var text = MemoryHelper.ReadSeStringNullTerminated((nint)textPointer).ToString();
_pluginLog.Verbose($" Choice {i} → {text}");
if (text == desiredText)
{
_pluginLog.Information($"Selecting choice {i} ({text})");
addonSelectString->AtkUnitBase.FireCallbackInt(i);
followUp();
return;
}
}
_pluginLog.Verbose($"Text '{desiredText}' was not found in prompt.");
}
private void OpenCraftingLogFollowUp()
{
CurrentStage = Stage.SelectCraftCategory;
}
private void ConfirmCollectProductFollowUp()
{
_configuration.CurrentlyCraftedItem = null;
_pluginInterface.SavePluginConfig(_configuration);
CurrentStage = Stage.TakeItemFromQueue;
_continueAt = DateTime.Now.AddSeconds(0.5);
}
}

View File

@ -13,15 +13,13 @@ partial class WorkshopPlugin
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString()
.Replace("\n", "", StringComparison.Ordinal)
.Replace("\r", "", StringComparison.Ordinal);
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", "");
_pluginLog.Verbose($"YesNo prompt: '{text}'");
if (_repairKitWindow.IsOpen)
{
_pluginLog.Verbose($"Checking for Repair Kit YesNo ({_repairKitWindow.AutoBuyEnabled}, {_repairKitWindow.IsAwaitingYesNo})");
if (_repairKitWindow.AutoBuyEnabled && _repairKitWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForGil.IsMatch(text))
if (_repairKitWindow.AutoBuyEnabled && _repairKitWindow.IsAwaitingYesNo && _gameStrings.PurchaseItem.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
_repairKitWindow.IsAwaitingYesNo = false;
@ -32,20 +30,6 @@ partial class WorkshopPlugin
_pluginLog.Verbose("Not a purchase confirmation match");
}
}
else if (_ceruleumTankWindow.IsOpen)
{
_pluginLog.Verbose($"Checking for Ceruleum Tank YesNo ({_ceruleumTankWindow.AutoBuyEnabled}, {_ceruleumTankWindow.IsAwaitingYesNo})");
if (_ceruleumTankWindow.AutoBuyEnabled && _ceruleumTankWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForCompanyCredits.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
_ceruleumTankWindow.IsAwaitingYesNo = false;
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
else
{
_pluginLog.Verbose("Not a purchase confirmation match");
}
}
else if (CurrentStage != Stage.Stopped)
{
if (CurrentStage == Stage.ConfirmMaterialDelivery && _gameStrings.TurnInHighQualityItem == text)
@ -69,13 +53,4 @@ partial class WorkshopPlugin
}
}
}
private void ConfirmCollectProductFollowUp()
{
_configuration.CurrentlyCraftedItem = null;
_pluginInterface.SavePluginConfig(_configuration);
CurrentStage = Stage.TakeItemFromQueue;
_continueAt = DateTime.Now.AddSeconds(0.5);
}
}

View File

@ -8,7 +8,6 @@ using Dalamud.Game.Command;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using LLib;
using Workshoppa.External;
using Workshoppa.GameData;
@ -16,16 +15,14 @@ using Workshoppa.Windows;
namespace Workshoppa;
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
[SuppressMessage("ReSharper", "UnusedType.Global")]
public sealed partial class WorkshopPlugin : IDalamudPlugin
{
private readonly IReadOnlyList<uint> _fabricationStationIds =
new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly();
private readonly IReadOnlyList<uint> FabricationStationIds = new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly();
internal readonly IReadOnlyList<ushort> WorkshopTerritories = new ushort[] { 423, 424, 425, 653, 984 }.AsReadOnly();
private readonly WindowSystem _windowSystem = new WindowSystem(nameof(WorkshopPlugin));
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly ICondition _condition;
@ -34,7 +31,6 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IChatGui _chatGui;
private readonly Configuration _configuration;
private readonly ExternalPluginHandler _externalPluginHandler;
@ -44,16 +40,14 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private readonly MainWindow _mainWindow;
private readonly ConfigWindow _configWindow;
private readonly RepairKitWindow _repairKitWindow;
private readonly CeruleumTankWindow _ceruleumTankWindow;
private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue;
private DateTime _fallbackAt = DateTime.MaxValue;
public WorkshopPlugin(IDalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework,
public WorkshopPlugin(DalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework,
ICondition condition, IClientState clientState, IObjectTable objectTable, IDataManager dataManager,
ICommandManager commandManager, IPluginLog pluginLog, IAddonLifecycle addonLifecycle, IChatGui chatGui,
ITextureProvider textureProvider)
ICommandManager commandManager, IPluginLog pluginLog, IAddonLifecycle addonLifecycle)
{
_pluginInterface = pluginInterface;
_gameGui = gameGui;
@ -64,24 +58,18 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_commandManager = commandManager;
_pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_chatGui = chatGui;
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _framework, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_workshopCache = new WorkshopCache(dataManager, _pluginLog);
_gameStrings = new(dataManager, _pluginLog);
_mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache,
new IconCache(textureProvider), _chatGui, new RecipeTree(dataManager, _pluginLog), _pluginLog);
_mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache);
_windowSystem.AddWindow(_mainWindow);
_configWindow = new(_pluginInterface, _configuration);
_windowSystem.AddWindow(_configWindow);
_repairKitWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler);
_repairKitWindow = new(this, _pluginInterface, _pluginLog, _gameGui, addonLifecycle, _configuration, _externalPluginHandler);
_windowSystem.AddWindow(_repairKitWindow);
_ceruleumTankWindow = new(_pluginLog, _gameGui, addonLifecycle, _configuration,
_externalPluginHandler, _chatGui);
_windowSystem.AddWindow(_ceruleumTankWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += OpenMainUi;
@ -91,19 +79,8 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
{
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, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh);
@ -120,11 +97,6 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_pluginLog.Debug($"Changing stage from {_currentStageInternal} to {value}");
_currentStageInternal = value;
}
if (value != Stage.Stopped)
_mainWindow.Flags |= ImGuiWindowFlags.NoCollapse;
else
_mainWindow.Flags &= ~ImGuiWindowFlags.NoCollapse;
}
}
@ -135,7 +107,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_condition[ConditionFlag.BoundByDuty] ||
_condition[ConditionFlag.BetweenAreas] ||
_condition[ConditionFlag.BetweenAreas51] ||
GetDistanceToEventObject(_fabricationStationIds, out var fabricationStation) >= 3f)
GetDistanceToEventObject(FabricationStationIds, out var fabricationStation) >= 3f)
{
_mainWindow.NearFabricationStation = false;
@ -168,8 +140,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
return;
}
else if (_mainWindow.State is MainWindow.ButtonState.Start or MainWindow.ButtonState.Resume &&
CurrentStage == Stage.Stopped)
else if (_mainWindow.State is MainWindow.ButtonState.Start or MainWindow.ButtonState.Resume && CurrentStage == Stage.Stopped)
{
// TODO Error checking, we should ensure the player has the required job level for *all* crafting parts
_mainWindow.State = MainWindow.ButtonState.None;
@ -189,17 +160,17 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
break;
case Stage.TargetFabricationStation:
if (_configuration.CurrentlyCraftedItem is { StartedCrafting: true })
CurrentStage = Stage.SelectCraftBranch;
else
CurrentStage = Stage.OpenCraftingLog;
if (_configuration.CurrentlyCraftedItem is { StartedCrafting: true })
CurrentStage = Stage.SelectCraftBranch;
else
CurrentStage = Stage.OpenCraftingLog;
InteractWithFabricationStation(fabricationStation!);
InteractWithFabricationStation(fabricationStation!);
break;
case Stage.OpenCraftingLog:
OpenCraftingLog();
// see SelectStringPostSetup
break;
case Stage.SelectCraftCategory:
@ -244,7 +215,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
break;
case Stage.ConfirmCollectProduct:
// see SelectYesNoPostSetup
// see SelectStringPostSetup
break;
case Stage.Stopped:
@ -259,8 +230,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private WorkshopCraft GetCurrentCraft()
{
return _workshopCache.Crafts.Single(
x => x.WorkshopItemId == _configuration.CurrentlyCraftedItem!.WorkshopItemId);
return _workshopCache.Crafts.Single(x => x.WorkshopItemId == _configuration.CurrentlyCraftedItem!.WorkshopItemId);
}
private void ProcessCommand(string command, string arguments)
@ -271,22 +241,6 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_mainWindow.Toggle(MainWindow.EOpenReason.Command);
}
private void ProcessBuyCommand(string command, string arguments)
{
if (_ceruleumTankWindow.TryParseBuyRequest(arguments, out int missingQuantity))
_ceruleumTankWindow.StartPurchase(missingQuantity);
else
_chatGui.PrintError($"Usage: {command} <stacks>");
}
private void ProcessFillCommand(string command, string arguments)
{
if (_ceruleumTankWindow.TryParseFillRequest(arguments, out int missingQuantity))
_ceruleumTankWindow.StartPurchase(missingQuantity);
else
_chatGui.PrintError($"Usage: {command} <stacks>");
}
private void OpenMainUi()
=> _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller);
@ -296,16 +250,13 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "Request", RequestPostRefresh);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Request", RequestPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_commandManager.RemoveHandler("/fill-tanks");
_commandManager.RemoveHandler("/buy-tanks");
_commandManager.RemoveHandler("/workshoppa");
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_commandManager.RemoveHandler("/ws");
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.OpenMainUi -= OpenMainUi;
_framework.Update -= FrameworkUpdate;
_ceruleumTankWindow.Dispose();
_repairKitWindow.Dispose();
_externalPluginHandler.RestoreTextAdvance();

View File

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

View File

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

View File

@ -1,86 +1,15 @@
{
"version": 1,
"dependencies": {
"net8.0-windows7.0": {
"net7.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[11.0.0, )",
"resolved": "11.0.0",
"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"
}
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"llib": {
"type": "Project",
"dependencies": {
"DalamudPackager": "[11.0.0, )"
}
"type": "Project"
}
}
}

View File

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