using System; using System.Collections.Generic; using System.Linq; using Dalamud.Game; using Dalamud.Plugin.Services; using Lumina.Excel; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Data; internal sealed class JournalData { internal sealed class Genre { public uint Id { get; } public string Name { get; } public uint CategoryId { get; } public List Quests { get; } public bool IsUnderOtherQuests { get; set; } public Genre(JournalGenre journalGenre, List quests) { Id = journalGenre.RowId; Name = journalGenre.Name.ToString(); CategoryId = journalGenre.JournalCategory.RowId; Quests = quests; IsUnderOtherQuests = false; } public Genre(uint id, string name, uint categoryId, List quests) { Id = id; Name = name; CategoryId = categoryId; Quests = quests; IsUnderOtherQuests = false; } public Genre(uint id, string name, uint categoryId, List quests, bool isUnderOtherQuests = false) { Id = id; Name = name; CategoryId = categoryId; Quests = quests; IsUnderOtherQuests = isUnderOtherQuests; } } internal sealed class Category { public uint Id { get; } public string Name { get; } public uint SectionId { get; } public IReadOnlyList Genres { get; } public Category(JournalCategory journalCategory, IReadOnlyList genres) { Id = journalCategory.RowId; Name = journalCategory.Name.ToString(); SectionId = journalCategory.JournalSection.RowId; Genres = genres; } public Category(uint id, string name, uint sectionId, IReadOnlyList genres) { Id = id; Name = name; SectionId = sectionId; Genres = genres; } } internal sealed class Section { public uint Id { get; } = journalSection.RowId; public string Name { get; } = journalSection.Name.ToString(); public IReadOnlyList Categories { get; } public Section(JournalSection journalSection, IReadOnlyList categories) { Categories = categories; base._002Ector(); } } private readonly ILogger _logger; private const uint UnassignedGenreId = 4294967291u; private const uint UnassignedCategoryId = 4294967291u; private static readonly Dictionary QuestGenreOverrides = new Dictionary { { 1119, "Zodiac Weapons" }, { 1120, "Zodiac Weapons" }, { 1121, "Zodiac Weapons" }, { 1122, "Zodiac Weapons" }, { 1123, "Zodiac Weapons" }, { 1124, "Zodiac Weapons" }, { 1125, "Zodiac Weapons" }, { 1126, "Zodiac Weapons" }, { 1127, "Zodiac Weapons" }, { 1579, "Zodiac Weapons" }, { 4173, "Radz-at-Han Sidequests" }, { 4196, "Thavnairian Sidequests" }, { 4197, "Thavnairian Sidequests" }, { 4198, "Thavnairian Sidequests" }, { 4199, "Thavnairian Sidequests" }, { 4200, "Thavnairian Sidequests" }, { 4201, "Thavnairian Sidequests" }, { 4202, "Thavnairian Sidequests" }, { 4203, "Thavnairian Sidequests" }, { 4204, "Thavnairian Sidequests" }, { 4205, "Thavnairian Sidequests" }, { 4206, "Thavnairian Sidequests" }, { 4207, "Thavnairian Sidequests" }, { 4208, "Thavnairian Sidequests" }, { 4209, "Thavnairian Sidequests" }, { 101, "Rogue Quests" }, { 177, "Gladiator Quests" }, { 178, "Pugilist Quests" }, { 179, "Marauder Quests" }, { 180, "Lancer Quests" }, { 181, "Archer Quests" }, { 182, "Conjurer Quests" }, { 183, "Thaumaturge Quests" }, { 184, "Carpenter Quests" }, { 185, "Blacksmith Quests" }, { 186, "Armorer Quests" }, { 187, "Goldsmith Quests" }, { 188, "Leatherworker Quests" }, { 189, "Weaver Quests" }, { 190, "Alchemist Quests" }, { 191, "Culinarian Quests" }, { 192, "Miner Quests" }, { 193, "Botanist Quests" }, { 451, "Arcanist Quests" }, { 1134, "Fisher Quests" }, { 2109, "Machinist Quests" }, { 437, "Ul'dahn Sidequests" }, { 488, "Ul'dahn Sidequests" }, { 489, "Ul'dahn Sidequests" }, { 2106, "Ishgardian Sidequests" }, { 2107, "Dravanian Forelands Sidequests" }, { 2110, "Ishgardian Sidequests" }, { 2123, "Ishgardian Sidequests" }, { 2334, "Anima Weapons" }, { 2387, "Palace of the Dead" }, { 2389, "Order of the Twin Adder Quests" }, { 2390, "Maelstrom Quests" }, { 2391, "Immortal Flames Quests" }, { 2612, "The Forbidden Land, Eureka" }, { 2613, "The Forbidden Land, Eureka" }, { 2920, "Rhalgr's Reach Sidequests" }, { 2921, "Kugane Sidequests" }, { 2941, "Rhalgr's Reach Sidequests" }, { 2942, "The Forbidden Land, Eureka" }, { 2943, "Kugane Sidequests" }, { 3019, "Gridanian Sidequests" }, { 3088, "Mor Dhonan Sidequests" }, { 3602, "Crystarium Sidequests" }, { 3603, "Eulmore Sidequests" }, { 3604, "Crystarium Sidequests" }, { 3605, "Crystarium Sidequests" }, { 3662, "Crystarium Sidequests" }, { 3759, "Ul'dahn Sidequests" }, { 3760, "Ishgardian Restoration Main Quests" }, { 3841, "Ishgardian Restoration Main Quests" }, { 3842, "Lochs Sidequests" }, { 3942, "Resistance Weapons" }, { 3972, "Ishgardian Restoration Main Quests" }, { 4027, "Resistance Weapons" }, { 4030, "Ul'dahn Sidequests" }, { 4041, "Resistance Weapons" }, { 4042, "Ishgardian Restoration Main Quests" }, { 4044, "YoRHa: Dark Apocalypse" }, { 4045, "YoRHa: Dark Apocalypse" }, { 4046, "YoRHa: Dark Apocalypse" }, { 4047, "YoRHa: Dark Apocalypse" }, { 4048, "YoRHa: Dark Apocalypse" }, { 4049, "YoRHa: Dark Apocalypse" }, { 4094, "Resistance Weapons" }, { 4170, "Old Sharlayan Sidequests" }, { 4171, "Old Sharlayan Sidequests" }, { 4174, "Old Sharlayan Sidequests" }, { 4175, "Radz-at-Han Sidequests" }, { 4644, "Island Sanctuary Quests" }, { 4651, "Old Sharlayan Sidequests" }, { 5003, "Tuliyollal Sidequests" }, { 5004, "Tuliyollal Sidequests" }, { 5006, "Tuliyollal Sidequests" }, { 5007, "Solution Nine Sidequests" }, { 5008, "Solution Nine Sidequests" }, { 5408, "Crystarium Sidequests" }, { 5459, "Old Sharlayan Sidequests" } }; private static readonly HashSet SortKeyZeroOverrides = new HashSet { 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1579 }; public uint? SeasonalEventsCategoryId { get; } public IReadOnlySet SeasonalEventGenreIds { get; } public uint? MoogleDeliveryGenreId { get; } public List Genres { get; } public List Categories { get; } public List
Sections { get; } public int? OtherQuestsSectionRowId { get; private set; } public JournalData(IDataManager dataManager, QuestData questData, ILogger logger) { JournalData journalData = this; _logger = logger; ExcelSheet excelSheet = dataManager.GetExcelSheet(); ExcelSheet excelSheet2 = dataManager.GetExcelSheet(ClientLanguage.English); JournalCategory journalCategory = dataManager.GetExcelSheet(ClientLanguage.English).FirstOrDefault((JournalCategory x) => x.Name.ToString().Equals("Seasonal Events", StringComparison.OrdinalIgnoreCase)); if (journalCategory.RowId != 0) { SeasonalEventsCategoryId = journalCategory.RowId; _logger.LogDebug("Resolved 'Seasonal Events' JournalCategory ID: {CategoryId}", SeasonalEventsCategoryId); } else { SeasonalEventsCategoryId = null; _logger.LogWarning("Could not find 'Seasonal Events' JournalCategory - seasonal event detection may not work correctly"); } if (SeasonalEventsCategoryId.HasValue) { SeasonalEventGenreIds = (from x in excelSheet where x.RowId != 0 && x.JournalCategory.RowId == journalData.SeasonalEventsCategoryId.Value select x.RowId).ToHashSet(); _logger.LogDebug("Resolved {Count} seasonal event JournalGenre IDs from JournalCategory {CategoryId}: [{Ids}]", SeasonalEventGenreIds.Count, SeasonalEventsCategoryId.Value, string.Join(", ", SeasonalEventGenreIds.OrderBy((uint x) => x))); } else { SeasonalEventGenreIds = new HashSet(); } JournalGenre journalGenre = excelSheet2.FirstOrDefault((JournalGenre x) => x.Name.ToString().Equals("Delivery Moogle Quests", StringComparison.OrdinalIgnoreCase)); if (journalGenre.RowId != 0) { MoogleDeliveryGenreId = journalGenre.RowId; _logger.LogDebug("Resolved 'Delivery Moogle Quests' JournalGenre ID: {GenreId}", MoogleDeliveryGenreId); } else { MoogleDeliveryGenreId = null; _logger.LogWarning("Could not find 'Delivery Moogle Quests' JournalGenre - moogle delivery quest detection may not work correctly"); } ApplyGenreOverrides(excelSheet2, questData); List list = (from x in excelSheet where x.RowId != 0 && x.Icon > 0 select new Genre(x, questData.GetAllByJournalGenre(x.RowId))).ToList(); QuestRedo row = dataManager.GetExcelSheet().GetRow(1u); QuestRedo row2 = dataManager.GetExcelSheet().GetRow(2u); QuestRedo row3 = dataManager.GetExcelSheet().GetRow(3u); Genre genreLimsa = new Genre(4294967292u, "Starting in Limsa Lominsa", 1u, (from x in new uint[2] { 108u, 109u }.Concat(row.QuestRedoParam.Select((QuestRedo.QuestRedoParamStruct x) => x.Quest.RowId)) where x != 0 select questData.GetQuestInfo(QuestId.FromRowId(x))).ToList()); Genre genreGridania = new Genre(4294967293u, "Starting in Gridania", 1u, (from x in new uint[3] { 85u, 123u, 124u }.Concat(row2.QuestRedoParam.Select((QuestRedo.QuestRedoParamStruct x) => x.Quest.RowId)) where x != 0 select questData.GetQuestInfo(QuestId.FromRowId(x))).ToList()); Genre genreUldah = new Genre(4294967294u, "Starting in Ul'dah", 1u, (from x in new uint[3] { 568u, 569u, 570u }.Concat(row3.QuestRedoParam.Select((QuestRedo.QuestRedoParamStruct x) => x.Quest.RowId)) where x != 0 select questData.GetQuestInfo(QuestId.FromRowId(x))).ToList()); list.InsertRange(0, new Genre[3] { genreLimsa, genreGridania, genreUldah }); list.Single((Genre x) => x.Id == 1).Quests.RemoveAll((IQuestInfo x) => genreLimsa.Quests.Contains(x) || genreGridania.Quests.Contains(x) || genreUldah.Quests.Contains(x)); HashSet assignedQuestIds = new HashSet(from q in list.SelectMany((Genre g) => g.Quests) select q.QuestId); List list2 = (from QuestInfo x in questData.AllQuests.Values.Where((IQuestInfo x) => x is QuestInfo && !assignedQuestIds.Contains(x.QuestId)) orderby x.SortKey, x.QuestId select x).ToList(); Genre genre = null; if (list2.Count > 0) { _logger.LogWarning("Found {Count} quests not assigned to any journal genre:", list2.Count); foreach (QuestInfo item in list2) { _logger.LogWarning(" Orphaned quest {QuestId} '{Name}' (JournalGenre={Genre})", item.QuestId, item.Name, item.JournalGenre); } genre = new Genre(4294967291u, "Unassigned Quests", 4294967291u, list2.Cast().ToList()); list.Add(genre); } Genres = list.ToList(); OtherQuestsSectionRowId = GetOtherQuestsSectionRowId(dataManager); Categories = (from x in dataManager.GetExcelSheet() where x.RowId != 0 select new Category(x, journalData.Genres.Where((Genre y) => y.CategoryId == x.RowId).ToList())).ToList(); int? otherQuestsSectionRowId; if (genre != null) { otherQuestsSectionRowId = OtherQuestsSectionRowId; if (otherQuestsSectionRowId.HasValue) { int valueOrDefault = otherQuestsSectionRowId.GetValueOrDefault(); Categories.Add(new Category(4294967291u, "Unassigned", (uint)valueOrDefault, new global::_003C_003Ez__ReadOnlySingleElementList(genre))); } else { _logger.LogWarning("'Other Quests' section not found - {Count} unassigned quests will not be shown in the journal", list2.Count); } } Sections = (from x in dataManager.GetExcelSheet() select new Section(x, journalData.Categories.Where((Category y) => y.SectionId == x.RowId).ToList())).ToList(); otherQuestsSectionRowId = OtherQuestsSectionRowId; if (otherQuestsSectionRowId.HasValue) { int valueOrDefault2 = otherQuestsSectionRowId.GetValueOrDefault(); uint otherIdU = (uint)valueOrDefault2; Section section = Sections.FirstOrDefault((Section s) => s.Id == otherIdU); if (section != null) { int num = 0; foreach (Category category in section.Categories) { foreach (Genre genre2 in category.Genres) { genre2.IsUnderOtherQuests = true; num++; } } _logger.LogDebug("Marked {Count} genres as under 'Other Quests' (OtherQuestsSectionRowId={Id})", num, valueOrDefault2); } else { _logger.LogWarning("OtherQuestsSectionRowId = {Id} found, but matching Section not present in constructed Sections", valueOrDefault2); } } else { _logger.LogDebug("OtherQuestsSectionRowId not found - 'Other Quests' genre marking skipped"); } } private void ApplyGenreOverrides(ExcelSheet journalGenreSheetEnglish, QuestData questData) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (JournalGenre item in journalGenreSheetEnglish) { if (item.RowId != 0 && !item.Name.IsEmpty) { dictionary.TryAdd(item.Name.ToString(), item.RowId); } } int num = 0; foreach (var (num3, text2) in QuestGenreOverrides) { if (!dictionary.TryGetValue(text2, out var value)) { _logger.LogWarning("Genre '{GenreName}' not found for quest {QuestId}", text2, num3); } else { if (!questData.TryGetQuestInfo(new QuestId(num3), out IQuestInfo questInfo)) { continue; } if (questInfo is QuestInfo questInfo2) { questInfo2.JournalGenre = value; if (SortKeyZeroOverrides.Contains(num3)) { questInfo2.SortKey = 0; } } num++; } } if (num > 0) { _logger.LogDebug("Applied {Count} journal genre overrides", num); } } private static int? GetOtherQuestsSectionRowId(IDataManager dataManager) { JournalSection journalSection = dataManager.GetExcelSheet(ClientLanguage.English).FirstOrDefault((JournalSection s) => s.Name.ToString() == "Other Quests"); if (journalSection.RowId == 0) { return null; } return (int)journalSection.RowId; } }