diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionType.cs
new file mode 100644
index 000000000..8e3d79b84
--- /dev/null
+++ b/Dalamud/Game/UnlockState/ItemActionType.cs
@@ -0,0 +1,95 @@
+using Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.UnlockState;
+
+///
+/// Enum for .
+///
+internal enum ItemActionType : ushort
+{
+ ///
+ /// No item action.
+ ///
+ None = 0,
+
+ ///
+ /// Unlocks a companion (minion).
+ ///
+ Companion = 853,
+
+ ///
+ /// Unlocks a chocobo companion barding.
+ ///
+ BuddyEquip = 1013,
+
+ ///
+ /// Unlocks a mount.
+ ///
+ Mount = 1322,
+
+ ///
+ /// Unlocks recipes from a crafting recipe book.
+ ///
+ SecretRecipeBook = 2136,
+
+ ///
+ /// Unlocks various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles).
+ ///
+ UnlockLink = 2633,
+
+ ///
+ /// Unlocks a Triple Triad Card.
+ ///
+ TripleTriadCard = 3357,
+
+ ///
+ /// Unlocks gathering nodes of a Folklore Tome.
+ ///
+ FolkloreTome = 4107,
+
+ ///
+ /// Unlocks an Orchestrion Roll.
+ ///
+ OrchestrionRoll = 25183,
+
+ ///
+ /// Unlocks portrait designs.
+ ///
+ FramersKit = 29459,
+
+ ///
+ /// Unlocks Bozjan Field Notes.
+ ///
+ /// These are server-side but are cached client-side.
+ FieldNotes = 19743,
+
+ ///
+ /// Unlocks an Ornament (fashion accessory).
+ ///
+ Ornament = 20086,
+
+ ///
+ /// Unlocks Glasses.
+ ///
+ Glasses = 37312,
+
+ ///
+ /// Company Seal Vouchers, which convert the item into Company Seals when used.
+ ///
+ CompanySealVouchers = 41120,
+
+ ///
+ /// Unlocks Occult Records in Occult Crescent.
+ ///
+ OccultRecords = 43141,
+
+ ///
+ /// Unlocks Phantom Jobs in Occult Crescent.
+ ///
+ SoulShards = 43142,
+
+ ///
+ /// Star Contributor Certificate, which grants the Star Contributor status in Cosmic Exploration.
+ ///
+ StarContributorCertificate = 45189,
+}
diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs
new file mode 100644
index 000000000..c419ba4fd
--- /dev/null
+++ b/Dalamud/Game/UnlockState/RecipeData.cs
@@ -0,0 +1,283 @@
+using System.Linq;
+
+using CommunityToolkit.HighPerformance;
+
+using Dalamud.Data;
+using Dalamud.Game.Gui;
+
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.Interop;
+
+using Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.UnlockState;
+
+///
+/// Represents recipe-related data for all crafting classes.
+///
+[ServiceManager.EarlyLoadedService]
+internal unsafe class RecipeData : IInternalDisposableService
+{
+ [ServiceManager.ServiceDependency]
+ private readonly DataManager dataManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly ClientState.ClientState clientState = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameGui gameGui = Service.Get();
+
+ private readonly ushort[] craftTypeLevels;
+ private readonly byte[] unlockedNoteBookDivisionsCount;
+ private readonly byte[] unlockedSecretNoteBookDivisionsCount;
+ private readonly ushort[,] noteBookDivisionIds;
+ private byte[]? cachedUnlockedSecretRecipeBooks;
+ private byte[]? cachedUnlockLinks;
+ private byte[]? cachedCompletedQuests;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [ServiceManager.ServiceConstructor]
+ public RecipeData()
+ {
+ var numCraftTypes = this.dataManager.GetExcelSheet().Count();
+ var numSecretNotBookDivisions = this.dataManager.GetExcelSheet().Count(row => row.RowId is >= 1000 and < 2000);
+
+ this.unlockedNoteBookDivisionsCount = new byte[numCraftTypes];
+ this.unlockedSecretNoteBookDivisionsCount = new byte[numCraftTypes];
+ this.noteBookDivisionIds = new ushort[numCraftTypes, numSecretNotBookDivisions];
+
+ this.craftTypeLevels = new ushort[numCraftTypes];
+
+ this.clientState.Login += this.Update;
+ this.clientState.Logout += this.OnLogout;
+ this.clientState.LevelChanged += this.OnlevelChanged;
+ this.gameGui.AgentUpdate += this.OnAgentUpdate;
+ }
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.clientState.Login -= this.Update;
+ this.clientState.Logout -= this.OnLogout;
+ this.clientState.LevelChanged -= this.OnlevelChanged;
+ this.gameGui.AgentUpdate -= this.OnAgentUpdate;
+ }
+
+ ///
+ /// Determines whether the specified Recipe is unlocked.
+ ///
+ /// The Recipe row to check.
+ /// if unlocked; otherwise, .
+ public bool IsRecipeUnlocked(Recipe row)
+ {
+ // E8 ?? ?? ?? ?? 48 63 76 (2025.09.04)
+ var division = row.RecipeNotebookList.RowId != 0 && row.RecipeNotebookList.IsValid
+ ? (row.RecipeNotebookList.RowId - 1000) / 8 + 1000
+ : ((uint)row.RecipeLevelTable.Value.ClassJobLevel - 1) / 5;
+
+ // E8 ?? ?? ?? ?? 33 ED 84 C0 75 (2025.09.04)
+ foreach (var craftTypeRow in this.dataManager.GetExcelSheet())
+ {
+ var craftType = (byte)craftTypeRow.RowId;
+
+ if (division < this.unlockedNoteBookDivisionsCount[craftType])
+ return true;
+
+ if (this.unlockedNoteBookDivisionsCount[craftType] == 0)
+ continue;
+
+ if (division is 5000 or 5001)
+ return true;
+
+ if (division < 1000)
+ continue;
+
+ if (this.unlockedSecretNoteBookDivisionsCount[craftType] == 0)
+ continue;
+
+ if (this.noteBookDivisionIds.GetRowSpan(craftType).Contains((ushort)division))
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnLogout(int type, int code)
+ {
+ this.cachedUnlockedSecretRecipeBooks = null;
+ this.cachedUnlockLinks = null;
+ this.cachedCompletedQuests = null;
+ }
+
+ private void OnlevelChanged(uint classJobId, uint level)
+ {
+ if (this.dataManager.GetExcelSheet().TryGetRow(classJobId, out var classJobRow) &&
+ classJobRow.ClassJobCategory.RowId == 33) // Crafter
+ {
+ this.Update();
+ }
+ }
+
+ private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
+ {
+ if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
+ this.Update();
+ }
+
+ private void Update()
+ {
+ // based on Client::Game::UI::RecipeNote.InitializeStructs
+
+ if (!this.clientState.IsLoggedIn || !this.NeedsUpdate())
+ return;
+
+ Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length);
+ Array.Clear(this.unlockedSecretNoteBookDivisionsCount, 0, this.unlockedSecretNoteBookDivisionsCount.Length);
+ Array.Clear(this.noteBookDivisionIds, 0, this.noteBookDivisionIds.Length);
+
+ foreach (var craftTypeRow in this.dataManager.GetExcelSheet())
+ {
+ var craftType = (byte)craftTypeRow.RowId;
+ var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
+ if (craftTypeLevel == 0)
+ continue;
+
+ var noteBookDivisionIndex = -1;
+
+ foreach (var noteBookDivisionRow in this.dataManager.GetExcelSheet())
+ {
+ if (noteBookDivisionRow.RowId < 1000)
+ {
+ if (craftTypeLevel >= noteBookDivisionRow.CraftOpeningLevel)
+ this.unlockedNoteBookDivisionsCount[craftType]++;
+ }
+ else if (noteBookDivisionRow.RowId < 2000)
+ {
+ noteBookDivisionIndex++;
+
+ // For future Lumina.Excel update, replace with:
+ // if (!notebookDivisionRow.AllowedCraftTypes[craftType])
+ // continue;
+
+ switch (craftTypeRow.RowId)
+ {
+ case 0 when !noteBookDivisionRow.CRPCraft: continue;
+ case 1 when !noteBookDivisionRow.BSMCraft: continue;
+ case 2 when !noteBookDivisionRow.ARMCraft: continue;
+ case 3 when !noteBookDivisionRow.GSMCraft: continue;
+ case 4 when !noteBookDivisionRow.LTWCraft: continue;
+ case 5 when !noteBookDivisionRow.WVRCraft: continue;
+ case 6 when !noteBookDivisionRow.ALCCraft: continue;
+ case 7 when !noteBookDivisionRow.CULCraft: continue;
+ }
+
+ if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue)
+ continue;
+
+ // For future Lumina.Excel update, replace with:
+ // if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
+ if (noteBookDivisionRow.Unknown1)
+ {
+ var secretRecipeBookUnlocked = false;
+
+ // For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups
+ for (var i = 0; i < 2; i++)
+ {
+ // For future Lumina.Excel update, replace with:
+ // if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid)
+ // continue;
+ var secretRecipeBookGroupRowId = i switch
+ {
+ 0 => noteBookDivisionRow.Unknown2,
+ 1 => noteBookDivisionRow.Unknown2,
+ _ => default,
+ };
+
+ if (secretRecipeBookGroupRowId == 0)
+ continue;
+
+ if (!this.dataManager.GetExcelSheet().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow))
+ continue;
+
+ // For future Lumina.Excel update, replace with:
+ // var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType];
+
+ var bitIndex = craftType switch
+ {
+ 0 => secretRecipeBookGroupRow.Unknown0,
+ 1 => secretRecipeBookGroupRow.Unknown1,
+ 2 => secretRecipeBookGroupRow.Unknown2,
+ 3 => secretRecipeBookGroupRow.Unknown3,
+ 4 => secretRecipeBookGroupRow.Unknown4,
+ 5 => secretRecipeBookGroupRow.Unknown5,
+ 6 => secretRecipeBookGroupRow.Unknown6,
+ 7 => secretRecipeBookGroupRow.Unknown7,
+ _ => default,
+ };
+
+ if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex))
+ {
+ secretRecipeBookUnlocked = true;
+ break;
+ }
+ }
+
+ if (noteBookDivisionRow.CraftOpeningLevel > craftTypeLevel && !secretRecipeBookUnlocked)
+ continue;
+ }
+ else if (craftTypeLevel < noteBookDivisionRow.CraftOpeningLevel)
+ {
+ continue;
+ }
+ else if (noteBookDivisionRow.QuestUnlock.RowId != 0 && !UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(noteBookDivisionRow.QuestUnlock.RowId))
+ {
+ continue;
+ }
+
+ this.unlockedSecretNoteBookDivisionsCount[craftType]++;
+ this.noteBookDivisionIds[craftType, noteBookDivisionIndex] = (ushort)noteBookDivisionRow.RowId;
+ }
+ }
+ }
+ }
+
+ private bool NeedsUpdate()
+ {
+ var changed = false;
+
+ foreach (var craftTypeRow in this.dataManager.GetExcelSheet())
+ {
+ var craftType = (byte)craftTypeRow.RowId;
+ var craftTypeLevel = RecipeNote.Instance()->GetCraftTypeLevel(craftType);
+
+ if (this.craftTypeLevels[craftType] != craftTypeLevel)
+ {
+ this.craftTypeLevels[craftType] = craftTypeLevel;
+ changed |= true;
+ }
+ }
+
+ if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooks.SequenceEqual(this.cachedUnlockedSecretRecipeBooks))
+ {
+ this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooks.ToArray();
+ changed |= true;
+ }
+
+ if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinks.SequenceEqual(this.cachedUnlockLinks))
+ {
+ this.cachedUnlockLinks = UIState.Instance()->UnlockLinks.ToArray();
+ changed |= true;
+ }
+
+ if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuests.SequenceEqual(this.cachedCompletedQuests))
+ {
+ this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuests.ToArray();
+ changed |= true;
+ }
+
+ return changed;
+ }
+}
diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs
new file mode 100644
index 000000000..a4b9381cc
--- /dev/null
+++ b/Dalamud/Game/UnlockState/UnlockState.cs
@@ -0,0 +1,858 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+using Dalamud.Data;
+using Dalamud.Game.Gui;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+using FFXIVClientStructs.FFXIV.Component.Exd;
+
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+
+using ActionSheet = Lumina.Excel.Sheets.Action;
+using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
+using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
+
+namespace Dalamud.Game.UnlockState;
+
+#pragma warning disable UnlockState
+
+///
+/// This class provides unlock state of various content in the game.
+///
+[ServiceManager.EarlyLoadedService]
+internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
+{
+ private static readonly ModuleLog Log = new(nameof(UnlockState));
+
+ private readonly ConcurrentDictionary> cachedUnlockedRowIds = [];
+
+ [ServiceManager.ServiceDependency]
+ private readonly DataManager dataManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly ClientState.ClientState clientState = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameGui gameGui = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly RecipeData recipeData = Service.Get();
+
+ [ServiceManager.ServiceConstructor]
+ private UnlockState()
+ {
+ this.clientState.Login += this.OnLogin;
+ this.clientState.Logout += this.OnLogout;
+ this.gameGui.AgentUpdate += this.OnAgentUpdate;
+ }
+
+ ///
+ public event IUnlockState.UnlockDelegate Unlock;
+
+ private bool IsLoaded => PlayerState.Instance()->IsLoaded;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.clientState.Login -= this.OnLogin;
+ this.clientState.Logout -= this.OnLogout;
+ this.gameGui.AgentUpdate -= this.OnAgentUpdate;
+ }
+
+ ///
+ public bool IsActionUnlocked(ActionSheet row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
+ }
+
+ ///
+ public bool IsAetherCurrentUnlocked(AetherCurrent row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsAetherCurrentZoneComplete(row.RowId);
+ }
+
+ ///
+ public bool IsAozActionUnlocked(AozAction row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ if (row.RowId == 0 || !row.Action.IsValid)
+ return false;
+
+ return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(row.Action.Value.UnlockLink.RowId);
+ }
+
+ ///
+ public bool IsBannerBgUnlocked(BannerBg row)
+ {
+ return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
+ }
+
+ ///
+ public bool IsBannerConditionUnlocked(BannerCondition row)
+ {
+ if (row.RowId == 0)
+ return false;
+
+ if (!this.IsLoaded)
+ return false;
+
+ var rowPtr = ExdModule.GetBannerConditionByIndex(row.RowId);
+ if (rowPtr == null)
+ return false;
+
+ return ExdModule.GetBannerConditionUnlockState(rowPtr) == 0;
+ }
+
+ ///
+ public bool IsBannerDecorationUnlocked(BannerDecoration row)
+ {
+ return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
+ }
+
+ ///
+ public bool IsBannerFacialUnlocked(BannerFacial row)
+ {
+ return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
+ }
+
+ ///
+ public bool IsBannerFrameUnlocked(BannerFrame row)
+ {
+ return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
+ }
+
+ ///
+ public bool IsBannerTimelineUnlocked(BannerTimeline row)
+ {
+ return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
+ }
+
+ ///
+ public bool IsBuddyActionUnlocked(BuddyAction row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsBuddyEquipUnlocked(BuddyEquip row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row)
+ {
+ return row.IsPurchasable && this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsCompanionUnlocked(Companion row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.Instance()->IsCompanionUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsCraftActionUnlocked(CraftAction row)
+ {
+ return this.IsUnlockLinkUnlocked(row.QuestRequirement.RowId);
+ }
+
+ ///
+ public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsEmoteUnlocked(Emote row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row)
+ {
+ return this.IsUnlockLinkUnlocked(row.Unknown26);
+ }
+
+ ///
+ public bool IsEmjCostumeUnlocked(EmjCostume row)
+ {
+ return this.dataManager.GetExcelSheet().TryGetRow(row.RowId, out var emjVoiceNpcRow)
+ && this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow)
+ && QuestManager.IsQuestComplete(row.Unknown1);
+ }
+
+ ///
+ public bool IsGeneralActionUnlocked(GeneralAction row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsGlassesUnlocked(Glasses row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId);
+ }
+
+ ///
+ public bool IsHowToUnlocked(HowTo row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.Instance()->IsHowToUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsInstanceContentUnlocked(InstanceContentSheet row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.IsInstanceContentUnlocked(row.RowId);
+ }
+
+ ///
+ public unsafe bool IsItemUnlocked(Item row)
+ {
+ if (row.ItemAction.RowId == 0)
+ return false;
+
+ if (!this.IsLoaded)
+ return false;
+
+ // To avoid the ExdModule.GetItemRowById call, which can return null if the excel page
+ // is not loaded, we're going to imitate the IsItemActionUnlocked call first:
+ switch ((ItemActionType)row.ItemAction.Value.Type)
+ {
+ case ItemActionType.Companion:
+ return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.BuddyEquip:
+ return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.Mount:
+ return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.SecretRecipeBook:
+ return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.UnlockLink:
+ case ItemActionType.OccultRecords:
+ return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.TripleTriadCard when row.AdditionalData.Is():
+ return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId);
+
+ case ItemActionType.FolkloreTome:
+ return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.OrchestrionRoll when row.AdditionalData.Is():
+ return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId);
+
+ case ItemActionType.FramersKit:
+ return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId);
+
+ case ItemActionType.Ornament:
+ return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]);
+
+ case ItemActionType.Glasses:
+ return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId);
+
+ case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null:
+ var supportJobId = (byte)row.ItemAction.Value.Data[0];
+ return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0;
+
+ case ItemActionType.CompanySealVouchers:
+ return false;
+ }
+
+ var nativeRow = ExdModule.GetItemRowById(row.RowId);
+ return nativeRow != null && UIState.Instance()->IsItemActionUnlocked(nativeRow) == 1;
+ }
+
+ ///
+ public bool IsMcGuffinUnlocked(McGuffin row)
+ {
+ return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsMJILandmarkUnlocked(MJILandmark row)
+ {
+ return this.IsUnlockLinkUnlocked(row.UnlockLink);
+ }
+
+ ///
+ public bool IsMKDLoreUnlocked(MKDLore row)
+ {
+ return this.IsUnlockLinkUnlocked(row.Unknown2);
+ }
+
+ ///
+ public bool IsMountUnlocked(Mount row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsMountUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsNotebookDivisionUnlocked(NotebookDivision row)
+ {
+ return this.IsUnlockLinkUnlocked(row.QuestUnlock.RowId);
+ }
+
+ ///
+ public bool IsOrchestrionUnlocked(Orchestrion row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsOrnamentUnlocked(Ornament row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsOrnamentUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsPerformUnlocked(Perform row)
+ {
+ return this.IsUnlockLinkUnlocked((uint)row.UnlockLink);
+ }
+
+ ///
+ public bool IsPublicContentUnlocked(PublicContentSheet row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.IsPublicContentUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsRecipeUnlocked(Recipe row)
+ {
+ return this.recipeData.IsRecipeUnlocked(row);
+ }
+
+ ///
+ public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
+ }
+
+ ///
+ public bool IsTraitUnlocked(Trait row)
+ {
+ return this.IsUnlockLinkUnlocked(row.Quest.RowId);
+ }
+
+ ///
+ public bool IsTripleTriadCardUnlocked(TripleTriadCard row)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.RowId);
+ }
+
+ ///
+ public bool IsItemUnlockable(Item row)
+ {
+ if (row.ItemAction.RowId == 0)
+ return false;
+
+ return (ItemActionType)row.ItemAction.Value.Type
+ is ItemActionType.Companion
+ or ItemActionType.BuddyEquip
+ or ItemActionType.Mount
+ or ItemActionType.SecretRecipeBook
+ or ItemActionType.UnlockLink
+ or ItemActionType.TripleTriadCard
+ or ItemActionType.FolkloreTome
+ or ItemActionType.OrchestrionRoll
+ or ItemActionType.FramersKit
+ or ItemActionType.Ornament
+ or ItemActionType.Glasses
+ or ItemActionType.OccultRecords
+ or ItemActionType.SoulShards;
+ }
+
+ ///
+ public bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow
+ {
+ return this.IsRowRefUnlocked((RowRef)rowRef);
+ }
+
+ ///
+ public bool IsRowRefUnlocked(RowRef rowRef)
+ {
+ if (!this.IsLoaded || rowRef.IsUntyped)
+ return false;
+
+ if (rowRef.TryGetValue(out var actionRow))
+ return this.IsActionUnlocked(actionRow);
+
+ if (rowRef.TryGetValue(out var aetherCurrentRow))
+ return this.IsAetherCurrentUnlocked(aetherCurrentRow);
+
+ if (rowRef.TryGetValue(out var aetherCurrentCompFlgSetRow))
+ return this.IsAetherCurrentCompFlgSetUnlocked(aetherCurrentCompFlgSetRow);
+
+ if (rowRef.TryGetValue(out var aozActionRow))
+ return this.IsAozActionUnlocked(aozActionRow);
+
+ if (rowRef.TryGetValue(out var bannerBgRow))
+ return this.IsBannerBgUnlocked(bannerBgRow);
+
+ if (rowRef.TryGetValue(out var bannerConditionRow))
+ return this.IsBannerConditionUnlocked(bannerConditionRow);
+
+ if (rowRef.TryGetValue(out var bannerDecorationRow))
+ return this.IsBannerDecorationUnlocked(bannerDecorationRow);
+
+ if (rowRef.TryGetValue(out var bannerFacialRow))
+ return this.IsBannerFacialUnlocked(bannerFacialRow);
+
+ if (rowRef.TryGetValue(out var bannerFrameRow))
+ return this.IsBannerFrameUnlocked(bannerFrameRow);
+
+ if (rowRef.TryGetValue(out var bannerTimelineRow))
+ return this.IsBannerTimelineUnlocked(bannerTimelineRow);
+
+ if (rowRef.TryGetValue(out var buddyActionRow))
+ return this.IsBuddyActionUnlocked(buddyActionRow);
+
+ if (rowRef.TryGetValue(out var buddyEquipRow))
+ return this.IsBuddyEquipUnlocked(buddyEquipRow);
+
+ if (rowRef.TryGetValue(out var csBonusContentTypeRow))
+ return this.IsCSBonusContentTypeUnlocked(csBonusContentTypeRow);
+
+ if (rowRef.TryGetValue(out var charaMakeCustomizeRow))
+ return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow);
+
+ if (rowRef.TryGetValue(out var chocoboTaxiStandRow))
+ return this.IsChocoboTaxiStandUnlocked(chocoboTaxiStandRow);
+
+ if (rowRef.TryGetValue(out var companionRow))
+ return this.IsCompanionUnlocked(companionRow);
+
+ if (rowRef.TryGetValue(out var craftActionRow))
+ return this.IsCraftActionUnlocked(craftActionRow);
+
+ if (rowRef.TryGetValue(out var emoteRow))
+ return this.IsEmoteUnlocked(emoteRow);
+
+ if (rowRef.TryGetValue(out var generalActionRow))
+ return this.IsGeneralActionUnlocked(generalActionRow);
+
+ if (rowRef.TryGetValue(out var glassesRow))
+ return this.IsGlassesUnlocked(glassesRow);
+
+ if (rowRef.TryGetValue(out var howToRow))
+ return this.IsHowToUnlocked(howToRow);
+
+ if (rowRef.TryGetValue(out var instanceContentRow))
+ return this.IsInstanceContentUnlocked(instanceContentRow);
+
+ if (rowRef.TryGetValue- (out var itemRow))
+ return this.IsItemUnlocked(itemRow);
+
+ if (rowRef.TryGetValue(out var mjiLandmarkRow))
+ return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
+
+ if (rowRef.TryGetValue(out var mkdLoreRow))
+ return this.IsMKDLoreUnlocked(mkdLoreRow);
+
+ if (rowRef.TryGetValue(out var mcGuffinRow))
+ return this.IsMcGuffinUnlocked(mcGuffinRow);
+
+ if (rowRef.TryGetValue(out var mountRow))
+ return this.IsMountUnlocked(mountRow);
+
+ if (rowRef.TryGetValue(out var notebookDivisionRow))
+ return this.IsNotebookDivisionUnlocked(notebookDivisionRow);
+
+ if (rowRef.TryGetValue(out var orchestrionRow))
+ return this.IsOrchestrionUnlocked(orchestrionRow);
+
+ if (rowRef.TryGetValue(out var ornamentRow))
+ return this.IsOrnamentUnlocked(ornamentRow);
+
+ if (rowRef.TryGetValue(out var performRow))
+ return this.IsPerformUnlocked(performRow);
+
+ if (rowRef.TryGetValue(out var publicContentRow))
+ return this.IsPublicContentUnlocked(publicContentRow);
+
+ if (rowRef.TryGetValue(out var recipeRow))
+ return this.IsRecipeUnlocked(recipeRow);
+
+ if (rowRef.TryGetValue(out var secretRecipeBookRow))
+ return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
+
+ if (rowRef.TryGetValue(out var traitRow))
+ return this.IsTraitUnlocked(traitRow);
+
+ if (rowRef.TryGetValue(out var tripleTriadCardRow))
+ return this.IsTripleTriadCardUnlocked(tripleTriadCardRow);
+
+ return false;
+ }
+
+ ///
+ public bool IsUnlockLinkUnlocked(ushort unlockLink)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ if (unlockLink == 0)
+ return false;
+
+ return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink);
+ }
+
+ ///
+ public bool IsUnlockLinkUnlocked(uint unlockLink)
+ {
+ if (!this.IsLoaded)
+ return false;
+
+ if (unlockLink == 0)
+ return false;
+
+ return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink);
+ }
+
+ private void OnLogin()
+ {
+ this.Update();
+ }
+
+ private void OnLogout(int type, int code)
+ {
+ this.cachedUnlockedRowIds.Clear();
+ }
+
+ private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag)
+ {
+ if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate))
+ this.Update();
+ }
+
+ private void Update()
+ {
+ if (!this.IsLoaded)
+ return;
+
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet
- ();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+ this.UpdateUnlocksForSheet();
+
+ // Not implemented:
+ // - DescriptionPage: quite complex
+ // - QuestAcceptAdditionCondition: ignored
+
+ // For some other day:
+ // - FishingSpot
+ // - Spearfishing
+ // - Adventure (Sightseeing)
+ // - MinerFolkloreTome
+ // - BotanistFolkloreTome
+ // - FishingFolkloreTome
+ // - VVD or is that unlocked via quest?
+ // - VVDNotebookContents?
+ // - FramersKit (is that just an Item?)
+ // - ... more?
+
+ // Subrow sheets, which are incompatible with the current Unlock event, since RowRef doesn't carry the SubrowId:
+ // - EmjCostume
+
+ // Probably not happening, because it requires fetching data from server:
+ // - Achievements
+ // - Titles
+ // - Bozjan Field Notes
+ // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0
+ }
+
+ private void UpdateUnlocksForSheet() where T : struct, IExcelRow
+ {
+ var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []);
+
+ foreach (var row in this.dataManager.GetExcelSheet())
+ {
+ if (unlockedRowIds.Contains(row.RowId))
+ continue;
+
+ var rowRef = LuminaUtils.CreateRef(row.RowId);
+
+ if (!this.IsRowRefUnlocked(rowRef))
+ continue;
+
+ unlockedRowIds.Add(row.RowId);
+
+ Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}");
+
+ foreach (var action in Delegate.EnumerateInvocationList(this.Unlock))
+ {
+ try
+ {
+ action((RowRef)rowRef);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Exception during raise of {handler}", action.Method);
+ }
+ }
+ }
+ }
+}
+
+///
+/// Plugin-scoped version of a service.
+///
+[PluginInterface]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockState
+{
+ [ServiceManager.ServiceDependency]
+ private readonly UnlockState unlockStateService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal UnlockStatePluginScoped()
+ {
+ this.unlockStateService.Unlock += this.UnlockForward;
+ }
+
+ ///
+ public event IUnlockState.UnlockDelegate? Unlock;
+
+ ///
+ public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);
+
+ ///
+ public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row);
+
+ ///
+ public bool IsAetherCurrentUnlocked(AetherCurrent row) => this.unlockStateService.IsAetherCurrentUnlocked(row);
+
+ ///
+ public bool IsAozActionUnlocked(AozAction row) => this.unlockStateService.IsAozActionUnlocked(row);
+
+ ///
+ public bool IsBannerBgUnlocked(BannerBg row) => this.unlockStateService.IsBannerBgUnlocked(row);
+
+ ///
+ public bool IsBannerConditionUnlocked(BannerCondition row) => this.unlockStateService.IsBannerConditionUnlocked(row);
+
+ ///
+ public bool IsBannerDecorationUnlocked(BannerDecoration row) => this.unlockStateService.IsBannerDecorationUnlocked(row);
+
+ ///
+ public bool IsBannerFacialUnlocked(BannerFacial row) => this.unlockStateService.IsBannerFacialUnlocked(row);
+
+ ///
+ public bool IsBannerFrameUnlocked(BannerFrame row) => this.unlockStateService.IsBannerFrameUnlocked(row);
+
+ ///
+ public bool IsBannerTimelineUnlocked(BannerTimeline row) => this.unlockStateService.IsBannerTimelineUnlocked(row);
+
+ ///
+ public bool IsBuddyActionUnlocked(BuddyAction row) => this.unlockStateService.IsBuddyActionUnlocked(row);
+
+ ///
+ public bool IsBuddyEquipUnlocked(BuddyEquip row) => this.unlockStateService.IsBuddyEquipUnlocked(row);
+
+ ///
+ public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row);
+
+ ///
+ public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) => this.unlockStateService.IsChocoboTaxiStandUnlocked(row);
+
+ ///
+ public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row);
+
+ ///
+ public bool IsCraftActionUnlocked(CraftAction row) => this.unlockStateService.IsCraftActionUnlocked(row);
+
+ ///
+ public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) => this.unlockStateService.IsCSBonusContentTypeUnlocked(row);
+
+ ///
+ public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row);
+
+ ///
+ public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row);
+
+ ///
+ public bool IsEmjCostumeUnlocked(EmjCostume row) => this.unlockStateService.IsEmjCostumeUnlocked(row);
+
+ ///
+ public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row);
+
+ ///
+ public bool IsGlassesUnlocked(Glasses row) => this.unlockStateService.IsGlassesUnlocked(row);
+
+ ///
+ public bool IsHowToUnlocked(HowTo row) => this.unlockStateService.IsHowToUnlocked(row);
+
+ ///
+ public bool IsInstanceContentUnlocked(InstanceContentSheet row) => this.unlockStateService.IsInstanceContentUnlocked(row);
+
+ ///
+ public bool IsItemUnlockable(Item row) => this.unlockStateService.IsItemUnlockable(row);
+
+ ///
+ public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row);
+
+ ///
+ public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
+
+ ///
+ public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row);
+
+ ///
+ public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row);
+
+ ///
+ public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row);
+
+ ///
+ public bool IsNotebookDivisionUnlocked(NotebookDivision row) => this.unlockStateService.IsNotebookDivisionUnlocked(row);
+
+ ///
+ public bool IsOrchestrionUnlocked(Orchestrion row) => this.unlockStateService.IsOrchestrionUnlocked(row);
+
+ ///
+ public bool IsOrnamentUnlocked(Ornament row) => this.unlockStateService.IsOrnamentUnlocked(row);
+
+ ///
+ public bool IsPerformUnlocked(Perform row) => this.unlockStateService.IsPerformUnlocked(row);
+
+ ///
+ public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row);
+
+ ///
+ public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row);
+
+ ///
+ public bool IsRowRefUnlocked(RowRef rowRef) => this.unlockStateService.IsRowRefUnlocked(rowRef);
+
+ ///
+ public bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow => this.unlockStateService.IsRowRefUnlocked(rowRef);
+
+ ///
+ public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row);
+
+ ///
+ public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row);
+
+ ///
+ public bool IsTripleTriadCardUnlocked(TripleTriadCard row) => this.unlockStateService.IsTripleTriadCardUnlocked(row);
+
+ ///
+ public bool IsUnlockLinkUnlocked(uint unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
+
+ ///
+ public bool IsUnlockLinkUnlocked(ushort unlockLink) => this.unlockStateService.IsUnlockLinkUnlocked(unlockLink);
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.unlockStateService.Unlock -= this.UnlockForward;
+ }
+
+ private void UnlockForward(RowRef rowRef) => this.Unlock?.Invoke(rowRef);
+}
diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs
new file mode 100644
index 000000000..00f2df190
--- /dev/null
+++ b/Dalamud/Plugin/Services/IUnlockState.cs
@@ -0,0 +1,328 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+
+namespace Dalamud.Plugin.Services;
+
+#pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default
+
+///
+/// Interface for determining unlock state of various content in the game.
+///
+[Experimental("UnlockState")]
+public interface IUnlockState
+{
+ ///
+ /// A delegate type used for the event.
+ ///
+ /// A RowRef of the unlocked thing.
+ delegate void UnlockDelegate(RowRef rowRef);
+
+ ///
+ /// Event triggered when something was unlocked.
+ ///
+ event UnlockDelegate? Unlock;
+
+ ///
+ /// Determines whether the specified Action is unlocked.
+ ///
+ /// The Action row to check.
+ /// if unlocked; otherwise, .
+ bool IsActionUnlocked(Lumina.Excel.Sheets.Action row);
+
+ ///
+ /// Determines whether the specified AetherCurrentCompFlgSet is unlocked.
+ ///
+ /// The AetherCurrentCompFlgSet row to check.
+ /// if unlocked; otherwise, .
+ bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row);
+
+ ///
+ /// Determines whether the specified AetherCurrent is unlocked.
+ ///
+ /// The AetherCurrent row to check.
+ /// if unlocked; otherwise, .
+ bool IsAetherCurrentUnlocked(AetherCurrent row);
+
+ ///
+ /// Determines whether the specified AozAction (Blue Mage Action) is unlocked.
+ ///
+ /// The AozAction row to check.
+ /// if unlocked; otherwise, .
+ bool IsAozActionUnlocked(AozAction row);
+
+ ///
+ /// Determines whether the specified BannerBg (Portrait Backgrounds) is unlocked.
+ ///
+ /// The BannerBg row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerBgUnlocked(BannerBg row);
+
+ ///
+ /// Determines whether the specified BannerCondition is unlocked.
+ ///
+ /// The BannerCondition row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerConditionUnlocked(BannerCondition row);
+
+ ///
+ /// Determines whether the specified BannerDecoration (Portrait Accents) is unlocked.
+ ///
+ /// The BannerDecoration row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerDecorationUnlocked(BannerDecoration row);
+
+ ///
+ /// Determines whether the specified BannerFacial (Portrait Expressions) is unlocked.
+ ///
+ /// The BannerFacial row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerFacialUnlocked(BannerFacial row);
+
+ ///
+ /// Determines whether the specified BannerFrame (Portrait Frames) is unlocked.
+ ///
+ /// The BannerFrame row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerFrameUnlocked(BannerFrame row);
+
+ ///
+ /// Determines whether the specified BannerTimeline (Portrait Poses) is unlocked.
+ ///
+ /// The BannerTimeline row to check.
+ /// if unlocked; otherwise, .
+ bool IsBannerTimelineUnlocked(BannerTimeline row);
+
+ ///
+ /// Determines whether the specified BuddyAction (Action of the players Chocobo Companion) is unlocked.
+ ///
+ /// The BuddyAction row to check.
+ /// if unlocked; otherwise, .
+ bool IsBuddyActionUnlocked(BuddyAction row);
+
+ ///
+ /// Determines whether the specified BuddyEquip (Equipment of the players Chocobo Companion) is unlocked.
+ ///
+ /// The BuddyEquip row to check.
+ /// if unlocked; otherwise, .
+ bool IsBuddyEquipUnlocked(BuddyEquip row);
+
+ ///
+ /// Determines whether the specified CharaMakeCustomize (Hairstyles and Face Paint patterns) is unlocked.
+ ///
+ /// The CharaMakeCustomize row to check.
+ /// if unlocked; otherwise, .
+ bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row);
+
+ ///
+ /// Determines whether the specified ChocoboTaxiStand (Chocobokeeps of the Chocobo Porter service) is unlocked.
+ ///
+ /// The ChocoboTaxiStand row to check.
+ /// if unlocked; otherwise, .
+ bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row);
+
+ ///
+ /// Determines whether the specified Companion (Minions) is unlocked.
+ ///
+ /// The Companion row to check.
+ /// if unlocked; otherwise, .
+ bool IsCompanionUnlocked(Companion row);
+
+ ///
+ /// Determines whether the specified CraftAction is unlocked.
+ ///
+ /// The CraftAction row to check.
+ /// if unlocked; otherwise, .
+ bool IsCraftActionUnlocked(CraftAction row);
+
+ ///
+ /// Determines whether the specified CSBonusContentType is unlocked.
+ ///
+ /// The CSBonusContentType row to check.
+ /// if unlocked; otherwise, .
+ bool IsCSBonusContentTypeUnlocked(CSBonusContentType row);
+
+ ///
+ /// Determines whether the specified Emote is unlocked.
+ ///
+ /// The Emote row to check.
+ /// if unlocked; otherwise, .
+ bool IsEmoteUnlocked(Emote row);
+
+ ///
+ /// Determines whether the specified EmjVoiceNpc (Doman Mahjong Characters) is unlocked.
+ ///
+ /// The EmjVoiceNpc row to check.
+ /// if unlocked; otherwise, .
+ bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row);
+
+ ///
+ /// Determines whether the specified EmjCostume (Doman Mahjong Character Costume) is unlocked.
+ ///
+ /// The EmjCostume row to check.
+ /// if unlocked; otherwise, .
+ bool IsEmjCostumeUnlocked(EmjCostume row);
+
+ ///
+ /// Determines whether the specified GeneralAction is unlocked.
+ ///
+ /// The GeneralAction row to check.
+ /// if unlocked; otherwise, .
+ bool IsGeneralActionUnlocked(GeneralAction row);
+
+ ///
+ /// Determines whether the specified Glasses is unlocked.
+ ///
+ /// The Glasses row to check.
+ /// if unlocked; otherwise, .
+ bool IsGlassesUnlocked(Glasses row);
+
+ ///
+ /// Determines whether the specified HowTo is unlocked.
+ ///
+ /// The HowTo row to check.
+ /// if unlocked; otherwise, .
+ bool IsHowToUnlocked(HowTo row);
+
+ ///
+ /// Determines whether the specified InstanceContent is unlocked.
+ ///
+ /// The InstanceContent row to check.
+ /// if unlocked; otherwise, .
+ bool IsInstanceContentUnlocked(InstanceContent row);
+
+ ///
+ /// Determines whether the specified Item is considered unlockable.
+ ///
+ /// The Item row to check.
+ /// if unlockable; otherwise, .
+ bool IsItemUnlockable(Item row);
+
+ ///
+ /// Determines whether the specified Item is unlocked.
+ ///
+ /// The Item row to check.
+ /// if unlocked; otherwise, .
+ bool IsItemUnlocked(Item row);
+
+ ///
+ /// Determines whether the specified McGuffin is unlocked.
+ ///
+ /// The McGuffin row to check.
+ /// if unlocked; otherwise, .
+ bool IsMcGuffinUnlocked(McGuffin row);
+
+ ///
+ /// Determines whether the specified MJILandmark (Island Sanctuary landmark) is unlocked.
+ ///
+ /// The MJILandmark row to check.
+ /// if unlocked; otherwise, .
+ bool IsMJILandmarkUnlocked(MJILandmark row);
+
+ ///
+ /// Determines whether the specified MKDLore (Occult Record) is unlocked.
+ ///
+ /// The MKDLore row to check.
+ /// if unlocked; otherwise, .
+ bool IsMKDLoreUnlocked(MKDLore row);
+
+ ///
+ /// Determines whether the specified Mount is unlocked.
+ ///
+ /// The Mount row to check.
+ /// if unlocked; otherwise, .
+ bool IsMountUnlocked(Mount row);
+
+ ///
+ /// Determines whether the specified NotebookDivision (Categories in Crafting/Gathering Log) is unlocked.
+ ///
+ /// The NotebookDivision row to check.
+ /// if unlocked; otherwise, .
+ bool IsNotebookDivisionUnlocked(NotebookDivision row);
+
+ ///
+ /// Determines whether the specified Orchestrion roll is unlocked.
+ ///
+ /// The Orchestrion row to check.
+ /// if unlocked; otherwise, .
+ bool IsOrchestrionUnlocked(Orchestrion row);
+
+ ///
+ /// Determines whether the specified Ornament (Fashion Accessories) is unlocked.
+ ///
+ /// The Ornament row to check.
+ /// if unlocked; otherwise, .
+ bool IsOrnamentUnlocked(Ornament row);
+
+ ///
+ /// Determines whether the specified Perform (Performance Instruments) is unlocked.
+ ///
+ /// The Perform row to check.
+ /// if unlocked; otherwise, .
+ bool IsPerformUnlocked(Perform row);
+
+ ///
+ /// Determines whether the specified PublicContent is unlocked.
+ ///
+ /// The PublicContent row to check.
+ /// if unlocked; otherwise, .
+ bool IsPublicContentUnlocked(PublicContent row);
+
+ ///
+ /// Determines whether the specified Recipe is unlocked.
+ ///
+ /// The Recipe row to check.
+ /// if unlocked; otherwise, .
+ bool IsRecipeUnlocked(Recipe row);
+
+ ///
+ /// Determines whether the underlying RowRef type is unlocked.
+ ///
+ /// The RowRef to check.
+ /// if unlocked; otherwise, .
+ bool IsRowRefUnlocked(RowRef rowRef);
+
+ ///
+ /// Determines whether the underlying RowRef type is unlocked.
+ ///
+ /// The type of the Excel row.
+ /// The RowRef to check.
+ /// if unlocked; otherwise, .
+ bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow;
+
+ ///
+ /// Determines whether the specified SecretRecipeBook (Master Recipe Books) is unlocked.
+ ///
+ /// The SecretRecipeBook row to check.
+ /// if unlocked; otherwise, .
+ bool IsSecretRecipeBookUnlocked(SecretRecipeBook row);
+
+ ///
+ /// Determines whether the specified Trait is unlocked.
+ ///
+ /// The Trait row to check.
+ /// if unlocked; otherwise, .
+ bool IsTraitUnlocked(Trait row);
+
+ ///
+ /// Determines whether the specified TripleTriadCard is unlocked.
+ ///
+ /// The TripleTriadCard row to check.
+ /// if unlocked; otherwise, .
+ bool IsTripleTriadCardUnlocked(TripleTriadCard row);
+
+ ///
+ /// Determines whether the specified unlock link is unlocked or quest is completed.
+ ///
+ /// The unlock link id or quest id (quest ids in this case are over 65536).
+ /// if unlocked; otherwise, .
+ bool IsUnlockLinkUnlocked(uint unlockLink);
+
+ ///
+ /// Determines whether the specified unlock link is unlocked.
+ ///
+ /// The unlock link id.
+ /// if unlocked; otherwise, .
+ bool IsUnlockLinkUnlocked(ushort unlockLink);
+}