Compare commits

...

40 Commits

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
f0eab9cdbe
Trying to turn in a Red Onion Helm should close the supply window/disable the turn in instead of being stuck 2024-02-17 09:58:50 +01:00
0161f3df31
Ensure retainer items are cached 2024-01-30 17:21:02 +01:00
9f91da23b6
Add 'check retainer inventory' option 2024-01-28 13:37:38 +01:00
11420ab403
Add 'purchase once'/'keep stocked' option as in ARC 2024-01-28 08:17:19 +01:00
eea9825723
Fix logic when buying a limited amount of items with stacksize 99 2024-01-17 23:13:35 +01:00
fe4637e941
Update icon URL 2024-01-16 06:49:29 +01:00
2c19f59ffb
Experimental (hardcoded) blacklist for items that won't be turned in 2024-01-13 20:48:03 +01:00
b8520ffc13
Fix turn-in attempts sometimes only exchanging seals, but not turning in items 2024-01-07 16:36:15 +01:00
c165b77d8f
Don't target Personnel Officer as last step (if all items have been delivered) 2023-12-30 21:54:38 +01:00
b0888ba0cb
Include item link in chat output 2023-12-24 00:22:04 +01:00
31 changed files with 2740 additions and 681 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 bool OverrideItemsToPurchase { get; set; }
public List<Configuration.PurchasePriority> ItemsToPurchase { get; set; } = new(); 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"); => 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); string path = ResolveFilePath(pluginInterface, localContentId);
if (!File.Exists(path)) if (!File.Exists(path))
@ -29,11 +29,11 @@ internal sealed class CharacterConfiguration
return JsonConvert.DeserializeObject<CharacterConfiguration>(File.ReadAllText(path)); 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)); 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)); File.Delete(ResolveFilePath(pluginInterface, LocalContentId));
} }

View File

@ -1,41 +1,77 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Deliveroo.GameData; using Deliveroo.GameData;
using LLib.ImGui;
namespace Deliveroo; namespace Deliveroo;
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 2;
public List<uint> ItemsAvailableForPurchase { get; set; } = new(); [Obsolete]
public List<PurchasePriority> ItemsToPurchase { get; set; } = new(); public List<uint> ItemsAvailableForPurchase { get; set; } = [];
public List<PurchaseOption> ItemsAvailableToPurchase { get; set; } = [];
public List<PurchasePriority> ItemsToPurchase { get; set; } = [];
public int ReservedSealCount { get; set; } public int ReservedSealCount { get; set; }
public bool ReserveDifferentSealCountAtMaxRank { get; set; } public bool ReserveDifferentSealCountAtMaxRank { get; set; }
public int ReservedSealCountAtMaxRank { get; set; } public int ReservedSealCountAtMaxRank { get; set; }
public XivChatType ChatType { get; set; } = XivChatType.Debug;
public int PauseAtRank { get; set; } public int PauseAtRank { get; set; }
public EBehaviorOnOtherWorld BehaviorOnOtherWorld { get; set; } = EBehaviorOnOtherWorld.Warning; 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> public MinimizableWindowConfig TurnInWindowConfig { get; } = new();
/// A config-only setting, not exposed in the UI. public WindowConfig ConfigWindowConfig { get; } = new();
///
/// If set, buys all GC items in their max quantity (otherwise, everything except ventures is capped to 99). internal sealed class PurchaseOption
/// </summary> {
public bool IgnoreCertainLimitations { get; set; } = false; public uint ItemId { get; set; }
public bool SameQuantityForAllLists { get; set; }
public int GlobalLimit { get; set; }
}
internal sealed class PurchasePriority internal sealed class PurchasePriority
{ {
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
public uint ItemId { get; set; } public uint ItemId { get; set; }
public int Limit { get; set; } public int Limit { get; set; }
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public PurchaseType Type { get; set; } = PurchaseType.KeepStocked;
public bool CheckRetainerInventory { get; set; }
public string GetIcon()
{
return Type switch
{
PurchaseType.PurchaseOneTime => SeIconChar.BoxedNumber1.ToIconString(),
PurchaseType.KeepStocked => SeIconChar.Circle.ToIconString(),
_ => SeIconChar.BoxedQuestionMark.ToIconString(),
};
}
}
public enum PurchaseType
{
PurchaseOneTime,
KeepStocked,
} }
public bool AddVentureIfNoItemToPurchaseSelected() public bool AddVentureIfNoItemToPurchaseSelected()
{ {
if (ItemsAvailableForPurchase.Count == 0) if (ItemsAvailableToPurchase.Count == 0)
{ {
ItemsAvailableForPurchase.Add(ItemIds.Venture); ItemsAvailableToPurchase.Add(new PurchaseOption { ItemId = ItemIds.Venture });
return true; return true;
} }
@ -48,4 +84,9 @@ internal sealed class Configuration : IPluginConfiguration
Warning, Warning,
DisableTurnIn, DisableTurnIn,
} }
internal sealed class MinimizableWindowConfig : WindowConfig
{
public bool IsMinimized { get; set; }
}
} }

View File

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

View File

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

View File

@ -4,5 +4,5 @@
"Punchline": "Better Grand Company Deliveries", "Punchline": "Better Grand Company Deliveries",
"Description": "", "Description": "",
"RepoUrl": "https://git.carvel.li/liza/Deliveroo", "RepoUrl": "https://git.carvel.li/liza/Deliveroo",
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/Deliveroo.png" "IconUrl": "https://plugins.carvel.li/icons/Deliveroo.png"
} }

View File

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

View File

