Compare commits

...

46 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
e240ef568f
Fix 'Trade High-Quality Item?' prompt for JP 2023-12-02 14:27:30 +01:00
576baf0175
Add options what to do when not on Home World 2023-12-02 13:52:55 +01:00
1cd1a2ede4
Purchase non-stackable items, filter 'add item' dropdown 2023-11-14 20:17:34 +01:00
63e2836027
Add icons to some UI parts 2023-11-11 14:04:18 +01:00
92db028e01
Move config button to titlebar 2023-11-11 02:36:47 +01:00
5b0bcd981b
Add an option to ignore 'Minimum Seals to keep' for a character 2023-11-09 12:25:37 +01:00
32 changed files with 2892 additions and 671 deletions

1017
Deliveroo/.editorconfig Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,63 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Dalamud.NET.Sdk/11.0.0">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>2.17</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Version>6.3</Version>
<OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<Import Project="..\LLib\LLib.targets"/>
<Import Project="..\LLib\RenameZip.targets"/>
<ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
</ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project>

View File

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

View File

@ -1,6 +1,7 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI;
@ -13,13 +14,13 @@ partial class DeliverooPlugin
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", "");
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().ReplaceLineEndings("");
_pluginLog.Verbose($"YesNo prompt: '{text}'");
if (CurrentStage == Stage.ConfirmReward &&
_gameStrings.ExchangeItems.IsMatch(text))
{
PurchaseItemRequest? item = GetNextItemToPurchase();
PurchaseItemRequest? item = _exchangeHandler.GetNextItemToPurchase();
if (item == null)
{
addonSelectYesNo->AtkUnitBase.FireCallbackInt(1);
@ -27,17 +28,20 @@ partial class DeliverooPlugin
return;
}
_pluginLog.Information($"Selecting 'yes' ({text})");
_pluginLog.Information($"Selecting 'yes' ({text}) (callback = {item.OnPurchase}, qty = {item.TemporaryPurchaseQuantity})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
var nextItem = GetNextItemToPurchase(item);
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
item.OnPurchase?.Invoke((int)item.TemporaryPurchaseQuantity);
item.TemporaryPurchaseQuantity = 0;
var nextItem = _exchangeHandler.GetNextItemToPurchase(item);
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.SelectRewardTier;
else
CurrentStage = Stage.CloseGcExchange;
_continueAt = DateTime.Now.AddSeconds(0.5);
ContinueAt = DateTime.Now.AddSeconds(0.5);
}
else if (CurrentStage == Stage.TurnInSelected &&
else if ((CurrentStage == Stage.TurnInSelected || (_configuration.QuickTurnInKey != VirtualKey.NO_KEY && _keyState[_configuration.QuickTurnInKey])) &&
_gameStrings.TradeHighQualityItem == text)
{
_pluginLog.Information($"Selecting 'yes' ({text})");

View File

@ -13,11 +13,12 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Deliveroo.External;
using Deliveroo.GameData;
using Deliveroo.Handlers;
using Deliveroo.Windows;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
namespace Deliveroo;
@ -25,64 +26,74 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName);
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IChatGui _chatGui;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly IClientState _clientState;
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
private readonly ICondition _condition;
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IKeyState _keyState;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly Configuration _configuration;
private readonly GameStrings _gameStrings;
private readonly GameFunctions _gameFunctions;
private readonly ExternalPluginHandler _externalPluginHandler;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly GcRewardsCache _gcRewardsCache;
private readonly IconCache _iconCache;
private readonly ItemCache _itemCache;
private readonly ExchangeHandler _exchangeHandler;
private readonly SupplyHandler _supplyHandler;
private readonly ConfigWindow _configWindow;
private readonly TurnInWindow _turnInWindow;
private readonly IReadOnlyDictionary<uint, uint> _sealCaps;
private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue;
private int _lastTurnInListSize = int.MaxValue;
private uint _turnInErrors = 0;
private List<PurchaseItemRequest> _itemsToPurchaseNow = new();
public DeliverooPlugin(DalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
public DeliverooPlugin(IDalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager,
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog,
IAddonLifecycle addonLifecycle)
IAddonLifecycle addonLifecycle, ITextureProvider textureProvider, IGameConfig gameConfig, IKeyState keyState)
{
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface;
_chatGui = chatGui;
_gameGui = gameGui;
_framework = framework;
_clientState = clientState;
_objectTable = objectTable;
_targetManager = targetManager;
_condition = condition;
_commandManager = commandManager;
_pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
_keyState = keyState;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
MigrateConfiguration();
_gameStrings = new GameStrings(dataManager, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, gameConfig, _configuration, _pluginLog);
_gameFunctions = new GameFunctions(objectTable, _clientState, targetManager, dataManager,
_externalPluginHandler, _pluginLog);
_gcRewardsCache = new GcRewardsCache(dataManager);
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState, _pluginLog);
_iconCache = new IconCache(textureProvider);
_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);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _gcRewardsCache, _configWindow);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _clientState,
_gcRewardsCache, _configWindow, _iconCache, _keyState, _gameFunctions);
_windowSystem.AddWindow(_turnInWindow);
_sealCaps = dataManager.GetExcelSheet<GrandCompanyRank>()!.Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, x => x.MaxSeals);
_framework.Update += FrameworkUpdate;
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
@ -103,9 +114,28 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
}
private void ChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
private void MigrateConfiguration()
{
#pragma warning disable CS0612 // Type or member is obsolete
if (_configuration.Version == 1)
{
_configuration.ItemsAvailableToPurchase = _configuration.ItemsAvailableForPurchase.Select(x =>
new Configuration.PurchaseOption
{
ItemId = x,
SameQuantityForAllLists = false,
}).ToList();
_configuration.Version = 2;
_pluginInterface.SavePluginConfig(_configuration);
}
#pragma warning restore CS0612 // Type or member is obsolete
}
private void ChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message,
ref bool isHandled)
{
if (_configuration.PauseAtRank <= 0)
return;
@ -123,7 +153,12 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
_turnInWindow.State = false;
_pluginLog.Information($"Pausing GC delivery, FC reached rank {rank}");
_chatGui.Print($"Pausing Deliveroo, your FC reached rank {rank}.");
DeliveryResult = new MessageDeliveryResult
{
Message = new SeStringBuilder()
.Append($"Pausing Deliveroo, your FC reached rank {rank}.")
.Build(),
};
return;
}
}
@ -145,10 +180,34 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
public int EffectiveReservedSealCount =>
_configuration.ReserveDifferentSealCountAtMaxRank && GetSealCap() == GetMaxSealCap()
? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount;
internal DateTime ContinueAt { private get; set; } = DateTime.MinValue;
internal List<PurchaseItemRequest> ItemsToPurchaseNow { get; private set; } = new();
internal int LastTurnInListSize { get; set; } = int.MaxValue;
internal IDeliveryResult? DeliveryResult { get; set; }
internal bool TurnInState
{
set => _turnInWindow.State = value;
}
internal string TurnInError
{
set => _turnInWindow.Error = value;
}
public int EffectiveReservedSealCount
{
get
{
if (CharacterConfiguration is { IgnoreMinimumSealsToKeep: true })
return 0;
return _configuration.ReserveDifferentSealCountAtMaxRank &&
_gameFunctions.GetSealCap() == _gameFunctions.MaxSealCap
? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount;
}
}
private void Login()
{
@ -159,11 +218,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() ||
CharacterConfiguration.CachedWorldName !=
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString())
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString())
{
CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString();
CharacterConfiguration.CachedWorldName =
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
CharacterConfiguration.Save(_pluginInterface);
}
@ -183,7 +242,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
private void Logout()
private void Logout(int type, int code)
{
CharacterConfiguration = null;
}
@ -194,59 +253,62 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (!_clientState.IsLoggedIn ||
_clientState.TerritoryType is not 128 and not 130 and not 132 ||
_condition[ConditionFlag.OccupiedInCutSceneEvent] ||
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f ||
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetQuartermasterId(), out IGameObject? quartermaster) >= 7f ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetPersonnelOfficerId(), out IGameObject? personnelOfficer) >=
7f ||
CharacterConfiguration is { DisableForCharacter: true } ||
_configWindow.IsOpen)
{
_turnInWindow.IsOpen = false;
_turnInWindow.State = false;
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
StopTurnIn();
}
else if (DateTime.Now > _continueAt)
else if (DateTime.Now > ContinueAt)
{
_turnInWindow.IsOpen = true;
_turnInWindow.Multiplier = GetSealMultiplier();
_turnInWindow.Multiplier = _gameFunctions.GetSealMultiplier();
if (!_turnInWindow.State)
{
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
StopTurnIn();
return;
}
else if (_turnInWindow.State && CurrentStage == Stage.Stopped)
{
CurrentStage = Stage.TargetPersonnelOfficer;
_itemsToPurchaseNow = _turnInWindow.SelectedItems;
if (_itemsToPurchaseNow.Count > 0)
ItemsToPurchaseNow = _turnInWindow.SelectedItems;
DeliveryResult = new MessageDeliveryResult();
_supplyHandler.ResetTurnInErrorHandling();
if (ItemsToPurchaseNow.Count > 0)
{
_pluginLog.Information("Items to purchase:");
foreach (var item in _itemsToPurchaseNow)
foreach (var item in ItemsToPurchaseNow)
_pluginLog.Information($" {item.Name} (limit = {item.EffectiveLimit})");
}
else
_pluginLog.Information("No items to purchase configured or available");
var nextItem = GetNextItemToPurchase();
if (nextItem != null && GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
var nextItem = _exchangeHandler.GetNextItemToPurchase();
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >=
EffectiveReservedSealCount + nextItem.SealCost)
CurrentStage = Stage.TargetQuartermaster;
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", out var gcSupplyList) &&
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var gcSupplyList) &&
LAddon.IsAddonReady(&gcSupplyList->AtkUnitBase))
CurrentStage = Stage.SelectExpertDeliveryTab;
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) &&
LAddon.IsAddonReady(gcExchange))
CurrentStage = Stage.SelectRewardTier;
if (_gameGui.TryGetAddonByName<AddonSelectString>("SelectString", out var addonSelectString) &&
LAddon.IsAddonReady(&addonSelectString->AtkUnitBase))
{
if (SelectStringPostSetup(addonSelectString, Stage.OpenGcSupply))
return;
}
}
if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_externalPluginHandler.Saved)
@ -255,7 +317,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
switch (CurrentStage)
{
case Stage.TargetPersonnelOfficer:
InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
_supplyHandler.InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
break;
case Stage.OpenGcSupply:
@ -263,43 +325,48 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.SelectExpertDeliveryTab:
SelectExpertDeliveryTab();
_supplyHandler.SelectExpertDeliveryTab();
break;
case Stage.SelectItemToTurnIn:
SelectItemToTurnIn();
_supplyHandler.SelectItemToTurnIn();
break;
case Stage.TurnInSelected:
TurnInSelectedItem();
// see GrandCompanySupplyReward
break;
case Stage.FinalizeTurnIn:
FinalizeTurnInItem();
case Stage.SingleFinalizeTurnIn:
_supplyHandler.FinalizeTurnInItem();
break;
case Stage.CloseGcSupply:
case Stage.CloseGcSupplySelectString:
// see SelectStringPostSetup
break;
case Stage.CloseGcSupplyThenStop:
case Stage.CloseGcSupplySelectStringThenStop:
// see SelectStringPostSetup
break;
case Stage.CloseGcSupplyWindowThenStop:
_supplyHandler.CloseGcSupplyWindow();
break;
case Stage.TargetQuartermaster:
InteractWithQuartermaster(personnelOfficer!, quartermaster!);
_exchangeHandler.InteractWithQuartermaster(personnelOfficer!, quartermaster!);
break;
case Stage.SelectRewardTier:
SelectRewardTier();
_exchangeHandler.SelectRewardTier();
break;
case Stage.SelectRewardSubCategory:
SelectRewardSubCategory();
_exchangeHandler.SelectRewardSubCategory();
break;
case Stage.SelectReward:
SelectReward();
_exchangeHandler.SelectReward();
break;
case Stage.ConfirmReward:
@ -307,14 +374,13 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.CloseGcExchange:
CloseGcExchange();
_exchangeHandler.CloseGcExchange();
break;
case Stage.RequestStop:
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
StopTurnIn();
break;
case Stage.Stopped:
break;
@ -325,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()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
@ -339,7 +454,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_framework.Update -= FrameworkUpdate;
_externalPluginHandler.Restore();
_gameFunctions.Dispose();
_externalPluginHandler.Dispose();
_iconCache.Dispose();
}
private void ProcessCommand(string command, string arguments)

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;
public DeliverooIpc(DalamudPluginInterface pluginInterface)
public DeliverooIpc(IDalamudPluginInterface pluginInterface)
{
_isTurnInRunning = pluginInterface.GetIpcProvider<bool>(IsTurnInRunning);
_turnInStarted = pluginInterface.GetIpcProvider<object>(TurnInStarted);

View File

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

View File

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

View File

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

View File

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

View File

@ -10,24 +10,29 @@ internal sealed class GcRewardItem : IEquatable<GcRewardItem>
{
ItemId = 0,
Name = "---",
IconId = 0,
GrandCompanies = new List<GrandCompany>().AsReadOnly(),
Tier = RewardTier.First,
SubCategory = RewardSubCategory.Unknown,
RequiredRank = 0,
StackSize = 0,
SealCost = 100_000,
InventoryLimit = int.MaxValue,
};
public required uint ItemId { get; init; }
public required string Name { get; init; }
public required ushort IconId { get; init; }
public required IReadOnlyList<GrandCompany> GrandCompanies { get; init; }
public required RewardTier Tier { get; init; }
public required RewardSubCategory SubCategory { get; init; }
public required uint RequiredRank { get; init; }
public required uint StackSize { get; init; }
public required uint SealCost { get; init; }
public required uint InventoryLimit { get; init; }
public bool IsValid() => ItemId > 0 && GrandCompanies.Count > 0;
public bool Limited => GrandCompanies.Count < 3;
public bool Equals(GcRewardItem? other)
{

View File

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

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 Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Deliveroo;
namespace Deliveroo.Handlers;
partial class DeliverooPlugin
internal sealed class ExchangeHandler
{
private void InteractWithQuartermaster(GameObject personnelOfficer, GameObject quartermaster)
private readonly DeliverooPlugin _plugin;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui;
private readonly IPluginLog _pluginLog;
public ExchangeHandler(DeliverooPlugin plugin, GameFunctions gameFunctions, ITargetManager targetManager,
IGameGui gameGui, IChatGui chatGui, IPluginLog pluginLog)
{
if (GetCurrentSealCount() < EffectiveReservedSealCount)
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_chatGui = chatGui;
_pluginLog = pluginLog;
}
public void InteractWithQuartermaster(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_gameFunctions.GetCurrentSealCount() < _plugin.EffectiveReservedSealCount)
{
CurrentStage = Stage.RequestStop;
_plugin.CurrentStage = Stage.RequestStop;
return;
}
if (_targetManager.Target == personnelOfficer)
return;
InteractWithTarget(quartermaster);
CurrentStage = Stage.SelectRewardTier;
_gameFunctions.InteractWithTarget(quartermaster);
_plugin.CurrentStage = Stage.SelectRewardTier;
}
private PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
public PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
{
foreach (PurchaseItemRequest request in _itemsToPurchaseNow)
foreach (PurchaseItemRequest request in _plugin.ItemsToPurchaseNow)
{
int offset = 0;
int toBuy = 0;
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)
return request;
if (request.Type == Configuration.PurchaseType.KeepStocked)
{
if (_gameFunctions.GetItemCount(request.ItemId, request.CheckRetainerInventory) + toBuy <
request.EffectiveLimit)
return request;
}
else
{
if (toBuy < request.EffectiveLimit)
return request;
}
}
return null;
}
private unsafe void SelectRewardTier()
public unsafe void SelectRewardTier()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
_plugin.CurrentStage = Stage.CloseGcExchange;
return;
}
@ -65,17 +100,17 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectRank);
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectRewardSubCategory;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectRewardSubCategory;
}
}
private unsafe void SelectRewardSubCategory()
public unsafe void SelectRewardSubCategory()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
_plugin.CurrentStage = Stage.CloseGcExchange;
return;
}
@ -96,25 +131,25 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectType);
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectReward;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectReward;
}
}
private unsafe void SelectReward()
public unsafe void SelectReward()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
{
if (SelectRewardItem(addonExchange))
{
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.ConfirmReward;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.ConfirmReward;
}
else
{
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.CloseGcExchange;
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.CloseGcExchange;
}
}
}
@ -132,10 +167,15 @@ partial class DeliverooPlugin
if (itemId == item.ItemId)
{
_pluginLog.Information($"Selecting item {itemId}, {i}");
long toBuy = (GetCurrentSealCount() - EffectiveReservedSealCount) / item.SealCost;
toBuy = Math.Min(toBuy, item.EffectiveLimit - GetItemCount(item.ItemId));
long toBuy = (_gameFunctions.GetCurrentSealCount() - _plugin.EffectiveReservedSealCount) /
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);
if (toBuy <= 0)
@ -144,7 +184,10 @@ partial class DeliverooPlugin
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[]
{
new() { Type = ValueType.Int, Int = 0 },
@ -166,13 +209,24 @@ partial class DeliverooPlugin
return false;
}
private unsafe void CloseGcExchange()
public unsafe void CloseGcExchange()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
{
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.Collections.Generic;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -8,25 +10,43 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Deliveroo;
namespace Deliveroo.Handlers;
partial class DeliverooPlugin
internal sealed class SupplyHandler
{
private void InteractWithPersonnelOfficer(GameObject personnelOfficer, GameObject quartermaster)
{
if (_targetManager.Target == quartermaster)
return;
private readonly DeliverooPlugin _plugin;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IGameGui _gameGui;
private readonly IPluginLog _pluginLog;
InteractWithTarget(personnelOfficer);
CurrentStage = Stage.OpenGcSupply;
private uint _turnInErrors;
public SupplyHandler(DeliverooPlugin plugin, GameFunctions gameFunctions, ITargetManager targetManager,
IGameGui gameGui, IPluginLog pluginLog)
{
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_pluginLog = pluginLog;
}
private unsafe void SelectExpertDeliveryTab()
public void InteractWithPersonnelOfficer(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_targetManager.Target?.EntityId == quartermaster.EntityId)
return;
_gameFunctions.InteractWithTarget(personnelOfficer);
_plugin.CurrentStage = Stage.OpenGcSupply;
}
public unsafe void SelectExpertDeliveryTab()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonID();
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
@ -40,7 +60,7 @@ partial class DeliverooPlugin
{
_pluginLog.Information("Tab already selected, probably due to haseltweaks");
ResetTurnInErrorHandling();
CurrentStage = Stage.SelectItemToTurnIn;
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
return;
}
@ -53,23 +73,23 @@ partial class DeliverooPlugin
};
addon->FireCallback(3, selectExpertDeliveryTab);
ResetTurnInErrorHandling();
CurrentStage = Stage.SelectItemToTurnIn;
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
}
}
private void ResetTurnInErrorHandling(int listSize = int.MaxValue)
public void ResetTurnInErrorHandling(int listSize = int.MaxValue)
{
_pluginLog.Verbose("Resetting error handling state");
_lastTurnInListSize = listSize;
_plugin.LastTurnInListSize = listSize;
_turnInErrors = 0;
}
private unsafe void SelectItemToTurnIn()
public unsafe void SelectItemToTurnIn()
{
var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply);
if (agentInterface != null && agentInterface->IsAgentActive())
{
var addonId = agentInterface->GetAddonID();
var addonId = agentInterface->GetAddonId();
if (addonId == 0)
return;
@ -78,28 +98,30 @@ partial class DeliverooPlugin
return;
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;
if (addonGc->SelectedTab != 2)
{
_turnInWindow.Error = "Wrong tab selected";
_plugin.TurnInError = "Wrong tab selected";
return;
}
ItemFilterType configuredFilter = ResolveSelectedSupplyFilter();
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter)
{
_turnInWindow.Error =
_plugin.TurnInError =
$"Wrong filter selected (expected {configuredFilter}, but is {(ItemFilterType)addonGc->SelectedFilter})";
return;
}
int currentListSize = addonGc->ExpertDeliveryList->ListLength;
if (addonGc->ListEmptyTextNode->AtkResNode.IsVisible || currentListSize == 0)
if (addonGc->ListEmptyTextNode->AtkResNode.IsVisible() || currentListSize == 0)
{
_pluginLog.Information($"No items to turn in {addonGc->ListEmptyTextNode->AtkResNode.IsVisible}, {currentListSize})");
CurrentStage = Stage.CloseGcSupplyThenStop;
_pluginLog.Information(
$"No items to turn in ({addonGc->ListEmptyTextNode->AtkResNode.IsVisible}, {currentListSize})");
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1);
return;
}
@ -108,28 +130,29 @@ partial class DeliverooPlugin
// something is wrong.
if (_turnInErrors > 10)
{
_turnInWindow.Error = "Unable to refresh item list";
_plugin.TurnInError = "Unable to refresh item list";
return;
}
if (currentListSize >= _lastTurnInListSize)
if (currentListSize >= _plugin.LastTurnInListSize)
{
_turnInErrors++;
_pluginLog.Information($"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_lastTurnInListSize}, new list size = {currentListSize})...");
_pluginLog.Information(
$"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_plugin.LastTurnInListSize}, new list size = {currentListSize})...");
addon->FireCallbackInt(2);
_continueAt = DateTime.Now.AddSeconds(0.1);
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.1);
return;
}
ResetTurnInErrorHandling(currentListSize);
var agent = (AgentGrandCompanySupply*)agentInterface;
List<TurnInItem> items = BuildTurnInList(agent);
List<TurnInItem> items = _gameFunctions.BuildTurnInList(agent);
if (items.Count == 0)
{
// probably shouldn't happen with the previous node visibility check
CurrentStage = Stage.CloseGcSupplyThenStop;
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
addon->FireCallbackInt(-1);
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
// 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);
return;
}
@ -167,24 +190,11 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addon->FireCallback(3, selectFirstItem);
CurrentStage = Stage.TurnInSelected;
_plugin.CurrentStage = Stage.TurnInSelected;
}
}
private unsafe void TurnInSelectedItem()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyReward>("GrandCompanySupplyReward",
out var addonSupplyReward) && LAddon.IsAddonReady(&addonSupplyReward->AtkUnitBase))
{
_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()
public unsafe void FinalizeTurnInItem()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var addonSupplyList) && LAddon.IsAddonReady(&addonSupplyList->AtkUnitBase))
@ -196,15 +206,36 @@ partial class DeliverooPlugin
new() { Type = 0, Int = 0 }
};
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter);
CurrentStage = Stage.SelectItemToTurnIn;
if (_plugin.CurrentStage == Stage.FinalizeTurnIn)
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
else
_plugin.CurrentStage = Stage.RequestStop;
}
}
private ItemFilterType ResolveSelectedSupplyFilter()
{
if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
if (_plugin.CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
return ItemFilterType.HideArmouryChestItems;
return ItemFilterType.HideGearSetItems;
}
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;
@ -6,9 +7,14 @@ internal sealed class PurchaseItemRequest
{
public required uint ItemId { get; init; }
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 RewardTier Tier { get; init; }
public required RewardSubCategory SubCategory { 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,
TurnInSelected,
FinalizeTurnIn,
CloseGcSupply,
CloseGcSupplyThenStop,
CloseGcSupplySelectString,
CloseGcSupplySelectStringThenStop,
CloseGcSupplyWindowThenStop,
TargetQuartermaster,
SelectRewardTier,
@ -20,4 +21,6 @@ internal enum Stage
RequestStop,
Stopped,
SingleFinalizeTurnIn,
}

View File

@ -2,31 +2,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Deliveroo.GameData;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows;
internal sealed class ConfigWindow : LImGui.LWindow
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DeliverooPlugin _plugin;
private readonly Configuration _configuration;
private readonly GcRewardsCache _gcRewardsCache;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly IconCache _iconCache;
private readonly GameFunctions _gameFunctions;
private readonly IReadOnlyDictionary<uint, GcRewardItem> _itemLookup;
private uint _dragDropSource;
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog)
private readonly List<(VirtualKey Key, string Label)> _keyCodes =
[
(VirtualKey.NO_KEY, "Disabled"),
(VirtualKey.CONTROL, "CTRL"),
(VirtualKey.SHIFT, "SHIFT"),
(VirtualKey.MENU, "ALT"),
];
private string _searchString = string.Empty;
private Configuration.PurchaseOption? _dragDropSource;
public ConfigWindow(IDalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog, IconCache iconCache,
GameFunctions gameFunctions)
: base("Deliveroo - Configuration###DeliverooConfig")
{
_pluginInterface = pluginInterface;
@ -35,6 +53,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
_gcRewardsCache = gcRewardsCache;
_clientState = clientState;
_pluginLog = pluginLog;
_iconCache = iconCache;
_gameFunctions = gameFunctions;
_itemLookup = _gcRewardsCache.RewardLookup;
@ -48,6 +68,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
};
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw()
{
if (_configuration.AddVentureIfNoItemToPurchaseSelected())
@ -67,49 +89,80 @@ internal sealed class ConfigWindow : LImGui.LWindow
{
if (ImGui.BeginTabItem("Items for Auto-Buy"))
{
uint? itemToRemove = null;
uint? itemToAdd = null;
Configuration.PurchaseOption? itemToRemove = null;
Configuration.PurchaseOption? itemToAdd = null;
int indexToAdd = 0;
if (ImGui.BeginChild("Items", new Vector2(-1, -30), true, ImGuiWindowFlags.NoSavedSettings))
if (ImGui.BeginChild("Items", new Vector2(-1, -ImGui.GetFrameHeightWithSpacing()), true,
ImGuiWindowFlags.NoSavedSettings))
{
for (int i = 0; i < _configuration.ItemsAvailableForPurchase.Count; ++i)
for (int i = 0; i < _configuration.ItemsAvailableToPurchase.Count; ++i)
{
uint itemId = _configuration.ItemsAvailableForPurchase[i];
var purchaseOption = _configuration.ItemsAvailableToPurchase[i];
ImGui.PushID($"###Item{i}");
ImGui.BeginDisabled(
_configuration.ItemsAvailableForPurchase.Count == 1 && itemId == ItemIds.Venture);
_configuration.ItemsAvailableToPurchase.Count == 1 && purchaseOption.ItemId == ItemIds.Venture);
ImGui.Selectable(_itemLookup[itemId].Name);
if (ImGui.BeginDragDropSource())
var item = _itemLookup[purchaseOption.ItemId];
using (var icon = _iconCache.GetIcon(item.IconId))
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = itemId;
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource > 0 &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
if (icon != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = 0;
ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X,
ImGui.GetStyle().ItemSpacing.Y / 2));
}
ImGui.EndDragDropTarget();
}
ImGui.Selectable(
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}{(purchaseOption.SameQuantityForAllLists ? $" {((SeIconChar)57412).ToIconString()} (Limit: {purchaseOption.GlobalLimit:N0})" : "")}",
false, ImGuiSelectableFlags.SpanAllColumns);
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopup($"###ctx{i}"))
{
if (ImGui.Selectable($"Remove {_itemLookup[itemId].Name}"))
itemToRemove = itemId;
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = purchaseOption;
ImGui.EndPopup();
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource != null &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = null;
}
ImGui.EndDragDropTarget();
}
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopup($"###ctx{i}"))
{
bool sameQuantityForAllLists = purchaseOption.SameQuantityForAllLists;
if (ImGui.MenuItem("Use same quantity for global and character-specific lists", null,
ref sameQuantityForAllLists))
{
purchaseOption.SameQuantityForAllLists = sameQuantityForAllLists;
Save();
}
if (ImGui.MenuItem($"Remove {_itemLookup[purchaseOption.ItemId].Name}"))
itemToRemove = purchaseOption;
ImGui.EndPopup();
}
if (icon != null)
{
ImGui.SameLine(0, 0);
ImGui.SetCursorPos(pos);
ImGui.Image(icon.ImGuiHandle, iconSize);
}
}
ImGui.EndDisabled();
@ -121,33 +174,63 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (itemToRemove != null)
{
_configuration.ItemsAvailableForPurchase.Remove(itemToRemove.Value);
_configuration.ItemsAvailableToPurchase.Remove(itemToRemove);
Save();
}
if (itemToAdd != null)
{
_configuration.ItemsAvailableForPurchase.Remove(itemToAdd.Value);
_configuration.ItemsAvailableForPurchase.Insert(indexToAdd, itemToAdd.Value);
_configuration.ItemsAvailableToPurchase.Remove(itemToAdd);
_configuration.ItemsAvailableToPurchase.Insert(indexToAdd, itemToAdd);
Save();
}
List<(uint ItemId, string Name)> 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.StackSize > 1)
.Where(x => !_configuration.ItemsAvailableForPurchase.Contains(x.ItemId))
.Select(x => (x.ItemId, x.Name))
.Where(x => _configuration.ItemsAvailableToPurchase.All(y => y.ItemId != x.ItemId))
.Select(x => (x.ItemId, x.Name, x.IconId, x.Limited))
.OrderBy(x => x.Name)
.ThenBy(x => x.GetHashCode())
.ToList();
comboValues.Insert(0, (0, ""));
int currentItem = 0;
if (ImGui.Combo("Add Item", ref currentItem, comboValues.Select(x => x.Name).ToArray(), comboValues.Count)
&& comboValues[currentItem].ItemId != GcRewardItem.None.ItemId)
if (ImGui.BeginCombo($"##ItemSelection", "Add Item...", ImGuiComboFlags.HeightLarge))
{
_configuration.ItemsAvailableForPurchase.Add(comboValues[currentItem].ItemId);
Save();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue);
foreach (var item in comboValues.Where(x =>
x.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase)))
{
using (var icon = _iconCache.GetIcon(item.IconId))
{
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.X);
}
}
bool addThis =
ImGui.Selectable(
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}##SelectVenture{item.IconId}");
if (addThis || addFirst)
{
_configuration.ItemsAvailableToPurchase.Add(new Configuration.PurchaseOption
{ ItemId = item.ItemId });
if (addFirst)
{
addFirst = false;
ImGui.CloseCurrentPopup();
}
Save();
}
}
ImGui.EndCombo();
}
ImGui.EndTabItem();
@ -161,7 +244,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
{
string currentCharacterName = _clientState.LocalPlayer!.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
ImGui.Text($"Current Character: {currentCharacterName} @ {currentWorldName}");
ImGui.Spacing();
ImGui.Separator();
@ -205,9 +288,20 @@ internal sealed class ConfigWindow : LImGui.LWindow
charConfiguration.Save(_pluginInterface);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
"The default filter for all characters is 'Hide Gear Set Items', but you may want to override this to hide all Armoury Chest items (regardless of whether they're part of a gear set) e.g. for your main character.");
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"The default filter for all characters is 'Hide Gear Set Items', but you may want to override this to hide all Armoury Chest items (regardless of whether they're part of a gear set) e.g. for your main character.");
bool ignoreMinimumSealsToKeep = charConfiguration.IgnoreMinimumSealsToKeep;
if (ImGui.Checkbox("Ignore 'Minimum Seals to keep' setting", ref ignoreMinimumSealsToKeep))
{
charConfiguration.IgnoreMinimumSealsToKeep = ignoreMinimumSealsToKeep;
charConfiguration.Save(_pluginInterface);
}
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"When enabled, all GC seals will be spent. This is effectively the same as setting 'Minimum Seals to keep' to 0.");
ImGui.EndDisabled();
ImGui.Spacing();
@ -262,7 +356,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
int reservedSealCount = _configuration.ReservedSealCount;
if (ImGui.InputInt("Minimum Seals to keep (e.g. for Squadron Missions)", ref reservedSealCount, 1000))
{
_configuration.ReservedSealCount = Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCount));
_configuration.ReservedSealCount =
Math.Max(0, Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCount));
Save();
}
@ -276,34 +371,96 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (reserveDifferentSealCountAtMaxRank)
{
ImGui.Indent();
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.Indent(indentSize);
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100);
int reservedSealCountAtMaxRank = _configuration.ReservedSealCountAtMaxRank;
if (ImGui.InputInt("Minimum seals to keep at max rank", ref reservedSealCountAtMaxRank))
{
_configuration.ReservedSealCountAtMaxRank = Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCountAtMaxRank));
_configuration.ReservedSealCountAtMaxRank = Math.Max(0,
Math.Min((int)_gameFunctions.MaxSealCap, reservedSealCountAtMaxRank));
Save();
}
ImGui.Unindent();
ImGui.Unindent(indentSize);
}
ImGui.EndDisabled();
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
int selectedKey = Math.Max(0, _keyCodes.FindIndex(x => x.Key == _configuration.QuickTurnInKey));
if (ImGui.Combo("Quick Turn-In Key", ref selectedKey, _keyCodes.Select(x => x.Label).ToArray(),
_keyCodes.Count))
{
_configuration.QuickTurnInKey = _keyCodes[selectedKey].Key;
Save();
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
var xivChatTypes = Enum.GetValues<XivChatType>()
.Where(x => x != XivChatType.StandardEmote)
.ToArray();
var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.ChatType);
string[] chatTypeNames = xivChatTypes
.Select(t => t.GetAttribute<XivChatTypeInfoAttribute>()?.FancyName ?? t.ToString())
.ToArray();
if (ImGui.Combo("Chat channel for status updates", ref selectedChatType, chatTypeNames,
chatTypeNames.Length))
{
_configuration.ChatType = xivChatTypes[selectedChatType];
Save();
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
List<(int Rank, string Name)> rankUpComboValues = Enumerable.Range(1, 30)
.Select(x => x == 1 ? (0, "---") : (x, $"Rank {x}"))
.ToList();
int pauseAtRank = Math.Max(rankUpComboValues.FindIndex(x => x.Rank == _configuration.PauseAtRank), 0);
if (ImGui.Combo("Pause when reaching selected FC Rank", ref pauseAtRank, rankUpComboValues.Select(x => x.Name).ToArray(),
if (ImGui.Combo("Pause when reaching selected FC Rank", ref pauseAtRank,
rankUpComboValues.Select(x => x.Name).ToArray(),
rankUpComboValues.Count))
{
_configuration.PauseAtRank = rankUpComboValues[pauseAtRank].Rank;
Save();
}
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 120);
string[] behaviorOnOtherWorldNames = { "---", "Show Warning", "Disable Turn-In" };
int behaviorOnOtherWorld = (int)_configuration.BehaviorOnOtherWorld;
if (ImGui.Combo("Behavior when not on Home World", ref behaviorOnOtherWorld,
behaviorOnOtherWorldNames, behaviorOnOtherWorldNames.Length))
{
_configuration.BehaviorOnOtherWorld = (Configuration.EBehaviorOnOtherWorld)behaviorOnOtherWorld;
Save();
}
ImGui.Separator();
bool disableFrameLimiter = _configuration.DisableFrameLimiter;
if (ImGui.Checkbox("Disable the game setting 'Limit frame rate when client is inactive'",
ref disableFrameLimiter))
{
_configuration.DisableFrameLimiter = disableFrameLimiter;
Save();
}
ImGui.Indent();
ImGui.BeginDisabled(!disableFrameLimiter);
bool uncapFrameRate = _configuration.UncapFrameRate;
if (ImGui.Checkbox("Set frame rate to uncapped", ref uncapFrameRate))
{
_configuration.UncapFrameRate = uncapFrameRate;
Save();
}
ImGui.EndDisabled();
ImGui.Unindent();
ImGui.EndTabItem();
}
}
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
public void SaveWindowConfig() => Save();
}

