Experimental Repair Kit calculator

This commit is contained in:
Liza 2023-10-13 22:08:22 +02:00
parent 2418750b06
commit f3a9ebba1a
Signed by: liza
GPG Key ID: 7199F8D727D55F67
9 changed files with 443 additions and 8 deletions

2
LLib

@ -1 +1 @@
Subproject commit abbbec4f26b1a8903b0cd7aa04f00d557602eaf3 Subproject commit e59d291f04473eae0b76712397733e2e25349953

View File

@ -10,8 +10,9 @@ internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public CurrentItem? CurrentlyCraftedItem = null; public CurrentItem? CurrentlyCraftedItem { get; set; } = null;
public List<QueuedItem> ItemQueue = new(); public List<QueuedItem> ItemQueue { get; set; } = new();
public bool EnableRepairKitCalculator { get; set; } = true;
internal sealed class QueuedItem internal sealed class QueuedItem
{ {

View File

@ -0,0 +1,18 @@
using System;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using LLib;
using Lumina.Excel.GeneratedSheets;
namespace Workshoppa.GameData;
internal sealed class GameStrings
{
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
PurchaseItem = dataManager.GetRegex<Addon>(3406, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(PurchaseItem)}");
}
public Regex PurchaseItem { get; }
}

View File

@ -0,0 +1,34 @@
using System.Numerics;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using ImGuiNET;
namespace Workshoppa.Windows;
internal sealed class ConfigWindow : Window
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration)
: base("Workshoppa - Configuration###WorkshoppaConfigWindow")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.AlwaysAutoResize;
}
public override void Draw()
{
bool enableRepairKitCalculator = _configuration.EnableRepairKitCalculator;
if (ImGui.Checkbox("Enable Repair Kit Calculator", ref enableRepairKitCalculator))
{
_configuration.EnableRepairKitCalculator = enableRepairKitCalculator;
_pluginInterface.SavePluginConfig(_configuration);
}
}
}

View File

@ -0,0 +1,300 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
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 ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Workshoppa.Windows;
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 ItemForSale? _itemForSale;
private PurchaseState? _purchaseState;
public RepairKitWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle, Configuration configuration)
: base("Repair Kits###WorkshoppaRepairKitWindow")
{
_plugin = plugin;
_pluginInterface = pluginInterface;
_pluginLog = pluginLog;
_gameGui = gameGui;
_addonLifecycle = addonLifecycle;
_configuration = configuration;
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 bool AutoBuyEnabled => _purchaseState != null;
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;
_plugin.RestoreYesAlready();
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)
{
_itemForSale = null;
return;
}
if (addon->AtkValuesCount != 625)
{
_pluginLog.Error($"Unexpected amount of atkvalues for Shop addon ({addon->AtkValuesCount})");
_itemForSale = null;
return;
}
var atkValues = addon->AtkValues;
// Check if on 'Current Stock' tab?
if (atkValues[0].UInt != 0)
{
_itemForSale = null;
return;
}
uint itemCount = atkValues[2].UInt;
if (itemCount == 0)
{
_itemForSale = null;
return;
}
_itemForSale = Enumerable.Range(0, (int)itemCount)
.Select(i => new ItemForSale
{
Position = i,
ItemName = atkValues[14 + i].ReadAtkString(),
Price = atkValues[75 + i].UInt,
OwnedItems = atkValues[136 + i].UInt,
ItemId = atkValues[441 + i].UInt,
})
.FirstOrDefault(x => x.ItemId == DarkMatterCluster6ItemId);
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() => GetItemCount(10335);
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 (_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: {_itemForSale.OwnedItems:N0}");
ImGui.Unindent();
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 (_purchaseState != null)
{
HandleNextPurchaseStep();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy"))
{
_purchaseState = null;
_plugin.RestoreYesAlready();
}
}
else
{
int toPurchase = Math.Min(GetMaxItemsToPurchase(), missingItems);
if (toPurchase > 0)
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign, "Auto-Buy missing Dark Matter"))
{
_purchaseState = new((int)_itemForSale.OwnedItems + toPurchase, (int)_itemForSale.OwnedItems);
_plugin.SaveYesAlready();
HandleNextPurchaseStep();
}
}
}
}
private unsafe void HandleNextPurchaseStep()
{
if (_itemForSale == null || _purchaseState == null)
return;
if (!_plugin.HasFreeInventorySlot())
{
_pluginLog.Warning($"No free inventory slots, can't buy more {_itemForSale.ItemName}");
_purchaseState = null;
_plugin.RestoreYesAlready();
}
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;
_plugin.RestoreYesAlready();
}
}
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