@ -1,6 +1,7 @@
using System; using System;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Memory; using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
@ -19,7 +20,7 @@ partial class DeliverooPlugin
if (CurrentStage == Stage.ConfirmReward && if (CurrentStage == Stage.ConfirmReward &&
_gameStrings.ExchangeItems.IsMatch(text)) _gameStrings.ExchangeItems.IsMatch(text))
{ {
PurchaseItemRequest? item = GetNextItemToPurchase(); PurchaseItemRequest? item = _exchangeHandler.GetNextItemToPurchase();
if (item == null) if (item == null)
{ {
addonSelectYesNo->AtkUnitBase.FireCallbackInt(1); addonSelectYesNo->AtkUnitBase.FireCallbackInt(1);
@ -27,17 +28,20 @@ partial class DeliverooPlugin
return; return;
} }
_pluginLog.Information($"Selecting 'yes' ({text})"); _pluginLog.Information($"Selecting 'yes' ({text}) (callback = {item.OnPurchase}, qty = {item.TemporaryPurchaseQuantity})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0); addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
var nextItem = GetNextItemToPurchase(item); item.OnPurchase?.Invoke((int)item.TemporaryPurchaseQuantity);
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost) item.TemporaryPurchaseQuantity = 0;
var nextItem = _exchangeHandler.GetNextItemToPurchase(item);
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.SelectRewardTier; CurrentStage = Stage.SelectRewardTier;
else else
CurrentStage = Stage.CloseGcExchange; 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) _gameStrings.TradeHighQualityItem == text)
{ {
_pluginLog.Information($"Selecting 'yes' ({text})"); _pluginLog.Information($"Selecting 'yes' ({text})");

View File

@ -13,12 +13,12 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Deliveroo.External; using Deliveroo.External;
using Deliveroo.GameData; using Deliveroo.GameData;
using Deliveroo.Handlers;
using Deliveroo.Windows; using Deliveroo.Windows;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib; using LLib;
using LLib.GameUI; using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
namespace Deliveroo; namespace Deliveroo;
@ -26,66 +26,74 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{ {
private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName); private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName);
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IKeyState _keyState;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly GameStrings _gameStrings; private readonly GameStrings _gameStrings;
private readonly GameFunctions _gameFunctions;
private readonly ExternalPluginHandler _externalPluginHandler; private readonly ExternalPluginHandler _externalPluginHandler;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly GcRewardsCache _gcRewardsCache; private readonly GcRewardsCache _gcRewardsCache;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly ItemCache _itemCache;
private readonly ExchangeHandler _exchangeHandler;
private readonly SupplyHandler _supplyHandler;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly TurnInWindow _turnInWindow; private readonly TurnInWindow _turnInWindow;
private readonly IReadOnlyDictionary<uint, uint> _sealCaps;
private Stage _currentStageInternal = Stage.Stopped; 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, IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager,
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog, 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; _pluginInterface = pluginInterface;
_chatGui = chatGui; _chatGui = chatGui;
_gameGui = gameGui; _gameGui = gameGui;
_framework = framework; _framework = framework;
_clientState = clientState; _clientState = clientState;
_objectTable = objectTable;
_targetManager = targetManager;
_condition = condition; _condition = condition;
_commandManager = commandManager; _commandManager = commandManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_keyState = keyState;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
MigrateConfiguration();
_gameStrings = new GameStrings(dataManager, _pluginLog); _gameStrings = new GameStrings(dataManager, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _pluginLog); _externalPluginHandler = new ExternalPluginHandler(_pluginInterface, gameConfig, _configuration, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _gameFunctions = new GameFunctions(objectTable, _clientState, targetManager, dataManager,
_externalPluginHandler, _pluginLog);
_gcRewardsCache = new GcRewardsCache(dataManager); _gcRewardsCache = new GcRewardsCache(dataManager);
_iconCache = new IconCache(textureProvider); _iconCache = new IconCache(textureProvider);
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState, _pluginLog, _iconCache); _itemCache = new ItemCache(dataManager);
_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); _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); _windowSystem.AddWindow(_turnInWindow);
_sealCaps = dataManager.GetExcelSheet<GrandCompanyRank>()!.Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, x => x.MaxSeals);
_framework.Update += FrameworkUpdate; _framework.Update += FrameworkUpdate;
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
@ -106,9 +114,28 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup); _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) if (_configuration.PauseAtRank <= 0)
return; return;
@ -126,7 +153,12 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{ {
_turnInWindow.State = false; _turnInWindow.State = false;
_pluginLog.Information($"Pausing GC delivery, FC reached rank {rank}"); _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; return;
} }
} }
@ -148,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 public int EffectiveReservedSealCount
{ {
get get
@ -155,7 +202,8 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (CharacterConfiguration is { IgnoreMinimumSealsToKeep: true }) if (CharacterConfiguration is { IgnoreMinimumSealsToKeep: true })
return 0; return 0;
return _configuration.ReserveDifferentSealCountAtMaxRank && GetSealCap() == GetMaxSealCap() return _configuration.ReserveDifferentSealCountAtMaxRank &&
_gameFunctions.GetSealCap() == _gameFunctions.MaxSealCap
? _configuration.ReservedSealCountAtMaxRank ? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount; : _configuration.ReservedSealCount;
} }
@ -170,11 +218,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{ {
if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() || if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() ||
CharacterConfiguration.CachedWorldName != CharacterConfiguration.CachedWorldName !=
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString()) _clientState.LocalPlayer.HomeWorld.Value.Name.ToString())
{ {
CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString(); CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString();
CharacterConfiguration.CachedWorldName = CharacterConfiguration.CachedWorldName =
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString(); _clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
CharacterConfiguration.Save(_pluginInterface); CharacterConfiguration.Save(_pluginInterface);
} }
@ -194,7 +242,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
} }
} }
private void Logout() private void Logout(int type, int code)
{ {
CharacterConfiguration = null; CharacterConfiguration = null;
} }
@ -205,59 +253,62 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (!_clientState.IsLoggedIn || if (!_clientState.IsLoggedIn ||
_clientState.TerritoryType is not 128 and not 130 and not 132 || _clientState.TerritoryType is not 128 and not 130 and not 132 ||
_condition[ConditionFlag.OccupiedInCutSceneEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f || _gameFunctions.GetDistanceToNpc(_gameFunctions.GetQuartermasterId(), out IGameObject? quartermaster) >= 7f ||
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f || _gameFunctions.GetDistanceToNpc(_gameFunctions.GetPersonnelOfficerId(), out IGameObject? personnelOfficer) >=
7f ||
CharacterConfiguration is { DisableForCharacter: true } || CharacterConfiguration is { DisableForCharacter: true } ||
_configWindow.IsOpen) _configWindow.IsOpen)
{ {
_turnInWindow.IsOpen = false; _turnInWindow.IsOpen = false;
_turnInWindow.State = false; _turnInWindow.State = false;
if (CurrentStage != Stage.Stopped) StopTurnIn();
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
} }
else if (DateTime.Now > _continueAt) else if (DateTime.Now > ContinueAt)
{ {
_turnInWindow.IsOpen = true; _turnInWindow.IsOpen = true;
_turnInWindow.Multiplier = GetSealMultiplier(); _turnInWindow.Multiplier = _gameFunctions.GetSealMultiplier();
if (!_turnInWindow.State) if (!_turnInWindow.State)
{ {
if (CurrentStage != Stage.Stopped) StopTurnIn();
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
return; return;
} }
else if (_turnInWindow.State && CurrentStage == Stage.Stopped) else if (_turnInWindow.State && CurrentStage == Stage.Stopped)
{ {
CurrentStage = Stage.TargetPersonnelOfficer; CurrentStage = Stage.TargetPersonnelOfficer;
_itemsToPurchaseNow = _turnInWindow.SelectedItems; ItemsToPurchaseNow = _turnInWindow.SelectedItems;
if (_itemsToPurchaseNow.Count > 0) DeliveryResult = new MessageDeliveryResult();
_supplyHandler.ResetTurnInErrorHandling();
if (ItemsToPurchaseNow.Count > 0)
{ {
_pluginLog.Information("Items to purchase:"); _pluginLog.Information("Items to purchase:");
foreach (var item in _itemsToPurchaseNow) foreach (var item in ItemsToPurchaseNow)
_pluginLog.Information($" {item.Name} (limit = {item.EffectiveLimit})"); _pluginLog.Information($" {item.Name} (limit = {item.EffectiveLimit})");
} }
else else
_pluginLog.Information("No items to purchase configured or available"); _pluginLog.Information("No items to purchase configured or available");
var nextItem = GetNextItemToPurchase(); var nextItem = _exchangeHandler.GetNextItemToPurchase();
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost) if (nextItem != null && _gameFunctions.GetCurrentSealCount() >=
EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.TargetQuartermaster; CurrentStage = Stage.TargetQuartermaster;
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", out var gcSupplyList) && if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var gcSupplyList) &&
LAddon.IsAddonReady(&gcSupplyList->AtkUnitBase)) LAddon.IsAddonReady(&gcSupplyList->AtkUnitBase))
CurrentStage = Stage.SelectExpertDeliveryTab; CurrentStage = Stage.SelectExpertDeliveryTab;
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) && if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) &&
LAddon.IsAddonReady(gcExchange)) LAddon.IsAddonReady(gcExchange))
CurrentStage = Stage.SelectRewardTier; 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) if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_externalPluginHandler.Saved)
@ -266,7 +317,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
switch (CurrentStage) switch (CurrentStage)
{ {
case Stage.TargetPersonnelOfficer: case Stage.TargetPersonnelOfficer:
InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!); _supplyHandler.InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
break; break;
case Stage.OpenGcSupply: case Stage.OpenGcSupply:
@ -274,43 +325,48 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break; break;
case Stage.SelectExpertDeliveryTab: case Stage.SelectExpertDeliveryTab:
SelectExpertDeliveryTab(); _supplyHandler.SelectExpertDeliveryTab();
break; break;
case Stage.SelectItemToTurnIn: case Stage.SelectItemToTurnIn:
SelectItemToTurnIn(); _supplyHandler.SelectItemToTurnIn();
break; break;
case Stage.TurnInSelected: case Stage.TurnInSelected:
TurnInSelectedItem(); // see GrandCompanySupplyReward
break; break;
case Stage.FinalizeTurnIn: case Stage.FinalizeTurnIn:
FinalizeTurnInItem(); case Stage.SingleFinalizeTurnIn:
_supplyHandler.FinalizeTurnInItem();
break; break;
case Stage.CloseGcSupply: case Stage.CloseGcSupplySelectString:
// see SelectStringPostSetup // see SelectStringPostSetup
break; break;
case Stage.CloseGcSupplyThenStop: case Stage.CloseGcSupplySelectStringThenStop:
// see SelectStringPostSetup // see SelectStringPostSetup
break; break;
case Stage.CloseGcSupplyWindowThenStop:
_supplyHandler.CloseGcSupplyWindow();
break;
case Stage.TargetQuartermaster: case Stage.TargetQuartermaster:
InteractWithQuartermaster(personnelOfficer!, quartermaster!); _exchangeHandler.InteractWithQuartermaster(personnelOfficer!, quartermaster!);
break; break;
case Stage.SelectRewardTier: case Stage.SelectRewardTier:
SelectRewardTier(); _exchangeHandler.SelectRewardTier();
break; break;
case Stage.SelectRewardSubCategory: case Stage.SelectRewardSubCategory:
SelectRewardSubCategory(); _exchangeHandler.SelectRewardSubCategory();
break; break;
case Stage.SelectReward: case Stage.SelectReward:
SelectReward(); _exchangeHandler.SelectReward();
break; break;
case Stage.ConfirmReward: case Stage.ConfirmReward:
@ -318,14 +374,13 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break; break;
case Stage.CloseGcExchange: case Stage.CloseGcExchange:
CloseGcExchange(); _exchangeHandler.CloseGcExchange();
break; break;
case Stage.RequestStop: case Stage.RequestStop:
_externalPluginHandler.Restore(); StopTurnIn();
CurrentStage = Stage.Stopped;
break; break;
case Stage.Stopped: case Stage.Stopped:
break; break;
@ -336,9 +391,58 @@ 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() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
@ -350,8 +454,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_framework.Update -= FrameworkUpdate; _framework.Update -= FrameworkUpdate;
_externalPluginHandler.Restore(); _gameFunctions.Dispose();
_externalPluginHandler.Dispose(); _externalPluginHandler.Dispose();
_iconCache.Dispose(); _iconCache.Dispose();
} }

