Compare commits

...

30 Commits
v3.1 ... master

Author SHA1 Message Date
0cbde57b79
Add 'compact' turn-in UI 2024-12-22 16:07:55 +01:00
03aec55fbe
Rename debug PackagePlugin target 2024-12-19 15:18:08 +01:00
64551c033b
Fix all other confirmations being broken in non-JP 2024-11-23 14:35:03 +01:00
265c5d7d99
Fix HQ confirmation being broken in JP (fixes #4) 2024-11-23 14:18:23 +01:00
7492760d4f
Fix code formatting 2024-11-19 16:01:29 +01:00
31a9aeaece
API 11 2024-11-16 14:37:45 +01:00
c04b9e2ac5
Fix 'Add Item' not being visible when it should be 2024-08-31 21:11:45 +02:00
4b598ed485
Add an option for global limits to override char-specifc ones 2024-08-30 18:21:33 +02:00
780a0cbdcd
Update drag & drop functionality 2024-08-30 14:23:51 +02:00
26991aa59b
Version bump 2024-08-29 08:47:38 +02:00
1b54ba9c41 [F] Fix null safety in GetItemIdFromItemName (#3)
Reviewed-on: #3
2024-08-29 06:46:52 +00:00
Azalea
d5d2247531 [F] Fix null safety in GetItemIdFromItemName 2024-08-28 17:59:14 -04:00
2ab7ae3c06
Fix right-click menu for 'items for auto-buy' 2024-07-17 02:50:40 +02:00
92f6c9bc8a
Fix KeyState checks 2024-07-12 19:54:03 +02:00
70b69b786e
API 10 2024-07-02 21:28:51 +02:00
2be09089f5
Add quick turn-in option for manual turn-ins 2024-06-06 22:03:42 +02:00
4290e1432d
Allow starting Deliveroo from SelectString window 2024-05-31 10:10:29 +02:00
45423c0cc3
Add code to save dalamud's window options (pinned/clickthrough/alpha) 2024-05-11 00:27:33 +02:00
6a76b259bb
Add chat message when turn in is finished 2024-05-09 01:08:01 +02:00
2ad03c86ef
Move 'uncapped FPS' to a separate setting 2024-05-02 11:40:34 +02:00
e0e1de103e
Disable FPS limiter while turning in items 2024-04-30 19:35:56 +02:00
dd074b7133
Rework some code parts 2024-04-26 19:54:08 +02:00
79f8959852
Show rank requirements when expert delivery is locked 2024-04-26 10:01:23 +02:00
33ad4617c9
Add delay to TargetPersonnelOfficer 2024-04-14 12:20:46 +02:00
69a23ef2b6
Fix "not on Homeworld" warning 2024-03-28 14:10:18 +01:00
00c9dbb0a2
Fix UI scaling issues 2024-03-27 19:27:22 +01:00
9a0dfafb85
Fix UI scaling issues 2024-03-27 18:29:49 +01:00
76d00fb2dc
NET 8 2024-03-22 21:43:11 +01:00
5836c6312e
Blacklist more QV-exclusive items 2024-02-28 19:08:15 +01:00
a5ce0e1aa8
Allow turn in window to be minimized if no turn-in is running 2024-02-20 20:10:49 +01:00
29 changed files with 2531 additions and 744 deletions

1017
Deliveroo/.editorconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -17,10 +17,10 @@ internal sealed class CharacterConfiguration
public bool OverrideItemsToPurchase { get; set; }
public List<Configuration.PurchasePriority> ItemsToPurchase { get; set; } = new();
public static string ResolveFilePath(DalamudPluginInterface pluginInterface, ulong localContentId)
public static string ResolveFilePath(IDalamudPluginInterface pluginInterface, ulong localContentId)
=> Path.Join(pluginInterface.GetPluginConfigDirectory(), $"char.{localContentId:X}.json");
public static CharacterConfiguration? Load(DalamudPluginInterface pluginInterface, ulong localContentId)
public static CharacterConfiguration? Load(IDalamudPluginInterface pluginInterface, ulong localContentId)
{
string path = ResolveFilePath(pluginInterface, localContentId);
if (!File.Exists(path))
@ -29,11 +29,11 @@ internal sealed class CharacterConfiguration
return JsonConvert.DeserializeObject<CharacterConfiguration>(File.ReadAllText(path));
}
public void Save(DalamudPluginInterface pluginInterface)
public void Save(IDalamudPluginInterface pluginInterface)
{
File.WriteAllText(ResolveFilePath(pluginInterface, LocalContentId), JsonConvert.SerializeObject(this, Formatting.Indented));
}
public void Delete(DalamudPluginInterface pluginInterface) =>
public void Delete(IDalamudPluginInterface pluginInterface) =>
File.Delete(ResolveFilePath(pluginInterface, LocalContentId));
}

View File

@ -1,32 +1,49 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Deliveroo.GameData;
using LLib.ImGui;
namespace Deliveroo;
internal sealed class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 1;
public int Version { get; set; } = 2;
public List<uint> ItemsAvailableForPurchase { get; set; } = new();
public List<PurchasePriority> ItemsToPurchase { get; set; } = new();
[Obsolete]
public List<uint> ItemsAvailableForPurchase { get; set; } = [];
public List<PurchaseOption> ItemsAvailableToPurchase { get; set; } = [];
public List<PurchasePriority> ItemsToPurchase { get; set; } = [];
public int ReservedSealCount { get; set; }
public bool ReserveDifferentSealCountAtMaxRank { get; set; }
public int ReservedSealCountAtMaxRank { get; set; }
public XivChatType ChatType { get; set; } = XivChatType.Debug;
public int PauseAtRank { get; set; }
public EBehaviorOnOtherWorld BehaviorOnOtherWorld { get; set; } = EBehaviorOnOtherWorld.Warning;
public bool DisableFrameLimiter { get; set; } = true;
public bool UncapFrameRate { get; set; }
public VirtualKey QuickTurnInKey { get; set; } = VirtualKey.SHIFT;
/// <summary>
/// A config-only setting, not exposed in the UI.
///
/// If set, buys all GC items in their max quantity (otherwise, everything except ventures is capped to 99).
/// </summary>
public bool IgnoreCertainLimitations { get; set; } = false;
public MinimizableWindowConfig TurnInWindowConfig { get; } = new();
public WindowConfig ConfigWindowConfig { get; } = new();
internal sealed class PurchaseOption
{
public uint ItemId { get; set; }
public bool SameQuantityForAllLists { get; set; }
public int GlobalLimit { get; set; }
}
internal sealed class PurchasePriority
{
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
public uint ItemId { get; set; }
public int Limit { get; set; }
public bool Enabled { get; set; } = true;
@ -52,9 +69,9 @@ internal sealed class Configuration : IPluginConfiguration
public bool AddVentureIfNoItemToPurchaseSelected()
{
if (ItemsAvailableForPurchase.Count == 0)
if (ItemsAvailableToPurchase.Count == 0)
{
ItemsAvailableForPurchase.Add(ItemIds.Venture);
ItemsAvailableToPurchase.Add(new PurchaseOption { ItemId = ItemIds.Venture });
return true;
}
@ -67,4 +84,9 @@ internal sealed class Configuration : IPluginConfiguration
Warning,
DisableTurnIn,
}
internal sealed class MinimizableWindowConfig : WindowConfig
{
public bool IsMinimized { get; set; }
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Target Name="PackagePluginDebug" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"

View File

@ -1,63 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Dalamud.NET.Sdk/11.0.0">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>3.1</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Version>6.3</Version>
<OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
</PropertyGroup>
<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>
<Import Project="..\LLib\LLib.targets"/>
<Import Project="..\LLib\RenameZip.targets"/>
<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>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project>

View File

@ -1,160 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Math;
namespace Deliveroo;
partial class DeliverooPlugin
{
private unsafe void InteractWithTarget(GameObject obj)
{
_pluginLog.Information($"Setting target to {obj}");
if (_targetManager.Target == null || _targetManager.Target != obj)
{
_targetManager.Target = obj;
}
TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false);
}
private unsafe int GetCurrentSealCount()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
switch ((GrandCompany)PlayerState.Instance()->GrandCompany)
{
case GrandCompany.Maelstrom:
return inventoryManager->GetInventoryItemCount(20, false, false, false);
case GrandCompany.TwinAdder:
return inventoryManager->GetInventoryItemCount(21, false, false, false);
case GrandCompany.ImmortalFlames:
return inventoryManager->GetInventoryItemCount(22, false, false, false);
default:
return 0;
}
}
internal unsafe GrandCompany GetGrandCompany() => (GrandCompany)PlayerState.Instance()->GrandCompany;
internal unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank();
private float GetDistanceToNpc(int npcId, out GameObject? o)
{
foreach (var obj in _objectTable)
{
if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c)
{
if (GetNpcId(obj) == npcId)
{
o = obj;
return Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, c.Position);
}
}
}
o = null;
return float.MaxValue;
}
private int GetNpcId(GameObject obj)
{
return Marshal.ReadInt32(obj.Address + 128);
}
private int GetPersonnelOfficerId()
{
return GetGrandCompany() switch
{
GrandCompany.Maelstrom => 0xF4B94,
GrandCompany.ImmortalFlames => 0xF4B97,
GrandCompany.TwinAdder => 0xF4B9A,
_ => int.MaxValue,
};
}
private int GetQuartermasterId()
{
return GetGrandCompany() switch
{
GrandCompany.Maelstrom => 0xF4B93,
GrandCompany.ImmortalFlames => 0xF4B96,
GrandCompany.TwinAdder => 0xF4B99,
_ => int.MaxValue,
};
}
private uint GetSealCap() => _sealCaps.TryGetValue(GetGrandCompanyRank(), out var cap) ? cap : 0;
public uint GetMaxSealCap() => _sealCaps[11];
public unsafe int GetItemCount(uint itemId, bool checkRetainerInventory)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
int count = inventoryManager->GetInventoryItemCount(itemId, false, false, false);
if (checkRetainerInventory)
{
if (!_retainerItemCache.TryGetValue(itemId, out int retainerCount))
{
_retainerItemCache[itemId] = retainerCount = (int)_externalPluginHandler.GetRetainerItemCount(itemId);
}
count += retainerCount;
}
return count;
}
private decimal GetSealMultiplier()
{
// priority seal allowance
if (_clientState.LocalPlayer!.StatusList.Any(x => x.StatusId == 1078))
return 1.15m;
// seal sweetener 1/2
var fcStatus = _clientState.LocalPlayer!.StatusList.FirstOrDefault(x => x.StatusId == 414);
if (fcStatus != null)
{
return 1m + fcStatus.StackCount / 100m;
}
return 1;
}
/// <summary>
/// This returns ALL items that can be turned in, regardless of filter settings.
/// </summary>
private unsafe List<TurnInItem> BuildTurnInList(AgentGrandCompanySupply* agent)
{
List<TurnInItem> list = new();
for (int i = 11 /* skip over provisioning items */; i < agent->NumItems; ++i)
{
GrandCompanyItem item = agent->ItemArray[i];
// this includes all items, even if they don't match the filter
list.Add(new TurnInItem
{
ItemId = item.ItemId,
Name = MemoryHelper.ReadSeString(&item.ItemName).ToString(),
SealsWithBonus = (int)Math.Round(item.SealReward * GetSealMultiplier(), MidpointRounding.AwayFromZero),
SealsWithoutBonus = item.SealReward,
ItemUiCategory = item.ItemUiCategory,
});
}
return list.OrderByDescending(x => x.SealsWithBonus)
.ThenBy(x => x.ItemUiCategory)
.ThenBy(x => x.ItemId)
.ToList();
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.UI;
using LLib.GameUI;
namespace Deliveroo;
partial class DeliverooPlugin
{
private unsafe void GrandCompanySupplyRewardPostSetup(AddonEvent type, AddonArgs args)
{
bool quickTurnIn = CurrentStage == Stage.Stopped && _configuration.QuickTurnInKey != VirtualKey.NO_KEY && _keyState[_configuration.QuickTurnInKey];
if (CurrentStage == Stage.TurnInSelected || quickTurnIn)
{
AddonGrandCompanySupplyReward* addonSupplyReward = (AddonGrandCompanySupplyReward*)args.Addon;
string? itemName = addonSupplyReward->AtkUnitBase.AtkValues[4].ReadAtkString();
if (itemName != null && _itemCache.GetItemIdFromItemName(itemName)
.Any(itemId => InternalConfiguration.QuickVentureExclusiveItems.Contains(itemId)))
{
DeliveryResult = new MessageDeliveryResult
{
Message = new SeStringBuilder()
.Append("Won't turn in ")
.AddItemLink(_itemCache.GetItemIdFromItemName(itemName).First())
.Append(", as it can only be obtained through Quick Ventures.")
.Build(),
};
addonSupplyReward->AtkUnitBase.FireCallbackInt(1);
if (quickTurnIn)
CurrentStage = Stage.RequestStop;
else
CurrentStage = Stage.CloseGcSupplyWindowThenStop;
return;
}
_pluginLog.Information($"Turning in '{itemName}'");
addonSupplyReward->AtkUnitBase.FireCallbackInt(0);
ContinueAt = DateTime.Now.AddSeconds(0.58);
if (quickTurnIn)
{
DeliveryResult = new NoDeliveryResult();
CurrentStage = Stage.SingleFinalizeTurnIn;
}
else
CurrentStage = Stage.FinalizeTurnIn;
}
}
}

View File

@ -9,31 +9,36 @@ namespace Deliveroo;
partial class DeliverooPlugin
{
private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
SelectStringPostSetup(addonSelectString, CurrentStage);
}
private unsafe bool SelectStringPostSetup(AddonSelectString* addonSelectString, Stage stage)
{
_pluginLog.Verbose("SelectString post-setup");
string desiredText;
Action followUp;
if (CurrentStage == Stage.OpenGcSupply)
if (stage == Stage.OpenGcSupply)
{
desiredText = _gameStrings.UndertakeSupplyAndProvisioningMission;
followUp = OpenGcSupplySelectStringFollowUp;
}
else if (CurrentStage == Stage.CloseGcSupplySelectString)
else if (stage == Stage.CloseGcSupplySelectString)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplySelectStringFollowUp;
}
else if (CurrentStage == Stage.CloseGcSupplySelectStringThenStop)
else if (stage == Stage.CloseGcSupplySelectStringThenStop)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplySelectStringThenStopFollowUp;
}
else
return;
return false;
_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)
@ -46,27 +51,27 @@ partial class DeliverooPlugin
_pluginLog.Verbose($" Choice {i} → {text}");
if (text == desiredText)
{
_pluginLog.Information($"Selecting choice {i} ({text})");
addonSelectString->AtkUnitBase.FireCallbackInt(i);
followUp();
return;
return true;
}
}
_pluginLog.Verbose($"Text '{desiredText}' was not found in prompt.");
return false;
}
private void OpenGcSupplySelectStringFollowUp()
{
ResetTurnInErrorHandling();
_supplyHandler.ResetTurnInErrorHandling();
CurrentStage = Stage.SelectExpertDeliveryTab;
}
private void CloseGcSupplySelectStringFollowUp()
{
if (GetNextItemToPurchase() == null)
if (_exchangeHandler.GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
@ -74,26 +79,27 @@ partial class DeliverooPlugin
else
{
// you can occasionally get a 'not enough seals' warning lol
_continueAt = DateTime.Now.AddSeconds(1);
ContinueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
private void CloseGcSupplySelectStringThenStopFollowUp()
{
if (GetNextItemToPurchase() == null)
if (_exchangeHandler.GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else if (GetCurrentSealCount() <= EffectiveReservedSealCount + GetNextItemToPurchase()!.SealCost)
else if (_gameFunctions.GetCurrentSealCount() <=
EffectiveReservedSealCount + _exchangeHandler.GetNextItemToPurchase()!.SealCost)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else
{
_continueAt = DateTime.Now.AddSeconds(1);
ContinueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}

View File

@ -1,6 +1,7 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
@ -19,7 +20,7 @@ partial class DeliverooPlugin
if (CurrentStage == Stage.ConfirmReward &&
_gameStrings.ExchangeItems.IsMatch(text))
{
PurchaseItemRequest? item = GetNextItemToPurchase();
PurchaseItemRequest? item = _exchangeHandler.GetNextItemToPurchase();
if (item == null)
{
addonSelectYesNo->AtkUnitBase.FireCallbackInt(1);
@ -33,14 +34,14 @@ partial class DeliverooPlugin
item.OnPurchase?.Invoke((int)item.TemporaryPurchaseQuantity);
item.TemporaryPurchaseQuantity = 0;
var nextItem = GetNextItemToPurchase(item);
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
var nextItem = _exchangeHandler.GetNextItemToPurchase(item);
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.SelectRewardTier;
else
CurrentStage = Stage.CloseGcExchange;
_continueAt = DateTime.Now.AddSeconds(0.5);
ContinueAt = DateTime.Now.AddSeconds(0.5);
}
else if (CurrentStage == Stage.TurnInSelected &&
else if ((CurrentStage == Stage.TurnInSelected || (_configuration.QuickTurnInKey != VirtualKey.NO_KEY && _keyState[_configuration.QuickTurnInKey])) &&
_gameStrings.TradeHighQualityItem == text)
{
_pluginLog.Information($"Selecting 'yes' ({text})");

View File

@ -13,36 +13,35 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Deliveroo.External;
using Deliveroo.GameData;
using Deliveroo.Handlers;
using Deliveroo.Windows;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
namespace Deliveroo;
public sealed partial class DeliverooPlugin : IDalamudPlugin
{
private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName);
private readonly IReadOnlyList<uint> DisabledTurnInItems = new List<uint> { 2820 }.AsReadOnly();
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IChatGui _chatGui;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly IClientState _clientState;
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
private readonly ICondition _condition;
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IKeyState _keyState;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly Configuration _configuration;
private readonly GameStrings _gameStrings;
private readonly GameFunctions _gameFunctions;
private readonly ExternalPluginHandler _externalPluginHandler;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
@ -50,53 +49,57 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
private readonly IconCache _iconCache;
private readonly ItemCache _itemCache;
private readonly ExchangeHandler _exchangeHandler;
private readonly SupplyHandler _supplyHandler;
private readonly ConfigWindow _configWindow;
private readonly TurnInWindow _turnInWindow;
private readonly IReadOnlyDictionary<uint, uint> _sealCaps;
private readonly Dictionary<uint, int> _retainerItemCache = new();
private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue;
private int _lastTurnInListSize = int.MaxValue;
private uint _turnInErrors = 0;
private List<PurchaseItemRequest> _itemsToPurchaseNow = new();
public DeliverooPlugin(DalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
public DeliverooPlugin(IDalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager,
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog,
IAddonLifecycle addonLifecycle, ITextureProvider textureProvider)
IAddonLifecycle addonLifecycle, ITextureProvider textureProvider, IGameConfig gameConfig, IKeyState keyState)
{
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface;
_chatGui = chatGui;
_gameGui = gameGui;
_framework = framework;
_clientState = clientState;
_objectTable = objectTable;
_targetManager = targetManager;
_condition = condition;
_commandManager = commandManager;
_pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_keyState = keyState;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
MigrateConfiguration();
_gameStrings = new GameStrings(dataManager, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, gameConfig, _configuration, _pluginLog);
_gameFunctions = new GameFunctions(objectTable, _clientState, targetManager, dataManager,
_externalPluginHandler, _pluginLog);
_gcRewardsCache = new GcRewardsCache(dataManager);
_iconCache = new IconCache(textureProvider);
_itemCache = new ItemCache(dataManager);
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState, _pluginLog, _iconCache);
_exchangeHandler = new ExchangeHandler(this, _gameFunctions, targetManager, _gameGui, _chatGui, _pluginLog);
_supplyHandler = new SupplyHandler(this, _gameFunctions, targetManager, _gameGui, _pluginLog);
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState,
_pluginLog, _iconCache, _gameFunctions);
_windowSystem.AddWindow(_configWindow);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _clientState, _gcRewardsCache, _configWindow, _iconCache);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _clientState,
_gcRewardsCache, _configWindow, _iconCache, _keyState, _gameFunctions);
_windowSystem.AddWindow(_turnInWindow);
_sealCaps = dataManager.GetExcelSheet<GrandCompanyRank>()!.Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, x => x.MaxSeals);
_framework.Update += FrameworkUpdate;
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_clientState.Login += Login;
_clientState.Logout += Logout;
_clientState.TerritoryChanged += TerritoryChanged;
_chatGui.ChatMessage += ChatMessage;
_commandManager.AddHandler("/deliveroo", new CommandInfo(ProcessCommand)
{
@ -111,9 +114,28 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
}
private void ChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
private void MigrateConfiguration()
{
#pragma warning disable CS0612 // Type or member is obsolete
if (_configuration.Version == 1)
{
_configuration.ItemsAvailableToPurchase = _configuration.ItemsAvailableForPurchase.Select(x =>
new Configuration.PurchaseOption
{
ItemId = x,
SameQuantityForAllLists = false,
}).ToList();
_configuration.Version = 2;
_pluginInterface.SavePluginConfig(_configuration);
}
#pragma warning restore CS0612 // Type or member is obsolete
}
private void ChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message,
ref bool isHandled)
{
if (_configuration.PauseAtRank <= 0)
return;
@ -131,7 +153,12 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
_turnInWindow.State = false;
_pluginLog.Information($"Pausing GC delivery, FC reached rank {rank}");
_chatGui.Print($"Pausing Deliveroo, your FC reached rank {rank}.");
DeliveryResult = new MessageDeliveryResult
{
Message = new SeStringBuilder()
.Append($"Pausing Deliveroo, your FC reached rank {rank}.")
.Build(),
};
return;
}
}
@ -153,6 +180,21 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
internal DateTime ContinueAt { private get; set; } = DateTime.MinValue;
internal List<PurchaseItemRequest> ItemsToPurchaseNow { get; private set; } = new();
internal int LastTurnInListSize { get; set; } = int.MaxValue;
internal IDeliveryResult? DeliveryResult { get; set; }
internal bool TurnInState
{
set => _turnInWindow.State = value;
}
internal string TurnInError
{
set => _turnInWindow.Error = value;
}
public int EffectiveReservedSealCount
{
get
@ -160,7 +202,8 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (CharacterConfiguration is { IgnoreMinimumSealsToKeep: true })
return 0;
return _configuration.ReserveDifferentSealCountAtMaxRank && GetSealCap() == GetMaxSealCap()
return _configuration.ReserveDifferentSealCountAtMaxRank &&
_gameFunctions.GetSealCap() == _gameFunctions.MaxSealCap
? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount;
}
@ -175,11 +218,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() ||
CharacterConfiguration.CachedWorldName !=
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString())
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString())
{
CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString();
CharacterConfiguration.CachedWorldName =
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
CharacterConfiguration.Save(_pluginInterface);
}
@ -199,16 +242,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
private void Logout()
private void Logout(int type, int code)
{
CharacterConfiguration = null;
_retainerItemCache.Clear();
}
private void TerritoryChanged(ushort territoryType)
{
// there is no GC area that is in the same zone as a retainer bell, so this should be often enough.
_retainerItemCache.Clear();
}
private unsafe void FrameworkUpdate(IFramework f)
@ -217,60 +253,62 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (!_clientState.IsLoggedIn ||
_clientState.TerritoryType is not 128 and not 130 and not 132 ||
_condition[ConditionFlag.OccupiedInCutSceneEvent] ||
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f ||
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetQuartermasterId(), out IGameObject? quartermaster) >= 7f ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetPersonnelOfficerId(), out IGameObject? personnelOfficer) >=
7f ||
CharacterConfiguration is { DisableForCharacter: true } ||
_configWindow.IsOpen)
{
_turnInWindow.IsOpen = false;
_turnInWindow.State = false;
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
StopTurnIn();
}
else if (DateTime.Now > _continueAt)
else if (DateTime.Now > ContinueAt)
{
_turnInWindow.IsOpen = true;
_turnInWindow.Multiplier = GetSealMultiplier();
_turnInWindow.Multiplier = _gameFunctions.GetSealMultiplier();
if (!_turnInWindow.State)
{
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
StopTurnIn();
return;
}
else if (_turnInWindow.State && CurrentStage == Stage.Stopped)
{
CurrentStage = Stage.TargetPersonnelOfficer;
_itemsToPurchaseNow = _turnInWindow.SelectedItems;
ResetTurnInErrorHandling();
if (_itemsToPurchaseNow.Count > 0)
ItemsToPurchaseNow = _turnInWindow.SelectedItems;
DeliveryResult = new MessageDeliveryResult();
_supplyHandler.ResetTurnInErrorHandling();
if (ItemsToPurchaseNow.Count > 0)
{
_pluginLog.Information("Items to purchase:");
foreach (var item in _itemsToPurchaseNow)
foreach (var item in ItemsToPurchaseNow)
_pluginLog.Information($" {item.Name} (limit = {item.EffectiveLimit})");
}
else
_pluginLog.Information("No items to purchase configured or available");
var nextItem = GetNextItemToPurchase();
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
var nextItem = _exchangeHandler.GetNextItemToPurchase();
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >=
EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.TargetQuartermaster;
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", out var gcSupplyList) &&
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var gcSupplyList) &&
LAddon.IsAddonReady(&gcSupplyList->AtkUnitBase))
CurrentStage = Stage.SelectExpertDeliveryTab;
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) &&
LAddon.IsAddonReady(gcExchange))
CurrentStage = Stage.SelectRewardTier;
if (_gameGui.TryGetAddonByName<AddonSelectString>("SelectString", out var addonSelectString) &&
LAddon.IsAddonReady(&addonSelectString->AtkUnitBase))
{
if (SelectStringPostSetup(addonSelectString, Stage.OpenGcSupply))
return;
}
}
if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_externalPluginHandler.Saved)
@ -279,7 +317,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
switch (CurrentStage)
{
case Stage.TargetPersonnelOfficer:
InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
_supplyHandler.InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
break;
case Stage.OpenGcSupply:
@ -287,19 +325,20 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.SelectExpertDeliveryTab:
SelectExpertDeliveryTab();
_supplyHandler.SelectExpertDeliveryTab();
break;
case Stage.SelectItemToTurnIn:
SelectItemToTurnIn();
_supplyHandler.SelectItemToTurnIn();
break;
case Stage.TurnInSelected:
TurnInSelectedItem();
// see GrandCompanySupplyReward
break;
case Stage.FinalizeTurnIn:
FinalizeTurnInItem();
case Stage.SingleFinalizeTurnIn:
_supplyHandler.FinalizeTurnInItem();
break;
case Stage.CloseGcSupplySelectString:
@ -311,23 +350,23 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.CloseGcSupplyWindowThenStop:
CloseGcSupplyWindow();
_supplyHandler.CloseGcSupplyWindow();
break;
case Stage.TargetQuartermaster:
InteractWithQuartermaster(personnelOfficer!, quartermaster!);
_exchangeHandler.InteractWithQuartermaster(personnelOfficer!, quartermaster!);
break;
case Stage.SelectRewardTier:
SelectRewardTier();
_exchangeHandler.SelectRewardTier();
break;
case Stage.SelectRewardSubCategory:
SelectRewardSubCategory();
_exchangeHandler.SelectRewardSubCategory();
break;
case Stage.SelectReward:
SelectReward();
_exchangeHandler.SelectReward();
break;
case Stage.ConfirmReward:
@ -335,14 +374,13 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.CloseGcExchange:
CloseGcExchange();
_exchangeHandler.CloseGcExchange();
break;
case Stage.RequestStop:
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
StopTurnIn();
break;
case Stage.Stopped:
break;
@ -353,23 +391,70 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
private void StopTurnIn()
{
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
if (DeliveryResult is null or MessageDeliveryResult)
{
var text = (DeliveryResult as MessageDeliveryResult)?.Message ?? "Delivery completed.";
var message = _configuration.ChatType switch
{
XivChatType.Say
or XivChatType.Shout
or XivChatType.TellOutgoing
or XivChatType.TellIncoming
or XivChatType.Party
or XivChatType.Alliance
or (>= XivChatType.Ls1 and <= XivChatType.Ls8)
or XivChatType.FreeCompany
or XivChatType.NoviceNetwork
or XivChatType.Yell
or XivChatType.CrossParty
or XivChatType.PvPTeam
or XivChatType.CrossLinkShell1
or XivChatType.NPCDialogue
or XivChatType.NPCDialogueAnnouncements
or (>= XivChatType.CrossLinkShell2 and <= XivChatType.CrossLinkShell8)
=> new XivChatEntry
{
Message = text,
Type = _configuration.ChatType,
Name = new SeStringBuilder().AddUiForeground("Deliveroo", 52).Build(),
},
_ => new XivChatEntry
{
Message = new SeStringBuilder().AddUiForeground("[Deliveroo] ", 52)
.Append(text)
.Build(),
Type = _configuration.ChatType,
}
};
_chatGui.Print(message);
}
DeliveryResult = null;
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_commandManager.RemoveHandler("/deliveroo");
_chatGui.ChatMessage -= ChatMessage;
_clientState.TerritoryChanged -= TerritoryChanged;
_clientState.Logout -= Logout;
_clientState.Login -= Login;
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_framework.Update -= FrameworkUpdate;
_externalPluginHandler.Restore();
_gameFunctions.Dispose();
_externalPluginHandler.Dispose();
_iconCache.Dispose();
}

View File

@ -5,9 +5,9 @@ using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace ARControl.External;
namespace Deliveroo.External;
public class AllaganToolsIpc
internal sealed class AllaganToolsIpc
{
private readonly IPluginLog _pluginLog;
@ -25,7 +25,7 @@ public class AllaganToolsIpc
private readonly ICallGateSubscriber<uint, bool, uint[], uint> _itemCountOwned;
public AllaganToolsIpc(DalamudPluginInterface pluginInterface, IPluginLog pluginLog)
public AllaganToolsIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_itemCountOwned = pluginInterface.GetIpcSubscriber<uint, bool, uint[], uint>("AllaganTools.ItemCountOwned");

View File

@ -16,7 +16,7 @@ internal sealed class DeliverooIpc : IDisposable
private bool _running;
public DeliverooIpc(DalamudPluginInterface pluginInterface)
public DeliverooIpc(IDalamudPluginInterface pluginInterface)
{
_isTurnInRunning = pluginInterface.GetIpcProvider<bool>(IsTurnInRunning);
_turnInStarted = pluginInterface.GetIpcProvider<object>(TurnInStarted);

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using ARControl.External;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
@ -8,17 +7,24 @@ namespace Deliveroo.External;
internal sealed class ExternalPluginHandler : IDisposable
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IGameConfig _gameConfig;
private readonly Configuration _configuration;
private readonly IPluginLog _pluginLog;
private readonly DeliverooIpc _deliverooIpc;
private readonly PandoraIpc _pandoraIpc;
private readonly AllaganToolsIpc _allaganToolsIpc;
private bool? _pandoraState;
private SystemConfigState? _limitFrameRateWhenClientInactive;
private SystemConfigState? _uncapFrameRate;
public ExternalPluginHandler(DalamudPluginInterface pluginInterface, IPluginLog pluginLog)
public ExternalPluginHandler(IDalamudPluginInterface pluginInterface, IGameConfig gameConfig,
Configuration configuration, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_gameConfig = gameConfig;
_configuration = configuration;
_pluginLog = pluginLog;
_deliverooIpc = new DeliverooIpc(pluginInterface);
_pandoraIpc = new PandoraIpc(pluginInterface, pluginLog);
@ -39,6 +45,7 @@ internal sealed class ExternalPluginHandler : IDisposable
_deliverooIpc.StartTurnIn();
SaveYesAlreadyState();
SavePandoraState();
SaveGameConfig();
Saved = true;
}
@ -58,12 +65,25 @@ internal sealed class ExternalPluginHandler : IDisposable
_pluginLog.Info($"Previous pandora feature state: {_pandoraState}");
}
private void SaveGameConfig()
{
if (!_configuration.DisableFrameLimiter)
return;
_limitFrameRateWhenClientInactive ??=
new SystemConfigState(_gameConfig, SystemConfigState.ConfigFpsInactive, 0);
if (_configuration.UncapFrameRate)
_uncapFrameRate ??= new SystemConfigState(_gameConfig, SystemConfigState.ConfigFps, 0);
}
public void Restore()
{
if (Saved)
{
RestoreYesAlready();
RestorePandora();
RestoreGameConfig();
}
Saved = false;
@ -88,10 +108,36 @@ internal sealed class ExternalPluginHandler : IDisposable
_pandoraIpc.Enable();
}
private void RestoreGameConfig()
{
_uncapFrameRate?.Restore(_gameConfig);
_uncapFrameRate = null;
_limitFrameRateWhenClientInactive?.Restore(_gameConfig);
_limitFrameRateWhenClientInactive = null;
}
public void Dispose()
{
_deliverooIpc.Dispose();
}
public uint GetRetainerItemCount(uint itemId) => _allaganToolsIpc.GetRetainerItemCount(itemId);
private sealed record SystemConfigState(string Key, uint OldValue)
{
public const string ConfigFps = "Fps";
public const string ConfigFpsInactive = "FPSInActive";
public SystemConfigState(IGameConfig gameConfig, string key, uint newValue)
: this(key, gameConfig.System.GetUInt(key))
{
gameConfig.System.Set(key, newValue);
}
public void Restore(IGameConfig gameConfig)
{
gameConfig.System.Set(Key, OldValue);
}
}
}

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(DalamudPluginInterface pluginInterface, IPluginLog pluginLog)
public PandoraIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled");

View File

@ -1,11 +1,12 @@
using System;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
using LLib;
using Lumina.Excel;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace Deliveroo.GameData;
@ -15,19 +16,19 @@ internal sealed class GameStrings
{
UndertakeSupplyAndProvisioningMission =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002", pluginLog)
?? throw new Exception($"Unable to resolve {nameof(UndertakeSupplyAndProvisioningMission)}");
?? throw new ConstraintException($"Unable to resolve {nameof(UndertakeSupplyAndProvisioningMission)}");
ClosePersonnelOfficerTalk =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004", pluginLog)
?? throw new Exception($"Unable to resolve {nameof(ClosePersonnelOfficerTalk)}");
?? throw new ConstraintException($"Unable to resolve {nameof(ClosePersonnelOfficerTalk)}");
ExchangeItems = dataManager.GetRegex<Addon>(3290, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(ExchangeItems)}");
?? throw new ConstraintException($"Unable to resolve {nameof(ExchangeItems)}");
TradeHighQualityItem =
dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)?.ReplaceLineEndings("")
?? throw new Exception($"Unable to resolve {nameof(TradeHighQualityItem)}");
dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(TradeHighQualityItem)}");
var rankUpFc = dataManager.GetExcelSheet<LogMessage>()!.GetRow(3123)!;
var rankUpFc = dataManager.GetExcelSheet<LogMessage>().GetRow(3123);
RankUpFc = rankUpFc.GetRegex(logMessage => logMessage.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(RankUpFc)}");
?? throw new ConstraintException($"Unable to resolve {nameof(RankUpFc)}");
RankUpFcType = (XivChatType)rankUpFc.LogKind;
}
@ -40,7 +41,17 @@ internal sealed class GameStrings
public XivChatType RankUpFcType { get; }
[Sheet("custom/000/ComDefGrandCompanyOfficer_00073")]
private class ComDefGrandCompanyOfficer : QuestDialogueText
[SuppressMessage("Performance", "CA1812")]
private readonly struct ComDefGrandCompanyOfficer(ExcelPage page, uint offset, uint row)
: IQuestDialogueText, IExcelRow<ComDefGrandCompanyOfficer>
{
public uint RowId => row;
public ReadOnlySeString Key => page.ReadString(offset, offset);
public ReadOnlySeString Value => page.ReadString(offset + 4, offset);
static ComDefGrandCompanyOfficer IExcelRow<ComDefGrandCompanyOfficer>.Create(ExcelPage page, uint offset,
uint row) =>
new(page, offset, row);
}
}

