using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Dalamud.Game; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using Lumina.Excel; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Data; internal sealed class DutyUnlockData { private static class ContentTypeIds { public const uint Dungeon = 2u; public const uint Guildhest = 3u; public const uint Trial = 4u; public const uint Raid = 5u; public const uint PvP = 6u; public const uint QuestBattle = 7u; public const uint DeepDungeon = 21u; public const uint AllianceRaid = 26u; public const uint Ultimate = 28u; public const uint VCDungeonFinder = 29u; public const uint Criterion = 30u; public const uint CriterionSavage = 31u; } private static class ContentMemberTypeIds { public const byte LightParty = 2; public const byte FullParty = 3; public const byte Alliance = 4; } private readonly ImmutableDictionary _duties; private readonly ImmutableDictionary> _questToDuties; private readonly ImmutableDictionary> _dutyToQuests; private readonly ILogger _logger; public DutyUnlockData(IDataManager dataManager, ILogger logger) { _logger = logger; ExcelSheet excelSheet = dataManager.GetExcelSheet(); ExcelSheet excelSheet2 = dataManager.GetExcelSheet(); ImmutableDictionary immutableDictionary = (from c in excelSheet where c.RowId != 0 && c.Content.RowId != 0 && c.ContentLinkType == 1 group c by c.Content.RowId).ToImmutableDictionary((IGrouping g) => g.Key, (IGrouping g) => g.First().RowId); Dictionary> dictionary = new Dictionary>(); Dictionary> dictionary2 = new Dictionary>(); foreach (QuestEx item in excelSheet2.Where((QuestEx q) => q.RowId != 0)) { uint rowId = item.InstanceContentUnlock.RowId; if (rowId != 0 && immutableDictionary.TryGetValue(rowId, out var value)) { QuestId questId = QuestId.FromRowId(item.RowId); if (!dictionary.ContainsKey(questId)) { dictionary[questId] = new List(); } dictionary[questId].Add(value); if (!dictionary2.ContainsKey(value)) { dictionary2[value] = new List(); } dictionary2[value].Add(questId); } } _questToDuties = dictionary.ToImmutableDictionary((KeyValuePair> x) => x.Key, (KeyValuePair> x) => x.Value.ToImmutableList()); _dutyToQuests = dictionary2.ToImmutableDictionary((KeyValuePair> x) => x.Key, (KeyValuePair> x) => x.Value.ToImmutableList()); Dictionary dictionary3 = new Dictionary(); foreach (ContentFinderCondition item2 in excelSheet.Where((ContentFinderCondition x) => x.RowId != 0 && x.Content.RowId != 0)) { if (item2.ContentLinkType != 1) { continue; } ContentType? valueNullable = item2.ContentType.ValueNullable; if (!valueNullable.HasValue) { continue; } uint rowId2 = valueNullable.Value.RowId; if (rowId2 - 6 <= 1) { continue; } string text = FixName(item2.Name.ToDalamudString().ToString(), dataManager.Language); if (!string.IsNullOrWhiteSpace(text) && !IsExcludedContent(text, item2.RowId)) { List value2; ImmutableList unlockQuests = (dictionary2.TryGetValue(item2.RowId, out value2) ? value2.ToImmutableList() : ImmutableList.Empty); uint rowId3 = item2.ContentMemberType.RowId; EDutyCategory? eDutyCategory = DetermineDutyCategory(rowId2, text, item2.HighEndDuty, item2.RowId, (byte)rowId3, item2.ContentLinkType); if (eDutyCategory.HasValue) { DutyInfo value3 = new DutyInfo(item2.RowId, item2.Content.RowId, text, rowId2, valueNullable.Value.Name.ToDalamudString().ToString(), item2.ClassJobLevelRequired, item2.ItemLevelRequired, unlockQuests, item2.HighEndDuty, eDutyCategory.Value); dictionary3[item2.RowId] = value3; } } } _duties = dictionary3.ToImmutableDictionary(); _logger.LogInformation("Loaded {DutyCount} duties with {QuestMappings} quest unlock mappings", _duties.Count, _questToDuties.Values.Sum((ImmutableList x) => x.Count)); } private static bool IsExcludedContent(string name, uint cfcId) { if (cfcId - 69 <= 2) { return true; } return false; } private static EDutyCategory? DetermineDutyCategory(uint contentTypeId, string name, bool isHighEndDuty, uint cfcId, byte contentMemberType, byte contentLinkType) { if (name.EndsWith("(Chaotic)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.ChaoticAllianceRaid; } switch (contentTypeId) { case 2u: if (name.EndsWith("(Hard)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.HardDungeon; } return EDutyCategory.Dungeon; case 3u: return EDutyCategory.Guildhest; case 4u: if (name.EndsWith("(Unreal)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.UnrealTrial; } if (name.EndsWith("(Extreme)", StringComparison.OrdinalIgnoreCase) || name.StartsWith("The Minstrel's Ballad:", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) { return EDutyCategory.ExtremeTrial; } if (name.EndsWith("(Hard)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.HardTrial; } return EDutyCategory.Trial; case 5u: if (contentMemberType == 4) { if (isHighEndDuty) { return EDutyCategory.ChaoticAllianceRaid; } return EDutyCategory.AllianceRaid; } if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase) || isHighEndDuty) { return EDutyCategory.SavageRaid; } return EDutyCategory.NormalRaid; case 21u: if (name.Contains("Quantum", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.QuantumBattle; } return EDutyCategory.DeepDungeon; case 26u: if (isHighEndDuty) { return EDutyCategory.ChaoticAllianceRaid; } return EDutyCategory.AllianceRaid; case 28u: return EDutyCategory.Ultimate; case 29u: case 30u: if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.CriterionSavage; } if (name.StartsWith("Another", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.CriterionDungeon; } return EDutyCategory.VariantDungeon; case 31u: return EDutyCategory.CriterionSavage; default: if (name.EndsWith("(Savage)", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.SavageRaid; } if (name.EndsWith("(Extreme)", StringComparison.OrdinalIgnoreCase) || name.StartsWith("The Minstrel's Ballad:", StringComparison.OrdinalIgnoreCase)) { return EDutyCategory.ExtremeTrial; } return null; } } private static string FixName(string name, ClientLanguage language) { if (string.IsNullOrEmpty(name) || language != ClientLanguage.English) { return name; } return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture).AsSpan(), name.AsSpan(1)); } public IEnumerable GetAllDuties() { return _duties.Values; } public IEnumerable GetDutiesByContentType(uint contentTypeId) { return _duties.Values.Where((DutyInfo d) => d.ContentTypeId == contentTypeId); } public IEnumerable GetDutiesByCategory(EDutyCategory category) { return _duties.Values.Where((DutyInfo d) => d.DutyCategory == category); } public IEnumerable GetDungeons() { return GetDutiesByCategory(EDutyCategory.Dungeon); } public IEnumerable GetHardDungeons() { return GetDutiesByCategory(EDutyCategory.HardDungeon); } public IEnumerable GetGuildhests() { return GetDutiesByCategory(EDutyCategory.Guildhest); } public IEnumerable GetTrials() { return GetDutiesByCategory(EDutyCategory.Trial); } public IEnumerable GetHardTrials() { return GetDutiesByCategory(EDutyCategory.HardTrial); } public IEnumerable GetExtremeTrials() { return GetDutiesByCategory(EDutyCategory.ExtremeTrial); } public IEnumerable GetUnrealTrials() { return GetDutiesByCategory(EDutyCategory.UnrealTrial); } public IEnumerable GetNormalRaids() { return GetDutiesByCategory(EDutyCategory.NormalRaid); } public IEnumerable GetSavageRaids() { return GetDutiesByCategory(EDutyCategory.SavageRaid); } public IEnumerable GetUltimateRaids() { return GetDutiesByCategory(EDutyCategory.Ultimate); } public IEnumerable GetAllianceRaids() { return GetDutiesByCategory(EDutyCategory.AllianceRaid); } public IEnumerable GetChaoticAllianceRaids() { return GetDutiesByCategory(EDutyCategory.ChaoticAllianceRaid); } public IEnumerable GetDeepDungeons() { return GetDutiesByCategory(EDutyCategory.DeepDungeon); } public IEnumerable GetVariantDungeons() { return GetDutiesByCategory(EDutyCategory.VariantDungeon); } public IEnumerable GetCriterionDungeons() { return GetDutiesByCategory(EDutyCategory.CriterionDungeon); } public IEnumerable GetCriterionSavageDungeons() { return GetDutiesByCategory(EDutyCategory.CriterionSavage); } public IEnumerable GetQuantumBattles() { return GetDutiesByCategory(EDutyCategory.QuantumBattle); } public IEnumerable GetDutiesUnlockedByQuest(QuestId questId) { if (!_questToDuties.TryGetValue(questId, out ImmutableList value)) { yield break; } foreach (uint item in value) { if (_duties.TryGetValue(item, out DutyInfo value2)) { yield return value2; } } } public IEnumerable GetUnlockQuestsForDuty(uint cfcId) { if (_dutyToQuests.TryGetValue(cfcId, out ImmutableList value)) { return value; } return Array.Empty(); } public bool IsDutyUnlocked(uint cfcId) { if (!_duties.TryGetValue(cfcId, out DutyInfo value)) { return false; } return UIState.IsInstanceContentUnlocked(value.ContentId); } public static bool IsContentUnlocked(uint contentId) { return UIState.IsInstanceContentUnlocked(contentId); } public bool TryGetDuty(uint cfcId, [NotNullWhen(true)] out DutyInfo? duty) { return _duties.TryGetValue(cfcId, out duty); } public DutyInfo? GetDuty(uint cfcId) { return _duties.GetValueOrDefault(cfcId); } public IEnumerable GetUnlockedDuties() { return _duties.Values.Where((DutyInfo d) => UIState.IsInstanceContentUnlocked(d.ContentId)); } public IEnumerable GetLockedDuties() { return _duties.Values.Where((DutyInfo d) => !UIState.IsInstanceContentUnlocked(d.ContentId)); } public IEnumerable GetDutiesWithUnlockQuests() { return _duties.Values.Where((DutyInfo d) => d.UnlockQuests.Count > 0); } }