@ -215,4 +215,24 @@ partial class WorkshopPlugin
return false; 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;
}
}
return false;
}
} }

View File

@ -0,0 +1,38 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Workshoppa;
partial class WorkshopPlugin
{
private unsafe void SelectYesNoPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").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.PurchaseItem.IsMatch(text))
{
_pluginLog.Information($"Selecting 'yes' ({text})");
_repairKitWindow.IsAwaitingYesNo = false;
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
}
else
{
_pluginLog.Verbose("Not a purchase confirmation match");
}
}
else if (_mainWindow.IsOpen)
{
// TODO
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
@ -28,11 +29,16 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly WorkshopCache _workshopCache; private readonly WorkshopCache _workshopCache;
private readonly GameStrings _gameStrings;
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
private readonly ConfigWindow _configWindow;
private readonly RepairKitWindow _repairKitWindow;
private Stage _currentStageInternal = Stage.Stopped; private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
@ -40,7 +46,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
public WorkshopPlugin(DalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework, public WorkshopPlugin(DalamudPluginInterface pluginInterface, IGameGui gameGui, IFramework framework,
ICondition condition, IClientState clientState, IObjectTable objectTable, IDataManager dataManager, ICondition condition, IClientState clientState, IObjectTable objectTable, IDataManager dataManager,
ICommandManager commandManager, IPluginLog pluginLog) ICommandManager commandManager, IPluginLog pluginLog, IAddonLifecycle addonLifecycle)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_gameGui = gameGui; _gameGui = gameGui;
@ -50,22 +56,31 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_objectTable = objectTable; _objectTable = objectTable;
_commandManager = commandManager; _commandManager = commandManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
var dalamudReflector = new DalamudReflector(_pluginInterface, _framework, _pluginLog); var dalamudReflector = new DalamudReflector(_pluginInterface, _framework, _pluginLog);
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector); _yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_workshopCache = new WorkshopCache(dataManager, _pluginLog); _workshopCache = new WorkshopCache(dataManager, _pluginLog);
_gameStrings = new(dataManager, _pluginLog);
_mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache); _mainWindow = new(this, _pluginInterface, _clientState, _configuration, _workshopCache);
_windowSystem.AddWindow(_mainWindow); _windowSystem.AddWindow(_mainWindow);
_configWindow = new(_pluginInterface, _configuration);
_windowSystem.AddWindow(_configWindow);
_repairKitWindow = new(this, _pluginInterface, _pluginLog, _gameGui, addonLifecycle, _configuration);
_windowSystem.AddWindow(_repairKitWindow);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += OpenMainUi; _pluginInterface.UiBuilder.OpenMainUi += OpenMainUi;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_framework.Update += FrameworkUpdate; _framework.Update += FrameworkUpdate;
_commandManager.AddHandler("/ws", new CommandInfo(ProcessCommand) _commandManager.AddHandler("/ws", new CommandInfo(ProcessCommand)
{ {
HelpMessage = "Open UI" HelpMessage = "Open UI"
}); });
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
} }
internal Stage CurrentStage internal Stage CurrentStage
@ -202,22 +217,31 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
} }
private void ProcessCommand(string command, string arguments) private void ProcessCommand(string command, string arguments)
=> _mainWindow.Toggle(MainWindow.EOpenReason.Command); {
if (arguments is "c" or "config")
_configWindow.Toggle();
else
_mainWindow.Toggle(MainWindow.EOpenReason.Command);
}
private void OpenMainUi() private void OpenMainUi()
=> _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller); => _mainWindow.Toggle(MainWindow.EOpenReason.PluginInstaller);
public void Dispose() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_commandManager.RemoveHandler("/ws"); _commandManager.RemoveHandler("/ws");
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.OpenMainUi -= OpenMainUi; _pluginInterface.UiBuilder.OpenMainUi -= OpenMainUi;
_framework.Update -= FrameworkUpdate; _framework.Update -= FrameworkUpdate;
_repairKitWindow.Dispose();
RestoreYesAlready(); RestoreYesAlready();
} }
private void SaveYesAlready() public void SaveYesAlready()
{ {
if (_yesAlreadyState.Saved) if (_yesAlreadyState.Saved)
{ {
@ -229,7 +253,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin
_pluginLog.Information($"Previous yesalready state: {_yesAlreadyState.PreviousState}"); _pluginLog.Information($"Previous yesalready state: {_yesAlreadyState.PreviousState}");
} }
private void RestoreYesAlready() public void RestoreYesAlready()
{ {
if (_yesAlreadyState.Saved) if (_yesAlreadyState.Saved)
{ {

View File

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