View File

@ -0,0 +1,33 @@
using System;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace Deliveroo.GameData
{
internal sealed class GcRankInfo
{
public required string NameTwinAddersMale { private get; init; }
public required string NameTwinAddersFemale { private get; init; }
public required string NameMaelstromMale { private get; init; }
public required string NameMaelstromFemale { private get; init; }
public required string NameImmortalFlamesMale { private get; init; }
public required string NameImmortalFlamesFemale { private get; init; }
public required uint MaxSeals { get; init; }
public required uint RequiredSeals { get; init; }
public required byte RequiredHuntingLog { get; init; }
public string GetName(GrandCompany grandCompany, bool female)
{
return (grandCompany, female) switch
{
(GrandCompany.TwinAdder, false) => NameTwinAddersMale,
(GrandCompany.TwinAdder, true) => NameTwinAddersFemale,
(GrandCompany.Maelstrom, false) => NameMaelstromMale,
(GrandCompany.Maelstrom, true) => NameMaelstromFemale,
(GrandCompany.ImmortalFlames, false) => NameImmortalFlamesMale,
(GrandCompany.ImmortalFlames, true) => NameImmortalFlamesFemale,
_ => throw new ArgumentOutOfRangeException(nameof(grandCompany) + "," + nameof(female)),
};
}
}
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Deliveroo.GameData;
@ -10,31 +10,32 @@ internal sealed class GcRewardsCache
{
public GcRewardsCache(IDataManager dataManager)
{
var categories = dataManager.GetExcelSheet<GCScripShopCategory>()!
var categories = dataManager.GetExcelSheet<GCScripShopCategory>()
.Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId,
x =>
(GrandCompany: (GrandCompany)x.GrandCompany.Row,
(GrandCompany: (GrandCompany)x.GrandCompany.RowId,
Tier: (RewardTier)x.Tier,
SubCategory: (RewardSubCategory)x.SubCategory));
Rewards = dataManager.GetExcelSheet<GCScripShopItem>()!
.Where(x => x.RowId > 0 && x.Item.Row > 0)
Rewards = dataManager.GetSubrowExcelSheet<GCScripShopItem>()
.SelectMany(x => x)
.Where(x => x.RowId > 0 && x.Item.RowId > 0)
.GroupBy(item =>
{
var category = categories[item.RowId];
return new
{
ItemId = item.Item.Row,
Name = item.Item.Value!.Name.ToString(),
IconId = item.Item.Row == ItemIds.Venture ? 25917 : item.Item.Value!.Icon,
ItemId = item.Item.RowId,
Name = item.Item.Value.Name.ToString(),
IconId = item.Item.RowId == ItemIds.Venture ? 25917 : item.Item.Value.Icon,
category.Tier,
category.SubCategory,
RequiredRank = item.RequiredGrandCompanyRank.Row,
item.Item!.Value.StackSize,
RequiredRank = item.RequiredGrandCompanyRank.RowId,
item.Item.Value.StackSize,
SealCost = item.CostGCSeals,
InventoryLimit = item.Item.Value!.IsUnique ? 1
: item.Item.Row == ItemIds.Venture ? item.Item.Value!.StackSize
InventoryLimit = item.Item.Value.IsUnique ? 1
: item.Item.RowId == ItemIds.Venture ? item.Item.Value.StackSize
: int.MaxValue,
};
})

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Linq;
namespace Deliveroo.GameData;
internal static class InternalConfiguration
{
public static readonly IReadOnlyList<uint> QuickVentureExclusiveItems = new List<uint>
{
// Weapons
1662, // Thormoen's Pride
1799, // Sibold's Reach
1870, // Gerbald's Redspike
1731, // Symon's Honeyclaws
1940, // Alesone's Songbow
2132, // Aubriest's Whisper
2043, // Chiran Zabran's Tempest
2271, // Thormoen's Purpose
2290, // Aubriest's Allegory
// Armor
2752, // Peregrine Helm
2820, // Red Onion Helm
2823, // Veteran's Pot Helm
2822, // Explorer's Bandana
2821, // Explorer's Calot
3170, // Explorer's Tabard
3168, // Veteran's Acton
3167, // Explorer's Tunic
3171, // Mage's Halfrobe
3676, // Spiked Armguards
3644, // Mage's Halfgloves
3418, // Thormoen's Subligar
3420, // Explorer's Breeches
3419, // Mage's Chausses
3864, // Explorer's Sabatons
3865, // Explorer's Moccasins
3863, // Mage's Pattens
// Accessories
4256, // Stonewall Earrings
4249, // Explorer's Earrings
4250, // Mage's Earrings
4257, // Blessed Earrings
4359, // Stonewall Choker
4357, // Explorer's Choker
4358, // Mage's Choker
4498, // Stonewall Ring
4483, // Explorer's Ring
4484, // Mage's Ring
4499, // Blessed Ring
}
.ToList()
.AsReadOnly();
}

View File

@ -1,16 +1,16 @@
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
namespace Deliveroo.GameData;
internal class ItemCache
internal sealed class ItemCache
{
private readonly Dictionary<string, HashSet<uint>> _itemNamesToIds = new();
public ItemCache(IDataManager dataManager)
{
foreach (var item in dataManager.GetExcelSheet<Item>()!)
foreach (var item in dataManager.GetExcelSheet<Item>())
{
string name = item.Name.ToString();
if (string.IsNullOrWhiteSpace(name))
@ -19,9 +19,10 @@ internal class ItemCache
if (_itemNamesToIds.TryGetValue(name, out HashSet<uint>? itemIds))
itemIds.Add(item.RowId);
else
_itemNamesToIds.Add(name, new HashSet<uint>{item.RowId});
_itemNamesToIds.Add(name, [item.RowId]);
}
}
public HashSet<uint> GetItemIdFromItemName(string name) => _itemNamesToIds[name];
public HashSet<uint> GetItemIdFromItemName(string name) =>
_itemNamesToIds.TryGetValue(name, out var itemIds) ? itemIds : [];
}

244
Deliveroo/GameFunctions.cs Normal file
View File

@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Deliveroo.External;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Math;
using FFXIVClientStructs.FFXIV.Component.Excel;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Deliveroo;
internal sealed class GameFunctions : IDisposable
{
private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly ITargetManager _targetManager;
private readonly ExternalPluginHandler _externalPluginHandler;
private readonly IPluginLog _pluginLog;
private readonly ReadOnlyDictionary<uint, GcRankInfo> _gcRankInfo;
private readonly Dictionary<uint, int> _retainerItemCache = new();
public GameFunctions(IObjectTable objectTable, IClientState clientState, ITargetManager targetManager,
IDataManager dataManager, ExternalPluginHandler externalPluginHandler, IPluginLog pluginLog)
{
_objectTable = objectTable;
_clientState = clientState;
_targetManager = targetManager;
_externalPluginHandler = externalPluginHandler;
_pluginLog = pluginLog;
_gcRankInfo = dataManager.GetExcelSheet<GrandCompanyRank>().Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, x => new GcRankInfo
{
NameTwinAddersMale = ExtractRankName<GCRankGridaniaMaleText>(dataManager, x.RowId, r => r.Singular),
NameTwinAddersFemale = ExtractRankName<GCRankGridaniaFemaleText>(dataManager, x.RowId, r => r.Singular),
NameMaelstromMale = ExtractRankName<GCRankLimsaMaleText>(dataManager, x.RowId, r => r.Singular),
NameMaelstromFemale = ExtractRankName<GCRankLimsaFemaleText>(dataManager, x.RowId, r => r.Singular),
NameImmortalFlamesMale = ExtractRankName<GCRankUldahMaleText>(dataManager, x.RowId, r => r.Singular),
NameImmortalFlamesFemale =
ExtractRankName<GCRankUldahFemaleText>(dataManager, x.RowId, r => r.Singular),
MaxSeals = x.MaxSeals,
RequiredSeals = x.RequiredSeals,
RequiredHuntingLog = x.Unknown0,
})
.AsReadOnly();
_clientState.Logout += Logout;
_clientState.TerritoryChanged += TerritoryChanged;
}
private static string ExtractRankName<T>(IDataManager dataManager, uint rankId, Func<T, ReadOnlySeString> func)
where T : struct, IExcelRow<T>
{
return func(dataManager.GetExcelSheet<T>().GetRow(rankId)).ToString();
}
private void Logout(int type, int code)
{
_retainerItemCache.Clear();
}
private void TerritoryChanged(ushort territoryType)
{
// there is no GC area that is in the same zone as a retainer bell, so this should be often enough.
_retainerItemCache.Clear();
}
public unsafe void InteractWithTarget(IGameObject obj)
{
_pluginLog.Information($"Setting target to {obj}");
if (_targetManager.Target == null || _targetManager.Target.EntityId != obj.EntityId)
{
_targetManager.Target = obj;
}
TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false);
}
public unsafe int GetCurrentSealCount()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
switch ((GrandCompany)PlayerState.Instance()->GrandCompany)
{
case GrandCompany.Maelstrom:
return inventoryManager->GetInventoryItemCount(20, false, false, false);
case GrandCompany.TwinAdder:
return inventoryManager->GetInventoryItemCount(21, false, false, false);
case GrandCompany.ImmortalFlames:
return inventoryManager->GetInventoryItemCount(22, false, false, false);
default:
return 0;
}
}
public unsafe GrandCompany GetGrandCompany() => (GrandCompany)PlayerState.Instance()->GrandCompany;
public unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank();
public float GetDistanceToNpc(int npcId, out IGameObject? o)
{
foreach (var obj in _objectTable)
{
if (obj.ObjectKind == ObjectKind.EventNpc && obj is ICharacter c)
{
if (c.DataId == npcId)
{
o = obj;
return Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, c.Position);
}
}
}
o = null;
return float.MaxValue;
}
public static int GetNpcId(IGameObject obj)
{
return Marshal.ReadInt32(obj.Address + 128);
}
public int GetPersonnelOfficerId()
{
return GetGrandCompany() switch
{
GrandCompany.Maelstrom => 0xF4B94,
GrandCompany.ImmortalFlames => 0xF4B97,
GrandCompany.TwinAdder => 0xF4B9A,
_ => int.MaxValue,
};
}
public int GetQuartermasterId()
{
return GetGrandCompany() switch
{
GrandCompany.Maelstrom => 0xF4B93,
GrandCompany.ImmortalFlames => 0xF4B96,
GrandCompany.TwinAdder => 0xF4B99,
_ => int.MaxValue,
};
}
public uint GetSealCap() => _gcRankInfo.TryGetValue(GetGrandCompanyRank(), out var rank) ? rank.MaxSeals : 0;
public uint MaxSealCap => _gcRankInfo[11].MaxSeals;
public uint GetSealsRequiredForNextRank()
=> _gcRankInfo.GetValueOrDefault(GetGrandCompanyRank())?.RequiredSeals ?? 0;
public byte GetRequiredHuntingLogForNextRank()
=> _gcRankInfo.GetValueOrDefault(GetGrandCompanyRank() + 1u)?.RequiredHuntingLog ?? 0;
public string? GetNextGrandCompanyRankName()
{
bool female = _clientState.LocalPlayer!.Customize[(int)CustomizeIndex.Gender] == 1;
GrandCompany grandCompany = GetGrandCompany();
return _gcRankInfo.GetValueOrDefault(GetGrandCompanyRank() + 1u)?.GetName(grandCompany, female);
}
public unsafe int GetItemCount(uint itemId, bool checkRetainerInventory)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
int count = inventoryManager->GetInventoryItemCount(itemId, false, false, false);
if (checkRetainerInventory)
{
if (!_retainerItemCache.TryGetValue(itemId, out int retainerCount))
{
_retainerItemCache[itemId] = retainerCount = (int)_externalPluginHandler.GetRetainerItemCount(itemId);
}
count += retainerCount;
}
return count;
}
public decimal GetSealMultiplier()
{
// priority seal allowance
if (_clientState.LocalPlayer!.StatusList.Any(x => x.StatusId == 1078))
return 1.15m;
// seal sweetener 1/2
var fcStatus = _clientState.LocalPlayer!.StatusList.FirstOrDefault(x => x.StatusId == 414);
if (fcStatus != null)
{
return 1m + fcStatus.StackCount / 100m;
}
return 1;
}
/// <summary>
/// This returns ALL items that can be turned in, regardless of filter settings.
/// </summary>
public unsafe List<TurnInItem> BuildTurnInList(AgentGrandCompanySupply* agent)
{
List<TurnInItem> list = new();
for (int i = 11 /* skip over provisioning items */; i < agent->NumItems; ++i)
{
GrandCompanyItem item = agent->ItemArray[i];
// this includes all items, even if they don't match the filter
list.Add(new TurnInItem
{
ItemId = item.ItemId,
Name = MemoryHelper.ReadSeString(&item.ItemName).ToString(),
SealsWithBonus = (int)Math.Round(item.SealReward * GetSealMultiplier(), MidpointRounding.AwayFromZero),
SealsWithoutBonus = item.SealReward,
ItemUiCategory = item.ItemUiCategory,
});
}
return list.OrderByDescending(x => x.SealsWithBonus)
.ThenBy(x => x.ItemUiCategory)
.ThenBy(x => x.ItemId)
.ToList();
}
public void Dispose()
{
_clientState.TerritoryChanged -= TerritoryChanged;
_clientState.Logout -= Logout;
}
}

