Compare commits

..

No commits in common. "master" and "v2.12" have entirely different histories.

34 changed files with 751 additions and 2952 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,92 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Collections.Generic;
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; } = 2;
public int Version { get; set; } = 1;
[Obsolete]
public List<uint> ItemsAvailableForPurchase { get; set; } = [];
public List<PurchaseOption> ItemsAvailableToPurchase { get; set; } = [];
public List<PurchasePriority> ItemsToPurchase { get; set; } = [];
public List<uint> ItemsAvailableForPurchase { get; set; } = new();
public List<PurchasePriority> ItemsToPurchase { get; set; } = new();
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;
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; }
}
/// <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;
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 (ItemsAvailableToPurchase.Count == 0)
if (ItemsAvailableForPurchase.Count == 0)
{
ItemsAvailableToPurchase.Add(new PurchaseOption { ItemId = ItemIds.Venture });
ItemsAvailableForPurchase.Add(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="PackagePluginDebug" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"

View File

@ -1,13 +1,63 @@
<Project Sdk="Dalamud.NET.Sdk/11.0.0">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>6.3</Version>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>2.12</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
</PropertyGroup>
<Import Project="..\LLib\LLib.targets"/>
<Import Project="..\LLib\RenameZip.targets"/>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
</ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</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://plugins.carvel.li/icons/Deliveroo.png"
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/Deliveroo.png"
}

View File

@ -1,85 +1,50 @@
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.Handlers;
namespace Deliveroo;
internal sealed class ExchangeHandler
partial class DeliverooPlugin
{
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)
private void InteractWithQuartermaster(GameObject personnelOfficer, GameObject quartermaster)
{
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_chatGui = chatGui;
_pluginLog = pluginLog;
}
public void InteractWithQuartermaster(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_gameFunctions.GetCurrentSealCount() < _plugin.EffectiveReservedSealCount)
if (GetCurrentSealCount() < EffectiveReservedSealCount)
{
_plugin.CurrentStage = Stage.RequestStop;
CurrentStage = Stage.RequestStop;
return;
}
if (_targetManager.Target == personnelOfficer)
return;
_gameFunctions.InteractWithTarget(quartermaster);
_plugin.CurrentStage = Stage.SelectRewardTier;
InteractWithTarget(quartermaster);
CurrentStage = Stage.SelectRewardTier;
}
public PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
private PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null)
{
foreach (PurchaseItemRequest request in _plugin.ItemsToPurchaseNow)
foreach (PurchaseItemRequest request in _itemsToPurchaseNow)
{
int toBuy = 0;
int offset = 0;
if (request == previousRequest)
{
toBuy = (int)request.StackSize;
if (request.ItemId != ItemIds.Venture)
toBuy = Math.Min(toBuy, 99);
}
offset = (int)request.StackSize;
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;
}
if (GetItemCount(request.ItemId) + offset < request.EffectiveLimit)
return request;
}
return null;
}
public unsafe void SelectRewardTier()
private unsafe void SelectRewardTier()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
_plugin.CurrentStage = Stage.CloseGcExchange;
CurrentStage = Stage.CloseGcExchange;
return;
}
@ -100,17 +65,17 @@ internal sealed class ExchangeHandler
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectRank);
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectRewardSubCategory;
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectRewardSubCategory;
}
}
public unsafe void SelectRewardSubCategory()
private unsafe void SelectRewardSubCategory()
{
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
_plugin.CurrentStage = Stage.CloseGcExchange;
CurrentStage = Stage.CloseGcExchange;
return;
}
@ -131,25 +96,25 @@ internal sealed class ExchangeHandler
new() { Type = 0, Int = 0 }
};
addonExchange->FireCallback(9, selectType);
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.5);
_plugin.CurrentStage = Stage.SelectReward;
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.SelectReward;
}
}
public unsafe void SelectReward()
private unsafe void SelectReward()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
{
if (SelectRewardItem(addonExchange))
{
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.ConfirmReward;
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.ConfirmReward;
}
else
{
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.2);
_plugin.CurrentStage = Stage.CloseGcExchange;
_continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.CloseGcExchange;
}
}
}
@ -167,15 +132,10 @@ internal sealed class ExchangeHandler
if (itemId == item.ItemId)
{
_pluginLog.Information($"Selecting item {itemId}, {i}");
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);
long toBuy = (GetCurrentSealCount() - EffectiveReservedSealCount) / item.SealCost;
toBuy = Math.Min(toBuy, item.EffectiveLimit - GetItemCount(item.ItemId));
if (item.ItemId != ItemIds.Venture)
if (item.ItemId != ItemIds.Venture && !_configuration.IgnoreCertainLimitations)
toBuy = Math.Min(toBuy, 99);
if (toBuy <= 0)
@ -184,10 +144,7 @@ internal sealed class ExchangeHandler
return false;
}
item.TemporaryPurchaseQuantity = toBuy;
_chatGui.Print(new SeString(new TextPayload($"Buying {toBuy}x "))
.Append(SeString.CreateItemLink(item.ItemId))
.Append(new TextPayload("...")));
_chatGui.Print($"Buying {toBuy}x {item.Name}...");
var selectReward = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
@ -209,24 +166,13 @@ internal sealed class ExchangeHandler
return false;
}
public unsafe void CloseGcExchange()
private unsafe void CloseGcExchange()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
LAddon.IsAddonReady(addonExchange))
{
addonExchange->FireCallbackInt(-1);
// 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;
}
CurrentStage = Stage.TargetPersonnelOfficer;
}
}
}

View File

@ -0,0 +1,161 @@
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 = Marshal.ReadInt32(new nint(&item) + 132),
Name = MemoryHelper.ReadSeString(&item.ItemName).ToString(),
SealsWithBonus = (int)Math.Round(item.SealReward * GetSealMultiplier(), MidpointRounding.AwayFromZero),
SealsWithoutBonus = item.SealReward,
ItemUiCategory = Marshal.ReadByte(new nint(&item) + 150),
});
// GrandCompanyItem + 104 = [int] InventoryType
// GrandCompanyItem + 108 = [int] ??
// GrandCompanyItem + 124 = [int] <Item's Column 19 in the sheet, but that has no name>
// GrandCompanyItem + 132 = [int] itemId
// GrandCompanyItem + 136 = [int] 0 (always)?
// GrandCompanyItem + 140 = [int] i (item's own position within the unsorted list)
// GrandCompanyItem + 148 = [short] ilvl
// GrandCompanyItem + 150 = [byte] ItemUICategory
// GrandCompanyItem + 151 = [byte] (unchecked) inventory slot in container
// GrandCompanyItem + 152 = [short] 512 (always)?
// int itemId = Marshal.ReadInt32(new nint(&item) + 132);
// PluginLog.Verbose($" {Marshal.ReadInt32(new nint(&item) + 132)};;;; {MemoryHelper.ReadSeString(&item.ItemName)}, {new nint(&agent->ItemArray[i]):X8}, {item.SealReward}, {item.IsTurnInAvailable}");
}
return list.OrderByDescending(x => x.SealsWithBonus)
.ThenBy(x => x.ItemUiCategory)
.ThenBy(x => x.ItemId)
.ToList();
}
}

