using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LLib.GameData; using LLib.GameUI; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Functions; using Questionable.GameStructs; using Questionable.Model; using Questionable.Model.Gathering; using Questionable.Model.Questing; namespace Questionable.Controller; internal sealed class ContextMenuController : IDisposable { private readonly IContextMenu _contextMenu; private readonly QuestController _questController; private readonly GatheringPointRegistry _gatheringPointRegistry; private readonly GatheringData _gatheringData; private readonly QuestRegistry _questRegistry; private readonly QuestData _questData; private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions; private readonly IGameGui _gameGui; private readonly IChatGui _chatGui; private readonly IClientState _clientState; private readonly ILogger _logger; public ContextMenuController(IContextMenu contextMenu, QuestController questController, GatheringPointRegistry gatheringPointRegistry, GatheringData gatheringData, QuestRegistry questRegistry, QuestData questData, GameFunctions gameFunctions, QuestFunctions questFunctions, IGameGui gameGui, IChatGui chatGui, IClientState clientState, ILogger logger) { _contextMenu = contextMenu; _questController = questController; _gatheringPointRegistry = gatheringPointRegistry; _gatheringData = gatheringData; _questRegistry = questRegistry; _questData = questData; _gameFunctions = gameFunctions; _questFunctions = questFunctions; _gameGui = gameGui; _chatGui = chatGui; _clientState = clientState; _logger = logger; _contextMenu.OnMenuOpened += MenuOpened; } private void MenuOpened(IMenuOpenedArgs args) { if (args.AddonName != null) { return; } uint num = GetHoveredSatisfactionSupplyItemId(); if (num == 0) { _logger.LogTrace("Ignoring context menu, no item hovered"); return; } if (num > 1000000) { num -= 1000000; } if (num >= 500000) { num -= 500000; } if (_gatheringData.TryGetCustomDeliveryNpc(num, out var npcId)) { AddContextMenuEntry(args, num, npcId, EClassJob.Miner, "Mine"); AddContextMenuEntry(args, num, npcId, EClassJob.Botanist, "Harvest"); } else { _logger.LogDebug("No custom delivery NPC found for item {ItemId}.", num); } } private unsafe uint GetHoveredSatisfactionSupplyItemId() { AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance(); if (ptr == null || !ptr->IsAgentActive()) { return 0u; } if (_gameGui.TryGetAddonByName("SatisfactionSupply", out var addonPtr) && LAddon.IsAddonReady(&addonPtr->AtkUnitBase)) { int hoveredElementIndex = addonPtr->HoveredElementIndex; if (hoveredElementIndex >= 0 && hoveredElementIndex <= 2) { return ptr->Items[addonPtr->HoveredElementIndex].Id; } } return 0u; } private unsafe void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob, string verb) { EClassJob rowId = (EClassJob)_clientState.LocalPlayer.ClassJob.RowId; bool flag = classJob != rowId; if (flag) { bool flag2 = rowId - 16 <= EClassJob.Gladiator; flag = flag2; } if (flag) { return; } if (!_gatheringPointRegistry.TryGetGatheringPointId(itemId, classJob, out GatheringPointId _)) { _logger.LogInformation("No gathering point found for {ClassJob}.", classJob); return; } ushort collectability = _gatheringData.GetRecommendedCollectability(itemId); int quantityToGather = ((collectability > 0) ? 6 : int.MaxValue); if (collectability != 0) { AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance(); if (ptr->IsAgentActive()) { int maxTurnIns = ((ptr->NpcInfo.SatisfactionRank == 1) ? 3 : 6); quantityToGather = Math.Min(ptr->NpcData.RemainingAllowances, ((AgentSatisfactionSupply2*)ptr)->CalculateTurnInsToNextRank(maxTurnIns)); } string text = string.Empty; if (!_questFunctions.IsClassJobUnlocked(classJob)) { text = $"{classJob} not unlocked"; } else if (quantityToGather == 0) { text = "No allowances"; } else if (quantityToGather > _gameFunctions.GetFreeInventorySlots()) { text = "Inventory full"; } else if (_gameFunctions.IsOccupied()) { text = "Can't be used while interacting"; } string text2 = verb + " with Questionable"; if (!string.IsNullOrEmpty(text)) { text2 = text2 + " (" + text + ")"; } args.AddMenuItem(new MenuItem { Prefix = SeIconChar.Hyadelyn, PrefixColor = 52, Name = text2, OnClicked = delegate { StartGathering(npcId, itemId, quantityToGather, collectability, classJob); }, IsEnabled = string.IsNullOrEmpty(text) }); } } private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability, EClassJob classJob) { SatisfactionSupplyInfo satisfactionSupplyInfo = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single((IQuestInfo x) => x is SatisfactionSupplyInfo); if (_questRegistry.TryGetQuest(satisfactionSupplyInfo.QuestId, out Quest quest)) { QuestSequence questSequence = quest.FindSequence(0); QuestStep questStep = questSequence.Steps.Single((QuestStep x) => x.InteractionType == EInteractionType.SwitchClass); questStep.TargetClass = classJob switch { EClassJob.Miner => EExtendedClassJob.Miner, EClassJob.Botanist => EExtendedClassJob.Botanist, _ => throw new ArgumentOutOfRangeException("classJob", classJob, null), }; QuestStep questStep2 = questSequence.Steps.Single((QuestStep x) => x.InteractionType == EInteractionType.Gather); int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); int index = 0; span[index] = new GatheredItem { ItemId = itemId, ItemCount = quantity, Collectability = collectability }; questStep2.ItemsToGather = list; _questController.SetGatheringQuest(quest); _questController.StartGatheringQuest("SatisfactionSupply prepare gathering"); } else { _chatGui.PrintError($"No associated quest ({satisfactionSupplyInfo.QuestId}).", "Questionable"); } } public void Dispose() { _contextMenu.OnMenuOpened -= MenuOpened; } }