View File

@ -1,46 +1,67 @@
using System;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Deliveroo;
namespace Deliveroo.Handlers;
partial class DeliverooPlugin
internal sealed class ExchangeHandler
{
private void InteractWithQuartermaster(GameObject personnelOfficer, GameObject quartermaster)
private readonly DeliverooPlugin _plugin;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui;
private readonly IPluginLog _pluginLog;
public ExchangeHandler(DeliverooPlugin plugin, GameFunctions gameFunctions, ITargetManager targetManager,
IGameGui gameGui, IChatGui chatGui, IPluginLog pluginLog)
{
if (GetCurrentSealCount() < EffectiveReservedSealCount)
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_chatGui = chatGui;
_pluginLog = pluginLog;
}
public void InteractWithQuartermaster(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_gameFunctions.GetCurrentSealCount() < _plugin.EffectiveReservedSealCount)
{
CurrentStage = Stage.RequestStop;
_plugin.CurrentStage = Stage.RequestStop;
return;
}
if (_targetManager.Target == personnelOfficer)
return;
InteractWithTarget(quartermaster);
CurrentStage = Stage.SelectRewardTier;
_gameFunctions.InteractWithTarget(quartermaster);
_plugin.CurrentStage = Stage.SelectRewardTier;
}
private PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
public PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
{
foreach (PurchaseItemRequest request in _itemsToPurchaseNow)
foreach (PurchaseItemRequest request in _plugin.ItemsToPurchaseNow)
{
int toBuy = 0;
if (request == previousRequest)
{
toBuy = (int)request.StackSize;
if (request.ItemId != ItemIds.Venture && !_configuration.IgnoreCertainLimitations)
if (request.ItemId != ItemIds.Venture)
toBuy = Math.Min(toBuy, 99);
}
if (request.Type == Configuration.PurchaseType.KeepStocked)
{
if (GetItemCount(request.ItemId, request.CheckRetainerInventory) + toBuy < request.EffectiveLimit)
if (_gameFunctions.GetItemCount(request.ItemId, request.CheckRetainerInventory) + toBuy <
request.EffectiveLimit)
return request;
}
else
@ -53,12 +74,12 @@ partial class DeliverooPlugin
return null;
}
private unsafe void SelectRewardTier()
public unsafe void SelectRewardTier()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
_plugin.CurrentStage = Stage.CloseGcExchange;
return;
}
@ -79,17 +100,17 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectRank);
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectRewardSubCategory;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectRewardSubCategory;
}
}
private unsafe void SelectRewardSubCategory()
public unsafe void SelectRewardSubCategory()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
_plugin.CurrentStage = Stage.CloseGcExchange;
return;
}
@ -110,25 +131,25 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectType);
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectReward;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectReward;
}
}
private unsafe void SelectReward()
public unsafe void SelectReward()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
{
if (SelectRewardItem(addonExchange))
{
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.ConfirmReward;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.ConfirmReward;
}
else
{
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.CloseGcExchange;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.CloseGcExchange;
}
}
}
@ -146,13 +167,15 @@ partial class DeliverooPlugin
if (itemId == item.ItemId)
{
_pluginLog.Information($"Selecting item {itemId}, {i}");
long toBuy = (GetCurrentSealCount() - EffectiveReservedSealCount) / item.SealCost;
long toBuy = (_gameFunctions.GetCurrentSealCount() - _plugin.EffectiveReservedSealCount) /
item.SealCost;
if (item.Type == Configuration.PurchaseType.KeepStocked)
toBuy = Math.Min(toBuy, item.EffectiveLimit - GetItemCount(item.ItemId, item.CheckRetainerInventory));
toBuy = Math.Min(toBuy,
item.EffectiveLimit - _gameFunctions.GetItemCount(item.ItemId, item.CheckRetainerInventory));
else
toBuy = Math.Min(toBuy, item.EffectiveLimit);
if (item.ItemId != ItemIds.Venture && !_configuration.IgnoreCertainLimitations)
if (item.ItemId != ItemIds.Venture)
toBuy = Math.Min(toBuy, 99);
if (toBuy <= 0)
@ -186,7 +209,7 @@ partial class DeliverooPlugin
return false;
}
private unsafe void CloseGcExchange()
public unsafe void CloseGcExchange()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
@ -194,13 +217,16 @@ partial class DeliverooPlugin
addonExchange->FireCallbackInt(-1);
// If we just turned in the final item, there's no need to talk to the personnel officer again
if (_lastTurnInListSize == 1)
if (_plugin.LastTurnInListSize == 1)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
_plugin.TurnInState = false;
_plugin.CurrentStage = Stage.RequestStop;
}
else
CurrentStage = Stage.TargetPersonnelOfficer;
{
_plugin.ContinueAt = DateTime.Now.AddSeconds(1);
_plugin.CurrentStage = Stage.TargetPersonnelOfficer;
}
}
}
}