View File

@ -3,10 +3,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
@ -14,28 +18,64 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace Deliveroo.Windows;
internal sealed class TurnInWindow : LImGui.LWindow
internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configuration.MinimizableWindowConfig>
{
private static readonly IReadOnlyList<InventoryType> InventoryTypes = new[]
{
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4,
InventoryType.ArmoryMainHand,
InventoryType.ArmoryOffHand,
InventoryType.ArmoryHead,
InventoryType.ArmoryBody,
InventoryType.ArmoryHands,
InventoryType.ArmoryLegs,
InventoryType.ArmoryFeets,
InventoryType.ArmoryEar,
InventoryType.ArmoryNeck,
InventoryType.ArmoryWrist,
InventoryType.ArmoryRings,
InventoryType.EquippedItems
}.AsReadOnly();
private static readonly string[] StockingTypeLabels = { "Purchase Once", "Keep in Stock" };
private readonly DeliverooPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly GcRewardsCache _gcRewardsCache;
private readonly ConfigWindow _configWindow;
private readonly IconCache _iconCache;
private readonly IKeyState _keyState;
private readonly GameFunctions _gameFunctions;
private readonly TitleBarButton _minimizeButton;
public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration,
ICondition condition, GcRewardsCache gcRewardsCache, ConfigWindow configWindow)
private bool _state;
private Guid? _draggedItem;
public TurnInWindow(DeliverooPlugin plugin, IDalamudPluginInterface pluginInterface, Configuration configuration,
ICondition condition, IClientState clientState, GcRewardsCache gcRewardsCache, ConfigWindow configWindow,
IconCache iconCache, IKeyState keyState, GameFunctions gameFunctions)
: base("GC Delivery###DeliverooTurnIn")
{
_plugin = plugin;
_pluginInterface = pluginInterface;
_configuration = configuration;
_condition = condition;
_clientState = clientState;
_gcRewardsCache = gcRewardsCache;
_configWindow = configWindow;
_iconCache = iconCache;
_keyState = keyState;
_gameFunctions = gameFunctions;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver;
@ -46,18 +86,74 @@ internal sealed class TurnInWindow : LImGui.LWindow
MaximumSize = new Vector2(500, 999),
};
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse;
State = false;
ShowCloseButton = false;
AllowClickthrough = false;
_minimizeButton = new TitleBarButton
{
Icon = FontAwesomeIcon.Minus,
Priority = int.MinValue,
IconOffset = new Vector2(1.5f, 1),
Click = _ =>
{
IsMinimized = !IsMinimized;
_minimizeButton!.Icon = IsMinimized ? FontAwesomeIcon.WindowMaximize : FontAwesomeIcon.Minus;
},
AvailableClickthrough = true,
};
TitleBarButtons.Insert(0, _minimizeButton);
TitleBarButtons.Add(new TitleBarButton
{
Icon = FontAwesomeIcon.Cog,
IconOffset = new Vector2(1.5f, 1),
Click = _ => configWindow.IsOpen = true,
Priority = int.MinValue,
ShowTooltip = () =>
{
ImGui.BeginTooltip();
ImGui.Text("Open Configuration");
ImGui.EndTooltip();
}
});
}
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 bool State { get; set; }
public decimal Multiplier { private get; set; }
public string Error { private get; set; } = string.Empty;
private bool UseCharacterSpecificItemsToPurchase =>
_plugin.CharacterConfiguration is { OverrideItemsToPurchase: true };
private bool IsOnHomeWorld =>
_clientState.LocalPlayer == null ||
_clientState.LocalPlayer.HomeWorld.RowId == _clientState.LocalPlayer.CurrentWorld.RowId;
private IItemsToPurchase ItemsWrapper => UseCharacterSpecificItemsToPurchase
? new CharacterSpecificItemsToPurchase(_plugin.CharacterConfiguration!, _pluginInterface)
: new GlobalItemsToPurchase(_configuration, _pluginInterface);
@ -66,29 +162,50 @@ internal sealed class TurnInWindow : LImGui.LWindow
{
get
{
GrandCompany grandCompany = _plugin.GetGrandCompany();
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None)
return new List<PurchaseItemRequest>();
var rank = _plugin.GetGrandCompanyRank();
var rank = _gameFunctions.GetGrandCompanyRank();
return ItemsWrapper.GetItemsToPurchase()
.Where(x => x.ItemId != GcRewardItem.None.ItemId)
.Where(x => x.Enabled)
.Where(x => x.Type == Configuration.PurchaseType.KeepStocked || x.Limit > 0)
.Select(x => new { Item = x, Reward = _gcRewardsCache.GetReward(x.ItemId) })
.Where(x => x.Reward.GrandCompanies.Contains(grandCompany))
.Where(x => x.Reward.RequiredRank <= rank)
.Select(x => new PurchaseItemRequest
.Select(x =>
{
ItemId = x.Item.ItemId,
Name = x.Reward.Name,
EffectiveLimit = CalculateEffectiveLimit(
x.Item.ItemId,
x.Item.Limit <= 0 ? uint.MaxValue : (uint)x.Item.Limit,
x.Reward.StackSize),
SealCost = x.Reward.SealCost,
Tier = x.Reward.Tier,
SubCategory = x.Reward.SubCategory,
StackSize = x.Reward.StackSize,
var purchaseOption = _configuration.ItemsAvailableToPurchase.Where(y => y.SameQuantityForAllLists)
.FirstOrDefault(y => y.ItemId == x.Item.ItemId);
int limit = purchaseOption?.GlobalLimit ?? x.Item.Limit;
var request = new PurchaseItemRequest
{
ItemId = x.Item.ItemId,
Name = x.Reward.Name,
EffectiveLimit = CalculateEffectiveLimit(
x.Item.ItemId,
limit <= 0 ? uint.MaxValue : (uint)limit,
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();
}
@ -96,7 +213,7 @@ internal sealed class TurnInWindow : LImGui.LWindow
public override unsafe void Draw()
{
GrandCompany grandCompany = _plugin.GetGrandCompany();
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
if (grandCompany == GrandCompany.None)
{
// not sure we should ever get here
@ -104,10 +221,19 @@ internal sealed class TurnInWindow : LImGui.LWindow
return;
}
if (_plugin.GetGrandCompanyRank() < 6)
if (_gameFunctions.GetGrandCompanyRank() < 6)
{
State = false;
ImGui.TextColored(ImGuiColors.DalamudRed, "You do not have the required rank for Expert Delivery.");
DrawNextRankPrequesites();
return;
}
else if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.DisableTurnIn &&
!IsOnHomeWorld)
{
State = false;
ImGui.TextColored(ImGuiColors.DalamudRed, "Turn-in disabled, you are not on your home world.");
return;
}
@ -117,51 +243,133 @@ internal sealed class TurnInWindow : LImGui.LWindow
State = state;
}
ImGui.SameLine();
if (ImGuiComponents.IconButton("###OpenConfig", FontAwesomeIcon.Cog))
_configWindow.IsOpen = true;
ImGui.Indent(27);
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
if (!string.IsNullOrEmpty(Error))
{
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
using (ImRaii.PushIndent(indentSize))
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
}
else
{
if (Multiplier == 1m)
using (ImRaii.PushIndent(indentSize))
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff.");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
}
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning &&
!IsOnHomeWorld)
{
ImGui.BeginDisabled(_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.Casting]);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Bolt, "Use Priority Seal Allowance (15%)"))
ImGui.TextColored(ImGuiColors.DalamudRed,
"You are not on your home world and will not earn FC points.");
}
if (Multiplier == 1m)
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff.");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
}
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
AgentInventoryContext* agentInventoryContext = AgentInventoryContext.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
{
AgentInventoryContext.Instance()->UseItem(ItemIds.PrioritySealAllowance);
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.EndDisabled();
}
}
ImGui.Unindent(27);
ImGui.Separator();
ImGui.BeginDisabled(state);
DrawItemsToBuy(grandCompany);
ImGui.EndDisabled();
if (!IsMinimized)
{
ImGui.Separator();
using (ImRaii.Disabled(state))
DrawItemsToBuy(grandCompany);
}
}
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
if (_configuration.QuickTurnInKey != VirtualKey.NO_KEY)
{
var key = _configuration.QuickTurnInKey switch
{
VirtualKey.MENU => "ALT",
VirtualKey.SHIFT => "SHIFT",
VirtualKey.CONTROL => "CTRL",
_ => _configuration.QuickTurnInKey.ToString()
};
if (!State && _keyState[_configuration.QuickTurnInKey])
{
ImGui.Separator();
ImGui.TextColored(ImGuiColors.HealerGreen, "Click an item to turn it in without confirmation");
}
else if (!IsMinimized)
{
ImGui.Separator();
ImGui.Text($"Hold '{key}' when clicking an item to turn it in without confirmation.");
}
}
if (!IsMinimized)
{
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
}
}
private unsafe void DrawNextRankPrequesites()
{
string? rankName = _gameFunctions.GetNextGrandCompanyRankName();
if (rankName != null)
{
int currentSeals = _gameFunctions.GetCurrentSealCount();
uint requiredSeals = _gameFunctions.GetSealsRequiredForNextRank();
int currentHuntingLog =
MonsterNoteManager.Instance()->RankData[(int)_gameFunctions.GetGrandCompany() + 7]
.Rank;
byte requiredHuntingLog = _gameFunctions.GetRequiredHuntingLogForNextRank();
bool enoughSeals = currentSeals >= requiredSeals;
bool enoughHuntingLog = currentHuntingLog >= requiredHuntingLog;
if (enoughSeals && enoughHuntingLog)
ImGui.TextColored(ImGuiColors.HealerGreen, $"You meet all requirements to rank up to {rankName}.");
else
ImGui.Text($"Ranking up to {rankName} requires:");
ImGui.Indent();
if (enoughSeals)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
ImGui.BulletText($"{currentSeals:N0} / {requiredSeals:N0} GC seals");
ImGui.PopStyleColor();
}
else
ImGui.BulletText($"{currentSeals:N0} / {requiredSeals:N0} GC seals");
if (requiredHuntingLog > 0)
{
if (enoughHuntingLog)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
ImGui.BulletText($"Complete Hunting Log #{requiredHuntingLog}");
ImGui.PopStyleColor();
}
else
ImGui.BulletText($"Complete Hunting Log #{requiredHuntingLog}");
}
ImGui.Unindent();
}
}
private void DrawItemsToBuy(GrandCompany grandCompany)
@ -169,18 +377,29 @@ internal sealed class TurnInWindow : LImGui.LWindow
var itemsWrapper = ItemsWrapper;
ImGui.Text($"Items to buy ({itemsWrapper.Name}):");
List<(uint ItemId, string Name, IReadOnlyList<GrandCompany> GrandCompanies, uint Rank)> comboValues = new()
List<(GcRewardItem Item, string NameWithoutRetainers, string NameWithRetainers)> comboValues = new()
{
(GcRewardItem.None.ItemId, GcRewardItem.None.Name, new List<GrandCompany>(), GcRewardItem.None.RequiredRank)
(GcRewardItem.None, GcRewardItem.None.Name, GcRewardItem.None.Name),
};
foreach (uint itemId in _configuration.ItemsAvailableForPurchase)
foreach (Configuration.PurchaseOption purchaseOption in _configuration.ItemsAvailableToPurchase)
{
var gcReward = _gcRewardsCache.GetReward(itemId);
int itemCount = _plugin.GetItemCount(itemId);
string itemName = gcReward.Name;
if (itemCount > 0)
itemName += $" ({itemCount:N0})";
comboValues.Add((itemId, itemName, gcReward.GrandCompanies, gcReward.RequiredRank));
var gcReward = _gcRewardsCache.GetReward(purchaseOption.ItemId);
int itemCountWithoutRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, false);
int itemCountWithRetainers = _gameFunctions.GetItemCount(purchaseOption.ItemId, true);
string itemNameWithoutRetainers = gcReward.Name;
string itemNameWithRetainers = gcReward.Name;
if (purchaseOption.SameQuantityForAllLists)
{
itemNameWithoutRetainers += $" {((SeIconChar)57412).ToIconString()}";
itemNameWithRetainers += $" {((SeIconChar)57412).ToIconString()}";
}
if (itemCountWithoutRetainers > 0)
itemNameWithoutRetainers += $" ({itemCountWithoutRetainers:N0})";
if (itemCountWithRetainers > 0)
itemNameWithRetainers += $" ({itemCountWithRetainers:N0})";
comboValues.Add((gcReward, itemNameWithoutRetainers, itemNameWithRetainers));
}
if (itemsWrapper.GetItemsToPurchase().Count == 0)
@ -189,113 +408,42 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save();
}
int? itemToRemove = null;
Configuration.PurchasePriority? itemToRemove = null;
Configuration.PurchasePriority? itemToAdd = null;
int indexToAdd = 0;
float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < itemsWrapper.GetItemsToPurchase().Count; ++i)
{
ImGui.PushID($"ItemToBuy{i}");
var item = itemsWrapper.GetItemsToPurchase()[i];
bool enabled = item.Enabled;
ImGui.PushID($"Enable{i}");
if (ImGui.Checkbox("", ref enabled))
DrawItemToBuy(grandCompany, i, itemsWrapper, comboValues, width, ref itemToRemove, itemPositions);
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = null;
else if (_draggedItem != null)
{
var items = itemsWrapper.GetItemsToPurchase().ToList();
var draggedItem = items.Single(x => x.InternalId == _draggedItem);
int oldIndex = items.IndexOf(draggedItem);
var (topLeft, bottomRight) = itemPositions[oldIndex];
ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f,
ImDrawFlags.RoundCornersAll);
int newIndex = itemPositions.FindIndex(x => ImGui.IsMouseHoveringRect(x.TopLeft, x.BottomRight, true));
if (newIndex >= 0 && oldIndex != newIndex)
{
item.Enabled = enabled;
itemsWrapper.Save();
itemToAdd = items.Single(x => x.InternalId == _draggedItem);
indexToAdd = newIndex;
}
ImGui.PopID();
}
ImGui.SameLine(0, 3);
ImGui.BeginDisabled(!enabled);
int comboValueIndex = comboValues.FindIndex(x => x.ItemId == item.ItemId);
if (comboValueIndex < 0)
{
item.ItemId = 0;
item.Limit = 0;
itemsWrapper.Save();
comboValueIndex = 0;
}
if (ImGui.Combo("", ref comboValueIndex, comboValues.Select(x => x.Name).ToArray(), comboValues.Count))
{
item.ItemId = comboValues[comboValueIndex].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(27);
if (comboValueIndex > 0)
{
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 130);
int limit = item.Limit;
if (item.ItemId == ItemIds.Venture)
limit = Math.Min(limit, 65_000);
if (ImGui.InputInt("Maximum items to buy", ref limit, 50, 500))
{
item.Limit = Math.Max(0, limit);
if (item.ItemId == ItemIds.Venture)
item.Limit = Math.Min(item.Limit, 65_000);
itemsWrapper.Save();
}
}
else if (item.Limit != 0)
{
item.Limit = 0;
itemsWrapper.Save();
}
if (comboValueIndex > 0)
{
var comboItem = comboValues[comboValueIndex];
if (!comboItem.GrandCompanies.Contains(grandCompany))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, as you are in the wrong Grand Company.");
}
else if (comboItem.Rank > _plugin.GetGrandCompanyRank())
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, your rank isn't high enough to buy it.");
}
}
ImGui.Unindent(27);
}
ImGui.PopID();
if (itemToRemove != null)
{
itemsWrapper.Remove(itemToRemove);
itemsWrapper.Save();
}
if (itemToAdd != null)
@ -305,66 +453,263 @@ internal sealed class TurnInWindow : LImGui.LWindow
itemsWrapper.Save();
}
if (itemToRemove != null)
{
itemsWrapper.RemoveAt(itemToRemove.Value);
itemsWrapper.Save();
}
if (_configuration.ItemsAvailableForPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x != y.ItemId)))
if (_configuration.ItemsAvailableToPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x.ItemId != y.ItemId)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item"))
ImGui.OpenPopup("##AddItem");
if (ImGui.BeginPopupContextItem("##AddItem", ImGuiPopupFlags.NoOpenOverItems))
{
foreach (var itemId in _configuration.ItemsAvailableForPurchase.Distinct())
foreach (var purchaseOption in _configuration.ItemsAvailableToPurchase.Distinct())
{
if (_gcRewardsCache.RewardLookup.TryGetValue(itemId, out var reward))
if (_gcRewardsCache.RewardLookup.TryGetValue(purchaseOption.ItemId, out var reward))
{
if (ImGui.MenuItem($"{reward.Name}##{itemId}"))
if (ImGui.MenuItem($"{reward.Name}##{purchaseOption.ItemId}"))
{
itemsWrapper.Add(new Configuration.PurchasePriority { ItemId = itemId, Limit = 0 });
itemsWrapper.Add(new Configuration.PurchasePriority
{ ItemId = purchaseOption.ItemId, Limit = 0 });
itemsWrapper.Save();
ImGui.CloseCurrentPopup();
}
}
}
ImGui.EndPopup();
}
ImGui.SameLine();
}
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, "Configure available Items"))
_configWindow.IsOpen = true;
}
private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize)
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)
{
if (itemId == ItemIds.Venture)
return Math.Min(limit, 65_000);
return Math.Min(limit, inventoryLimit);
else
{
uint slotsThatCanBeUsed = 0;
InventoryManager* inventoryManager = InventoryManager.Instance();
for (InventoryType inventoryType = InventoryType.Inventory1;
inventoryType <= InventoryType.Inventory4;
++inventoryType)
foreach (var inventoryType in InventoryTypes)
{
var container = inventoryManager->GetInventoryContainer(inventoryType);
for (int i = 0; i < container->Size; ++i)
{
var item = container->GetInventorySlot(i);
if (item == null || item->ItemID == 0 || item->ItemID == itemId)
if (item == null || item->ItemId == 0 || item->ItemId == itemId)
{
slotsThatCanBeUsed++;
}
}
}
return Math.Min(limit, slotsThatCanBeUsed * stackSize);
return Math.Min(Math.Min(limit, slotsThatCanBeUsed * stackSize), inventoryLimit);
}
}
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
private interface IItemsToPurchase
{
string Name { get; }
@ -380,10 +725,10 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class CharacterSpecificItemsToPurchase : IItemsToPurchase
{
private readonly CharacterConfiguration _characterConfiguration;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
public CharacterSpecificItemsToPurchase(CharacterConfiguration characterConfiguration,
DalamudPluginInterface pluginInterface)
IDalamudPluginInterface pluginInterface)
{
_characterConfiguration = characterConfiguration;
_pluginInterface = pluginInterface;
@ -413,9 +758,9 @@ internal sealed class TurnInWindow : LImGui.LWindow
private sealed class GlobalItemsToPurchase : IItemsToPurchase
{
private readonly Configuration _configuration;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
public GlobalItemsToPurchase(Configuration configuration, DalamudPluginInterface pluginInterface)
public GlobalItemsToPurchase(Configuration configuration, IDalamudPluginInterface pluginInterface)
{
_configuration = configuration;
_pluginInterface = pluginInterface;

View File

@ -1,15 +1,86 @@
{
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"net8.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
"requested": "[11.0.0, )",
"resolved": "11.0.0",
"contentHash": "bjT7XUlhIJSmsE/O76b7weUX+evvGQctbQB8aKXt94o+oPWxHpCepxAGMs7Thow3AzCyqWs7cOpp9/2wcgRRQA=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.SourceLink.AzureRepos.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"llib": {
"type": "Project"
"type": "Project",
"dependencies": {
"DalamudPackager": "[11.0.0, )"
}
}
}
}

2
LLib

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

View File

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