48
Deliveroo/External/AllaganToolsIpc.cs vendored Normal file
View File

@ -0,0 +1,48 @@
using System.Linq;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Deliveroo.External;
internal sealed class AllaganToolsIpc
{
private readonly IPluginLog _pluginLog;
private static readonly uint[] RetainerInventoryTypes = new[]
{
InventoryType.RetainerPage1,
InventoryType.RetainerPage2,
InventoryType.RetainerPage3,
InventoryType.RetainerPage4,
InventoryType.RetainerPage5,
InventoryType.RetainerPage6,
InventoryType.RetainerPage7,
}
.Select(x => (uint)x).ToArray();
private readonly ICallGateSubscriber<uint, bool, uint[], uint> _itemCountOwned;
public AllaganToolsIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_itemCountOwned = pluginInterface.GetIpcSubscriber<uint, bool, uint[], uint>("AllaganTools.ItemCountOwned");
}
public uint GetRetainerItemCount(uint itemId)
{
try
{
uint itemCount = _itemCountOwned.InvokeFunc(itemId, true, RetainerInventoryTypes);
_pluginLog.Verbose($"Found {itemCount} items in retainer inventories for itemId {itemId}");
return itemCount;
}
catch (IpcError)
{
_pluginLog.Warning("Could not query allagantools for retainer inventory counts");
return 0;
}
}
}

View File

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

View File

