commit b069af3a2447a6dbee243832ada50100a99b268e Author: Liza Carvelli Date: Thu Sep 21 15:43:22 2023 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05dc549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +*.user diff --git a/Deliveroo.sln b/Deliveroo.sln new file mode 100644 index 0000000..0bc9a1a --- /dev/null +++ b/Deliveroo.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deliveroo", "Deliveroo\Deliveroo.csproj", "{978F4598-921A-4F9D-A975-1463D3BA96C3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {978F4598-921A-4F9D-A975-1463D3BA96C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {978F4598-921A-4F9D-A975-1463D3BA96C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {978F4598-921A-4F9D-A975-1463D3BA96C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {978F4598-921A-4F9D-A975-1463D3BA96C3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Deliveroo/.gitignore b/Deliveroo/.gitignore new file mode 100644 index 0000000..958518b --- /dev/null +++ b/Deliveroo/.gitignore @@ -0,0 +1,3 @@ +/dist +/obj +/bin diff --git a/Deliveroo/Deliveroo.csproj b/Deliveroo/Deliveroo.csproj new file mode 100644 index 0000000..615ab88 --- /dev/null +++ b/Deliveroo/Deliveroo.csproj @@ -0,0 +1,62 @@ + + + net7.0-windows + 1.0 + 11.0 + enable + true + false + false + dist + true + true + portable + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + + + + $(DALAMUD_HOME)/ + + + + + + + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + + + + + diff --git a/Deliveroo/Deliveroo.json b/Deliveroo/Deliveroo.json new file mode 100644 index 0000000..ab2ccbd --- /dev/null +++ b/Deliveroo/Deliveroo.json @@ -0,0 +1,7 @@ +{ + "Name": "Deliveroo", + "Author": "Liza Carvelli", + "Punchline": "", + "Description": "", + "RepoUrl": "https://git.carvel.li/liza/Deliveroo" +} diff --git a/Deliveroo/DeliverooPlugin.cs b/Deliveroo/DeliverooPlugin.cs new file mode 100644 index 0000000..3d79d43 --- /dev/null +++ b/Deliveroo/DeliverooPlugin.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Gui; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Memory; +using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using Character = Dalamud.Game.ClientState.Objects.Types.Character; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Deliveroo; + +public class DeliverooPlugin : IDalamudPlugin +{ + private readonly WindowSystem _windowSystem = new(typeof(DeliverooPlugin).AssemblyQualifiedName); + + private readonly DalamudPluginInterface _pluginInterface; + private readonly ChatGui _chatGui; + private readonly GameGui _gameGui; + private readonly Framework _framework; + private readonly ClientState _clientState; + private readonly ObjectTable _objectTable; + private readonly TargetManager _targetManager; + + private readonly TurnInWindow _turnInWindow; + + private Stage _currentStageInternal = Stage.Stop; + private DateTime _continueAt = DateTime.MinValue; + + public DeliverooPlugin(DalamudPluginInterface pluginInterface, ChatGui chatGui, GameGui gameGui, + Framework framework, ClientState clientState, ObjectTable objectTable, TargetManager targetManager) + { + _pluginInterface = pluginInterface; + _chatGui = chatGui; + _gameGui = gameGui; + _framework = framework; + _clientState = clientState; + _objectTable = objectTable; + _targetManager = targetManager; + + _turnInWindow = new TurnInWindow(); + _windowSystem.AddWindow(_turnInWindow); + + _framework.Update += FrameworkUpdate; + _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; + } + + public string Name => "Deliveroo"; + + private Stage CurrentStage + { + get => _currentStageInternal; + set + { + if (_currentStageInternal != value) + { + PluginLog.Information($"Changing stage from {_currentStageInternal} to {value}"); + _currentStageInternal = value; + } + } + } + + private unsafe void FrameworkUpdate(Framework f) + { + if (!_clientState.IsLoggedIn || _clientState.TerritoryType is not 128 and not 130 and not 132 || + GetDistanceToNpc(GetQuartermasterId(), out GameObject? quartermaster) >= 7f || + GetDistanceToNpc(GetPersonnelOfficerId(), out GameObject? personnelOfficer) >= 7f) + { + _turnInWindow.IsOpen = false; + } + else if (DateTime.Now > _continueAt) + { + _turnInWindow.IsOpen = true; + _turnInWindow.Multiplier = GetSealMultiplier(); + _turnInWindow.CurrentVentureCount = GetCurrentVentureCount(); + + if (!_turnInWindow.State) + { + CurrentStage = Stage.Stop; + return; + } + + if (_turnInWindow.State && CurrentStage == Stage.Stop) + { + CurrentStage = Stage.TargetPersonnelOfficer; + } + + _turnInWindow.Debug = CurrentStage.ToString(); + switch (CurrentStage) + { + case Stage.TargetPersonnelOfficer: + if (_targetManager.Target == quartermaster!) + break; + + InteractWithTarget(personnelOfficer!); + CurrentStage = Stage.OpenGcSupply; + break; + case Stage.OpenGcSupply: + if (SelectSelectString(0)) + CurrentStage = Stage.SelectItemToTurnIn; + + break; + case Stage.SelectItemToTurnIn: + var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply); + if (agentInterface != null && agentInterface->IsAgentActive()) + { + var addonId = agentInterface->GetAddonID(); + if (addonId == 0) + break; + + AtkUnitBase* addon = GetAddonById(addonId); + if (addon == null || !IsAddonReady(addon) || addon->UldManager.NodeListCount <= 20 || + !addon->UldManager.NodeList[5]->IsVisible) + break; + + var addonGc = (AddonGrandCompanySupplyList*)addon; + if (addonGc->SelectedTab != 2 || addonGc->SelectedFilter != 1) + break; + + var agent = (AgentGrandCompanySupply*)agentInterface; + List items = BuildTurnInList(agent); + if (items.Count == 0 || addon->UldManager.NodeList[20]->IsVisible) + { + CurrentStage = Stage.CloseGcSupplyThenStop; + addon->FireCallbackInt(-1); + break; + } + + if (GetCurrentSealCount() + items[0].SealsWithBonus > GetSealCap()) + { + CurrentStage = Stage.CloseGcSupply; + addon->FireCallbackInt(-1); + break; + } + + var selectFirstItem = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 1 }, + new() { Type = ValueType.Int, Int = 0 /* position within list */ }, + new() { Type = 0, Int = 0 } + }; + addon->FireCallback(3, selectFirstItem); + CurrentStage = Stage.TurnInSelected; + } + + break; + case Stage.TurnInSelected: + if (TryGetAddonByName("GrandCompanySupplyReward", + out var addonSupplyReward) && IsAddonReady(&addonSupplyReward->AtkUnitBase)) + { + addonSupplyReward->AtkUnitBase.FireCallbackInt(0); + _continueAt = DateTime.Now.AddSeconds(0.58); + CurrentStage = Stage.FinalizeTurnIn; + } + + break; + + case Stage.FinalizeTurnIn: + if (TryGetAddonByName("GrandCompanySupplyList", + out var addonSupplyList) && IsAddonReady(&addonSupplyList->AtkUnitBase)) + { + var updateFilter = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 5 }, + new() { Type = ValueType.Int, Int = addonSupplyList->SelectedFilter }, + new() { Type = 0, Int = 0 } + }; + addonSupplyList->AtkUnitBase.FireCallback(3, updateFilter); + CurrentStage = Stage.SelectItemToTurnIn; + } + + break; + + case Stage.CloseGcSupply: + if (SelectSelectString(3)) + { + // you can occasionally get a 'not enough seals' warning lol + _continueAt = DateTime.Now.AddSeconds(1); + CurrentStage = Stage.TargetQuartermaster; + } + + break; + + case Stage.CloseGcSupplyThenStop: + if (SelectSelectString(3)) + { + if (GetCurrentSealCount() <= 2000 + 200) + { + _turnInWindow.State = false; + CurrentStage = Stage.Stop; + } + else + { + _continueAt = DateTime.Now.AddSeconds(1); + CurrentStage = Stage.TargetQuartermaster; + } + } + + break; + + case Stage.TargetQuartermaster: + if (GetCurrentSealCount() < 2000) // fixme this should be selectable/dependent on shop item + { + CurrentStage = Stage.Stop; + break; + } + + if (_targetManager.Target == personnelOfficer!) + break; + + InteractWithTarget(quartermaster!); + CurrentStage = Stage.SelectRewardRank; + break; + + case Stage.SelectRewardRank: + { + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && + IsAddonReady(addonExchange)) + { + var selectRank = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 1 }, + new() { Type = ValueType.Int, Int = 0 /* position within list */ }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonExchange->FireCallback(9, selectRank); + _continueAt = DateTime.Now.AddSeconds(0.5); + CurrentStage = Stage.SelectRewardType; + } + + break; + } + + case Stage.SelectRewardType: + { + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && + IsAddonReady(addonExchange)) + { + var selectType = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 2 }, + /* + * 2 = weapons + * 3 = armor + * 1 = materiel + * 4 = materials + */ + new() { Type = ValueType.Int, Int = 1 /* position within list */ }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonExchange->FireCallback(9, selectType); + _continueAt = DateTime.Now.AddSeconds(0.5); + CurrentStage = Stage.SelectReward; + } + + break; + } + + case Stage.SelectReward: + { + // coke: 0i, 31i, 5i[count], unknown, true, false, unknown, unknown, unknown + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && + IsAddonReady(addonExchange)) + { + int venturesToBuy = (GetCurrentSealCount() - 2000) / 200; + venturesToBuy = Math.Min(venturesToBuy, 65000 - GetCurrentVentureCount()); + if (venturesToBuy == 0) + { + CurrentStage = Stage.Stop; + break; + } + + _chatGui.Print($"Buying {venturesToBuy} ventures..."); + var selectReward = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.Int, Int = 0 /* position within list?? */ }, + new() { Type = ValueType.Int, Int = venturesToBuy }, + new() { Type = 0, Int = 0 }, + new() { Type = ValueType.Bool, Byte = 1 }, + new() { Type = ValueType.Bool, Byte = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonExchange->FireCallback(9, selectReward); + _continueAt = DateTime.Now.AddSeconds(1); + CurrentStage = Stage.CloseGcExchange; + } + + break; + } + + case Stage.CloseGcExchange: + { + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && + IsAddonReady(addonExchange)) + { + addonExchange->FireCallbackInt(-1); + CurrentStage = Stage.TargetPersonnelOfficer; + } + + break; + } + + case Stage.Stop: + break; + default: + PluginLog.Warning($"Unknown stage {CurrentStage}"); + break; + } + } + } + + 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, c.Position); + } + } + } + + o = null; + return float.MaxValue; + } + + private int GetNpcId(GameObject obj) + { + return Marshal.ReadInt32(obj.Address + 128); + } + + public void Dispose() + { + _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + _framework.Update -= FrameworkUpdate; + } + + private unsafe List BuildTurnInList(AgentGrandCompanySupply* agent) + { + List 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 GcItem + { + 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] + // 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(); + } + + private unsafe AtkUnitBase* GetAddonById(uint id) + { + var unitManagers = &AtkStage.GetSingleton()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; + for (var i = 0; i < 18; i++) + { + var unitManager = &unitManagers[i]; + var unitBaseArray = &(unitManager->AtkUnitEntries); + for (var j = 0; j < unitManager->Count; j++) + { + var unitBase = unitBaseArray[j]; + if (unitBase->ID == id) + { + return unitBase; + } + } + } + + return null; + } + + private unsafe bool TryGetAddonByName(string addonName, out T* addonPtr) + where T : unmanaged + { + var a = _gameGui.GetAddonByName(addonName); + if (a != IntPtr.Zero) + { + addonPtr = (T*)a; + return true; + } + else + { + addonPtr = null; + return false; + } + } + + private unsafe bool IsAddonReady(AtkUnitBase* addon) + { + return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; + } + + 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; + } + } + + private unsafe int GetPersonnelOfficerId() + { + return ((GrandCompany)PlayerState.Instance()->GrandCompany) switch + { + GrandCompany.Maelstrom => 0xF4B94, + GrandCompany.ImmortalFlames => 0xF4B97, + GrandCompany.TwinAdder => 0xF4B9A, + _ => int.MaxValue, + }; + } + + private unsafe int GetQuartermasterId() + { + return ((GrandCompany)PlayerState.Instance()->GrandCompany) switch + { + GrandCompany.Maelstrom => 0xF4B93, + GrandCompany.ImmortalFlames => 0xF4B96, + GrandCompany.TwinAdder => 0xF4B99, + _ => int.MaxValue, + }; + } + + private unsafe int GetSealCap() + { + return PlayerState.Instance()->GetGrandCompanyRank() switch + { + 1 => 10_000, + 2 => 15_000, + 3 => 20_000, + 4 => 25_000, + 5 => 30_000, + 6 => 35_000, + 7 => 40_000, + 8 => 45_000, + 9 => 50_000, + 10 => 80_000, + 11 => 90_000, + _ => 0, + }; + } + + private unsafe int GetCurrentVentureCount() + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + return inventoryManager->GetInventoryItemCount(21072, false, false, false); + } + + private unsafe bool SelectSelectString(int choice) + { + if (TryGetAddonByName("SelectString", out var addonSelectString) && + IsAddonReady(&addonSelectString->AtkUnitBase)) + { + addonSelectString->AtkUnitBase.FireCallbackInt(choice); + return true; + } + + return 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; + } + + 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 enum Stage + { + TargetPersonnelOfficer, + OpenGcSupply, + SelectItemToTurnIn, + TurnInSelected, + FinalizeTurnIn, + CloseGcSupply, + CloseGcSupplyThenStop, + + TargetQuartermaster, + SelectRewardRank, + SelectRewardType, + SelectReward, + CloseGcExchange, + + Stop, + } +} diff --git a/Deliveroo/GcItem.cs b/Deliveroo/GcItem.cs new file mode 100644 index 0000000..a375108 --- /dev/null +++ b/Deliveroo/GcItem.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.String; + +namespace Deliveroo; + +internal sealed class GcItem +{ + public required int ItemId { get; init; } + public required string Name { get; init; } + public required int SealsWithBonus { get; init; } + public required int SealsWithoutBonus { get; init; } + public required byte ItemUiCategory { get; init; } +} diff --git a/Deliveroo/TurnInWindow.cs b/Deliveroo/TurnInWindow.cs new file mode 100644 index 0000000..96c9dae --- /dev/null +++ b/Deliveroo/TurnInWindow.cs @@ -0,0 +1,53 @@ +using System.Numerics; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Deliveroo; + +internal sealed class TurnInWindow : Window +{ + public TurnInWindow() + : base("Turn In###DeliverooTurnIn") + { + Position = new Vector2(100, 100); + PositionCondition = ImGuiCond.FirstUseEver; + + Flags = ImGuiWindowFlags.AlwaysAutoResize; + } + + public bool State { get; set; } + public decimal Multiplier { get; set; } + + public int CurrentVentureCount { get; set; } + + public string Debug { get; set; } + + public override void Draw() + { + bool state = State; + if (ImGui.Checkbox("Handle GC turn ins/exchange automatically", ref state)) + { + State = state; + } + + ImGui.Indent(27); + if (Multiplier == 1m) + { + ImGui.TextColored(ImGuiColors.DalamudRed, "You do not have a buff active"); + } + else + { + ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%"); + } + + ImGui.Spacing(); + int current = 0; + ImGui.Combo("", ref current, new string[] { $"Ventures ({CurrentVentureCount:N0})" }, 1); + + ImGui.Unindent(27); + + ImGui.Separator(); + ImGui.Text($"Debug (State): {Debug}"); + } +} diff --git a/Deliveroo/packages.lock.json b/Deliveroo/packages.lock.json new file mode 100644 index 0000000..467f0f2 --- /dev/null +++ b/Deliveroo/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net7.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.11, )", + "resolved": "2.1.11", + "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + } + } + } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..aaac9e0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file