View File

@ -1,56 +0,0 @@
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,36 +9,31 @@ 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 (stage == Stage.OpenGcSupply)
if (CurrentStage == Stage.OpenGcSupply)
{
desiredText = _gameStrings.UndertakeSupplyAndProvisioningMission;
followUp = OpenGcSupplySelectStringFollowUp;
followUp = OpenGcSupplyFollowUp;
}
else if (stage == Stage.CloseGcSupplySelectString)
else if (CurrentStage == Stage.CloseGcSupply)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplySelectStringFollowUp;
followUp = CloseGcSupplyFollowUp;
}
else if (stage == Stage.CloseGcSupplySelectStringThenStop)
else if (CurrentStage == Stage.CloseGcSupplyThenStop)
{
desiredText = _gameStrings.ClosePersonnelOfficerTalk;
followUp = CloseGcSupplySelectStringThenStopFollowUp;
followUp = CloseGcSupplyThenCloseFollowUp;
}
else
return false;
return;
_pluginLog.Verbose($"Looking for '{desiredText}' in prompt");
AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon;
int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount;
for (int i = 0; i < entries; ++i)
@ -51,27 +46,27 @@ partial class DeliverooPlugin
_pluginLog.Verbose($" Choice {i} → {text}");
if (text == desiredText)
{
_pluginLog.Information($"Selecting choice {i} ({text})");
addonSelectString->AtkUnitBase.FireCallbackInt(i);
followUp();
return true;
return;
}
}
_pluginLog.Verbose($"Text '{desiredText}' was not found in prompt.");
return false;
}
private void OpenGcSupplySelectStringFollowUp()
private void OpenGcSupplyFollowUp()
{
_supplyHandler.ResetTurnInErrorHandling();
ResetTurnInErrorHandling();
CurrentStage = Stage.SelectExpertDeliveryTab;
}
private void CloseGcSupplySelectStringFollowUp()
private void CloseGcSupplyFollowUp()
{
if (_exchangeHandler.GetNextItemToPurchase() == null)
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
@ -79,27 +74,26 @@ partial class DeliverooPlugin
else
{
// you can occasionally get a 'not enough seals' warning lol
ContinueAt = DateTime.Now.AddSeconds(1);
_continueAt = DateTime.Now.AddSeconds(1);
CurrentStage = Stage.TargetQuartermaster;
}
}
private void CloseGcSupplySelectStringThenStopFollowUp()
private void CloseGcSupplyThenCloseFollowUp()
{
if (_exchangeHandler.GetNextItemToPurchase() == null)
if (GetNextItemToPurchase() == null)
{
_turnInWindow.State = false;
CurrentStage = Stage.RequestStop;
}
else if (_gameFunctions.GetCurrentSealCount() <=
EffectiveReservedSealCount + _exchangeHandler.GetNextItemToPurchase()!.SealCost)
else if (GetCurrentSealCount() <= EffectiveReservedSealCount + 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,7 +1,6 @@
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;
@ -14,13 +13,13 @@ partial class DeliverooPlugin
_pluginLog.Verbose("SelectYesNo post-setup");
AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon;
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().ReplaceLineEndings("");
string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", "");
_pluginLog.Verbose($"YesNo prompt: '{text}'");
if (CurrentStage == Stage.ConfirmReward &&
_gameStrings.ExchangeItems.IsMatch(text))
{
PurchaseItemRequest? item = _exchangeHandler.GetNextItemToPurchase();
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
addonSelectYesNo->AtkUnitBase.FireCallbackInt(1);
@ -28,20 +27,17 @@ partial class DeliverooPlugin
return;
}
_pluginLog.Information($"Selecting 'yes' ({text}) (callback = {item.OnPurchase}, qty = {item.TemporaryPurchaseQuantity})");
_pluginLog.Information($"Selecting 'yes' ({text})");
addonSelectYesNo->AtkUnitBase.FireCallbackInt(0);
item.OnPurchase?.Invoke((int)item.TemporaryPurchaseQuantity);
item.TemporaryPurchaseQuantity = 0;
var nextItem = _exchangeHandler.GetNextItemToPurchase(item);
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >= EffectiveReservedSealCount + nextItem.SealCost)
var nextItem = GetNextItemToPurchase(item);
if (nextItem != null && 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 || (_configuration.QuickTurnInKey != VirtualKey.NO_KEY && _keyState[_configuration.QuickTurnInKey])) &&
else if (CurrentStage == Stage.TurnInSelected &&
_gameStrings.TradeHighQualityItem == text)
{
_pluginLog.Information($"Selecting 'yes' ({text})");

View File

@ -1,8 +1,6 @@
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;
@ -10,43 +8,25 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Deliveroo.Handlers;
namespace Deliveroo;
internal sealed class SupplyHandler
partial class DeliverooPlugin
{
private readonly DeliverooPlugin _plugin;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IGameGui _gameGui;
private readonly IPluginLog _pluginLog;
private uint _turnInErrors;
public SupplyHandler(DeliverooPlugin plugin, GameFunctions gameFunctions, ITargetManager targetManager,
IGameGui gameGui, IPluginLog pluginLog)
private void InteractWithPersonnelOfficer(GameObject personnelOfficer, GameObject quartermaster)
{
_plugin = plugin;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_gameGui = gameGui;
_pluginLog = pluginLog;
}
public void InteractWithPersonnelOfficer(IGameObject personnelOfficer, IGameObject quartermaster)
{
if (_targetManager.Target?.EntityId == quartermaster.EntityId)
if (_targetManager.Target == quartermaster)
return;
_gameFunctions.InteractWithTarget(personnelOfficer);
_plugin.CurrentStage = Stage.OpenGcSupply;
InteractWithTarget(personnelOfficer);
CurrentStage = Stage.OpenGcSupply;
}
public unsafe void SelectExpertDeliveryTab()
private 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;
@ -60,7 +40,7 @@ internal sealed class SupplyHandler
{
_pluginLog.Information("Tab already selected, probably due to haseltweaks");
ResetTurnInErrorHandling();
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
CurrentStage = Stage.SelectItemToTurnIn;
return;
}
@ -73,23 +53,23 @@ internal sealed class SupplyHandler
};
addon->FireCallback(3, selectExpertDeliveryTab);
ResetTurnInErrorHandling();
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
CurrentStage = Stage.SelectItemToTurnIn;
}
}
public void ResetTurnInErrorHandling(int listSize = int.MaxValue)
private void ResetTurnInErrorHandling(int listSize = int.MaxValue)
{
_pluginLog.Verbose("Resetting error handling state");
_plugin.LastTurnInListSize = listSize;
_lastTurnInListSize = listSize;
_turnInErrors = 0;
}
public unsafe void SelectItemToTurnIn()
private 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;
@ -98,30 +78,28 @@ internal sealed class SupplyHandler
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)
{
_plugin.TurnInError = "Wrong tab selected";
_turnInWindow.Error = "Wrong tab selected";
return;
}
ItemFilterType configuredFilter = ResolveSelectedSupplyFilter();
if (addonGc->SelectedFilter == 0 || addonGc->SelectedFilter != (int)configuredFilter)
{
_plugin.TurnInError =
_turnInWindow.Error =
$"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})");
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
_pluginLog.Information($"No items to turn in {addonGc->ListEmptyTextNode->AtkResNode.IsVisible}, {currentListSize})");
CurrentStage = Stage.CloseGcSupplyThenStop;
addon->FireCallbackInt(-1);
return;
}
@ -130,29 +108,28 @@ internal sealed class SupplyHandler
// something is wrong.
if (_turnInErrors > 10)
{
_plugin.TurnInError = "Unable to refresh item list";
_turnInWindow.Error = "Unable to refresh item list";
return;
}
if (currentListSize >= _plugin.LastTurnInListSize)
if (currentListSize >= _lastTurnInListSize)
{
_turnInErrors++;
_pluginLog.Information(
$"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_plugin.LastTurnInListSize}, new list size = {currentListSize})...");
_pluginLog.Information($"Trying to refresh expert delivery list manually ({_turnInErrors}, old list size = {_lastTurnInListSize}, new list size = {currentListSize})...");
addon->FireCallbackInt(2);
_plugin.ContinueAt = DateTime.Now.AddSeconds(0.1);
_continueAt = DateTime.Now.AddSeconds(0.1);
return;
}
ResetTurnInErrorHandling(currentListSize);
var agent = (AgentGrandCompanySupply*)agentInterface;
List<TurnInItem> items = _gameFunctions.BuildTurnInList(agent);
List<TurnInItem> items = BuildTurnInList(agent);
if (items.Count == 0)
{
// probably shouldn't happen with the previous node visibility check
_plugin.CurrentStage = Stage.CloseGcSupplySelectStringThenStop;
CurrentStage = Stage.CloseGcSupplyThenStop;
addon->FireCallbackInt(-1);
return;
}
@ -176,9 +153,9 @@ internal sealed class SupplyHandler
// ---------------------------------------------------------------------------------------------------------
// 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 (_gameFunctions.GetCurrentSealCount() + items[0].SealsWithBonus > _gameFunctions.GetSealCap())
if (GetCurrentSealCount() + items[0].SealsWithBonus > GetSealCap())
{
_plugin.CurrentStage = Stage.CloseGcSupplySelectString;
CurrentStage = Stage.CloseGcSupply;
addon->FireCallbackInt(-1);
return;
}
@ -190,11 +167,24 @@ internal sealed class SupplyHandler
new() { Type = 0, Int = 0 }
};
addon->FireCallback(3, selectFirstItem);
_plugin.CurrentStage = Stage.TurnInSelected;
CurrentStage = Stage.TurnInSelected;
}
}
public unsafe void FinalizeTurnInItem()
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()
{
if (_gameGui.TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList",
out var addonSupplyList) && LAddon.IsAddonReady(&addonSupplyList->AtkUnitBase))
@ -206,36 +196,15 @@ internal sealed class SupplyHandler
new() { Type = 0, Int = 0 }
};
addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter);
if (_plugin.CurrentStage == Stage.FinalizeTurnIn)
_plugin.CurrentStage = Stage.SelectItemToTurnIn;
else
_plugin.CurrentStage = Stage.RequestStop;
CurrentStage = Stage.SelectItemToTurnIn;
}
}
private ItemFilterType ResolveSelectedSupplyFilter()
{
if (_plugin.CharacterConfiguration is { UseHideArmouryChestItemsFilter: true })
if (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

@ -6,19 +6,16 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Command;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Windowing;
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;
@ -26,81 +23,70 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName);
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _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(IDalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
public DeliverooPlugin(DalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui,
IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager,
IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog,
IAddonLifecycle addonLifecycle, ITextureProvider textureProvider, IGameConfig gameConfig, IKeyState keyState)
IAddonLifecycle addonLifecycle)
{
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, gameConfig, _configuration, _pluginLog);
_gameFunctions = new GameFunctions(objectTable, _clientState, targetManager, dataManager,
_externalPluginHandler, _pluginLog);
_externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _framework, _pluginLog);
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_gcRewardsCache = new GcRewardsCache(dataManager);
_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);
_configWindow = new ConfigWindow(_pluginInterface, this, _configuration, _gcRewardsCache, _clientState, _pluginLog);
_windowSystem.AddWindow(_configWindow);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _clientState,
_gcRewardsCache, _configWindow, _iconCache, _keyState, _gameFunctions);
_turnInWindow = new TurnInWindow(this, _pluginInterface, _configuration, _condition, _gcRewardsCache, _configWindow);
_windowSystem.AddWindow(_turnInWindow);
_sealCaps = dataManager.GetExcelSheet<GrandCompanyRank>()!.Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, x => x.MaxSeals);
_framework.Update += FrameworkUpdate;
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
_clientState.Login += Login;
_clientState.Logout += Logout;
_chatGui.ChatMessage += ChatMessage;
_commandManager.AddHandler("/deliveroo", new CommandInfo(ProcessCommand)
{
HelpMessage = "Open the configuration"
@ -114,54 +100,6 @@ 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 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;
if (type != _gameStrings.RankUpFcType)
return;
var match = _gameStrings.RankUpFc.Match(message.ToString());
if (!match.Success)
return;
foreach (var group in match.Groups.Values)
{
if (int.TryParse(group.Value, out int rank) && rank == _configuration.PauseAtRank)
{
_turnInWindow.State = false;
_pluginLog.Information($"Pausing GC delivery, FC reached rank {rank}");
DeliveryResult = new MessageDeliveryResult
{
Message = new SeStringBuilder()
.Append($"Pausing Deliveroo, your FC reached rank {rank}.")
.Build(),
};
return;
}
}
}
internal CharacterConfiguration? CharacterConfiguration { get; set; }
@ -180,34 +118,10 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
internal DateTime ContinueAt { private get; set; } = DateTime.MinValue;
internal List<PurchaseItemRequest> ItemsToPurchaseNow { get; private set; } = new();
internal int LastTurnInListSize { get; set; } = int.MaxValue;
internal IDeliveryResult? DeliveryResult { get; set; }
internal bool TurnInState
{
set => _turnInWindow.State = value;
}
internal string TurnInError
{
set => _turnInWindow.Error = value;
}
public int EffectiveReservedSealCount
{
get
{
if (CharacterConfiguration is { IgnoreMinimumSealsToKeep: true })
return 0;
return _configuration.ReserveDifferentSealCountAtMaxRank &&
_gameFunctions.GetSealCap() == _gameFunctions.MaxSealCap
? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount;
}
}
public int EffectiveReservedSealCount =>
_configuration.ReserveDifferentSealCountAtMaxRank && GetSealCap() == GetMaxSealCap()
? _configuration.ReservedSealCountAtMaxRank
: _configuration.ReservedSealCount;
private void Login()
{
@ -218,11 +132,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
{
if (CharacterConfiguration.CachedPlayerName != _clientState.LocalPlayer!.Name.ToString() ||
CharacterConfiguration.CachedWorldName !=
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString())
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString())
{
CharacterConfiguration.CachedPlayerName = _clientState.LocalPlayer!.Name.ToString();
CharacterConfiguration.CachedWorldName =
_clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
_clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
CharacterConfiguration.Save(_pluginInterface);
}
@ -242,7 +156,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
private void Logout(int type, int code)
private void Logout()
{
CharacterConfiguration = null;
}
@ -253,62 +167,59 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (!_clientState.IsLoggedIn ||
_clientState.TerritoryType is not 128 and not 130 and not 132 ||
_condition[ConditionFlag.OccupiedInCutSceneEvent] ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetQuartermasterId(), out IGameObject? quartermaster) >= 7f ||
_gameFunctions.GetDistanceToNpc(_gameFunctions.GetPersonnelOfficerId(), out IGameObject? personnelOfficer) >=
7f ||
GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f ||
GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f ||
CharacterConfiguration is { DisableForCharacter: true } ||
_configWindow.IsOpen)
{
_turnInWindow.IsOpen = false;
_turnInWindow.State = false;
StopTurnIn();
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
}
else if (DateTime.Now > ContinueAt)
else if (DateTime.Now > _continueAt)
{
_turnInWindow.IsOpen = true;
_turnInWindow.Multiplier = _gameFunctions.GetSealMultiplier();
_turnInWindow.Multiplier = GetSealMultiplier();
if (!_turnInWindow.State)
{
StopTurnIn();
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
}
return;
}
else if (_turnInWindow.State && CurrentStage == Stage.Stopped)
{
CurrentStage = Stage.TargetPersonnelOfficer;
ItemsToPurchaseNow = _turnInWindow.SelectedItems;
DeliveryResult = new MessageDeliveryResult();
_supplyHandler.ResetTurnInErrorHandling();
if (ItemsToPurchaseNow.Count > 0)
_itemsToPurchaseNow = _turnInWindow.SelectedItems;
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 = _exchangeHandler.GetNextItemToPurchase();
if (nextItem != null && _gameFunctions.GetCurrentSealCount() >=
EffectiveReservedSealCount + nextItem.SealCost)
var nextItem = GetNextItemToPurchase();
if (nextItem != null && 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)
@ -317,7 +228,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
switch (CurrentStage)
{
case Stage.TargetPersonnelOfficer:
_supplyHandler.InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
InteractWithPersonnelOfficer(personnelOfficer!, quartermaster!);
break;
case Stage.OpenGcSupply:
@ -325,48 +236,43 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.SelectExpertDeliveryTab:
_supplyHandler.SelectExpertDeliveryTab();
SelectExpertDeliveryTab();
break;
case Stage.SelectItemToTurnIn:
_supplyHandler.SelectItemToTurnIn();
SelectItemToTurnIn();
break;
case Stage.TurnInSelected:
// see GrandCompanySupplyReward
TurnInSelectedItem();
break;
case Stage.FinalizeTurnIn:
case Stage.SingleFinalizeTurnIn:
_supplyHandler.FinalizeTurnInItem();
FinalizeTurnInItem();
break;
case Stage.CloseGcSupplySelectString:
case Stage.CloseGcSupply:
// see SelectStringPostSetup
break;
case Stage.CloseGcSupplySelectStringThenStop:
case Stage.CloseGcSupplyThenStop:
// see SelectStringPostSetup
break;
case Stage.CloseGcSupplyWindowThenStop:
_supplyHandler.CloseGcSupplyWindow();
break;
case Stage.TargetQuartermaster:
_exchangeHandler.InteractWithQuartermaster(personnelOfficer!, quartermaster!);
InteractWithQuartermaster(personnelOfficer!, quartermaster!);
break;
case Stage.SelectRewardTier:
_exchangeHandler.SelectRewardTier();
SelectRewardTier();
break;
case Stage.SelectRewardSubCategory:
_exchangeHandler.SelectRewardSubCategory();
SelectRewardSubCategory();
break;
case Stage.SelectReward:
_exchangeHandler.SelectReward();
SelectReward();
break;
case Stage.ConfirmReward:
@ -374,13 +280,14 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
break;
case Stage.CloseGcExchange:
_exchangeHandler.CloseGcExchange();
CloseGcExchange();
break;
case Stage.RequestStop:
StopTurnIn();
break;
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
break;
case Stage.Stopped:
break;
@ -391,72 +298,20 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
}
}
private void StopTurnIn()
{
if (CurrentStage != Stage.Stopped)
{
_externalPluginHandler.Restore();
CurrentStage = Stage.Stopped;
if (DeliveryResult is null or MessageDeliveryResult)
{
var text = (DeliveryResult as MessageDeliveryResult)?.Message ?? "Delivery completed.";
var message = _configuration.ChatType switch
{
XivChatType.Say
or XivChatType.Shout
or XivChatType.TellOutgoing
or XivChatType.TellIncoming
or XivChatType.Party
or XivChatType.Alliance
or (>= XivChatType.Ls1 and <= XivChatType.Ls8)
or XivChatType.FreeCompany
or XivChatType.NoviceNetwork
or XivChatType.Yell
or XivChatType.CrossParty
or XivChatType.PvPTeam
or XivChatType.CrossLinkShell1
or XivChatType.NPCDialogue
or XivChatType.NPCDialogueAnnouncements
or (>= XivChatType.CrossLinkShell2 and <= XivChatType.CrossLinkShell8)
=> new XivChatEntry
{
Message = text,
Type = _configuration.ChatType,
Name = new SeStringBuilder().AddUiForeground("Deliveroo", 52).Build(),
},
_ => new XivChatEntry
{
Message = new SeStringBuilder().AddUiForeground("[Deliveroo] ", 52)
.Append(text)
.Build(),
Type = _configuration.ChatType,
}
};
_chatGui.Print(message);
}
DeliveryResult = null;
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanySupplyReward", GrandCompanySupplyRewardPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_commandManager.RemoveHandler("/deliveroo");
_chatGui.ChatMessage -= ChatMessage;
_clientState.Logout -= Logout;
_clientState.Login -= Login;
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_framework.Update -= FrameworkUpdate;
_gameFunctions.Dispose();
_externalPluginHandler.Dispose();
_iconCache.Dispose();
_externalPluginHandler.Restore();
}
private void ProcessCommand(string command, string arguments)

View File

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

View File

@ -1,34 +1,27 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LLib;
namespace Deliveroo.External;
internal sealed class ExternalPluginHandler : IDisposable
internal sealed class ExternalPluginHandler
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IGameConfig _gameConfig;
private readonly Configuration _configuration;
private readonly IPluginLog _pluginLog;
private readonly DeliverooIpc _deliverooIpc;
private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly PandoraIpc _pandoraIpc;
private readonly AllaganToolsIpc _allaganToolsIpc;
private bool? _yesAlreadyState;
private bool? _pandoraState;
private SystemConfigState? _limitFrameRateWhenClientInactive;
private SystemConfigState? _uncapFrameRate;
public ExternalPluginHandler(IDalamudPluginInterface pluginInterface, IGameConfig gameConfig,
Configuration configuration, IPluginLog pluginLog)
public ExternalPluginHandler(DalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_gameConfig = gameConfig;
_configuration = configuration;
_pluginLog = pluginLog;
_deliverooIpc = new DeliverooIpc(pluginInterface);
var dalamudReflector = new DalamudReflector(pluginInterface, framework, pluginLog);
_yesAlreadyIpc = new YesAlreadyIpc(dalamudReflector);
_pandoraIpc = new PandoraIpc(pluginInterface, pluginLog);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog);
}
public bool Saved { get; private set; }
@ -45,18 +38,13 @@ internal sealed class ExternalPluginHandler : IDisposable
_deliverooIpc.StartTurnIn();
SaveYesAlreadyState();
SavePandoraState();
SaveGameConfig();
Saved = true;
}
private void SaveYesAlreadyState()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
!data.Contains(nameof(Deliveroo)))
{
_pluginLog.Debug("Disabling YesAlready");
data.Add(nameof(Deliveroo));
}
_yesAlreadyState = _yesAlreadyIpc.DisableIfNecessary();
_pluginLog.Information($"Previous yesalready state: {_yesAlreadyState}");
}
private void SavePandoraState()
@ -65,40 +53,25 @@ internal sealed class ExternalPluginHandler : IDisposable
_pluginLog.Info($"Previous pandora feature state: {_pandoraState}");
}
private void SaveGameConfig()
{
if (!_configuration.DisableFrameLimiter)
return;
_limitFrameRateWhenClientInactive ??=
new SystemConfigState(_gameConfig, SystemConfigState.ConfigFpsInactive, 0);
if (_configuration.UncapFrameRate)
_uncapFrameRate ??= new SystemConfigState(_gameConfig, SystemConfigState.ConfigFps, 0);
}
public void Restore()
{
if (Saved)
{
RestoreYesAlready();
RestorePandora();
RestoreGameConfig();
}
Saved = false;
_yesAlreadyState = null;
_pandoraState = null;
_deliverooIpc.StopTurnIn();
}
private void RestoreYesAlready()
{
if (_pluginInterface.TryGetData<HashSet<string>>("YesAlready.StopRequests", out var data) &&
data.Contains(nameof(Deliveroo)))
{
_pluginLog.Debug("Restoring YesAlready");
data.Remove(nameof(Deliveroo));
}
_pluginLog.Information($"Restoring previous yesalready state: {_yesAlreadyState}");
if (_yesAlreadyState == true)
_yesAlreadyIpc.Enable();
}
private void RestorePandora()
@ -107,37 +80,4 @@ internal sealed class ExternalPluginHandler : IDisposable
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(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
public PandoraIpc(DalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_getEnabled = pluginInterface.GetIpcSubscriber<string, bool?>("PandorasBox.GetFeatureEnabled");

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

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

View File

@ -1,12 +1,10 @@
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System;
using System.Text.RegularExpressions;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
using LLib;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
namespace Deliveroo.GameData;
@ -15,21 +13,13 @@ internal sealed class GameStrings
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
UndertakeSupplyAndProvisioningMission =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002", pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(UndertakeSupplyAndProvisioningMission)}");
dataManager.GetDialogue<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002", pluginLog);
ClosePersonnelOfficerTalk =
dataManager.GetString<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004", pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(ClosePersonnelOfficerTalk)}");
ExchangeItems = dataManager.GetRegex<Addon>(3290, addon => addon.Text, pluginLog)
?? 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);
RankUpFc = rankUpFc.GetRegex(logMessage => logMessage.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(RankUpFc)}");
RankUpFcType = (XivChatType)rankUpFc.LogKind;
dataManager.GetDialogue<ComDefGrandCompanyOfficer>("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004", pluginLog);
ExchangeItems = dataManager.GetRegex<Addon>(4928, 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)}");
}
@ -37,21 +27,9 @@ internal sealed class GameStrings
public string ClosePersonnelOfficerTalk { get; }
public Regex ExchangeItems { get; }
public string TradeHighQualityItem { get; }
public Regex RankUpFc { get; }
public XivChatType RankUpFcType { get; }
[Sheet("custom/000/ComDefGrandCompanyOfficer_00073")]
[SuppressMessage("Performance", "CA1812")]
private readonly struct ComDefGrandCompanyOfficer(ExcelPage page, uint offset, uint row)
: IQuestDialogueText, IExcelRow<ComDefGrandCompanyOfficer>
private class ComDefGrandCompanyOfficer : QuestDialogueText
{
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

@ -1,33 +0,0 @@
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,29 +10,24 @@ 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.Sheets;
using Lumina.Excel.GeneratedSheets;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Deliveroo.GameData;
@ -10,40 +10,34 @@ 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.RowId,
(GrandCompany: (GrandCompany)x.GrandCompany.Row,
Tier: (RewardTier)x.Tier,
SubCategory: (RewardSubCategory)x.SubCategory));
Rewards = dataManager.GetSubrowExcelSheet<GCScripShopItem>()
.SelectMany(x => x)
.Where(x => x.RowId > 0 && x.Item.RowId > 0)
Rewards = dataManager.GetExcelSheet<GCScripShopItem>()!
.Where(x => x.RowId > 0 && x.Item.Row > 0)
.GroupBy(item =>
{
var category = categories[item.RowId];
return new
{
ItemId = item.Item.RowId,
Name = item.Item.Value.Name.ToString(),
IconId = item.Item.RowId == ItemIds.Venture ? 25917 : item.Item.Value.Icon,
ItemId = item.Item.Row,
Name = item.Item.Value!.Name.ToString(),
category.Tier,
category.SubCategory,
RequiredRank = item.RequiredGrandCompanyRank.RowId,
item.Item.Value.StackSize,
RequiredRank = item.RequiredGrandCompanyRank.Row,
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,
@ -52,7 +46,6 @@ internal sealed class GcRewardsCache
GrandCompanies = item.Select(x => categories[x.RowId].GrandCompany)
.ToList()
.AsReadOnly(),
InventoryLimit = item.Key.InventoryLimit,
})
.ToList()
.AsReadOnly();

View File

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

View File

@ -1,28 +0,0 @@
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 : [];
}