@ -7,19 +7,28 @@ namespace Deliveroo.External;
internal sealed class ExternalPluginHandler : IDisposable 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 IPluginLog _pluginLog;
private readonly DeliverooIpc _deliverooIpc; private readonly DeliverooIpc _deliverooIpc;
private readonly PandoraIpc _pandoraIpc; private readonly PandoraIpc _pandoraIpc;
private readonly AllaganToolsIpc _allaganToolsIpc;
private bool? _pandoraState; 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; _pluginInterface = pluginInterface;
_gameConfig = gameConfig;
_configuration = configuration;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_deliverooIpc = new DeliverooIpc(pluginInterface); _deliverooIpc = new DeliverooIpc(pluginInterface);
_pandoraIpc = new PandoraIpc(pluginInterface, pluginLog); _pandoraIpc = new PandoraIpc(pluginInterface, pluginLog);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog);
} }
public bool Saved { get; private set; } public bool Saved { get; private set; }
@ -36,6 +45,7 @@ internal sealed class ExternalPluginHandler : IDisposable
_deliverooIpc.StartTurnIn(); _deliverooIpc.StartTurnIn();
SaveYesAlreadyState(); SaveYesAlreadyState();
SavePandoraState(); SavePandoraState();
SaveGameConfig();
Saved = true; Saved = true;
} }
@ -55,12 +65,25 @@ internal sealed class ExternalPluginHandler : IDisposable
_pluginLog.Info($"Previous pandora feature state: {_pandoraState}"); _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() public void Restore()
{ {
if (Saved) if (Saved)
{ {
RestoreYesAlready(); RestoreYesAlready();
RestorePandora(); RestorePandora();
RestoreGameConfig();
} }
Saved = false; Saved = false;
@ -85,8 +108,36 @@ internal sealed class ExternalPluginHandler : IDisposable
_pandoraIpc.Enable(); _pandoraIpc.Enable();
} }
private void RestoreGameConfig()
{
_uncapFrameRate?.Restore(_gameConfig);
_uncapFrameRate = null;
_limitFrameRateWhenClientInactive?.Restore(_gameConfig);
_limitFrameRateWhenClientInactive = null;
}
public void Dispose() public void Dispose()
{ {
_deliverooIpc.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?> _getEnabled;
private readonly ICallGateSubscriber<string, bool, object?> _setEnabled; private readonly ICallGateSubscriber<string, bool, object?> _setEnabled;
public PandoraIpc(DalamudPluginInterface pluginInterface, IPluginLog pluginLog) public PandoraIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{ {
_pluginLog = pluginLog; _pluginLog = pluginLog;
_getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled"); _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 System.Text.RegularExpressions;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib; using LLib;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.CustomSheets; using Lumina.Excel.Sheets;
using Lumina.Excel.GeneratedSheets; using Lumina.Text.ReadOnly;
namespace Deliveroo.GameData; namespace Deliveroo.GameData;
@ -15,19 +16,19 @@ internal sealed class GameStrings
{ {
UndertakeSupplyAndProvisioningMission = UndertakeSupplyAndProvisioningMission =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002", pluginLog) 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 = ClosePersonnelOfficerTalk =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004", pluginLog) 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) 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 = TradeHighQualityItem =
dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)?.ReplaceLineEndings("") dataManager.GetString<Addon>(102434, addon => addon.Text, pluginLog)
?? throw new Exception($"Unable to resolve {nameof(TradeHighQualityItem)}"); ?? 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) 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; RankUpFcType = (XivChatType)rankUpFc.LogKind;
} }
@ -40,7 +41,17 @@ internal sealed class GameStrings
public XivChatType RankUpFcType { get; } public XivChatType RankUpFcType { get; }
[Sheet("custom/000/ComDefGrandCompanyOfficer_00073")] [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.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Deliveroo.GameData; namespace Deliveroo.GameData;
@ -10,31 +10,32 @@ internal sealed class GcRewardsCache
{ {
public GcRewardsCache(IDataManager dataManager) public GcRewardsCache(IDataManager dataManager)
{ {
var categories = dataManager.GetExcelSheet<GCScripShopCategory>()! var categories = dataManager.GetExcelSheet<GCScripShopCategory>()
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, .ToDictionary(x => x.RowId,
x => x =>
(GrandCompany: (GrandCompany)x.GrandCompany.Row, (GrandCompany: (GrandCompany)x.GrandCompany.RowId,
Tier: (RewardTier)x.Tier, Tier: (RewardTier)x.Tier,
SubCategory: (RewardSubCategory)x.SubCategory)); SubCategory: (RewardSubCategory)x.SubCategory));
Rewards = dataManager.GetExcelSheet<GCScripShopItem>()! Rewards = dataManager.GetSubrowExcelSheet<GCScripShopItem>()
.Where(x => x.RowId > 0 && x.Item.Row > 0) .SelectMany(x => x)
.Where(x => x.RowId > 0 && x.Item.RowId > 0)
.GroupBy(item => .GroupBy(item =>
{ {
var category = categories[item.RowId]; var category = categories[item.RowId];
return new return new
{ {
ItemId = item.Item.Row, ItemId = item.Item.RowId,
Name = item.Item.Value!.Name.ToString(), Name = item.Item.Value.Name.ToString(),
IconId = item.Item.Row == ItemIds.Venture ? 25917 : item.Item.Value!.Icon, IconId = item.Item.RowId == ItemIds.Venture ? 25917 : item.Item.Value.Icon,
category.Tier, category.Tier,
category.SubCategory, category.SubCategory,
RequiredRank = item.RequiredGrandCompanyRank.Row, RequiredRank = item.RequiredGrandCompanyRank.RowId,
item.Item!.Value.StackSize, item.Item.Value.StackSize,
SealCost = item.CostGCSeals, SealCost = item.CostGCSeals,
InventoryLimit = item.Item.Value!.IsUnique ? 1 InventoryLimit = item.Item.Value.IsUnique ? 1
: item.Item.Row == ItemIds.Venture ? item.Item.Value!.StackSize : item.Item.RowId == ItemIds.Venture ? item.Item.Value.StackSize
: int.MaxValue, : 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

@ -0,0 +1,28 @@
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
namespace Deliveroo.GameData;
internal sealed class ItemCache
{
private readonly Dictionary<string, HashSet<uint>> _itemNamesToIds = new();
public ItemCache(IDataManager dataManager)
{
foreach (var item in dataManager.GetExcelSheet<Item>())
{
string name = item.Name.ToString();
if (string.IsNullOrWhiteSpace(name))
continue;
if (_itemNamesToIds.TryGetValue(name, out HashSet<uint>? itemIds))
itemIds.Add(item.RowId);
else
_itemNamesToIds.Add(name, [item.RowId]);
}
}
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,50 +1,85 @@
using System; using System;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; 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 Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; 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; return;
} }
if (_targetManager.Target == personnelOfficer) if (_targetManager.Target == personnelOfficer)
return; return;
InteractWithTarget(quartermaster); _gameFunctions.InteractWithTarget(quartermaster);
CurrentStage = Stage.SelectRewardTier; _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 offset = 0; int toBuy = 0;
if (request == previousRequest) if (request == previousRequest)
offset = (int)request.StackSize; {
toBuy = (int)request.StackSize;
if (request.ItemId != ItemIds.Venture)
toBuy = Math.Min(toBuy, 99);
}
if (GetItemCount(request.ItemId) + offset < request.EffectiveLimit) if (request.Type == Configuration.PurchaseType.KeepStocked)
return request; {
if (_gameFunctions.GetItemCount(request.ItemId, request.CheckRetainerInventory) + toBuy <
request.EffectiveLimit)
return request;
}
else
{
if (toBuy < request.EffectiveLimit)
return request;
}
} }
return null; return null;
} }
private unsafe void SelectRewardTier() public unsafe void SelectRewardTier()
{ {
PurchaseItemRequest? item = GetNextItemToPurchase(); PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null) if (item == null)
{ {
CurrentStage = Stage.CloseGcExchange; _plugin.CurrentStage = Stage.CloseGcExchange;
return; return;
} }
@ -65,17 +100,17 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 } new() { Type = 0, Int = 0 }
}; };
addonExchange->FireCallback(9, selectRank); addonExchange->FireCallback(9, selectRank);
_continueAt = DateTime.Now.AddSeconds(0.5); _plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectRewardSubCategory; _plugin.CurrentStage = Stage.SelectRewardSubCategory;
} }
} }
private unsafe void SelectRewardSubCategory() public unsafe void SelectRewardSubCategory()
{ {
PurchaseItemRequest? item = GetNextItemToPurchase(); PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null) if (item == null)
{ {
CurrentStage = Stage.CloseGcExchange; _plugin.CurrentStage = Stage.CloseGcExchange;
return; return;
} }
@ -96,25 +131,25 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 } new() { Type = 0, Int = 0 }
}; };
addonExchange->FireCallback(9, selectType); addonExchange->FireCallback(9, selectType);
_continueAt = DateTime.Now.AddSeconds(0.5); _plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectReward; _plugin.CurrentStage = Stage.SelectReward;
} }
} }
private unsafe void SelectReward() public unsafe void SelectReward()
{ {
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) && if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange)) LAddon.IsAddonReady(addonExchange))
{ {
if (SelectRewardItem(addonExchange)) if (SelectRewardItem(addonExchange))
{ {
_continueAt = DateTime.Now.AddSeconds(0.2); _plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.ConfirmReward; _plugin.CurrentStage = Stage.ConfirmReward;
} }
else else
{ {
_continueAt = DateTime.Now.AddSeconds(0.2); _plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.CloseGcExchange; _plugin.CurrentStage = Stage.CloseGcExchange;
} }
} }
} }
@ -132,10 +167,15 @@ partial class DeliverooPlugin
if (itemId == item.ItemId) if (itemId == item.ItemId)
{ {
_pluginLog.Information($"Selecting item {itemId}, {i}"); _pluginLog.Information($"Selecting item {itemId}, {i}");
long toBuy = (GetCurrentSealCount() - EffectiveReservedSealCount) / item.SealCost; long toBuy = (_gameFunctions.GetCurrentSealCount() - _plugin.EffectiveReservedSealCount) /
toBuy = Math.Min(toBuy, item.EffectiveLimit - GetItemCount(item.ItemId)); item.SealCost;
if (item.Type == Configuration.PurchaseType.KeepStocked)
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); toBuy = Math.Min(toBuy, 99);
if (toBuy <= 0) if (toBuy <= 0)
@ -144,7 +184,10 @@ partial class DeliverooPlugin
return false; return false;
} }
_chatGui.Print($"Buying {toBuy}x {item.Name}..."); item.TemporaryPurchaseQuantity = toBuy;
_chatGui.Print(new SeString(new TextPayload($"Buying {toBuy}x "))
.Append(SeString.CreateItemLink(item.ItemId))
.Append(new TextPayload("...")));
var selectReward = stackalloc AtkValue[] var selectReward = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 0 }, new() { Type = ValueType.Int, Int = 0 },
@ -166,13 +209,24 @@ partial class DeliverooPlugin
return false; return false;
} }
private unsafe void CloseGcExchange() public unsafe void CloseGcExchange()
{ {
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) && if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange)) LAddon.IsAddonReady(addonExchange))
{ {
addonExchange->FireCallbackInt(-1); addonExchange->FireCallbackInt(-1);
CurrentStage = Stage.TargetPersonnelOfficer;
// If we just turned in the final item, there's no need to talk to the personnel officer again
if (_plugin.LastTurnInListSize == 1)
{
_plugin.TurnInState = false;
_plugin.CurrentStage = Stage.RequestStop;
}
else
{
_plugin.ContinueAt = DateTime.Now.AddSeconds(1);
_plugin.CurrentStage = Stage.TargetPersonnelOfficer;
}
} }
} }
} }

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Deliveroo.GameData; using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -8,25 +10,43 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; 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) private readonly DeliverooPlugin _plugin;
{ private readonly GameFunctions _gameFunctions;
if (_targetManager.Target == quartermaster) private readonly ITargetManager _targetManager;
return; private readonly IGameGui _gameGui;
private readonly IPluginLog _pluginLog;
InteractWithTarget(personnelOfficer); private uint _turnInErrors;
CurrentStage = Stage.OpenGcSupply;
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); var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive()) if (agentInterface != null && agentInterface->IsAgentActive())
{ {
var addonId = agentInterface->GetAddonID(); var addonId = agentInterface->GetAddonId();
if (addonId == 0) if (addonId == 0)
return; return;
@ -40,7 +60,7 @@ partial class DeliverooPlugin
{ {
_pluginLog.Information("Tab already selected, probably due to haseltweaks"); _pluginLog.Information("Tab already selected, probably due to haseltweaks");
ResetTurnInErrorHandling(); ResetTurnInErrorHandling();
CurrentStage = Stage.SelectItemToTurnIn; _plugin.CurrentStage = Stage.SelectItemToTurnIn;
return; return;
} }
@ -53,23 +73,23 @@ partial class DeliverooPlugin
}; };
addon->FireCallback(3, selectExpertDeliveryTab); addon->FireCallback(3, selectExpertDeliveryTab);
ResetTurnInErrorHandling(); 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"); _pluginLog.Verbose("Resetting error handling state");
_lastTurnInListSize = listSize; _plugin.LastTurnInListSize = listSize;
_turnInErrors = 0; _turnInErrors = 0;
} }
private unsafe void SelectItemToTurnIn() public unsafe void SelectItemToTurnIn()
{ {
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply); var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive()) if (agentInterface != null && agentInterface->IsAgentActive())
{ {
var addonId = agentInterface->GetAddonID(); var addonId = agentInterface->GetAddonId();
if (addonId == 0) if (addonId == 0)
return; return;
@ -78,28 +98,30 @@ partial class DeliverooPlugin
return; return;
var addonGc = (AddonGrandCompanySupplyList*)addon; var addonGc = (AddonGrandCompanySupplyList*)addon;
if (addonGc->ExpertDeliveryList == null || !addonGc->ExpertDeliveryList->AtkComponentBase.OwnerNode->AtkResNode.IsVisible) if (addonGc->ExpertDeliveryList == null ||
!addonGc->ExpertDeliveryList->AtkComponentBase.OwnerNode->AtkResNode.IsVisible())
return; return;
if (addonGc->SelectedTab != 2) if (addonGc->SelectedTab != 2)
{ {
_turnInWindow.Error = "Wrong tab selected"; _plugin.TurnInError = "Wrong tab selected";
return; return;
} }
ItemFilterType configuredFilter = ResolveSelectedSupplyFilter(); ItemFilterType configuredFilter = ResolveSelectedSupplyFilter();
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter) if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter)
{ {
_turnInWindow.Error = _plugin.TurnInError =
$"Wrong filter selected (expected {configuredFilter}, but is {(ItemFilterType)addonGc->SelectedFilter})"; $"Wrong filter selected (expected {configuredFilter}, but is {(ItemFilterType)addonGc->SelectedFilter})";
return; return;
} }
int currentListSize = addonGc->ExpertDeliveryList->ListLength; 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})"); _pluginLog.Information(
CurrentStage = Stage.CloseGcSupplyThenStop; $"No items to turn in ({addonGc->ListEmptyTextNode->AtkResNode.IsVisible}, {currentListSize})");
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1); addon->FireCallbackInt(-1);
return; return;
} }
@ -108,28 +130,29 @@ partial class DeliverooPlugin
// something is wrong. // something is wrong.
if (_turnInErrors > 10) if (_turnInErrors > 10)
{ {
_turnInWindow.Error = "Unable to refresh item list"; _plugin.TurnInError = "Unable to refresh item list";
return; return;
} }
if (currentListSize >= _lastTurnInListSize) if (currentListSize >= _plugin.LastTurnInListSize)
{ {
_turnInErrors++; _turnInErrors++;
_pluginLog.Information($"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_lastTurnInListSize}, new list size = {currentListSize})..."); _pluginLog.Information(
$"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_plugin.LastTurnInListSize}, new list size = {currentListSize})...");
addon->FireCallbackInt(2); addon->FireCallbackInt(2);
_continueAt = DateTime.Now.AddSeconds(0.1); _plugin.ContinueAt = DateTime.Now.AddSeconds(0.1);
return; return;
} }
ResetTurnInErrorHandling(currentListSize); ResetTurnInErrorHandling(currentListSize);
var agent = (AgentGrandCompanySupply*)agentInterface; var agent = (AgentGrandCompanySupply*)agentInterface;
List<TurnInItem> items = BuildTurnInList(agent); List<TurnInItem> items = _gameFunctions.BuildTurnInList(agent);
if (items.Count == 0) if (items.Count == 0)
{ {
// probably shouldn't happen with the previous node visibility check // probably shouldn't happen with the previous node visibility check
CurrentStage = Stage.CloseGcSupplyThenStop; _plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1); addon->FireCallbackInt(-1);
return; return;
} }
@ -153,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 // 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. // Helms from being turned in.
if (GetCurrentSealCount() + items[0].SealsWithBonus > GetSealCap()) if (_gameFunctions.GetCurrentSealCount() + items[0].SealsWithBonus > _gameFunctions.GetSealCap())
{ {
CurrentStage = Stage.CloseGcSupply; _plugin.CurrentStage = Stage.CloseGcSupplySelectString;
addon->FireCallbackInt(-1); addon->FireCallbackInt(-1);
return; return;
} }
@ -167,24 +190,11 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 } new() { Type = 0, Int = 0 }
}; };
addon->FireCallback(3, selectFirstItem); addon->FireCallback(3, selectFirstItem);
CurrentStage = Stage.TurnInSelected; _plugin.CurrentStage = Stage.TurnInSelected;
} }
} }
private unsafe void TurnInSelectedItem() public unsafe void FinalizeTurnInItem()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward",
out var addonSupplyReward) && LAddon.IsAddonReady(&addonSupplyReward->AtkUnitBase))
{
_pluginLog.Information($"Turning in '{addonSupplyReward->AtkUnitBase.AtkValues[4].ReadAtkString()}'");
addonSupplyReward->AtkUnitBase.FireCallbackInt(0);
_continueAt = DateTime.Now.AddSeconds(0.58);
CurrentStage = Stage.FinalizeTurnIn;
}
}
private unsafe void FinalizeTurnInItem()
{ {
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var addonSupplyList) && LAddon.IsAddonReady(&addonSupplyList->AtkUnitBase)) out var addonSupplyList) && LAddon.IsAddonReady(&addonSupplyList->AtkUnitBase))
@ -196,15 +206,36 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 } new() { Type = 0, Int = 0 }
}; };
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter); 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() private ItemFilterType ResolveSelectedSupplyFilter()
{ {
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true }) if (_plugin.CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
return ItemFilterType.HideArmouryChestItems; return ItemFilterType.HideArmouryChestItems;
return ItemFilterType.HideGearSetItems; return ItemFilterType.HideGearSetItems;
} }
public unsafe void CloseGcSupplyWindow()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
AtkUnitBase* addon = LAddon.GetAddonById(addonId);
if (addon == null || !LAddon.IsAddonReady(addon))
return;
_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

