using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; namespace Questionable.Windows.JournalComponents; internal sealed class AlliedSocietyJournalComponent { private static readonly string[] RankNames = new string[8] { "Neutral", "Recognized", "Friendly", "Trusted", "Respected", "Honored", "Sworn", "Allied" }; private const int DefaultDailyQuestLimit = 3; private const int SharedDailyAllowanceLimit = 12; private readonly AlliedSocietyQuestFunctions _alliedSocietyQuestFunctions; private readonly QuestData _questData; private readonly QuestRegistry _questRegistry; private readonly QuestJournalUtils _questJournalUtils; private readonly QuestTooltipComponent _questTooltipComponent; private readonly UiUtils _uiUtils; private readonly QuestController _questController; private readonly IChatGui _chatGui; private readonly ILogger _logger; public AlliedSocietyJournalComponent(AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, QuestData questData, QuestRegistry questRegistry, QuestJournalUtils questJournalUtils, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, QuestController questController, IChatGui chatGui, ILogger logger) { _alliedSocietyQuestFunctions = alliedSocietyQuestFunctions; _questData = questData; _questRegistry = questRegistry; _questJournalUtils = questJournalUtils; _questTooltipComponent = questTooltipComponent; _uiUtils = uiUtils; _questController = questController; _chatGui = chatGui; _logger = logger; } public void DrawAlliedSocietyQuests() { using ImRaii.IEndObject endObject = ImRaii.TabItem("Allied Societies"); if (!endObject) { return; } DrawDailyAllowanceHeader(); foreach (EAlliedSociety item in from x in Enum.GetValues() where x != EAlliedSociety.None select x) { List list = (from x in _alliedSocietyQuestFunctions.GetAvailableAlliedSocietyQuests(item) select (QuestInfo)_questData.GetQuestInfo(x)).ToList(); if (list.Count != 0 && DrawAlliedSocietyHeader(item, list)) { using (ImRaii.PushIndent()) { DrawAddToPriorityButtons(item, list); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); DrawQuestList(item, list); } } } } private unsafe void DrawDailyAllowanceHeader() { QuestManager* ptr = QuestManager.Instance(); if (ptr != null) { byte b = (byte)ptr->GetBeastTribeAllowance(); int value = 12 - b; (DateTime ResetTime, TimeSpan TimeUntilReset) tuple = CalculateTimeUntilReset(); DateTime item = tuple.ResetTime; string value2 = FormatTimeSpan(tuple.TimeUntilReset); Vector4 col = ((b > 0) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); ImU8String text = new ImU8String(31, 2); text.AppendLiteral("Daily Allowances: "); text.AppendFormatted(b); text.AppendLiteral(" / "); text.AppendFormatted(12); text.AppendLiteral(" remaining"); ImGui.TextColored(in col, text); ImGui.SameLine(); col = ImGuiColors.DalamudGrey3; text = new ImU8String(13, 1); text.AppendLiteral("(Resets in: "); text.AppendFormatted(value2); text.AppendLiteral(")"); ImGui.TextColored(in col, text); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); ImGui.TextUnformatted("Shared across ALL allied societies (all expansions)"); text = new ImU8String(12, 1); text.AppendLiteral("Used today: "); text.AppendFormatted(value); ImGui.TextUnformatted(text); ImGui.TextUnformatted("Each allied society has a limit of 3 quests per day"); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); col = ImGuiColors.DalamudGrey3; text = new ImU8String(12, 1); text.AppendLiteral("Next reset: "); text.AppendFormatted(item, "g"); ImGui.TextColored(in col, text); col = ImGuiColors.DalamudGrey3; text = new ImU8String(18, 1); text.AppendLiteral("Time until reset: "); text.AppendFormatted(value2); ImGui.TextColored(in col, text); ImGui.EndTooltip(); } ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); } } internal static (DateTime ResetTime, TimeSpan TimeUntilReset) CalculateTimeUntilReset() { DateTime utcNow = DateTime.UtcNow; DateTime dateTime = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, 15, 0, 0, DateTimeKind.Utc); DateTime dateTime2 = ((utcNow >= dateTime) ? dateTime.AddDays(1.0) : dateTime); TimeSpan item = dateTime2 - utcNow; return (ResetTime: dateTime2.ToLocalTime(), TimeUntilReset: item); } private static string FormatTimeSpan(TimeSpan timeSpan) { if (timeSpan.TotalMinutes < 1.0) { return "< 1m"; } if (!(timeSpan.TotalHours < 1.0)) { if (!(timeSpan.TotalDays < 1.0)) { return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours}h"; } return $"{timeSpan.Hours}h {timeSpan.Minutes}m"; } return $"{timeSpan.Minutes}m"; } private bool DrawAlliedSocietyHeader(EAlliedSociety alliedSociety, List quests) { return ImGui.CollapsingHeader($"{alliedSociety}###AlliedSociety{alliedSociety}"); } private void DrawQuestList(EAlliedSociety alliedSociety, List quests) { if ((int)alliedSociety <= 5) { byte rank = 1; while (rank <= 8) { List list = quests.Where((QuestInfo x) => x.AlliedSocietyRank == rank).ToList(); if (list.Count != 0) { ImGui.Text(RankNames[rank - 1]); foreach (QuestInfo item in list) { DrawQuest(item); } } byte b = (byte)(rank + 1); rank = b; } return; } foreach (QuestInfo quest in quests) { DrawQuest(quest); } } private void DrawAddToPriorityButtons(EAlliedSociety alliedSociety, List quests) { bool flag = (int)alliedSociety <= 5; int dailyLimit = (flag ? 12 : 3); int remainingAllowances = GetRemainingAllowances(); Quest quest; List list = (from q in quests where _questRegistry.TryGetQuest(q.QuestId, out quest) && !quest.Root.Disabled where !_questController.ManualPriorityQuests.Any((Quest pq) => pq.Id.Equals(q.QuestId)) orderby q.AlliedSocietyRank descending select q).ToList(); if (list.Count == 0) { DrawDisabledAddButton(); } else if (flag && list.Count > 3) { DrawArrSocietyButtons(alliedSociety, list, dailyLimit, remainingAllowances); } else { DrawStandardAddButton(alliedSociety, list, remainingAllowances); } } private unsafe int GetRemainingAllowances() { QuestManager* ptr = QuestManager.Instance(); if (ptr == null) { return 12; } return (int)ptr->GetBeastTribeAllowance(); } private static void DrawDisabledAddButton() { using (ImRaii.Disabled(disabled: true)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add to Priority"); } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("No quests available to add (may be disabled, already in priority, or not available)"); } } private void DrawArrSocietyButtons(EAlliedSociety alliedSociety, List availableQuests, int dailyLimit, int remainingAllowances) { int num = Math.Min(Math.Min(dailyLimit, remainingAllowances), availableQuests.Count); using (ImRaii.Disabled(remainingAllowances == 0)) { string text = ((num == remainingAllowances) ? $"Add {num} (Today's Remaining)" : $"Add {num} (Recommended)"); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, text)) { AddQuestsToPriority(alliedSociety, availableQuests.Take(num).ToList()); } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { DrawRecommendedButtonTooltip(num, remainingAllowances, dailyLimit, alliedSociety, availableQuests.Count); } ImGui.SameLine(); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ListOl, $"Add All {availableQuests.Count} (Ignore Limit)")) { AddQuestsToPriority(alliedSociety, availableQuests); } if (ImGui.IsItemHovered()) { DrawAddAllButtonTooltip(availableQuests.Count, remainingAllowances, dailyLimit, alliedSociety); } } private static void DrawRecommendedButtonTooltip(int questsToAddCount, int remainingAllowances, int dailyLimit, EAlliedSociety alliedSociety, int availableCount) { ImGui.BeginTooltip(); if (remainingAllowances == 0) { ImGui.TextUnformatted("No daily allowances remaining"); Vector4 col = ImGuiColors.DalamudGrey3; ImU8String text = new ImU8String(44, 1); text.AppendLiteral("You've used all "); text.AppendFormatted(12); text.AppendLiteral(" shared allowances for today"); ImGui.TextColored(in col, text); } else if (questsToAddCount == remainingAllowances) { ImU8String text = new ImU8String(47, 1); text.AppendLiteral("Add "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" quests - matches your remaining allowances"); ImGui.TextUnformatted(text); Vector4 col = ImGuiColors.ParsedGreen; text = new ImU8String(50, 1); text.AppendLiteral("You can complete "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" more allied society quests today"); ImGui.TextColored(in col, text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } else if (questsToAddCount == dailyLimit) { ImU8String text = new ImU8String(41, 1); text.AppendLiteral("Add "); text.AppendFormatted(dailyLimit); text.AppendLiteral(" quests - using all shared allowances"); ImGui.TextUnformatted(text); ImGui.TextColored(ImGuiColors.DalamudGrey3, "This will use all 12 shared allowances"); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } else { ImU8String text = new ImU8String(12, 2); text.AppendLiteral("Add "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" quests"); ImGui.TextUnformatted(text); Vector4 col = ImGuiColors.DalamudGrey3; text = new ImU8String(55, 2); text.AppendLiteral("Limited by available quests ("); text.AppendFormatted(availableCount); text.AppendLiteral(") and shared allowances ("); text.AppendFormatted(remainingAllowances); text.AppendLiteral(")"); ImGui.TextColored(in col, text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } ImGui.EndTooltip(); } private static void DrawAddAllButtonTooltip(int availableCount, int remainingAllowances, int dailyLimit, EAlliedSociety alliedSociety) { ImGui.BeginTooltip(); if (availableCount > remainingAllowances) { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Warning: Exceeds shared allowances"); } ImGui.Spacing(); ImU8String text = new ImU8String(54, 1); text.AppendLiteral("This adds all "); text.AppendFormatted(availableCount); text.AppendLiteral(" available quests to your priority list,"); ImGui.TextUnformatted(text); text = new ImU8String(47, 1); text.AppendLiteral("but you only have "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared allowances remaining."); ImGui.TextUnformatted(text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "The excess quests won't be completable until tomorrow."); } else if (availableCount > dailyLimit) { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, "Warning: Exceeds shared allowances"); } ImGui.Spacing(); ImU8String text = new ImU8String(32, 1); text.AppendLiteral("This adds all "); text.AppendFormatted(availableCount); text.AppendLiteral(" available quests,"); ImGui.TextUnformatted(text); text = new ImU8String(57, 1); text.AppendLiteral("but you can only complete "); text.AppendFormatted(12); text.AppendLiteral(" allied society quests per day."); ImGui.TextUnformatted(text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "You'll need multiple days to complete them all."); } else { ImU8String text = new ImU8String(26, 2); text.AppendLiteral("Add all "); text.AppendFormatted(availableCount); text.AppendLiteral(" available "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" quests"); ImGui.TextUnformatted(text); using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, $"All can be completed today ({remainingAllowances} shared allowances remaining)"); } } ImGui.EndTooltip(); } private void DrawStandardAddButton(EAlliedSociety alliedSociety, List availableQuests, int remainingAllowances) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, availableQuests.Count switch { 1 => "Add Quest to Priority", 2 => "Add Both Quests to Priority", 3 => "Add All 3 Quests to Priority", _ => $"Add All {availableQuests.Count} Quests to Priority", })) { AddQuestsToPriority(alliedSociety, availableQuests); } if (!ImGui.IsItemHovered()) { return; } ImGui.BeginTooltip(); ImU8String text = new ImU8String(18, 3); text.AppendLiteral("Add "); text.AppendFormatted(availableQuests.Count); text.AppendLiteral(" "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" "); text.AppendFormatted((availableQuests.Count == 1) ? "quest" : "quests"); text.AppendLiteral(" to priority"); ImGui.TextUnformatted(text); if (availableQuests.Count <= remainingAllowances) { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, $"Uses {availableQuests.Count} of {remainingAllowances} shared allowances"); } } else { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, $"Exceeds remaining allowances ({remainingAllowances})"); } } ImGui.EndTooltip(); } private void AddQuestsToPriority(EAlliedSociety alliedSociety, List questsToAdd) { int num = 0; foreach (QuestInfo item in questsToAdd) { if (_questController.AddQuestPriority(item.QuestId)) { num++; } } if (num > 0) { string text = ((num == 1) ? "quest" : "quests"); _logger.LogInformation("Added {Count} {Society} {QuestWord} to priority list", num, alliedSociety, text); _chatGui.Print($"Added {num} {alliedSociety} {text} to priority list.", "Questionable", 576); } } private void DrawQuest(QuestInfo questInfo) { var (color, icon, value) = _uiUtils.GetQuestStyle(questInfo.QuestId); if (!_questRegistry.TryGetQuest(questInfo.QuestId, out Quest quest) || quest.Root.Disabled) { color = ImGuiColors.DalamudGrey; } string text = $"{questInfo.Name} ({value}) [{questInfo.QuestId}]"; if (_uiUtils.ChecklistItem(text, color, icon)) { _questTooltipComponent.Draw(questInfo); } _questJournalUtils.ShowContextMenu(questInfo, quest, "AlliedSocietyJournalComponent"); } }