View File

@ -1,8 +1,11 @@
namespace Deliveroo.GameData;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.String;
namespace Deliveroo.GameData;
internal sealed class TurnInItem
{
public required uint ItemId { get; init; }
public required int ItemId { get; init; }
public required string Name { get; init; }
public required int SealsWithBonus { get; init; }
public required int SealsWithoutBonus { get; init; }

View File

@ -1,244 +0,0 @@
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,12 +0,0 @@
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,5 +1,4 @@
using System;
using Deliveroo.GameData;
using Deliveroo.GameData;
namespace Deliveroo;
@ -7,14 +6,9 @@ internal sealed class PurchaseItemRequest
{
public required uint ItemId { get; init; }
public required string Name { get; set; }
public required uint EffectiveLimit { get; set; }
public required uint EffectiveLimit { get; init; }
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,9 +8,8 @@ internal enum Stage
SelectItemToTurnIn,
TurnInSelected,
FinalizeTurnIn,
CloseGcSupplySelectString,
CloseGcSupplySelectStringThenStop,
CloseGcSupplyWindowThenStop,
CloseGcSupply,
CloseGcSupplyThenStop,
TargetQuartermaster,
SelectRewardTier,
@ -21,6 +20,4 @@ internal enum Stage
RequestStop,
Stopped,
SingleFinalizeTurnIn,
}

View File

@ -2,49 +2,32 @@
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.Interface.Windowing;
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 : LWindow, IPersistableWindowConfig
internal sealed class ConfigWindow : Window
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _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;
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)
public ConfigWindow(DalamudPluginInterface pluginInterface, DeliverooPlugin plugin, Configuration configuration,
GcRewardsCache gcRewardsCache, IClientState clientState, IPluginLog pluginLog)
: base("Deliveroo - Configuration###DeliverooConfig")
{
_pluginInterface = pluginInterface;
@ -53,8 +36,6 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
_gcRewardsCache = gcRewardsCache;
_clientState = clientState;
_pluginLog = pluginLog;
_iconCache = iconCache;
_gameFunctions = gameFunctions;
_itemLookup = _gcRewardsCache.RewardLookup;
@ -68,10 +49,10 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
};
}
public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
public override void Draw()
{
LImGui.AddPatreonIcon(_pluginInterface);
if (_configuration.AddVentureIfNoItemToPurchaseSelected())
Save();
@ -89,80 +70,49 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
if (ImGui.BeginTabItem("Items for Auto-Buy"))
{
Configuration.PurchaseOption? itemToRemove = null;
Configuration.PurchaseOption? itemToAdd = null;
uint? itemToRemove = null;
uint? itemToAdd = null;
int indexToAdd = 0;
if (ImGui.BeginChild("Items", new Vector2(-1, -ImGui.GetFrameHeightWithSpacing()), true,
ImGuiWindowFlags.NoSavedSettings))
if (ImGui.BeginChild("Items", new Vector2(-1, -30), true, ImGuiWindowFlags.NoSavedSettings))
{
for (int i = 0; i < _configuration.ItemsAvailableToPurchase.Count; ++i)
for (int i = 0; i < _configuration.ItemsAvailableForPurchase.Count; ++i)
{
var purchaseOption = _configuration.ItemsAvailableToPurchase[i];
uint itemId = _configuration.ItemsAvailableForPurchase[i];
ImGui.PushID($"###Item{i}");
ImGui.BeginDisabled(
_configuration.ItemsAvailableToPurchase.Count == 1 && purchaseOption.ItemId == ItemIds.Venture);
_configuration.ItemsAvailableForPurchase.Count == 1 && itemId == ItemIds.Venture);
var item = _itemLookup[purchaseOption.ItemId];
using (var icon = _iconCache.GetIcon(item.IconId))
ImGui.Selectable(_itemLookup[itemId].Name);
if (ImGui.BeginDragDropSource())
{
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = itemId;
if (icon != null)
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource > 0 &&
ImGui.AcceptDragDropPayload("DeliverooDragDrop").NativePtr != null)
{
ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X,
ImGui.GetStyle().ItemSpacing.Y / 2));
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = 0;
}
ImGui.Selectable(
$"{item.Name}{(item.Limited ? $" {SeIconChar.Hyadelyn.ToIconString()}" : "")}{(purchaseOption.SameQuantityForAllLists ? $" {((SeIconChar)57412).ToIconString()} (Limit: {purchaseOption.GlobalLimit:N0})" : "")}",
false, ImGuiSelectableFlags.SpanAllColumns);
ImGui.EndDragDropTarget();
}
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("DeliverooDragDrop", nint.Zero, 0);
_dragDropSource = purchaseOption;
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopup($"###ctx{i}"))
{
if (ImGui.Selectable($"Remove {_itemLookup[itemId].Name}"))
itemToRemove = itemId;
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.EndPopup();
}
ImGui.EndDisabled();
@ -174,63 +124,33 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
if (itemToRemove != null)
{
_configuration.ItemsAvailableToPurchase.Remove(itemToRemove);
_configuration.ItemsAvailableForPurchase.Remove(itemToRemove.Value);
Save();
}
if (itemToAdd != null)
{
_configuration.ItemsAvailableToPurchase.Remove(itemToAdd);
_configuration.ItemsAvailableToPurchase.Insert(indexToAdd, itemToAdd);
_configuration.ItemsAvailableForPurchase.Remove(itemToAdd.Value);
_configuration.ItemsAvailableForPurchase.Insert(indexToAdd, itemToAdd.Value);
Save();
}
List<(uint ItemId, string Name, ushort IconId, bool Limited)> comboValues = _gcRewardsCache.Rewards
List<(uint ItemId, string Name)> comboValues = _gcRewardsCache.Rewards
.Where(x => x.SubCategory is RewardSubCategory.Materials or RewardSubCategory.Materiel)
.Where(x => _configuration.ItemsAvailableToPurchase.All(y => y.ItemId != x.ItemId))
.Select(x => (x.ItemId, x.Name, x.IconId, x.Limited))
.Where(x => x.StackSize > 1)
.Where(x => !_configuration.ItemsAvailableForPurchase.Contains(x.ItemId))
.Select(x => (x.ItemId, x.Name))
.OrderBy(x => x.Name)
.ThenBy(x => x.GetHashCode())
.ToList();
comboValues.Insert(0, (0, ""));
if (ImGui.BeginCombo($"##ItemSelection", "Add Item...", ImGuiComboFlags.HeightLarge))
int currentItem = 0;
if (ImGui.Combo("Add Item", ref currentItem, comboValues.Select(x => x.Name).ToArray(), comboValues.Count)
&& comboValues[currentItem].ItemId != GcRewardItem.None.ItemId)
{
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();
_configuration.ItemsAvailableForPurchase.Add(comboValues[currentItem].ItemId);
Save();
}
ImGui.EndTabItem();
@ -244,7 +164,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
{
string currentCharacterName = _clientState.LocalPlayer!.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
string currentWorldName = _clientState.LocalPlayer.HomeWorld.GameData!.Name.ToString();
ImGui.Text($"Current Character: {currentCharacterName} @ {currentWorldName}");
ImGui.Spacing();
ImGui.Separator();
@ -288,20 +208,9 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
charConfiguration.Save(_pluginInterface);
}
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.");
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.EndDisabled();
ImGui.Spacing();
@ -356,8 +265,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
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)_gameFunctions.MaxSealCap, reservedSealCount));
_configuration.ReservedSealCount = Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCount));
Save();
}
@ -371,96 +279,22 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
if (reserveDifferentSealCountAtMaxRank)
{
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.Indent(indentSize);
ImGui.Indent();
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)_gameFunctions.MaxSealCap, reservedSealCountAtMaxRank));
_configuration.ReservedSealCountAtMaxRank = Math.Max(0, Math.Min((int)_plugin.GetMaxSealCap(), reservedSealCountAtMaxRank));
Save();
}
ImGui.Unindent(indentSize);
ImGui.Unindent();
}
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(),
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,14 +3,11 @@ 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.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Deliveroo.GameData;
@ -18,64 +15,28 @@ 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 : LWindow, IPersistableWindowConfig<Configuration.MinimizableWindowConfig>
internal sealed class TurnInWindow : Window
{
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 IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _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;
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)
public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration,
ICondition condition, GcRewardsCache gcRewardsCache, ConfigWindow configWindow)
: 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;
@ -86,74 +47,17 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
MaximumSize = new Vector2(500, 999),
};
State = false;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse;
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);
@ -162,50 +66,29 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
{
get
{
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
GrandCompany grandCompany = _plugin.GetGrandCompany();
if (grandCompany == GrandCompany.None)
return new List<PurchaseItemRequest>();
var rank = _gameFunctions.GetGrandCompanyRank();
var rank = _plugin.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 =>
.Select(x => new PurchaseItemRequest
{
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;
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,
})
.ToList();
}
@ -213,7 +96,9 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
public override unsafe void Draw()
{
GrandCompany grandCompany = _gameFunctions.GetGrandCompany();
LImGui.AddPatreonIcon(_pluginInterface);
GrandCompany grandCompany = _plugin.GetGrandCompany();
if (grandCompany == GrandCompany.None)
{
// not sure we should ever get here
@ -221,19 +106,10 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
return;
}
if (_gameFunctions.GetGrandCompanyRank() < 6)
if (_plugin.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;
}
@ -243,133 +119,51 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
State = state;
}
float indentSize = ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X;
ImGui.SameLine();
if (ImGuiComponents.IconButton("###OpenConfig", FontAwesomeIcon.Cog))
_configWindow.IsOpen = true;
ImGui.Indent(27);
if (!string.IsNullOrEmpty(Error))
{
using (ImRaii.PushIndent(indentSize))
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
ImGui.TextColored(ImGuiColors.DalamudRed, Error);
}
else
{
using (ImRaii.PushIndent(indentSize))
if (Multiplier == 1m)
{
if (_configuration.BehaviorOnOtherWorld == Configuration.EBehaviorOnOtherWorld.Warning &&
!IsOnHomeWorld)
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"You are not on your home world and will not earn FC points.");
}
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 == 1m)
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff.");
}
else
{
ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
}
if (Multiplier <= 1.10m)
{
InventoryManager* inventoryManager = InventoryManager.Instance();
AgentInventoryContext* agentInventoryContext = AgentInventoryContext.Instance();
if (inventoryManager->GetInventoryItemCount(ItemIds.PrioritySealAllowance) > 0)
ImGui.BeginDisabled(_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.Casting]);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Bolt, "Use Priority Seal Allowance (15%)"))
{
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();
AgentInventoryContext.Instance()->UseItem(ItemIds.PrioritySealAllowance);
}
ImGui.EndDisabled();
}
}
if (!IsMinimized)
{
ImGui.Separator();
using (ImRaii.Disabled(state))
DrawItemsToBuy(grandCompany);
}
}
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.Unindent(27);
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
ImGui.BeginDisabled(state);
DrawItemsToBuy(grandCompany);
ImGui.EndDisabled();
}
}
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();
}
ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}");
}
private void DrawItemsToBuy(GrandCompany grandCompany)
@ -377,29 +171,18 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
var itemsWrapper = ItemsWrapper;
ImGui.Text($"Items to buy ({itemsWrapper.Name}):");
List<(GcRewardItem Item, string NameWithoutRetainers, string NameWithRetainers)> comboValues = new()
List<(uint ItemId, string Name, IReadOnlyList<GrandCompany> GrandCompanies, uint Rank)> comboValues = new()
{
(GcRewardItem.None, GcRewardItem.None.Name, GcRewardItem.None.Name),
(GcRewardItem.None.ItemId, GcRewardItem.None.Name, new List<GrandCompany>(), GcRewardItem.None.RequiredRank)
};
foreach (Configuration.PurchaseOption purchaseOption in _configuration.ItemsAvailableToPurchase)
foreach (uint itemId in _configuration.ItemsAvailableForPurchase)
{
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));
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));
}
if (itemsWrapper.GetItemsToPurchase().Count == 0)
@ -408,42 +191,113 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
itemsWrapper.Save();
}
Configuration.PurchasePriority? itemToRemove = null;
int? 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)
{
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)
ImGui.PushID($"ItemToBuy{i}");
var item = itemsWrapper.GetItemsToPurchase()[i];
bool enabled = item.Enabled;
ImGui.PushID($"Enable{i}");
if (ImGui.Checkbox("", ref enabled))
{
itemToAdd = items.Single(x => x.InternalId == _draggedItem);
indexToAdd = newIndex;
item.Enabled = enabled;
itemsWrapper.Save();
}
}
ImGui.PopID();
if (itemToRemove != null)
{
itemsWrapper.Remove(itemToRemove);
itemsWrapper.Save();
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 (itemToAdd != null)
@ -453,263 +307,66 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
itemsWrapper.Save();
}
if (_configuration.ItemsAvailableToPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x.ItemId != y.ItemId)))
if (itemToRemove != null)
{
itemsWrapper.RemoveAt(itemToRemove.Value);
itemsWrapper.Save();
}
if (_configuration.ItemsAvailableForPurchase.Any(x =>
itemsWrapper.GetItemsToPurchase().All(y => x != y.ItemId)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item"))
ImGui.OpenPopup("##AddItem");
if (ImGui.BeginPopupContextItem("##AddItem", ImGuiPopupFlags.NoOpenOverItems))
{
foreach (var purchaseOption in _configuration.ItemsAvailableToPurchase.Distinct())
foreach (var itemId in _configuration.ItemsAvailableForPurchase.Distinct())
{
if (_gcRewardsCache.RewardLookup.TryGetValue(purchaseOption.ItemId, out var reward))
if (_gcRewardsCache.RewardLookup.TryGetValue(itemId, out var reward))
{
if (ImGui.MenuItem($"{reward.Name}##{purchaseOption.ItemId}"))
if (ImGui.MenuItem($"{reward.Name}##{itemId}"))
{
itemsWrapper.Add(new Configuration.PurchasePriority
{ ItemId = purchaseOption.ItemId, Limit = 0 });
itemsWrapper.Add(new Configuration.PurchasePriority { ItemId = itemId, Limit = 0 });
itemsWrapper.Save();
ImGui.CloseCurrentPopup();
}
}
}
ImGui.EndPopup();
}
ImGui.SameLine();
}
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, "Configure available Items"))
_configWindow.IsOpen = true;
}
private void DrawItemToBuy(GrandCompany grandCompany, int i, IItemsToPurchase itemsWrapper,
List<(GcRewardItem Item, string NameWithoutRetainers, string NameWithRetainers)> comboValues, float width,
ref Configuration.PurchasePriority? itemToRemove, List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions)
{
Vector2 topLeft = ImGui.GetCursorScreenPos() + new Vector2(0, -ImGui.GetStyle().ItemSpacing.Y / 2);
ImGui.PushID($"ItemToBuy{i}");
Configuration.PurchasePriority item = itemsWrapper.GetItemsToPurchase()[i];
float indentX = ImGui.GetCursorPosX();
bool enabled = item.Enabled;
int popColors = 0;
if (!enabled)
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.5f, 0.35f, 1f));
popColors++;
}
if (ImGui.Button($"{item.GetIcon()}"))
ImGui.OpenPopup($"Configure{i}");
ImGui.PopStyleColor(popColors);
if (ImGui.BeginPopup($"Configure{i}"))
{
if (ImGui.Checkbox($"Enabled##Enabled{i}", ref enabled))
{
item.Enabled = enabled;
itemsWrapper.Save();
}
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
int type = (int)item.Type;
if (ImGui.Combo($"##Type{i}", ref type, StockingTypeLabels, StockingTypeLabels.Length))
{
item.Type = (Configuration.PurchaseType)type;
if (item.Type != Configuration.PurchaseType.KeepStocked)
item.CheckRetainerInventory = false;
itemsWrapper.Save();
}
if (item.Type == Configuration.PurchaseType.KeepStocked && item.ItemId != ItemIds.Venture)
{
bool checkRetainerInventory = item.CheckRetainerInventory;
if (ImGui.Checkbox("Check Retainer Inventory for items (requires AllaganTools)",
ref checkRetainerInventory))
{
item.CheckRetainerInventory = checkRetainerInventory;
itemsWrapper.Save();
}
}
ImGui.EndPopup();
}
ImGui.SameLine(0, 3);
ImGui.BeginDisabled(!enabled);
int comboValueIndex = comboValues.FindIndex(x => x.Item.ItemId == item.ItemId);
if (comboValueIndex < 0)
{
item.ItemId = 0;
item.Limit = 0;
itemsWrapper.Save();
comboValueIndex = 0;
}
var comboItem = comboValues[comboValueIndex];
using (var icon = _iconCache.GetIcon(comboItem.Item.IconId))
{
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, 3);
}
}
indentX = ImGui.GetCursorPosX() - indentX;
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SetNextItemWidth(width -
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.Circle.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 8 -
ImGui.GetStyle().ItemSpacing.X * 2 - 3 * 3);
ImGui.PopFont();
if (ImGui.Combo("", ref comboValueIndex,
comboValues.Select(x => item.CheckRetainerInventory ? x.NameWithRetainers : x.NameWithoutRetainers)
.ToArray(), comboValues.Count))
{
comboItem = comboValues[comboValueIndex];
item.ItemId = comboItem.Item.ItemId;
itemsWrapper.Save();
}
ImGui.EndDisabled();
if (itemsWrapper.GetItemsToPurchase().Count >= 2)
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 4 -
ImGui.GetStyle().ItemSpacing.X);
ImGui.PopFont();
if (_draggedItem == item.InternalId)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown,
ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = item.InternalId;
ImGui.SameLine();
}
else
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 2);
ImGui.PopFont();
}
if (ImGuiComponents.IconButton($"###Remove{i}", FontAwesomeIcon.Times))
itemToRemove = item;
if (enabled)
{
ImGui.Indent(indentX);
if (comboValueIndex > 0)
{
var purchaseOption =
_configuration.ItemsAvailableToPurchase
.Where(x => x.SameQuantityForAllLists)
.FirstOrDefault(x => x.ItemId == item.ItemId);
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 130);
int limit = Math.Min(purchaseOption?.GlobalLimit ?? item.Limit, (int)comboItem.Item.InventoryLimit);
int stepSize = comboItem.Item.StackSize < 99 ? 1 : 50;
string label = item.Type == Configuration.PurchaseType.KeepStocked
? "Maximum items to buy"
: "Remaining items to buy";
if (ImGui.InputInt(label, ref limit, stepSize, stepSize * 10))
{
int newLimit = Math.Min(Math.Max(0, limit), (int)comboItem.Item.InventoryLimit);
if (purchaseOption != null)
{
purchaseOption.GlobalLimit = newLimit;
_pluginInterface.SavePluginConfig(_configuration);
}
else
{
item.Limit = newLimit;
itemsWrapper.Save();
}
}
}
else if (item.Limit != 0)
{
item.Limit = 0;
itemsWrapper.Save();
}
if (comboValueIndex > 0)
{
if (!comboItem.Item.GrandCompanies.Contains(grandCompany))
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, as you are in the wrong Grand Company.");
}
else if (comboItem.Item.RequiredRank > _gameFunctions.GetGrandCompanyRank())
{
ImGui.TextColored(ImGuiColors.DalamudRed,
"This item will be skipped, your rank isn't high enough to buy it.");
}
}
ImGui.Unindent(indentX);
}
ImGui.PopID();
Vector2 bottomRight = new Vector2(topLeft.X + width,
ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2);
itemPositions.Add((topLeft, bottomRight));
}
private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize, uint inventoryLimit)
private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize)
{
if (itemId == ItemIds.Venture)
return Math.Min(limit, inventoryLimit);
return Math.Min(limit, 65_000);
else
{
uint slotsThatCanBeUsed = 0;
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var inventoryType in InventoryTypes)
for (InventoryType inventoryType = InventoryType.Inventory1;
inventoryType <= InventoryType.Inventory4;
++inventoryType)
{
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(Math.Min(limit, slotsThatCanBeUsed * stackSize), inventoryLimit);
return Math.Min(limit, slotsThatCanBeUsed * stackSize);
}
}
public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
private interface IItemsToPurchase
{
string Name { get; }
@ -725,10 +382,10 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
private sealed class CharacterSpecificItemsToPurchase : IItemsToPurchase
{
private readonly CharacterConfiguration _characterConfiguration;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _pluginInterface;
public CharacterSpecificItemsToPurchase(CharacterConfiguration characterConfiguration,
IDalamudPluginInterface pluginInterface)
DalamudPluginInterface pluginInterface)
{
_characterConfiguration = characterConfiguration;
_pluginInterface = pluginInterface;
@ -758,9 +415,9 @@ internal sealed class TurnInWindow : LWindow, IPersistableWindowConfig<Configura
private sealed class GlobalItemsToPurchase : IItemsToPurchase
{
private readonly Configuration _configuration;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _pluginInterface;
public GlobalItemsToPurchase(Configuration configuration, IDalamudPluginInterface pluginInterface)
public GlobalItemsToPurchase(Configuration configuration, DalamudPluginInterface pluginInterface)
{
_configuration = configuration;
_pluginInterface = pluginInterface;

View File

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

2
LLib

@ -1 +1 @@
Subproject commit b581e2ea2a61f44ed3f0cb4f6ea8cc1595525544
Subproject commit 2f6ef354c401a9f1bb9b807327acad7e98662a2e

View File

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