@ -1,4 +1,5 @@
using Deliveroo.GameData; using System;
using Deliveroo.GameData;
namespace Deliveroo; namespace Deliveroo;
@ -6,9 +7,14 @@ internal sealed class PurchaseItemRequest
{ {
public required uint ItemId { get; init; } public required uint ItemId { get; init; }
public required string Name { get; set; } public required string Name { get; set; }
public required uint EffectiveLimit { get; init; } public required uint EffectiveLimit { get; set; }
public required uint SealCost { get; init; } public required uint SealCost { get; init; }
public required RewardTier Tier { get; init; } public required RewardTier Tier { get; init; }
public required RewardSubCategory SubCategory { get; init; } public required RewardSubCategory SubCategory { get; init; }
public required uint StackSize { get; init; } public required uint StackSize { get; init; }
public required Configuration.PurchaseType Type { get; init; }
public required bool CheckRetainerInventory { get; init; }
public Action<int>? OnPurchase { get; set; }
public long TemporaryPurchaseQuantity { get; set; }
} }

View File

@ -8,8 +8,9 @@ internal enum Stage
SelectItemToTurnIn, SelectItemToTurnIn,
TurnInSelected, TurnInSelected,
FinalizeTurnIn, FinalizeTurnIn,
CloseGcSupply, CloseGcSupplySelectString,
CloseGcSupplyThenStop, CloseGcSupplySelectStringThenStop,
CloseGcSupplyWindowThenStop,
TargetQuartermaster, TargetQuartermaster,
SelectRewardTier, SelectRewardTier,
@ -20,4 +21,6 @@ internal enum Stage
RequestStop, RequestStop,
Stopped, Stopped,
SingleFinalizeTurnIn,
} }