View File

@ -1,35 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Text.Payloads;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Deliveroo;
namespace Deliveroo.Handlers;
partial class DeliverooPlugin
internal sealed class SupplyHandler
{
private void InteractWithPersonnelOfficer(GameObject personnelOfficer, GameObject quartermaster)
{
if (_targetManager.Target == quartermaster)
return;
private readonly DeliverooPlugin _plugin;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IGameGui _gameGui;
private readonly IPluginLog _pluginLog;
InteractWithTarget(personnelOfficer);
CurrentStage = Stage.OpenGcSupply;
private uint _turnInErrors;
public SupplyHandler(DeliverooPlugin plugin, GameFunctions gameFunctions, ITargetManager targetManager,
IGameGui gameGui, IPluginLog pluginLog)
{
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_pluginLog = pluginLog;
}
private unsafe void SelectExpertDeliveryTab()
public void InteractWithPersonnelOfficer(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_targetManager.Target?.EntityId == quartermaster.EntityId)
return;
_gameFunctions.InteractWithTarget(personnelOfficer);
_plugin.CurrentStage = Stage.OpenGcSupply;
}
public unsafe void SelectExpertDeliveryTab()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonID();
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
@ -43,7 +60,7 @@ partial class DeliverooPlugin
{
_pluginLog.Information("Tab already selected, probably due to haseltweaks");
ResetTurnInErrorHandling();
CurrentStage = Stage.SelectItemToTurnIn;
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
return;
}
@ -56,23 +73,23 @@ partial class DeliverooPlugin
};
addon->FireCallback(3, selectExpertDeliveryTab);
ResetTurnInErrorHandling();
CurrentStage = Stage.SelectItemToTurnIn;
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
}
}
private void ResetTurnInErrorHandling(int listSize = int.MaxValue)
public void ResetTurnInErrorHandling(int listSize = int.MaxValue)
{
_pluginLog.Verbose("Resetting error handling state");
_lastTurnInListSize = listSize;
_plugin.LastTurnInListSize = listSize;
_turnInErrors = 0;
}
private unsafe void SelectItemToTurnIn()
public unsafe void SelectItemToTurnIn()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonID();
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
@ -82,29 +99,29 @@ partial class DeliverooPlugin
var addonGc = (AddonGrandCompanySupplyList*)addon;
if (addonGc->ExpertDeliveryList == null ||
!addonGc->ExpertDeliveryList->AtkComponentBase.OwnerNode->AtkResNode.IsVisible)
!addonGc->ExpertDeliveryList->AtkComponentBase.OwnerNode->AtkResNode.IsVisible())
return;
if (addonGc->SelectedTab != 2)
{
_turnInWindow.Error = "Wrong tab selected";
_plugin.TurnInError = "Wrong tab selected";
return;
}
ItemFilterType configuredFilter = ResolveSelectedSupplyFilter();
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter)
{
_turnInWindow.Error =
_plugin.TurnInError =
$"Wrong filter selected (expected {configuredFilter}, but is {(ItemFilterType)addonGc->SelectedFilter})";
return;
}
int currentListSize = addonGc->ExpertDeliveryList->ListLength;
if (addonGc->ListEmptyTextNode->AtkResNode.IsVisible || currentListSize == 0)
if (addonGc->ListEmptyTextNode->AtkResNode.IsVisible() || currentListSize == 0)
{
_pluginLog.Information(
$"No items to turn in ({addonGc->ListEmptyTextNode->AtkResNode.IsVisible}, {currentListSize})");
CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1);
return;
}
@ -113,29 +130,29 @@ partial class DeliverooPlugin
// something is wrong.
if (_turnInErrors > 10)
{
_turnInWindow.Error = "Unable to refresh item list";
_plugin.TurnInError = "Unable to refresh item list";
return;
}
if (currentListSize >= _lastTurnInListSize)
if (currentListSize >= _plugin.LastTurnInListSize)
{
_turnInErrors++;
_pluginLog.Information(
$"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_lastTurnInListSize}, new list size = {currentListSize})...");
$"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_plugin.LastTurnInListSize}, new list size = {currentListSize})...");
addon->FireCallbackInt(2);
_continueAt = DateTime.Now.AddSeconds(0.1);
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.1);
return;
}
ResetTurnInErrorHandling(currentListSize);
var agent = (AgentGrandCompanySupply*)agentInterface;
List<TurnInItem> items = BuildTurnInList(agent);
List<TurnInItem> items = _gameFunctions.BuildTurnInList(agent);
if (items.Count == 0)
{
// probably shouldn't happen with the previous node visibility check
CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1);
return;
}
@ -159,9 +176,9 @@ partial class DeliverooPlugin
// ---------------------------------------------------------------------------------------------------------
// TODO If we ever manage to obtain a mapping name to itemId here, we can try and exclude e.g. Red Onion
// Helms from being turned in.
if (GetCurrentSealCount() + items[0].SealsWithBonus > GetSealCap())
if (_gameFunctions.GetCurrentSealCount() + items[0].SealsWithBonus > _gameFunctions.GetSealCap())
{
CurrentStage = Stage.CloseGcSupplySelectString;
_plugin.CurrentStage = Stage.CloseGcSupplySelectString;
addon->FireCallbackInt(-1);
return;
}
@ -173,38 +190,11 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addon->FireCallback(3, selectFirstItem);
CurrentStage = Stage.TurnInSelected;
_plugin.CurrentStage = Stage.TurnInSelected;
}
}
private unsafe void TurnInSelectedItem()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward",
out var addonSupplyReward) && LAddon.IsAddonReady(&addonSupplyReward->AtkUnitBase))
{
string? itemName = addonSupplyReward->AtkUnitBase.AtkValues[4].ReadAtkString();
if (itemName != null && _itemCache.GetItemIdFromItemName(itemName)
.Any(itemId => DisabledTurnInItems.Contains(itemId)))
{
_chatGui.Print(new SeStringBuilder().Append("Won't turn in ")
.AddItemLink(_itemCache.GetItemIdFromItemName(itemName).First())
.Append(".")
.Build());
addonSupplyReward->AtkUnitBase.FireCallbackInt(1);
CurrentStage = Stage.CloseGcSupplyWindowThenStop;
return;
}
_pluginLog.Information($"Turning in '{itemName}'");
addonSupplyReward->AtkUnitBase.FireCallbackInt(0);
_continueAt = DateTime.Now.AddSeconds(0.58);
CurrentStage = Stage.FinalizeTurnIn;
}
}
private unsafe void FinalizeTurnInItem()
public unsafe void FinalizeTurnInItem()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var addonSupplyList) && LAddon.IsAddonReady(&addonSupplyList->AtkUnitBase))
@ -216,24 +206,27 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter);
CurrentStage = Stage.SelectItemToTurnIn;
if (_plugin.CurrentStage == Stage.FinalizeTurnIn)
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
else
_plugin.CurrentStage = Stage.RequestStop;
}
}
private ItemFilterType ResolveSelectedSupplyFilter()
{
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
if (_plugin.CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
return ItemFilterType.HideArmouryChestItems;
return ItemFilterType.HideGearSetItems;
}
private unsafe void CloseGcSupplyWindow()
public unsafe void CloseGcSupplyWindow()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonID();
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
@ -241,7 +234,7 @@ partial class DeliverooPlugin
if (addon == null || !LAddon.IsAddonReady(addon))
return;
CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1);
}
}

