using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; 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[9] { "Neutral", "Recognized", "Friendly", "Trusted", "Respected", "Honored", "Sworn", "Bloodsworn", "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"); text = new ImU8String(12, 1); text.AppendLiteral("Used today: "); text.AppendFormatted(value); ImGui.TextUnformatted(text); 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 <= 4) { byte rank = 1; while (rank <= 4) { List list = quests.Where((QuestInfo x) => x.AlliedSocietyRank == rank).ToList(); if (list.Count != 0) { ImGui.Text(RankNames[rank]); foreach (QuestInfo item in list) { DrawQuest(item); } } byte b = (byte)(rank + 1); rank = b; } } else if (alliedSociety == EAlliedSociety.Ixal) { byte rank2 = 1; while (rank2 <= 8) { List list2 = quests.Where((QuestInfo x) => x.AlliedSocietyRank == rank2).ToList(); if (list2.Count != 0) { ImGui.Text(RankNames[rank2]); foreach (QuestInfo item2 in list2) { DrawQuest(item2); } } byte b = (byte)(rank2 + 1); rank2 = b; } } else { if ((int)alliedSociety < 6) { return; } byte rank3 = 1; while (rank3 <= 9) { List list3 = quests.Where((QuestInfo x) => x.AlliedSocietyRank == rank3).ToList(); if (list3.Count != 0) { ImGui.Text(RankNames[rank3]); foreach (QuestInfo item3 in list3) { DrawQuest(item3); } } byte b = (byte)(rank3 + 1); rank3 = b; } } } 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) { 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)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, num switch { 1 => "Add 1 Quest (Optimal)", 2 => "Add 2 Quests (Optimal)", 3 => "Add 3 Quests (Optimal)", _ => (num != remainingAllowances) ? $"Add {num} (Optimal)" : $"Add {num} (Today's Remaining)", })) { 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} Available")) { 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) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.Cross.ToIconString()); ImGui.Text(text); ImGui.SameLine(); ImGui.TextUnformatted("No daily allowances remaining"); } Vector4 col = ImGuiColors.DalamudGrey3; 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 && questsToAddCount < availableCount) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.QuestSync.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(5, 2); text.AppendLiteral("Add "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" "); text.AppendFormatted((questsToAddCount == 1) ? "quest" : "quests"); ImGui.TextUnformatted(text); } Vector4 col = ImGuiColors.DalamudGrey3; text = new ImU8String(44, 2); text.AppendLiteral("You can complete "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" more allied society "); text.AppendFormatted((remainingAllowances == 1) ? "quest" : "quests"); text.AppendLiteral(" today"); ImGui.TextColored(in col, text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } else if (questsToAddCount == dailyLimit) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.QuestSync.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(11, 1); text.AppendLiteral("Add "); text.AppendFormatted(dailyLimit); text.AppendLiteral(" quests"); ImGui.TextUnformatted(text); } Vector4 col = ImGuiColors.DalamudGrey3; text = new ImU8String(51, 2); text.AppendLiteral("This will use "); text.AppendFormatted(dailyLimit); text.AppendLiteral(" of your "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" remaining shared allowances"); ImGui.TextColored(in col, text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } else if (questsToAddCount == availableCount) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.QuestSync.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(20, 3); text.AppendLiteral("Add all "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" available "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" "); text.AppendFormatted((questsToAddCount == 1) ? "quest" : "quests"); ImGui.TextUnformatted(text); } Vector4 col = ImGuiColors.DalamudGrey3; text = new ImU8String(37, 2); text.AppendLiteral("Uses "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" of "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared allowances remaining"); ImGui.TextColored(in col, text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "Prioritises highest rank quests first"); } else { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.BoxedLetterQ.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(6, 3); text.AppendLiteral("Add "); text.AppendFormatted(questsToAddCount); text.AppendLiteral(" "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" "); text.AppendFormatted((questsToAddCount == 1) ? "quest" : "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) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.Cross.ToIconString()); ImGui.Text(text); ImGui.SameLine(); ImGui.TextUnformatted("Warning: Exceeds remaining allowances"); } ImGui.Spacing(); 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(37, 2); text.AppendLiteral("but you only have "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared "); text.AppendFormatted((remainingAllowances == 1) ? "allowance" : "allowances"); text.AppendLiteral(" remaining."); ImGui.TextUnformatted(text); ImGui.Spacing(); ImGui.TextColored(ImGuiColors.DalamudGrey3, "The excess quests won't be completable until tomorrow."); } else if (availableCount > dailyLimit) { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.Cross.ToIconString()); ImGui.Text(text); ImGui.SameLine(); ImGui.TextUnformatted("Warning: Exceeds daily optimal amount"); } ImGui.Spacing(); text = new ImU8String(32, 1); text.AppendLiteral("This adds all "); text.AppendFormatted(availableCount); text.AppendLiteral(" available quests,"); ImGui.TextUnformatted(text); text = new ImU8String(48, 1); text.AppendLiteral("using more than the typical "); text.AppendFormatted(3); text.AppendLiteral(" quests per society."); ImGui.TextUnformatted(text); ImGui.Spacing(); Vector4 col = ImGuiColors.ParsedGreen; text = new ImU8String(37, 1); text.AppendLiteral("You have "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared allowances remaining"); ImGui.TextColored(in col, text); ImGui.TextColored(ImGuiColors.DalamudGrey3, "You may want to save allowances for other societies."); } else { ImU8String text; using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.QuestSync.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(20, 3); text.AppendLiteral("Add all "); text.AppendFormatted(availableCount); text.AppendLiteral(" available "); text.AppendFormatted(alliedSociety); text.AppendLiteral(" "); text.AppendFormatted((availableCount == 1) ? "quest" : "quests"); ImGui.TextUnformatted(text); } ImGui.Spacing(); Vector4 col = ImGuiColors.DalamudGrey3; text = new ImU8String(27, 2); text.AppendLiteral("Uses "); text.AppendFormatted(availableCount); text.AppendLiteral(" of "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared allowances"); ImGui.TextColored(in col, text); ImGui.TextColored(ImGuiColors.DalamudGrey3, "All can be completed today"); } ImGui.EndTooltip(); } private void DrawStandardAddButton(EAlliedSociety alliedSociety, List availableQuests, int remainingAllowances) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, availableQuests.Count switch { 1 => "Add 1 Quest", 2 => "Add 2 Quests", 3 => "Add 3 Quests", _ => $"Add All {availableQuests.Count} Quests", })) { 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); ImGui.Spacing(); if (availableQuests.Count <= remainingAllowances) { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.QuestSync.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(17, 3); text.AppendLiteral("Uses "); text.AppendFormatted(availableQuests.Count); text.AppendLiteral(" of "); text.AppendFormatted(remainingAllowances); text.AppendLiteral(" shared "); text.AppendFormatted((remainingAllowances == 1) ? "allowance" : "allowances"); ImGui.TextUnformatted(text); } ImGui.TextColored(ImGuiColors.DalamudGrey3, "All can be completed today"); } else { using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) { text = new ImU8String(0, 1); text.AppendFormatted(SeIconChar.Cross.ToIconString()); ImGui.Text(text); ImGui.SameLine(); text = new ImU8String(31, 1); text.AppendLiteral("Exceeds remaining allowances ("); text.AppendFormatted(remainingAllowances); text.AppendLiteral(")"); ImGui.TextUnformatted(text); } ImGui.TextColored(ImGuiColors.DalamudGrey3, "Some quests won't be completable until tomorrow"); } 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"); } }