View File

@ -2,35 +2,49 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Deliveroo.GameData; using Deliveroo.GameData;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows; 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 DeliverooPlugin _plugin;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly GcRewardsCache _gcRewardsCache; private readonly GcRewardsCache _gcRewardsCache;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly GameFunctions _gameFunctions;
private readonly IReadOnlyDictionary<uint, GcRewardItem> _itemLookup; private readonly IReadOnlyDictionary<uint, GcRewardItem> _itemLookup;
private string _searchString = string.Empty;
private uint _dragDropSource;
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration, private readonly List<(VirtualKey Key, string Label)> _keyCodes =
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog, IconCache iconCache) [
(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") : base("Deliveroo - Configuration###DeliverooConfig")
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@ -40,6 +54,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
_clientState = clientState; _clientState = clientState;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_iconCache = iconCache; _iconCache = iconCache;
_gameFunctions = gameFunctions;
_itemLookup = _gcRewardsCache.RewardLookup; _itemLookup = _gcRewardsCache.RewardLookup;
@ -53,6 +68,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
}; };
} }
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw() public override void Draw()
{ {
if (_configuration.AddVentureIfNoItemToPurchaseSelected()) if (_configuration.AddVentureIfNoItemToPurchaseSelected())
@ -72,58 +89,80 @@ internal sealed class ConfigWindow : LImGui.LWindow
{ {
if (ImGui.BeginTabItem("Items for Auto-Buy")) if (ImGui.BeginTabItem("Items for Auto-Buy"))
{ {
uint? itemToRemove = null; Configuration.PurchaseOption? itemToRemove = null;
uint? itemToAdd = null; Configuration.PurchaseOption? itemToAdd = null;
int indexToAdd = 0; 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.PushID($"###Item{i}");
ImGui.BeginDisabled( ImGui.BeginDisabled(
_configuration.ItemsAvailableForPurchase.Count == 1 && itemId == ItemIds.Venture); _configuration.ItemsAvailableToPurchase.Count == 1 && purchaseOption.ItemId == ItemIds.Venture);
var item = _itemLookup[itemId]; var item = _itemLookup[purchaseOption.ItemId];
IDalamudTextureWrap? icon = _iconCache.GetIcon(item.IconId); using (var icon = _iconCache.GetIcon(item.IconId))
if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); Vector2 pos = ImGui.GetCursorPos();
ImGui.SameLine(); Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
}
ImGui.Selectable($"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}"); if (icon != null)
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = itemId;
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource > 0 &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
{ {
itemToAdd = _dragDropSource; ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X,
indexToAdd = i; ImGui.GetStyle().ItemSpacing.Y / 2));
_dragDropSource = 0;
} }
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.BeginDragDropSource())
if (ImGui.BeginPopup($"###ctx{i}")) {
{ ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
if (ImGui.Selectable($"Remove {_itemLookup[itemId].Name}")) _dragDropSource = purchaseOption;
itemToRemove = itemId;
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(); ImGui.EndDisabled();
@ -135,20 +174,20 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (itemToRemove != null) if (itemToRemove != null)
{ {
_configuration.ItemsAvailableForPurchase.Remove(itemToRemove.Value); _configuration.ItemsAvailableToPurchase.Remove(itemToRemove);
Save(); Save();
} }
if (itemToAdd != null) if (itemToAdd != null)
{ {
_configuration.ItemsAvailableForPurchase.Remove(itemToAdd.Value); _configuration.ItemsAvailableToPurchase.Remove(itemToAdd);
_configuration.ItemsAvailableForPurchase.Insert(indexToAdd, itemToAdd.Value); _configuration.ItemsAvailableToPurchase.Insert(indexToAdd, itemToAdd);
Save(); Save();
} }
List<(uint ItemId, string Name, ushort IconId, bool Limited)> comboValues = _gcRewardsCache.Rewards List<(uint ItemId, string Name, ushort IconId, bool Limited)> comboValues = _gcRewardsCache.Rewards
.Where(x => x.SubCategory is RewardSubCategory.Materials or RewardSubCategory.Materiel) .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)) .Select(x => (x.ItemId, x.Name, x.IconId, x.Limited))
.OrderBy(x => x.Name) .OrderBy(x => x.Name)
.ThenBy(x => x.GetHashCode()) .ThenBy(x => x.GetHashCode())
@ -160,14 +199,17 @@ internal sealed class ConfigWindow : LImGui.LWindow
bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256, bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue); 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); using (var icon = _iconCache.GetIcon(item.IconId))
if (icon != null)
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); if (icon != null)
ImGui.SameLine(); {
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.X);
}
} }
bool addThis = bool addThis =
@ -175,7 +217,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}##SelectVenture{item.IconId}"); $"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}##SelectVenture{item.IconId}");
if (addThis || addFirst) if (addThis || addFirst)
{ {
_configuration.ItemsAvailableForPurchase.Add(item.ItemId); _configuration.ItemsAvailableToPurchase.Add(new Configuration.PurchaseOption
{ ItemId = item.ItemId });
if (addFirst) if (addFirst)
{ {
@ -201,7 +244,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 }) if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
{ {
string currentCharacterName = _clientState.LocalPlayer!.Name.ToString(); 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.Text($"Current Character: {currentCharacterName} @ {currentWorldName}");
ImGui.Spacing(); ImGui.Spacing();
ImGui.Separator(); 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)) if (ImGui.InputInt("Minimum Seals to keep (e.g. for Squadron Missions)", ref reservedSealCount, 1000))
{ {
_configuration.ReservedSealCount = _configuration.ReservedSealCount =
Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCount)); Math.Max(0, Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCount));
Save(); Save();
} }
@ -328,21 +371,46 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (reserveDifferentSealCountAtMaxRank) if (reserveDifferentSealCountAtMaxRank)
{ {
ImGui.Indent(); float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.Indent(indentSize);
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100); ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100);
int reservedSealCountAtMaxRank = _configuration.ReservedSealCountAtMaxRank; int reservedSealCountAtMaxRank = _configuration.ReservedSealCountAtMaxRank;
if (ImGui.InputInt("Minimum seals to keep at max rank", ref reservedSealCountAtMaxRank)) if (ImGui.InputInt("Minimum seals to keep at max rank", ref reservedSealCountAtMaxRank))
{ {
_configuration.ReservedSealCountAtMaxRank = Math.Max(0, _configuration.ReservedSealCountAtMaxRank = Math.Max(0,
Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCountAtMaxRank)); Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCountAtMaxRank));
Save(); Save();
} }
ImGui.Unindent(); ImGui.Unindent(indentSize);
} }
ImGui.EndDisabled(); 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); ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
List<(int Rank, string Name)> rankUpComboValues = Enumerable.Range(1, 30) List<(int Rank, string Name)> rankUpComboValues = Enumerable.Range(1, 30)
.Select(x => x == 1 ? (0, "---") : (x, $"Rank {x}")) .Select(x => x == 1 ? (0, "---") : (x, $"Rank {x}"))
@ -366,10 +434,33 @@ internal sealed class ConfigWindow : LImGui.LWindow
Save(); 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(); ImGui.EndTabItem();
} }
} }
private void Save() => _pluginInterface.SavePluginConfig(_configuration); 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.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Deliveroo.GameData; using Deliveroo.GameData;
@ -15,12 +18,13 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows; namespace Deliveroo.Windows;
internal sealed class TurnInWindow : LImGui.LWindow internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configuration.MinimizableWindowConfig>
{ {
private readonly IReadOnlyList<InventoryType> _inventoryTypes = new[] private static readonly IReadOnlyList<InventoryType> InventoryTypes = new[]
{ {
InventoryType.Inventory1, InventoryType.Inventory1,
InventoryType.Inventory2, InventoryType.Inventory2,
@ -40,17 +44,26 @@ internal sealed class TurnInWindow : LImGui.LWindow
InventoryType.EquippedItems InventoryType.EquippedItems
}.AsReadOnly(); }.AsReadOnly();
private static readonly string[] StockingTypeLabels = { "Purchase Once", "Keep in Stock" };
private readonly DeliverooPlugin _plugin; private readonly DeliverooPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly GcRewardsCache _gcRewardsCache; private readonly GcRewardsCache _gcRewardsCache;
private readonly ConfigWindow _configWindow;
private readonly IconCache _iconCache; 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, ICondition condition, IClientState clientState, GcRewardsCache gcRewardsCache, ConfigWindow configWindow,
IconCache iconCache) IconCache iconCache, IKeyState keyState, GameFunctions gameFunctions)
: base("GC Delivery###DeliverooTurnIn") : base("GC Delivery###DeliverooTurnIn")
{ {
_plugin = plugin; _plugin = plugin;
@ -59,7 +72,10 @@ internal sealed class TurnInWindow : LImGui.LWindow
_condition = condition; _condition = condition;
_clientState = clientState; _clientState = clientState;
_gcRewardsCache = gcRewardsCache; _gcRewardsCache = gcRewardsCache;
_configWindow = configWindow;
_iconCache = iconCache; _iconCache = iconCache;
_keyState = keyState;
_gameFunctions = gameFunctions;
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver; PositionCondition = ImGuiCond.FirstUseEver;
@ -70,10 +86,24 @@ internal sealed class TurnInWindow : LImGui.LWindow
MaximumSize = new Vector2(500, 999), MaximumSize = new Vector2(500, 999),
}; };
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse; State = false;
ShowCloseButton = false; ShowCloseButton = false;
AllowClickthrough = 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 TitleBarButtons.Add(new TitleBarButton
{ {
Icon = FontAwesomeIcon.Cog, Icon = FontAwesomeIcon.Cog,
@ -89,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 decimal Multiplier { private get; set; }
public string Error { private get; set; } = string.Empty; public string Error { private get; set; } = string.Empty;
@ -98,7 +152,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
private bool IsOnHomeWorld => private bool IsOnHomeWorld =>
_clientState.LocalPlayer == null || _clientState.LocalPlayer == null ||
_clientState.LocalPlayer.HomeWorld.Id == _clientState.LocalPlayer.CurrentWorld.Id; _clientState.LocalPlayer.HomeWorld.RowId == _clientState.LocalPlayer.CurrentWorld.RowId;
private IItemsToPurchase ItemsWrapper => UseCharacterSpecificItemsToPurchase private IItemsToPurchase ItemsWrapper => UseCharacterSpecificItemsToPurchase
? new CharacterSpecificItemsToPurchase(_plugin.CharacterConfiguration!, _pluginInterface) ? new CharacterSpecificItemsToPurchase(_plugin.CharacterConfiguration!, _pluginInterface)
@ -108,30 +162,50 @@ internal sealed class TurnInWindow : LImGui.LWindow
{ {
get get
{ {
GrandCompany grandCompany = _plugin.GetGrandCompany(); GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None) if (grandCompany == GrandCompany.None)
return new List<PurchaseItemRequest>(); return new List<PurchaseItemRequest>();
var rank = _plugin.GetGrandCompanyRank(); var rank = _gameFunctions.GetGrandCompanyRank();
return ItemsWrapper.GetItemsToPurchase() return ItemsWrapper.GetItemsToPurchase()
.Where(x => x.ItemId != GcRewardItem.None.ItemId) .Where(x => x.ItemId != GcRewardItem.None.ItemId)
.Where(x => x.Enabled) .Where(x => x.Enabled)
.Where(x => x.Type == Configuration.PurchaseType.KeepStocked || x.Limit > 0)
.Select(x => new { Item = x, Reward = _gcRewardsCache.GetReward(x.ItemId) }) .Select(x => new { Item = x, Reward = _gcRewardsCache.GetReward(x.ItemId) })
.Where(x => x.Reward.GrandCompanies.Contains(grandCompany)) .Where(x => x.Reward.GrandCompanies.Contains(grandCompany))
.Where(x => x.Reward.RequiredRank <= rank) .Where(x => x.Reward.RequiredRank <= rank)
.Select(x => new PurchaseItemRequest .Select(x =>
{ {
ItemId = x.Item.ItemId, var purchaseOption = _configuration.ItemsAvailableToPurchase.Where(y => y.SameQuantityForAllLists)
Name = x.Reward.Name, .FirstOrDefault(y => y.ItemId == x.Item.ItemId);
EffectiveLimit = CalculateEffectiveLimit( int limit = purchaseOption?.GlobalLimit ?? x.Item.Limit;
x.Item.ItemId, var request = new PurchaseItemRequest
x.Item.Limit <= 0 ? uint.MaxValue : (uint)x.Item.Limit, {
x.Reward.StackSize, ItemId = x.Item.ItemId,
x.Reward.InventoryLimit), Name = x.Reward.Name,
SealCost = x.Reward.SealCost, EffectiveLimit = CalculateEffectiveLimit(
Tier = x.Reward.Tier, x.Item.ItemId,
SubCategory = x.Reward.SubCategory, limit <= 0 ? uint.MaxValue : (uint)limit,
StackSize = x.Reward.StackSize, x.Reward.StackSize,
x.Reward.InventoryLimit),
SealCost = x.Reward.SealCost,
Tier = x.Reward.Tier,
SubCategory = x.Reward.SubCategory,
StackSize = x.Reward.StackSize,
Type = x.Item.Type,
CheckRetainerInventory = x.Item.CheckRetainerInventory,
};
if (x.Item.Type == Configuration.PurchaseType.PurchaseOneTime)
{
request.OnPurchase = qty =>
{
request.EffectiveLimit -= (uint)qty;
x.Item.Limit -= qty;
ItemsWrapper.Save();
};
}
return request;
}) })
.ToList(); .ToList();
} }
@ -139,7 +213,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
public override unsafe void Draw() public override unsafe void Draw()
{ {
GrandCompany grandCompany = _plugin.GetGrandCompany(); GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None) if (grandCompany == GrandCompany.None)
{ {
// not sure we should ever get here // not sure we should ever get here
@ -147,16 +221,19 @@ internal sealed class TurnInWindow : LImGui.LWindow
return; return;
} }
if (_plugin.GetGrandCompanyRank() < 6) if (_gameFunctions.GetGrandCompanyRank() < 6)
{ {
State = false; State = false;
ImGui.TextColored(ImGuiColors.DalamudRed, "You do not have the required rank for Expert Delivery."); ImGui.TextColored(ImGuiColors.DalamudRed, "You do not have the required rank for Expert Delivery.");
DrawNextRankPrequesites();
return; return;
} }
else if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.DisableTurnIn && !IsOnHomeWorld) else if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.DisableTurnIn &&
!IsOnHomeWorld)
{ {
State = false; 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; return;
} }
@ -166,55 +243,133 @@ internal sealed class TurnInWindow : LImGui.LWindow
State = state; State = state;
} }
ImGui.Indent(27); float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
if (!string.IsNullOrEmpty(Error)) if (!string.IsNullOrEmpty(Error))
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, Error); using (ImRaii.PushIndent(indentSize))
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
} }
else else
{ {
if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning && !IsOnHomeWorld) using (ImRaii.PushIndent(indentSize))
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning &&
"Turn-In disabled, you are not on your home world and will not earn FC points."); !IsOnHomeWorld)
}
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)
{ {
ImGui.BeginDisabled(_condition[ConditionFlag.OccupiedInQuestEvent] || ImGui.TextColored(ImGuiColors.DalamudRed,
_condition[ConditionFlag.Casting]); "You are not on your home world and will not earn FC points.");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Bolt, "Use Priority Seal Allowance (15%)")) }
{
AgentInventoryContext.Instance()->UseItem(ItemIds.PrioritySealAllowance);
}
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); if (!IsMinimized)
ImGui.Separator(); {
ImGui.BeginDisabled(state); ImGui.Separator();
using (ImRaii.Disabled(state))
DrawItemsToBuy(grandCompany); DrawItemsToBuy(grandCompany);
}
ImGui.EndDisabled();
} }
ImGui.Separator(); if (_configuration.QuickTurnInKey != VirtualKey.NO_KEY)
ImGui.Text($"Debug (State): {_plugin.CurrentStage}"); {
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) private void DrawItemsToBuy(GrandCompany grandCompany)
@ -222,18 +377,29 @@ internal sealed class TurnInWindow : LImGui.LWindow
var itemsWrapper = ItemsWrapper; var itemsWrapper = ItemsWrapper;
ImGui.Text($"Items to buy ({itemsWrapper.Name}):"); ImGui.Text($"Items to buy ({itemsWrapper.Name}):");
List<(GcRewardItem Item, string Name)> comboValues = new() List<(GcRewardItem Item, string NameWithoutRetainers, string NameWithRetainers)> comboValues = new()
{ {
(GcRewardItem.None, GcRewardItem.None.Name), (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); var gcReward = _gcRewardsCache.GetReward(purchaseOption.ItemId);
int itemCount = _plugin.GetItemCount(itemId); int itemCountWithoutRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, false);
string itemName = gcReward.Name; int itemCountWithRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, true);
if (itemCount > 0) string itemNameWithoutRetainers = gcReward.Name;
itemName += $" ({itemCount:N0})"; string itemNameWithRetainers = gcReward.Name;
comboValues.Add((gcReward, itemName));
if (purchaseOption.SameQuantityForAllLists)
{
itemNameWithoutRetainers += $" {((SeIconChar)57412).ToIconString()}";
itemNameWithRetainers += $" {((SeIconChar)57412).ToIconString()}";
}
if (itemCountWithoutRetainers > 0)
itemNameWithoutRetainers += $" ({itemCountWithoutRetainers:N0})";
if (itemCountWithRetainers > 0)
itemNameWithRetainers += $" ({itemCountWithRetainers:N0})";
comboValues.Add((gcReward, itemNameWithoutRetainers, itemNameWithRetainers));
} }
if (itemsWrapper.GetItemsToPurchase().Count == 0) if (itemsWrapper.GetItemsToPurchase().Count == 0)
@ -242,118 +408,42 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save(); itemsWrapper.Save();
} }
int? itemToRemove = null; Configuration.PurchasePriority? itemToRemove = null;
Configuration.PurchasePriority? itemToAdd = null; Configuration.PurchasePriority? itemToAdd = null;
int indexToAdd = 0; int indexToAdd = 0;
float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < itemsWrapper.GetItemsToPurchase().Count; ++i) for (int i = 0; i < itemsWrapper.GetItemsToPurchase().Count; ++i)
{ {
ImGui.PushID($"ItemToBuy{i}"); DrawItemToBuy(grandCompany, i, itemsWrapper, comboValues, width, ref itemToRemove, itemPositions);
var item = itemsWrapper.GetItemsToPurchase()[i]; }
bool enabled = item.Enabled; if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
ImGui.PushID($"Enable{i}"); _draggedItem = null;
if (ImGui.Checkbox("", ref enabled)) 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)
{ {
item.Enabled = enabled; itemToAdd = items.Single(x => x.InternalId == _draggedItem);
itemsWrapper.Save(); indexToAdd = newIndex;
} }
}
ImGui.PopID(); if (itemToRemove != null)
{
ImGui.SameLine(0, 3); itemsWrapper.Remove(itemToRemove);
ImGui.BeginDisabled(!enabled); itemsWrapper.Save();
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);
}
if (ImGui.Combo("", ref comboValueIndex, comboValues.Select(x => x.Name).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(52);
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;
if (ImGui.InputInt("Maximum items to buy", 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(52);
}
ImGui.PopID();
} }
if (itemToAdd != null) if (itemToAdd != null)
@ -363,27 +453,22 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save(); itemsWrapper.Save();
} }
if (itemToRemove != null) if (_configuration.ItemsAvailableToPurchase.Any(x =>
{ itemsWrapper.GetItemsToPurchase().All(y => x.ItemId != y.ItemId)))
itemsWrapper.RemoveAt(itemToRemove.Value);
itemsWrapper.Save();
}
if (_configuration.ItemsAvailableForPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x != y.ItemId)))
{ {
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item")) if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item"))
ImGui.OpenPopup("##AddItem"); ImGui.OpenPopup("##AddItem");
if (ImGui.BeginPopupContextItem("##AddItem", ImGuiPopupFlags.NoOpenOverItems)) 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(); itemsWrapper.Save();
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
@ -392,7 +477,210 @@ internal sealed class TurnInWindow : LImGui.LWindow
ImGui.EndPopup(); 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) private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize, uint inventoryLimit)
@ -403,13 +691,13 @@ internal sealed class TurnInWindow : LImGui.LWindow
{ {
uint slotsThatCanBeUsed = 0; uint slotsThatCanBeUsed = 0;
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var inventoryType in _inventoryTypes) foreach (var inventoryType in InventoryTypes)
{ {
var container = inventoryManager->GetInventoryContainer(inventoryType); var container = inventoryManager->GetInventoryContainer(inventoryType);
for (int i = 0; i < container->Size; ++i) for (int i = 0; i < container->Size; ++i)
{ {
var item = container->GetInventorySlot(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++; slotsThatCanBeUsed++;
} }
@ -420,6 +708,8 @@ internal sealed class TurnInWindow : LImGui.LWindow
} }
} }
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
private interface IItemsToPurchase private interface IItemsToPurchase
{ {
string Name { get; } string Name { get; }
@ -435,10 +725,10 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class CharacterSpecificItemsToPurchase : IItemsToPurchase private sealed class CharacterSpecificItemsToPurchase : IItemsToPurchase
{ {
private readonly CharacterConfiguration _characterConfiguration; private readonly CharacterConfiguration _characterConfiguration;
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
public CharacterSpecificItemsToPurchase(CharacterConfiguration characterConfiguration, public CharacterSpecificItemsToPurchase(CharacterConfiguration characterConfiguration,
DalamudPluginInterface pluginInterface) IDalamudPluginInterface pluginInterface)
{ {
_characterConfiguration = characterConfiguration; _characterConfiguration = characterConfiguration;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@ -468,9 +758,9 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class GlobalItemsToPurchase : IItemsToPurchase private sealed class GlobalItemsToPurchase : IItemsToPurchase
{ {
private readonly Configuration _configuration; 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; _configuration = configuration;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;

View File

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