View File

@ -0,0 +1,12 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Deliveroo;
internal interface IDeliveryResult;
internal sealed class MessageDeliveryResult : IDeliveryResult
{
public SeString? Message { get; init; }
}
internal sealed class NoDeliveryResult : IDeliveryResult;

View File

@ -21,4 +21,6 @@ internal enum Stage
RequestStop,
Stopped,
SingleFinalizeTurnIn,
}

View File

@ -2,35 +2,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Deliveroo.GameData;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows;
internal sealed class ConfigWindow : LImGui.LWindow
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DeliverooPlugin _plugin;
private readonly Configuration _configuration;
private readonly GcRewardsCache _gcRewardsCache;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly IconCache _iconCache;
private readonly GameFunctions _gameFunctions;
private readonly IReadOnlyDictionary<uint, GcRewardItem> _itemLookup;
private string _searchString = string.Empty;
private uint _dragDropSource;
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog, IconCache iconCache)
private readonly List<(VirtualKey Key, string Label)> _keyCodes =
[
(VirtualKey.NO_KEY, "Disabled"),
(VirtualKey.CONTROL, "CTRL"),
(VirtualKey.SHIFT, "SHIFT"),
(VirtualKey.MENU, "ALT"),
];
private string _searchString = string.Empty;
private Configuration.PurchaseOption? _dragDropSource;
public ConfigWindow(IDalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog, IconCache iconCache,
GameFunctions gameFunctions)
: base("Deliveroo - Configuration###DeliverooConfig")
{
_pluginInterface = pluginInterface;
@ -40,6 +54,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
_clientState = clientState;
_pluginLog = pluginLog;
_iconCache = iconCache;
_gameFunctions = gameFunctions;
_itemLookup = _gcRewardsCache.RewardLookup;
@ -53,6 +68,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
};
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw()
{
if (_configuration.AddVentureIfNoItemToPurchaseSelected())
@ -72,58 +89,80 @@ internal sealed class ConfigWindow : LImGui.LWindow
{
if (ImGui.BeginTabItem("Items for Auto-Buy"))
{
uint? itemToRemove = null;
uint? itemToAdd = null;
Configuration.PurchaseOption? itemToRemove = null;
Configuration.PurchaseOption? itemToAdd = null;
int indexToAdd = 0;
if (ImGui.BeginChild("Items", new Vector2(-1, -30), true, ImGuiWindowFlags.NoSavedSettings))
if (ImGui.BeginChild("Items", new Vector2(-1, -ImGui.GetFrameHeightWithSpacing()), true,
ImGuiWindowFlags.NoSavedSettings))
{
for (int i = 0; i < _configuration.ItemsAvailableForPurchase.Count; ++i)
for (int i = 0; i < _configuration.ItemsAvailableToPurchase.Count; ++i)
{
uint itemId = _configuration.ItemsAvailableForPurchase[i];
var purchaseOption = _configuration.ItemsAvailableToPurchase[i];
ImGui.PushID($"###Item{i}");
ImGui.BeginDisabled(
_configuration.ItemsAvailableForPurchase.Count == 1 && itemId == ItemIds.Venture);
_configuration.ItemsAvailableToPurchase.Count == 1 && purchaseOption.ItemId == ItemIds.Venture);
var item = _itemLookup[itemId];
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId);
if (icon != null)
var item = _itemLookup[purchaseOption.ItemId];
using (var icon = _iconCache.GetIcon(item.IconId))
{
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
}
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.Selectable($"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}");
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = itemId;
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource > 0 &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
if (icon != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = 0;
ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X,
ImGui.GetStyle().ItemSpacing.Y / 2));
}
ImGui.EndDragDropTarget();
}
ImGui.Selectable(
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}{(purchaseOption.SameQuantityForAllLists ? $" {((SeIconChar)57412).ToIconString()} (Limit: {purchaseOption.GlobalLimit:N0})" : "")}",
false, ImGuiSelectableFlags.SpanAllColumns);
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopup($"###ctx{i}"))
{
if (ImGui.Selectable($"Remove {_itemLookup[itemId].Name}"))
itemToRemove = itemId;
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = purchaseOption;
ImGui.EndPopup();
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource != null &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = null;
}
ImGui.EndDragDropTarget();
}
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopup($"###ctx{i}"))
{
bool sameQuantityForAllLists = purchaseOption.SameQuantityForAllLists;
if (ImGui.MenuItem("Use same quantity for global and character-specific lists", null,
ref sameQuantityForAllLists))
{
purchaseOption.SameQuantityForAllLists = sameQuantityForAllLists;
Save();
}
if (ImGui.MenuItem($"Remove {_itemLookup[purchaseOption.ItemId].Name}"))
itemToRemove = purchaseOption;
ImGui.EndPopup();
}
if (icon != null)
{
ImGui.SameLine(0, 0);
ImGui.SetCursorPos(pos);
ImGui.Image(icon.ImGuiHandle, iconSize);
}
}
ImGui.EndDisabled();
@ -135,20 +174,20 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (itemToRemove != null)
{
_configuration.ItemsAvailableForPurchase.Remove(itemToRemove.Value);
_configuration.ItemsAvailableToPurchase.Remove(itemToRemove);
Save();
}
if (itemToAdd != null)
{
_configuration.ItemsAvailableForPurchase.Remove(itemToAdd.Value);
_configuration.ItemsAvailableForPurchase.Insert(indexToAdd, itemToAdd.Value);
_configuration.ItemsAvailableToPurchase.Remove(itemToAdd);
_configuration.ItemsAvailableToPurchase.Insert(indexToAdd, itemToAdd);
Save();
}
List<(uint ItemId, string Name, ushort IconId, bool Limited)> comboValues = _gcRewardsCache.Rewards
.Where(x => x.SubCategory is RewardSubCategory.Materials or RewardSubCategory.Materiel)
.Where(x => !_configuration.ItemsAvailableForPurchase.Contains(x.ItemId))
.Where(x => _configuration.ItemsAvailableToPurchase.All(y => y.ItemId != x.ItemId))
.Select(x => (x.ItemId, x.Name, x.IconId, x.Limited))
.OrderBy(x => x.Name)
.ThenBy(x => x.GetHashCode())
@ -160,14 +199,17 @@ internal sealed class ConfigWindow : LImGui.LWindow
bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue);
foreach (var item in comboValues.Where(x => x.Name.ToLower().Contains(_searchString.ToLower())))
foreach (var item in comboValues.Where(x =>
x.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase)))
{
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId);
if (icon != null)
using (var icon = _iconCache.GetIcon(item.IconId))
{
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.X);
}
}
bool addThis =
@ -175,7 +217,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}##SelectVenture{item.IconId}");
if (addThis || addFirst)
{
_configuration.ItemsAvailableForPurchase.Add(item.ItemId);
_configuration.ItemsAvailableToPurchase.Add(new Configuration.PurchaseOption
{ ItemId = item.ItemId });
if (addFirst)
{
@ -201,7 +244,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
{
string currentCharacterName = _clientState.LocalPlayer!.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
ImGui.Text($"Current Character: {currentCharacterName} @ {currentWorldName}");
ImGui.Spacing();
ImGui.Separator();
@ -314,7 +357,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (ImGui.InputInt("Minimum Seals to keep (e.g. for Squadron Missions)", ref reservedSealCount, 1000))
{
_configuration.ReservedSealCount =
Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCount));
Math.Max(0, Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCount));
Save();
}
@ -328,21 +371,46 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (reserveDifferentSealCountAtMaxRank)
{
ImGui.Indent();
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.Indent(indentSize);
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100);
int reservedSealCountAtMaxRank = _configuration.ReservedSealCountAtMaxRank;
if (ImGui.InputInt("Minimum seals to keep at max rank", ref reservedSealCountAtMaxRank))
{
_configuration.ReservedSealCountAtMaxRank = Math.Max(0,
Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCountAtMaxRank));
Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCountAtMaxRank));
Save();
}
ImGui.Unindent();
ImGui.Unindent(indentSize);
}
ImGui.EndDisabled();
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
int selectedKey = Math.Max(0, _keyCodes.FindIndex(x => x.Key == _configuration.QuickTurnInKey));
if (ImGui.Combo("Quick Turn-In Key", ref selectedKey, _keyCodes.Select(x => x.Label).ToArray(),
_keyCodes.Count))
{
_configuration.QuickTurnInKey = _keyCodes[selectedKey].Key;
Save();
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
var xivChatTypes = Enum.GetValues<XivChatType>()
.Where(x => x != XivChatType.StandardEmote)
.ToArray();
var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.ChatType);
string[] chatTypeNames = xivChatTypes
.Select(t => t.GetAttribute<XivChatTypeInfoAttribute>()?.FancyName ?? t.ToString())
.ToArray();
if (ImGui.Combo("Chat channel for status updates", ref selectedChatType, chatTypeNames,
chatTypeNames.Length))
{
_configuration.ChatType = xivChatTypes[selectedChatType];
Save();
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
List<(int Rank, string Name)> rankUpComboValues = Enumerable.Range(1, 30)
.Select(x => x == 1 ? (0, "---") : (x, $"Rank {x}"))
@ -366,10 +434,33 @@ internal sealed class ConfigWindow : LImGui.LWindow
Save();
}
ImGui.Separator();
bool disableFrameLimiter = _configuration.DisableFrameLimiter;
if (ImGui.Checkbox("Disable the game setting 'Limit frame rate when client is inactive'",
ref disableFrameLimiter))
{
_configuration.DisableFrameLimiter = disableFrameLimiter;
Save();
}
ImGui.Indent();
ImGui.BeginDisabled(!disableFrameLimiter);
bool uncapFrameRate = _configuration.UncapFrameRate;
if (ImGui.Checkbox("Set frame rate to uncapped", ref uncapFrameRate))
{
_configuration.UncapFrameRate = uncapFrameRate;
Save();
}
ImGui.EndDisabled();
ImGui.Unindent();
ImGui.EndTabItem();
}
}
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
public void SaveWindowConfig() => Save();
}

View File

@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
@ -15,10 +18,11 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows;
internal sealed class TurnInWindow : LImGui.LWindow
internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configuration.MinimizableWindowConfig>
{
private static readonly IReadOnlyList<InventoryType> InventoryTypes = new[]
{
@ -43,16 +47,23 @@ internal sealed class TurnInWindow : LImGui.LWindow
private static readonly string[] StockingTypeLabels = { "Purchase Once", "Keep in Stock" };
private readonly DeliverooPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly GcRewardsCache _gcRewardsCache;
private readonly ConfigWindow _configWindow;
private readonly IconCache _iconCache;
private readonly IKeyState _keyState;
private readonly GameFunctions _gameFunctions;
private readonly TitleBarButton _minimizeButton;
public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration,
private bool _state;
private Guid? _draggedItem;
public TurnInWindow(DeliverooPlugin plugin, IDalamudPluginInterface pluginInterface, Configuration configuration,
ICondition condition, IClientState clientState, GcRewardsCache gcRewardsCache, ConfigWindow configWindow,
IconCache iconCache)
IconCache iconCache, IKeyState keyState, GameFunctions gameFunctions)
: base("GC Delivery###DeliverooTurnIn")
{
_plugin = plugin;
@ -61,7 +72,10 @@ internal sealed class TurnInWindow : LImGui.LWindow
_condition = condition;
_clientState = clientState;
_gcRewardsCache = gcRewardsCache;
_configWindow = configWindow;
_iconCache = iconCache;
_keyState = keyState;
_gameFunctions = gameFunctions;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver;
@ -72,10 +86,24 @@ internal sealed class TurnInWindow : LImGui.LWindow
MaximumSize = new Vector2(500, 999),
};
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse;
State = false;
ShowCloseButton = false;
AllowClickthrough = false;
_minimizeButton = new TitleBarButton
{
Icon = FontAwesomeIcon.Minus,
Priority = int.MinValue,
IconOffset = new Vector2(1.5f, 1),
Click = _ =>
{
IsMinimized = !IsMinimized;
_minimizeButton!.Icon = IsMinimized ? FontAwesomeIcon.WindowMaximize : FontAwesomeIcon.Minus;
},
AvailableClickthrough = true,
};
TitleBarButtons.Insert(0, _minimizeButton);
TitleBarButtons.Add(new TitleBarButton
{
Icon = FontAwesomeIcon.Cog,
@ -91,7 +119,31 @@ internal sealed class TurnInWindow : LImGui.LWindow
});
}
public bool State { get; set; }
public Configuration.MinimizableWindowConfig WindowConfig => _configuration.TurnInWindowConfig;
private bool IsMinimized
{
get => WindowConfig.IsMinimized;
set
{
WindowConfig.IsMinimized = value;
SaveWindowConfig();
}
}
public bool State
{
get => _state;
set
{
_state = value;
if (value)
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse;
else
Flags = ImGuiWindowFlags.AlwaysAutoResize;
}
}
public decimal Multiplier { private get; set; }
public string Error { private get; set; } = string.Empty;
@ -100,7 +152,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
private bool IsOnHomeWorld =>
_clientState.LocalPlayer == null ||
_clientState.LocalPlayer.HomeWorld.Id == _clientState.LocalPlayer.CurrentWorld.Id;
_clientState.LocalPlayer.HomeWorld.RowId == _clientState.LocalPlayer.CurrentWorld.RowId;
private IItemsToPurchase ItemsWrapper => UseCharacterSpecificItemsToPurchase
? new CharacterSpecificItemsToPurchase(_plugin.CharacterConfiguration!, _pluginInterface)
@ -110,11 +162,11 @@ internal sealed class TurnInWindow : LImGui.LWindow
{
get
{
GrandCompany grandCompany = _plugin.GetGrandCompany();
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None)
return new List<PurchaseItemRequest>();
var rank = _plugin.GetGrandCompanyRank();
var rank = _gameFunctions.GetGrandCompanyRank();
return ItemsWrapper.GetItemsToPurchase()
.Where(x => x.ItemId != GcRewardItem.None.ItemId)
.Where(x => x.Enabled)
@ -124,13 +176,16 @@ internal sealed class TurnInWindow : LImGui.LWindow
.Where(x => x.Reward.RequiredRank <= rank)
.Select(x =>
{
var purchaseOption = _configuration.ItemsAvailableToPurchase.Where(y => y.SameQuantityForAllLists)
.FirstOrDefault(y => y.ItemId == x.Item.ItemId);
int limit = purchaseOption?.GlobalLimit ?? x.Item.Limit;
var request = new PurchaseItemRequest
{
ItemId = x.Item.ItemId,
Name = x.Reward.Name,
EffectiveLimit = CalculateEffectiveLimit(
x.Item.ItemId,
x.Item.Limit <= 0 ? uint.MaxValue : (uint)x.Item.Limit,
limit <= 0 ? uint.MaxValue : (uint)limit,
x.Reward.StackSize,
x.Reward.InventoryLimit),
SealCost = x.Reward.SealCost,
@ -158,7 +213,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
public override unsafe void Draw()
{
GrandCompany grandCompany = _plugin.GetGrandCompany();
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None)
{
// not sure we should ever get here
@ -166,17 +221,19 @@ internal sealed class TurnInWindow : LImGui.LWindow
return;
}
if (_plugin.GetGrandCompanyRank() < 6)
if (_gameFunctions.GetGrandCompanyRank() < 6)
{
State = false;
ImGui.TextColored(ImGuiColors.DalamudRed, "You do not have the required rank for Expert Delivery.");
DrawNextRankPrequesites();
return;
}
else if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.DisableTurnIn &&
!IsOnHomeWorld)
{
State = false;
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not on your home world.");
ImGui.TextColored(ImGuiColors.DalamudRed, "Turn-in disabled, you are not on your home world.");
return;
}
@ -186,55 +243,133 @@ internal sealed class TurnInWindow : LImGui.LWindow
State = state;
}
ImGui.Indent(27);
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
if (!string.IsNullOrEmpty(Error))
{
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
using (ImRaii.PushIndent(indentSize))
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
}
else
{
if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning && !IsOnHomeWorld)
using (ImRaii.PushIndent(indentSize))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"Turn-In disabled, you are not on your home world and will not earn FC points.");
}
if (Multiplier == 1m)
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff.");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
}
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning &&
!IsOnHomeWorld)
{
ImGui.BeginDisabled(_condition[ConditionFlag.OccupiedInQuestEvent] ||
_condition[ConditionFlag.Casting]);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Bolt, "Use Priority Seal Allowance (15%)"))
{
AgentInventoryContext.Instance()->UseItem(ItemIds.PrioritySealAllowance);
}
ImGui.TextColored(ImGuiColors.DalamudRed,
"You are not on your home world and will not earn FC points.");
}
ImGui.EndDisabled();
if (Multiplier == 1m)
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff.");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
}
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
AgentInventoryContext* agentInventoryContext = AgentInventoryContext.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
{
ImGui.BeginDisabled(_condition[ConditionFlag.OccupiedInQuestEvent] ||
_condition[ConditionFlag.Casting] ||
agentInventoryContext == null);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Bolt,
"Use Priority Seal Allowance (15%)"))
{
agentInventoryContext->UseItem(ItemIds.PrioritySealAllowance);
}
ImGui.EndDisabled();
}
}
}
ImGui.Unindent(27);
ImGui.Separator();
ImGui.BeginDisabled(state);
DrawItemsToBuy(grandCompany);
ImGui.EndDisabled();
if (!IsMinimized)
{
ImGui.Separator();
using (ImRaii.Disabled(state))
DrawItemsToBuy(grandCompany);
}
}
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
if (_configuration.QuickTurnInKey != VirtualKey.NO_KEY)
{
var key = _configuration.QuickTurnInKey switch
{
VirtualKey.MENU => "ALT",
VirtualKey.SHIFT => "SHIFT",
VirtualKey.CONTROL => "CTRL",
_ => _configuration.QuickTurnInKey.ToString()
};
if (!State && _keyState[_configuration.QuickTurnInKey])
{
ImGui.Separator();
ImGui.TextColored(ImGuiColors.HealerGreen, "Click an item to turn it in without confirmation");
}
else if (!IsMinimized)
{
ImGui.Separator();
ImGui.Text($"Hold '{key}' when clicking an item to turn it in without confirmation.");
}
}
if (!IsMinimized)
{
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
}
}
private unsafe void DrawNextRankPrequesites()
{
string? rankName = _gameFunctions.GetNextGrandCompanyRankName();
if (rankName != null)
{
int currentSeals = _gameFunctions.GetCurrentSealCount();
uint requiredSeals = _gameFunctions.GetSealsRequiredForNextRank();
int currentHuntingLog =
MonsterNoteManager.Instance()->RankData[(int)_gameFunctions.GetGrandCompany() + 7]
.Rank;
byte requiredHuntingLog = _gameFunctions.GetRequiredHuntingLogForNextRank();
bool enoughSeals = currentSeals >= requiredSeals;
bool enoughHuntingLog = currentHuntingLog >= requiredHuntingLog;
if (enoughSeals && enoughHuntingLog)
ImGui.TextColored(ImGuiColors.HealerGreen, $"You meet all requirements to rank up to {rankName}.");
else
ImGui.Text($"Ranking up to {rankName} requires:");
ImGui.Indent();
if (enoughSeals)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
ImGui.BulletText($"{currentSeals:N0} / {requiredSeals:N0} GC seals");
ImGui.PopStyleColor();
}
else
ImGui.BulletText($"{currentSeals:N0} / {requiredSeals:N0} GC seals");
if (requiredHuntingLog > 0)
{
if (enoughHuntingLog)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
ImGui.BulletText($"Complete Hunting Log #{requiredHuntingLog}");
ImGui.PopStyleColor();
}
else
ImGui.BulletText($"Complete Hunting Log #{requiredHuntingLog}");
}
ImGui.Unindent();
}
}
private void DrawItemsToBuy(GrandCompany grandCompany)
@ -246,13 +381,20 @@ internal sealed class TurnInWindow : LImGui.LWindow
{
(GcRewardItem.None, GcRewardItem.None.Name, GcRewardItem.None.Name),
};
foreach (uint itemId in _configuration.ItemsAvailableForPurchase)
foreach (Configuration.PurchaseOption purchaseOption in _configuration.ItemsAvailableToPurchase)
{
var gcReward = _gcRewardsCache.GetReward(itemId);
int itemCountWithoutRetainers = _plugin.GetItemCount(itemId, false);
int itemCountWithRetainers = _plugin.GetItemCount(itemId, true);
var gcReward = _gcRewardsCache.GetReward(purchaseOption.ItemId);
int itemCountWithoutRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, false);
int itemCountWithRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, true);
string itemNameWithoutRetainers = gcReward.Name;
string itemNameWithRetainers = gcReward.Name;
if (purchaseOption.SameQuantityForAllLists)
{
itemNameWithoutRetainers += $" {((SeIconChar)57412).ToIconString()}";
itemNameWithRetainers += $" {((SeIconChar)57412).ToIconString()}";
}
if (itemCountWithoutRetainers > 0)
itemNameWithoutRetainers += $" ({itemCountWithoutRetainers:N0})";
if (itemCountWithRetainers > 0)
@ -266,161 +408,42 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save();
}
int? itemToRemove = null;
Configuration.PurchasePriority? itemToRemove = null;
Configuration.PurchasePriority? itemToAdd = null;
int indexToAdd = 0;
float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < itemsWrapper.GetItemsToPurchase().Count; ++i)
{
ImGui.PushID($"ItemToBuy{i}");
Configuration.PurchasePriority item = itemsWrapper.GetItemsToPurchase()[i];
DrawItemToBuy(grandCompany, i, itemsWrapper, comboValues, width, ref itemToRemove, itemPositions);
}
float indentX = ImGui.GetCursorPosX();
bool enabled = item.Enabled;
int popColors = 0;
if (!enabled)
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = null;
else if (_draggedItem != null)
{
var items = itemsWrapper.GetItemsToPurchase().ToList();
var draggedItem = items.Single(x => x.InternalId == _draggedItem);
int oldIndex = items.IndexOf(draggedItem);
var (topLeft, bottomRight) = itemPositions[oldIndex];
ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f,
ImDrawFlags.RoundCornersAll);
int newIndex = itemPositions.FindIndex(x => ImGui.IsMouseHoveringRect(x.TopLeft, x.BottomRight, true));
if (newIndex >= 0 && oldIndex != newIndex)
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.5f, 0.35f, 1f));
popColors++;
itemToAdd = items.Single(x => x.InternalId == _draggedItem);
indexToAdd = newIndex;
}
}
if (ImGui.Button($"{item.GetIcon()}"))
ImGui.OpenPopup($"Configure{i}");
ImGui.PopStyleColor(popColors);
if (ImGui.BeginPopup($"Configure{i}"))
{
if (ImGui.Checkbox($"Enabled##Enabled{i}", ref enabled))
{
item.Enabled = enabled;
itemsWrapper.Save();
}
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
int type = (int)item.Type;
if (ImGui.Combo($"##Type{i}", ref type, StockingTypeLabels, StockingTypeLabels.Length))
{
item.Type = (Configuration.PurchaseType)type;
if (item.Type != Configuration.PurchaseType.KeepStocked)
item.CheckRetainerInventory = false;
itemsWrapper.Save();
}
if (item.Type == Configuration.PurchaseType.KeepStocked && item.ItemId != ItemIds.Venture)
{
bool checkRetainerInventory = item.CheckRetainerInventory;
if (ImGui.Checkbox("Check Retainer Inventory for items (requires AllaganTools)",
ref checkRetainerInventory))
{
item.CheckRetainerInventory = checkRetainerInventory;
itemsWrapper.Save();
}
}
ImGui.EndPopup();
}
ImGui.SameLine(0, 3);
ImGui.BeginDisabled(!enabled);
int comboValueIndex = comboValues.FindIndex(x => x.Item.ItemId == item.ItemId);
if (comboValueIndex < 0)
{
item.ItemId = 0;
item.Limit = 0;
itemsWrapper.Save();
comboValueIndex = 0;
}
var comboItem = comboValues[comboValueIndex];
IDalamudTextureWrap? icon = _iconCache.GetIcon(comboItem.Item.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine(0, 3);
}
indentX = ImGui.GetCursorPosX() - indentX;
if (ImGui.Combo("", ref comboValueIndex,
comboValues.Select(x => item.CheckRetainerInventory ? x.NameWithRetainers : x.NameWithoutRetainers)
.ToArray(), comboValues.Count))
{
comboItem = comboValues[comboValueIndex];
item.ItemId = comboItem.Item.ItemId;
itemsWrapper.Save();
}
ImGui.EndDisabled();
if (itemsWrapper.GetItemsToPurchase().Count >= 2)
{
ImGui.SameLine();
if (ImGuiComponents.IconButton($"##Up{i}", FontAwesomeIcon.ArrowUp))
{
itemToAdd = item;
if (i > 0)
indexToAdd = i - 1;
else
indexToAdd = itemsWrapper.GetItemsToPurchase().Count - 1;
}
ImGui.SameLine(0, 0);
if (ImGuiComponents.IconButton($"##Down{i}", FontAwesomeIcon.ArrowDown))
{
itemToAdd = item;
if (i < itemsWrapper.GetItemsToPurchase().Count - 1)
indexToAdd = i + 1;
else
indexToAdd = 0;
}
ImGui.SameLine();
if (ImGuiComponents.IconButton($"###Remove{i}", FontAwesomeIcon.Times))
itemToRemove = i;
}
if (enabled)
{
ImGui.Indent(indentX);
if (comboValueIndex > 0)
{
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 130);
int limit = Math.Min(item.Limit, (int)comboItem.Item.InventoryLimit);
int stepSize = comboItem.Item.StackSize < 99 ? 1 : 50;
string label = item.Type == Configuration.PurchaseType.KeepStocked
? "Maximum items to buy"
: "Remaining items to buy";
if (ImGui.InputInt(label, ref limit, stepSize, stepSize * 10))
{
item.Limit = Math.Min(Math.Max(0, limit), (int)comboItem.Item.InventoryLimit);
itemsWrapper.Save();
}
}
else if (item.Limit != 0)
{
item.Limit = 0;
itemsWrapper.Save();
}
if (comboValueIndex > 0)
{
if (!comboItem.Item.GrandCompanies.Contains(grandCompany))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, as you are in the wrong Grand Company.");
}
else if (comboItem.Item.RequiredRank > _plugin.GetGrandCompanyRank())
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, your rank isn't high enough to buy it.");
}
}
ImGui.Unindent(indentX);
}
ImGui.PopID();
if (itemToRemove != null)
{
itemsWrapper.Remove(itemToRemove);
itemsWrapper.Save();
}
if (itemToAdd != null)
@ -430,27 +453,22 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save();
}
if (itemToRemove != null)
{
itemsWrapper.RemoveAt(itemToRemove.Value);
itemsWrapper.Save();
}
if (_configuration.ItemsAvailableForPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x != y.ItemId)))
if (_configuration.ItemsAvailableToPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x.ItemId != y.ItemId)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item"))
ImGui.OpenPopup("##AddItem");
if (ImGui.BeginPopupContextItem("##AddItem", ImGuiPopupFlags.NoOpenOverItems))
{
foreach (var itemId in _configuration.ItemsAvailableForPurchase.Distinct())
foreach (var purchaseOption in _configuration.ItemsAvailableToPurchase.Distinct())
{
if (_gcRewardsCache.RewardLookup.TryGetValue(itemId, out var reward))
if (_gcRewardsCache.RewardLookup.TryGetValue(purchaseOption.ItemId, out var reward))
{
if (ImGui.MenuItem($"{reward.Name}##{itemId}"))
if (ImGui.MenuItem($"{reward.Name}##{purchaseOption.ItemId}"))
{
itemsWrapper.Add(new Configuration.PurchasePriority { ItemId = itemId, Limit = 0 });
itemsWrapper.Add(new Configuration.PurchasePriority
{ ItemId = purchaseOption.ItemId, Limit = 0 });
itemsWrapper.Save();
ImGui.CloseCurrentPopup();
}
@ -459,7 +477,210 @@ internal sealed class TurnInWindow : LImGui.LWindow
ImGui.EndPopup();
}
ImGui.SameLine();
}
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, "Configure available Items"))
_configWindow.IsOpen = true;
}
private void DrawItemToBuy(GrandCompany grandCompany, int i, IItemsToPurchase itemsWrapper,
List<(GcRewardItem Item, string NameWithoutRetainers, string NameWithRetainers)> comboValues, float width,
ref Configuration.PurchasePriority? itemToRemove, List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions)
{
Vector2 topLeft = ImGui.GetCursorScreenPos() + new Vector2(0, -ImGui.GetStyle().ItemSpacing.Y / 2);
ImGui.PushID($"ItemToBuy{i}");
Configuration.PurchasePriority item = itemsWrapper.GetItemsToPurchase()[i];
float indentX = ImGui.GetCursorPosX();
bool enabled = item.Enabled;
int popColors = 0;
if (!enabled)
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.5f, 0.35f, 1f));
popColors++;
}
if (ImGui.Button($"{item.GetIcon()}"))
ImGui.OpenPopup($"Configure{i}");
ImGui.PopStyleColor(popColors);
if (ImGui.BeginPopup($"Configure{i}"))
{
if (ImGui.Checkbox($"Enabled##Enabled{i}", ref enabled))
{
item.Enabled = enabled;
itemsWrapper.Save();
}
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
int type = (int)item.Type;
if (ImGui.Combo($"##Type{i}", ref type, StockingTypeLabels, StockingTypeLabels.Length))
{
item.Type = (Configuration.PurchaseType)type;
if (item.Type != Configuration.PurchaseType.KeepStocked)
item.CheckRetainerInventory = false;
itemsWrapper.Save();
}
if (item.Type == Configuration.PurchaseType.KeepStocked && item.ItemId != ItemIds.Venture)
{
bool checkRetainerInventory = item.CheckRetainerInventory;
if (ImGui.Checkbox("Check Retainer Inventory for items (requires AllaganTools)",
ref checkRetainerInventory))
{
item.CheckRetainerInventory = checkRetainerInventory;
itemsWrapper.Save();
}
}
ImGui.EndPopup();
}
ImGui.SameLine(0, 3);
ImGui.BeginDisabled(!enabled);
int comboValueIndex = comboValues.FindIndex(x => x.Item.ItemId == item.ItemId);
if (comboValueIndex < 0)
{
item.ItemId = 0;
item.Limit = 0;
itemsWrapper.Save();
comboValueIndex = 0;
}
var comboItem = comboValues[comboValueIndex];
using (var icon = _iconCache.GetIcon(comboItem.Item.IconId))
{
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, 3);
}
}
indentX = ImGui.GetCursorPosX() - indentX;
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SetNextItemWidth(width -
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.Circle.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 8 -
ImGui.GetStyle().ItemSpacing.X * 2 - 3 * 3);
ImGui.PopFont();
if (ImGui.Combo("", ref comboValueIndex,
comboValues.Select(x => item.CheckRetainerInventory ? x.NameWithRetainers : x.NameWithoutRetainers)
.ToArray(), comboValues.Count))
{
comboItem = comboValues[comboValueIndex];
item.ItemId = comboItem.Item.ItemId;
itemsWrapper.Save();
}
ImGui.EndDisabled();
if (itemsWrapper.GetItemsToPurchase().Count >= 2)
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 4 -
ImGui.GetStyle().ItemSpacing.X);
ImGui.PopFont();
if (_draggedItem == item.InternalId)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown,
ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = item.InternalId;
ImGui.SameLine();
}
else
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 2);
ImGui.PopFont();
}
if (ImGuiComponents.IconButton($"###Remove{i}", FontAwesomeIcon.Times))
itemToRemove = item;
if (enabled)
{
ImGui.Indent(indentX);
if (comboValueIndex > 0)
{
var purchaseOption =
_configuration.ItemsAvailableToPurchase
.Where(x => x.SameQuantityForAllLists)
.FirstOrDefault(x => x.ItemId == item.ItemId);
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 130);
int limit = Math.Min(purchaseOption?.GlobalLimit ?? item.Limit, (int)comboItem.Item.InventoryLimit);
int stepSize = comboItem.Item.StackSize < 99 ? 1 : 50;
string label = item.Type == Configuration.PurchaseType.KeepStocked
? "Maximum items to buy"
: "Remaining items to buy";
if (ImGui.InputInt(label, ref limit, stepSize, stepSize * 10))
{
int newLimit = Math.Min(Math.Max(0, limit), (int)comboItem.Item.InventoryLimit);
if (purchaseOption != null)
{
purchaseOption.GlobalLimit = newLimit;
_pluginInterface.SavePluginConfig(_configuration);
}
else
{
item.Limit = newLimit;
itemsWrapper.Save();
}
}
}
else if (item.Limit != 0)
{
item.Limit = 0;
itemsWrapper.Save();
}
if (comboValueIndex > 0)
{
if (!comboItem.Item.GrandCompanies.Contains(grandCompany))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, as you are in the wrong Grand Company.");
}
else if (comboItem.Item.RequiredRank > _gameFunctions.GetGrandCompanyRank())
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, your rank isn't high enough to buy it.");
}
}
ImGui.Unindent(indentX);
}
ImGui.PopID();
Vector2 bottomRight = new Vector2(topLeft.X + width,
ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2);
itemPositions.Add((topLeft, bottomRight));
}
private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize, uint inventoryLimit)
@ -476,7 +697,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
for (int i = 0; i < container->Size; ++i)
{
var item = container->GetInventorySlot(i);
if (item == null || item->ItemID == 0 || item->ItemID == itemId)
if (item == null || item->ItemId == 0 || item->ItemId == itemId)
{
slotsThatCanBeUsed++;
}
@ -487,6 +708,8 @@ internal sealed class TurnInWindow : LImGui.LWindow
}
}
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
private interface IItemsToPurchase
{
string Name { get; }
@ -502,10 +725,10 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class CharacterSpecificItemsToPurchase : IItemsToPurchase
{
private readonly CharacterConfiguration _characterConfiguration;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
public CharacterSpecificItemsToPurchase(CharacterConfiguration characterConfiguration,
DalamudPluginInterface pluginInterface)
IDalamudPluginInterface pluginInterface)
{
_characterConfiguration = characterConfiguration;
_pluginInterface = pluginInterface;
@ -535,9 +758,9 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class GlobalItemsToPurchase : IItemsToPurchase
{
private readonly Configuration _configuration;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
public GlobalItemsToPurchase(Configuration configuration, DalamudPluginInterface pluginInterface)
public GlobalItemsToPurchase(Configuration configuration, IDalamudPluginInterface pluginInterface)
{
_configuration = configuration;
_pluginInterface = pluginInterface;

View File

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

2
LLib

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

View File

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