diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 4c32873c6..45c1c84ab 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -16,13 +16,12 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Event; -using FFXIVClientStructs.FFXIV.Client.Game.UI; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.Sheets; using Action = System.Action; +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; namespace Dalamud.Game.ClientState; @@ -37,7 +36,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; - private readonly Hook uiModuleHandlePacketHook; private readonly Hook onLogoutHook; [ServiceManager.ServiceDependency] @@ -63,14 +61,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}"); this.setupTerritoryTypeHook = Hook.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour); - this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.onLogoutHook = Hook.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour); this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.setupTerritoryTypeHook.Enable(); - this.uiModuleHandlePacketHook.Enable(); this.onLogoutHook.Enable(); } @@ -80,10 +76,10 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public event Action? TerritoryChanged; /// - public event IClientState.ClassJobChangeDelegate? ClassJobChanged; + public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged; /// - public event IClientState.LevelChangeDelegate? LevelChanged; + public event IPlayerState.LevelChangeDelegate? LevelChanged; /// public event Action? Login; @@ -120,7 +116,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public IPlayerCharacter? LocalPlayer => Service.GetNullable()?[0] as IPlayerCharacter; /// - public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId; + public ulong LocalContentId => 0; /// public unsafe bool IsLoggedIn @@ -173,7 +169,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState void IInternalDisposableService.DisposeService() { this.setupTerritoryTypeHook.Dispose(); - this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent; @@ -211,54 +206,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.setupTerritoryTypeHook.Original(eventFramework, territoryType); } - private unsafe void UIModuleHandlePacketDetour( - UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) - { - this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet); - - switch (type) - { - case UIModulePacketType.ClassJobChange: - { - var classJobId = uintParam; - - foreach (var action in Delegate.EnumerateInvocationList(this.ClassJobChanged)) - { - try - { - action(classJobId); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } - } - - break; - } - - case UIModulePacketType.LevelChange: - { - var classJobId = *(uint*)packet; - var level = *(ushort*)((nint)packet + 4); - - foreach (var action in Delegate.EnumerateInvocationList(this.LevelChanged)) - { - try - { - action(classJobId, level); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } - } - - break; - } - } - } - private void FrameworkOnOnUpdateEvent(IFramework framework1) { var condition = Service.GetNullable(); @@ -356,10 +303,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat public event Action? TerritoryChanged; /// - public event IClientState.ClassJobChangeDelegate? ClassJobChanged; + public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged; /// - public event IClientState.LevelChangeDelegate? LevelChanged; + public event IPlayerState.LevelChangeDelegate? LevelChanged; /// public event Action? Login; diff --git a/Dalamud/Game/ItemActionType.cs b/Dalamud/Game/ItemActionType.cs new file mode 100644 index 000000000..3f2ac5f17 --- /dev/null +++ b/Dalamud/Game/ItemActionType.cs @@ -0,0 +1,76 @@ +using Lumina.Excel.Sheets; + +namespace Dalamud.Game; + +/// +/// Enum for . +/// +public enum ItemActionType : ushort +{ + /// + /// Used to unlock a companion (minion). + /// + Companion = 853, + + /// + /// Used to unlock a chocobo companion barding. + /// + BuddyEquip = 1013, + + /// + /// Used to unlock a mount. + /// + Mount = 1322, + + /// + /// Used to unlock recipes from a crafting recipe book. + /// + SecretRecipeBook = 2136, + + /// + /// Used to unlock various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles). + /// + UnlockLink = 2633, + + /// + /// Used to unlock a Triple Triad Card. + /// + TripleTriadCard = 3357, + + /// + /// Used to unlock gathering nodes of a Folklore Tome. + /// + FolkloreTome = 4107, + + /// + /// Used to unlock an Orchestrion Roll. + /// + OrchestrionRoll = 25183, + + /// + /// Used to unlock portrait designs. + /// + FramersKit = 29459, + + /// + /// Used to unlock Bozjan Field Notes. These are server-side but are cached client-side. + /// + FieldNotes = 19743, + + /// + /// Used to unlock an Ornament (fashion accessory). + /// + Ornament = 20086, + + /// + /// Used to unlock glasses. + /// + Glasses = 37312, + + /// + /// Used for Company Seal Vouchers, which convert the item into Company Seals when used.
+ /// Can be used only if in a Grand Company.
+ /// IsUnlocked always returns false. + ///
+ CompanySealVouchers = 41120, +} diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 7d6304655..ed9070fb8 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -18,13 +18,14 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; using Lumina.Excel.Sheets; using Serilog; +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; + namespace Dalamud.Game.Network.Internal; /// @@ -269,12 +270,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private static (ulong UploaderId, uint WorldId) GetUploaderInfo() { + // TODO: switch to use Dalamuds PlayerState service + var agentLobby = AgentLobby.Instance(); var uploaderId = agentLobby->LobbyData.ContentId; if (uploaderId == 0) { - var playerState = PlayerState.Instance(); + var playerState = CSPlayerState.Instance(); if (playerState->IsLoaded == 1) { uploaderId = playerState->ContentId; diff --git a/Dalamud/Game/PlayerState/MentorVersion.cs b/Dalamud/Game/PlayerState/MentorVersion.cs new file mode 100644 index 000000000..701eda112 --- /dev/null +++ b/Dalamud/Game/PlayerState/MentorVersion.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Specifies the mentor certification version for a player. +/// +public enum MentorVersion : byte +{ + /// + /// Indicates that the player has never held mentor status in any expansion. + /// + None = 0, + + /// + /// Indicates that the player was last a mentor during the Shadowbringers expansion. + /// + Shadowbringers = 1, + + /// + /// Indicates that the player was last a mentor during the Endwalker expansion. + /// + Endwalker = 2, + + /// + /// Indicates that the player was last a mentor during the Dawntrail expansion. + /// + Dawntrail = 3, +} diff --git a/Dalamud/Game/PlayerState/PlayerAttribute.cs b/Dalamud/Game/PlayerState/PlayerAttribute.cs new file mode 100644 index 000000000..4db8af107 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerAttribute.cs @@ -0,0 +1,489 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Represents a player's attribute. +/// +public enum PlayerAttribute +{ + /// + /// Strength. + /// + /// + /// Affects physical damage dealt by gladiator's arms, marauder's arms, dark knight's arms, gunbreaker's arms, pugilist's arms, lancer's arms, samurai's arms, reaper's arms, thaumaturge's arms, arcanist's arms, red mage's arms, pictomancer's arms, conjurer's arms, astrologian's arms, sage's arms, and blue mage's arms. + /// + Strength = 1, + + /// + /// Dexterity. + /// + /// + /// Affects physical damage dealt by rogue's arms, viper's arms, archer's arms, machinist's arms, and dancer's arms. + /// + Dexterity = 2, + + /// + /// Vitality. + /// + /// + /// Affects maximum HP. + /// + Vitality = 3, + + /// + /// Intelligence. + /// + /// + /// Affects attack magic potency when role is DPS. + /// + Intelligence = 4, + + /// + /// Mind. + /// + /// + /// Affects healing magic potency. Also affects attack magic potency when role is Healer. + /// + Mind = 5, + + /// + /// Piety. + /// + /// + /// Affects MP regeneration. Regeneration rate is determined by piety. Only applicable when in battle and role is Healer. + /// + Piety = 6, + + /// + /// Health Points. + /// + HP = 7, + + /// + /// Mana Points. + /// + MP = 8, + + /// + /// Tactical Points. + /// + TP = 9, + + /// + /// Gathering Point. + /// + GP = 10, + + /// + /// Crafting Points. + /// + CP = 11, + + /// + /// Physical Damage. + /// + PhysicalDamage = 12, + + /// + /// Magic Damage. + /// + MagicDamage = 13, + + /// + /// Delay. + /// + Delay = 14, + + /// + /// Additional Effect. + /// + AdditionalEffect = 15, + + /// + /// Attack Speed. + /// + AttackSpeed = 16, + + /// + /// Block Rate. + /// + BlockRate = 17, + + /// + /// Block Strength. + /// + BlockStrength = 18, + + /// + /// Tenacity. + /// + /// + /// Affects the amount of physical and magic damage dealt and received, as well as HP restored. The higher the value, the more damage dealt, the more HP restored, and the less damage taken. Only applicable when role is Tank. + /// + Tenacity = 19, + + /// + /// Attack Power. + /// + /// + /// Affects amount of damage dealt by physical attacks. The higher the value, the more damage dealt. + /// + AttackPower = 20, + + /// + /// Defense. + /// + /// + /// Affects the amount of damage taken by physical attacks. The higher the value, the less damage taken. + /// + Defense = 21, + + /// + /// Direct Hit Rate. + /// + /// + /// Affects the rate at which your physical and magic attacks land direct hits, dealing slightly more damage than normal hits. The higher the value, the higher the frequency with which your hits will be direct. Higher values will also result in greater damage for actions which guarantee direct hits. + /// + DirectHitRate = 22, + + /// + /// Evasion. + /// + Evasion = 23, + + /// + /// Magic Defense. + /// + /// + /// Affects the amount of damage taken by magic attacks. The higher the value, the less damage taken. + /// + MagicDefense = 24, + + /// + /// Critical Hit Power. + /// + CriticalHitPower = 25, + + /// + /// Critical Hit Resilience. + /// + CriticalHitResilience = 26, + + /// + /// Critical Hit. + /// + /// + /// Affects the amount of physical and magic damage dealt, as well as HP restored. The higher the value, the higher the frequency with which your hits will be critical/higher the potency of critical hits. + /// + CriticalHit = 27, + + /// + /// Critical Hit Evasion. + /// + CriticalHitEvasion = 28, + + /// + /// Slashing Resistance. + /// + /// + /// Decreases damage done by slashing attacks. + /// + SlashingResistance = 29, + + /// + /// Piercing Resistance. + /// + /// + /// Decreases damage done by piercing attacks. + /// + PiercingResistance = 30, + + /// + /// Blunt Resistance. + /// + /// + /// Decreases damage done by blunt attacks. + /// + BluntResistance = 31, + + /// + /// Projectile Resistance. + /// + ProjectileResistance = 32, + + /// + /// Attack Magic Potency. + /// + /// + /// Affects the amount of damage dealt by magic attacks. + /// + AttackMagicPotency = 33, + + /// + /// Healing Magic Potency. + /// + /// + /// Affects the amount of HP restored via healing magic. + /// + HealingMagicPotency = 34, + + /// + /// Enhancement Magic Potency. + /// + EnhancementMagicPotency = 35, + + /// + /// Elemental Bonus. + /// + ElementalBonus = 36, + + /// + /// Fire Resistance. + /// + /// + /// Decreases fire-aspected damage. + /// + FireResistance = 37, + + /// + /// Ice Resistance. + /// + /// + /// Decreases ice-aspected damage. + /// + IceResistance = 38, + + /// + /// Wind Resistance. + /// + /// + /// Decreases wind-aspected damage. + /// + WindResistance = 39, + + /// + /// Earth Resistance. + /// + /// + /// Decreases earth-aspected damage. + /// + EarthResistance = 40, + + /// + /// Lightning Resistance. + /// + /// + /// Decreases lightning-aspected damage. + /// + LightningResistance = 41, + + /// + /// Water Resistance. + /// + /// + /// Decreases water-aspected damage. + /// + WaterResistance = 42, + + /// + /// Magic Resistance. + /// + MagicResistance = 43, + + /// + /// Determination. + /// + /// + /// Affects the amount of damage dealt by both physical and magic attacks, as well as the amount of HP restored by healing spells. + /// + Determination = 44, + + /// + /// Skill Speed. + /// + /// + /// Affects both the casting and recast timers, as well as the damage over time potency for weaponskills and auto-attacks. The higher the value, the shorter the timers/higher the potency. + /// + SkillSpeed = 45, + + /// + /// Spell Speed. + /// + /// + /// Affects both the casting and recast timers for spells. The higher the value, the shorter the timers. Also affects a spell's damage over time or healing over time potency. + /// + SpellSpeed = 46, + + /// + /// Haste. + /// + Haste = 47, + + /// + /// Morale. + /// + /// + /// In PvP, replaces physical and magical defense in determining damage inflicted by other players. Also influences the amount of damage dealt to other players. + /// + Morale = 48, + + /// + /// Enmity. + /// + Enmity = 49, + + /// + /// Enmity Reduction. + /// + EnmityReduction = 50, + + /// + /// Desynthesis Skill Gain. + /// + DesynthesisSkillGain = 51, + + /// + /// EXP Bonus. + /// + EXPBonus = 52, + + /// + /// Regen. + /// + Regen = 53, + + /// + /// Special Attribute. + /// + SpecialAttribute = 54, + + /// + /// Main Attribute. + /// + MainAttribute = 55, + + /// + /// Secondary Attribute. + /// + SecondaryAttribute = 56, + + /// + /// Slow Resistance. + /// + /// + /// Shortens the duration of slow. + /// + SlowResistance = 57, + + /// + /// Petrification Resistance. + /// + PetrificationResistance = 58, + + /// + /// Paralysis Resistance. + /// + ParalysisResistance = 59, + + /// + /// Silence Resistance. + /// + /// + /// Shortens the duration of silence. + /// + SilenceResistance = 60, + + /// + /// Blind Resistance. + /// + /// + /// Shortens the duration of blind. + /// + BlindResistance = 61, + + /// + /// Poison Resistance. + /// + /// + /// Shortens the duration of poison. + /// + PoisonResistance = 62, + + /// + /// Stun Resistance. + /// + /// + /// Shortens the duration of stun. + /// + StunResistance = 63, + + /// + /// Sleep Resistance. + /// + /// + /// Shortens the duration of sleep. + /// + SleepResistance = 64, + + /// + /// Bind Resistance. + /// + /// + /// Shortens the duration of bind. + /// + BindResistance = 65, + + /// + /// Heavy Resistance. + /// + /// + /// Shortens the duration of heavy. + /// + HeavyResistance = 66, + + /// + /// Doom Resistance. + /// + DoomResistance = 67, + + /// + /// Reduced Durability Loss. + /// + ReducedDurabilityLoss = 68, + + /// + /// Increased Spiritbond Gain. + /// + IncreasedSpiritbondGain = 69, + + /// + /// Craftsmanship. + /// + /// + /// Affects the amount of progress achieved in a single synthesis step. + /// + Craftsmanship = 70, + + /// + /// Control. + /// + /// + /// Affects the amount of quality improved in a single synthesis step. + /// + Control = 71, + + /// + /// Gathering. + /// + /// + /// Affects the rate at which items are gathered. + /// + Gathering = 72, + + /// + /// Perception. + /// + /// + /// Affects item yield when gathering as a botanist or miner, and the size of fish when fishing or spearfishing. + /// + Perception = 73, +} diff --git a/Dalamud/Game/PlayerState/PlayerState.Unlocks.cs b/Dalamud/Game/PlayerState/PlayerState.Unlocks.cs new file mode 100644 index 000000000..2725fc894 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerState.Unlocks.cs @@ -0,0 +1,588 @@ +using Dalamud.Data; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Component.Exd; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; + +namespace Dalamud.Game.PlayerState; + +/// +/// This class represents the state of the players unlocks. +/// +[ServiceManager.EarlyLoadedService] +internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState +{ + /// + public bool IsActionUnlocked(Lumina.Excel.Sheets.Action row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); + } + + /// + public bool IsAetherCurrentUnlocked(AetherCurrent row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId); + } + + /// + public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.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 IsChocoboTaxiUnlocked(ChocoboTaxi 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 IsGeneralActionUnlocked(GeneralAction row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsGlassesUnlocked(Glasses row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId); + } + + /// + public bool IsHowToUnlocked(HowTo row) + { + if (!this.IsLoaded) + return false; + + return UIState.Instance()->IsHowToUnlocked(row.RowId); + } + + /// + public bool IsInstanceContentUnlocked(Lumina.Excel.Sheets.InstanceContent row) + { + if (!this.IsLoaded) + return false; + + return UIState.IsInstanceContentUnlocked(row.RowId); + } + + /// + public unsafe bool IsItemUnlocked(Item item) + { + if (item.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)item.ItemAction.Value.Type) + { + case ItemActionType.Companion: + return UIState.Instance()->IsCompanionUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.BuddyEquip: + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.Mount: + return CSPlayerState.Instance()->IsMountUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.SecretRecipeBook: + return CSPlayerState.Instance()->IsSecretRecipeBookUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.UnlockLink: + return UIState.Instance()->IsUnlockLinkUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.TripleTriadCard when item.AdditionalData.Is(): + return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)item.AdditionalData.RowId); + + case ItemActionType.FolkloreTome: + return CSPlayerState.Instance()->IsFolkloreBookUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.OrchestrionRoll when item.AdditionalData.Is(): + return CSPlayerState.Instance()->IsOrchestrionRollUnlocked(item.AdditionalData.RowId); + + case ItemActionType.FramersKit: + return CSPlayerState.Instance()->IsFramersKitUnlocked(item.AdditionalData.RowId); + + case ItemActionType.Ornament: + return CSPlayerState.Instance()->IsOrnamentUnlocked(item.ItemAction.Value.Data[0]); + + case ItemActionType.Glasses: + return CSPlayerState.Instance()->IsGlassesUnlocked((ushort)item.AdditionalData.RowId); + + case ItemActionType.CompanySealVouchers: + return false; + } + + var row = ExdModule.GetItemRowById(item.RowId); + return row != null && UIState.Instance()->IsItemActionUnlocked(row) == 1; + } + + /// + public bool IsMcGuffinUnlocked(McGuffin row) + { + return CSPlayerState.Instance()->IsMcGuffinUnlocked(row.RowId); + } + + /// + public bool IsMJILandmarkUnlocked(MJILandmark row) + { + return this.IsUnlockLinkUnlocked(row.UnlockLink); + } + + /// + public bool IsMountUnlocked(Mount row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.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 CSPlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId); + } + + /// + public bool IsOrnamentUnlocked(Ornament row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.Instance()->IsOrnamentUnlocked(row.RowId); + } + + /// + public bool IsPerformUnlocked(Perform row) + { + return this.IsUnlockLinkUnlocked((uint)row.UnlockLink); + } + + /// + public bool IsPublicContentUnlocked(PublicContent row) + { + if (!this.IsLoaded) + return false; + + return UIState.IsPublicContentUnlocked(row.RowId); + } + + /// + public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) + { + if (!this.IsLoaded) + return false; + + return CSPlayerState.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 item) + { + if (item.ItemAction.RowId == 0) + return false; + + return (ItemActionType)item.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; + } + + /// + 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 chocoboTaxiRow)) + return this.IsChocoboTaxiUnlocked(chocoboTaxiRow); + + 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 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 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 UpdateUnlocks(bool fireEvent) + { + if (!this.IsLoaded) + return; + + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); + + // Not implemented: + // - DescriptionPage: quite complex + // - QuestAcceptAdditionCondition: ignored + + // For some other day: + // - FishingSpot + // - Spearfishing + // - Adventure (Sightseeing) + // - Recipes + // - MinerFolkloreTome + // - BotanistFolkloreTome + // - FishingFolkloreTome + // - VVD or is that unlocked via quest? + // - VVDNotebookContents? + // - FramersKit (is that just an Item?) + // - ... more? + + // Probably not happening, because it requires fetching data from server: + // - Achievements + // - Titles + // - Bozjan Field Notes + } + + private void UpdateUnlocksForSheet(bool fireEvent = true) 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); + + if (fireEvent) + { + Log.Verbose("Unlock detected: {row}", $"{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); + } + } + } + } + } +} diff --git a/Dalamud/Game/PlayerState/PlayerState.Wrapper.cs b/Dalamud/Game/PlayerState/PlayerState.Wrapper.cs new file mode 100644 index 000000000..54b2b2d1b --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerState.Wrapper.cs @@ -0,0 +1,281 @@ +using Dalamud.Data; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; +using GrandCompany = Lumina.Excel.Sheets.GrandCompany; + +namespace Dalamud.Game.PlayerState; + +/// +/// This class contains the PlayerState wrappers. +/// +internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState +{ + /// + /// Gets a value indicating whether the local character is loaded. + /// + /// + /// This is equivalent with being logged in.
+ /// The actual GameObject will not immediately exist when this changes to true. + ///
+ public bool IsLoaded => CSPlayerState.Instance()->IsLoaded == 1; + + /// + /// Gets the name of the local character. + /// + public string CharacterName => this.IsLoaded ? CSPlayerState.Instance()->CharacterNameString : string.Empty; + + /// + /// Gets the entity ID of the local character. + /// + public uint EntityId => this.IsLoaded ? CSPlayerState.Instance()->EntityId : default; + + /// + /// Gets the content ID of the local character. + /// + public ulong ContentId => this.IsLoaded ? CSPlayerState.Instance()->ContentId : default; + + /// + /// Gets the World row for the local character's current world. + /// + public RowRef CurrentWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.CurrentWorldId) + : default; + } + } + + /// + /// Gets the World row for the local character's home world. + /// + public RowRef HomeWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.HomeWorldId) + : default; + } + } + + /// + /// Gets the sex of the local character. + /// + public Sex Sex => this.IsLoaded ? (Sex)CSPlayerState.Instance()->Sex : default; + + /// + /// Gets the Race row for the local character. + /// + public RowRef Race => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Race) : default; + + /// + /// Gets the Tribe row for the local character. + /// + public RowRef Tribe => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Tribe) : default; + + /// + /// Gets the ClassJob row for the local character's current class/job. + /// + public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default; + + /// + /// Gets the current class/job's level of the local character. + /// + public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; + + /// + /// Gets a value indicating whether the local character's level is synced. + /// + public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced == 1; + + /// + /// Gets the effective level of the local character. + /// + public short EffectiveLevel => this.IsLoaded ? (this.IsLevelSynced ? CSPlayerState.Instance()->SyncedLevel : CSPlayerState.Instance()->CurrentLevel) : default; + + /// + /// Gets the GuardianDeity row for the local character. + /// + public RowRef GuardianDeity => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GuardianDeity) : default; + + /// + /// Gets the birth month of the local character. + /// + public byte BirthMonth => this.IsLoaded ? CSPlayerState.Instance()->BirthMonth : default; + + /// + /// Gets the birth day of the local character. + /// + public byte BirthDay => this.IsLoaded ? CSPlayerState.Instance()->BirthDay : default; + + /// + /// Gets the ClassJob row for the local character's starting class. + /// + public RowRef FirstClass => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FirstClass) : default; + + /// + /// Gets the Town row for the local character's starting town. + /// + public RowRef StartTown => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->StartTown) : default; + + /// + /// Gets the base strength of the local character. + /// + public int BaseStrength => this.IsLoaded ? CSPlayerState.Instance()->BaseStrength : default; + + /// + /// Gets the base dexterity of the local character. + /// + public int BaseDexterity => this.IsLoaded ? CSPlayerState.Instance()->BaseDexterity : default; + + /// + /// Gets the base vitality of the local character. + /// + public int BaseVitality => this.IsLoaded ? CSPlayerState.Instance()->BaseVitality : default; + + /// + /// Gets the base intelligence of the local character. + /// + public int BaseIntelligence => this.IsLoaded ? CSPlayerState.Instance()->BaseIntelligence : default; + + /// + /// Gets the base mind of the local character. + /// + public int BaseMind => this.IsLoaded ? CSPlayerState.Instance()->BaseMind : default; + + /// + /// Gets the piety mind of the local character. + /// + public int BasePiety => this.IsLoaded ? CSPlayerState.Instance()->BasePiety : default; + + /// + /// Gets the GrandCompany row for the local character's current Grand Company affiliation. + /// + public RowRef GrandCompany => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GrandCompany) : default; + + /// + /// Gets the Aetheryte row for the local character's home aetheryte. + /// + public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; + + /// + /// Gets a span of Aetheryte rows for the local character's favourite aetherytes. + /// + public ReadOnlySpan> FavouriteAetherytes + { + get + { + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded != 1 || playerState->FavouriteAetheryteCount == 0) + return []; + + var count = playerState->FavouriteAetheryteCount; + var array = new RowRef[count]; + + for (var i = 0; i < count; i++) + array[i] = LuminaUtils.CreateRef(playerState->FavouriteAetherytes[i]); + + return array; + } + } + + /// + /// Gets the Aetheryte row for the local character's free aetheryte. + /// + public RowRef FreeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FreeAetheryteId) : default; + + /// + /// Gets the amount of received player commendations of the local character. + /// + public uint BaseRestedExperience => this.IsLoaded ? CSPlayerState.Instance()->BaseRestedExperience : default; + + /// + /// Gets the amount of received player commendations of the local character. + /// + public short PlayerCommendations => this.IsLoaded ? CSPlayerState.Instance()->PlayerCommendations : default; + + /// + /// Gets the Carrier Level of Delivery Moogle Quests of the local character. + /// + public byte DeliveryLevel => this.IsLoaded ? CSPlayerState.Instance()->DeliveryLevel : default; + + /// + /// Gets the mentor version of the local character. + /// + public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default; + + /// + /// Gets the value of an attribute of the local character. + /// + /// The attribute to check. + /// The value of the specific attribute. + public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default; + + /// + /// Gets the Grand Company rank of the local character. + /// + /// The Grand Company to check. + /// The Grand Company rank of the local character. + public byte GetGrandCompanyRank(GrandCompany grandCompany) + { + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded != 1) + return default; + + return grandCompany.RowId switch + { + 1 => playerState->GCRankMaelstrom, + 2 => playerState->GCRankTwinAdders, + 3 => playerState->GCRankImmortalFlames, + _ => default, + }; + } + + /// + /// Gets the level of the local character's class/job. + /// + /// The ClassJob row to check. + /// The level of the requested class/job. + public short GetClassJobLevel(ClassJob classJob) => this.IsLoaded ? CSPlayerState.Instance()->ClassJobLevels[classJob.ExpArrayIndex] : default; + + /// + /// Gets the experience of the local character's class/job. + /// + /// The ClassJob row to check. + /// The experience of the requested class/job. + public int GetClassJobExperience(ClassJob classJob) + { + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded != 1) + return default; + + return playerState->ClassJobExperience[classJob.ExpArrayIndex]; + } + + /// + /// Gets the desynthesis level of the local character's crafter job. + /// + /// The ClassJob row to check. + /// The desynthesis level of the requested crafter job. + public float GetDesynthesisLevel(ClassJob classJob) + { + if (classJob.ExpArrayIndex == -1) + return default; + + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded != 1) + return default; + + return playerState->DesynthesisLevels[classJob.DohDolJobIndex] / 100f; + } +} diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs new file mode 100644 index 000000000..ded2dffeb --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -0,0 +1,153 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Data; +using Dalamud.Hooking; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +namespace Dalamud.Game.PlayerState; + +/// +/// This class represents the state of the local player. +/// +internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState +{ + private static readonly ModuleLog Log = new("PlayerState"); + + private readonly PlayerStateAddressResolver address; + private readonly Hook uiModuleHandlePacketHook; + private readonly Hook performMateriaActionMigrationDelegateHook; + private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + + [ServiceManager.ServiceDependency] + private readonly DataManager dataManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + + [ServiceManager.ServiceConstructor] + private PlayerState(TargetSigScanner sigScanner) + { + this.address = new PlayerStateAddressResolver(); + this.address.Setup(sigScanner); + + this.clientState.Login += this.OnLogin; + this.clientState.Logout += this.OnLogout; + + this.uiModuleHandlePacketHook = Hook.FromAddress( + (nint)UIModule.StaticVirtualTablePointer->HandlePacket, + this.UIModuleHandlePacketDetour); + + this.performMateriaActionMigrationDelegateHook = Hook.FromAddress( + this.address.PerformMateriaActionMigration, + this.PerformMateriaActionMigrationDetour); + + this.uiModuleHandlePacketHook.Enable(); + this.performMateriaActionMigrationDelegateHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void PerformMateriaActionMigrationDelegate(RaptureHotbarModule* thisPtr); + + /// + public event IPlayerState.ClassJobChangeDelegate? ClassJobChange; + + /// + public event IPlayerState.LevelChangeDelegate? LevelChange; + + /// + public event IPlayerState.UnlockDelegate Unlock; + + /// + void IInternalDisposableService.DisposeService() + { + this.clientState.Login -= this.OnLogin; + this.clientState.Logout -= this.OnLogout; + this.uiModuleHandlePacketHook.Dispose(); + this.performMateriaActionMigrationDelegateHook.Dispose(); + } + + private void OnLogin() + { + try + { + this.UpdateUnlocks(false); + } + catch (Exception ex) + { + Log.Error(ex, "Error during initial unlock check"); + } + } + + private void OnLogout(int type, int code) + { + this.cachedUnlockedRowIds.Clear(); + } + + private unsafe void UIModuleHandlePacketDetour( + UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) + { + this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet); + + switch (type) + { + case UIModulePacketType.ClassJobChange: + { + var classJobId = uintParam; + + foreach (var action in Delegate.EnumerateInvocationList(this.ClassJobChange)) + { + try + { + action(classJobId); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); + } + } + + break; + } + + case UIModulePacketType.LevelChange: + { + var classJobId = *(uint*)packet; + var level = *(ushort*)((nint)packet + 4); + + foreach (var action in Delegate.EnumerateInvocationList(this.LevelChange)) + { + try + { + action(classJobId, level); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); + } + } + + break; + } + } + } + + private void PerformMateriaActionMigrationDetour(RaptureHotbarModule* thisPtr) + { + try + { + this.UpdateUnlocks(true); + } + catch (Exception ex) + { + Log.Error(ex, "Error during unlock check"); + } + + this.performMateriaActionMigrationDelegateHook.Original(thisPtr); + } +} diff --git a/Dalamud/Game/PlayerState/PlayerStateAddressResolver.cs b/Dalamud/Game/PlayerState/PlayerStateAddressResolver.cs new file mode 100644 index 000000000..4d6378a60 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerStateAddressResolver.cs @@ -0,0 +1,24 @@ +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Dalamud.Game.PlayerState; + +/// +/// Unlock state memory address resolver. +/// +internal class PlayerStateAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of a method which is called when has . + /// + public nint PerformMateriaActionMigration { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(ISigScanner sig) + { + // RaptureHotbarModule.PerformMateriaActionMigration + this.PerformMateriaActionMigration = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 8B 01"); + } +} diff --git a/Dalamud/Game/PlayerState/PlayerStatePluginScoped.cs b/Dalamud/Game/PlayerState/PlayerStatePluginScoped.cs new file mode 100644 index 000000000..28613ac19 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerStatePluginScoped.cs @@ -0,0 +1,335 @@ +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.PlayerState; + +/// +/// This class represents the state of the local player. +/// +[PluginInterface] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal unsafe class PlayerStatePluginScoped : IInternalDisposableService, IPlayerState +{ + [ServiceManager.ServiceDependency] + private readonly PlayerState playerStateService = Service.Get(); + + private readonly LocalPlugin plugin; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin. + internal PlayerStatePluginScoped(LocalPlugin plugin) + { + this.plugin = plugin; + this.playerStateService.ClassJobChange += this.OnClassJobChange; + this.playerStateService.LevelChange += this.OnLevelChange; + this.playerStateService.Unlock += this.OnUnlock; + } + + /// + public event IPlayerState.ClassJobChangeDelegate? ClassJobChange; + + /// + public event IPlayerState.LevelChangeDelegate? LevelChange; + + /// + public event IPlayerState.UnlockDelegate? Unlock; + + /// + public bool IsLoaded => this.playerStateService.IsLoaded; + + /// + public string CharacterName => this.playerStateService.CharacterName; + + /// + public uint EntityId => this.playerStateService.EntityId; + + /// + public ulong ContentId => this.playerStateService.ContentId; + + /// + public RowRef CurrentWorld => this.playerStateService.CurrentWorld; + + /// + public RowRef HomeWorld => this.playerStateService.HomeWorld; + + /// + public Sex Sex => this.playerStateService.Sex; + + /// + public RowRef Race => this.playerStateService.Race; + + /// + public RowRef Tribe => this.playerStateService.Tribe; + + /// + public RowRef ClassJob => this.playerStateService.ClassJob; + + /// + public short Level => this.playerStateService.Level; + + /// + public bool IsLevelSynced => this.playerStateService.IsLevelSynced; + + /// + public short EffectiveLevel => this.playerStateService.EffectiveLevel; + + /// + public RowRef GuardianDeity => this.playerStateService.GuardianDeity; + + /// + public byte BirthMonth => this.playerStateService.BirthMonth; + + /// + public byte BirthDay => this.playerStateService.BirthDay; + + /// + public RowRef FirstClass => this.playerStateService.FirstClass; + + /// + public RowRef StartTown => this.playerStateService.StartTown; + + /// + public int BaseStrength => this.playerStateService.BaseStrength; + + /// + public int BaseDexterity => this.playerStateService.BaseDexterity; + + /// + public int BaseVitality => this.playerStateService.BaseVitality; + + /// + public int BaseIntelligence => this.playerStateService.BaseIntelligence; + + /// + public int BaseMind => this.playerStateService.BaseMind; + + /// + public int BasePiety => this.playerStateService.BasePiety; + + /// + public RowRef GrandCompany => this.playerStateService.GrandCompany; + + /// + public RowRef HomeAetheryte => this.playerStateService.HomeAetheryte; + + /// + public ReadOnlySpan> FavouriteAetherytes => this.playerStateService.FavouriteAetherytes; + + /// + public RowRef FreeAetheryte => this.playerStateService.FreeAetheryte; + + /// + public uint BaseRestedExperience => this.playerStateService.BaseRestedExperience; + + /// + public short PlayerCommendations => this.playerStateService.PlayerCommendations; + + /// + public byte DeliveryLevel => this.playerStateService.DeliveryLevel; + + /// + public MentorVersion MentorVersion => this.playerStateService.MentorVersion; + + /// + public int GetAttribute(PlayerAttribute attribute) + => this.playerStateService.GetAttribute(attribute); + + /// + public int GetClassJobExperience(ClassJob classJob) + => this.playerStateService.GetClassJobExperience(classJob); + + /// + public short GetClassJobLevel(ClassJob classJob) + => this.playerStateService.GetClassJobLevel(classJob); + + /// + public float GetDesynthesisLevel(ClassJob classJob) + => this.playerStateService.GetDesynthesisLevel(classJob); + + /// + public byte GetGrandCompanyRank(GrandCompany grandCompany) + => this.playerStateService.GetGrandCompanyRank(grandCompany); + + /// + public bool IsActionUnlocked(Lumina.Excel.Sheets.Action row) + => this.playerStateService.IsActionUnlocked(row); + + /// + public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) + => this.playerStateService.IsAetherCurrentCompFlgSetUnlocked(row); + + /// + public bool IsAetherCurrentUnlocked(AetherCurrent row) + => this.playerStateService.IsAetherCurrentUnlocked(row); + + /// + public bool IsAozActionUnlocked(AozAction row) + => this.playerStateService.IsAozActionUnlocked(row); + + /// + public bool IsBannerBgUnlocked(BannerBg row) + => this.playerStateService.IsBannerBgUnlocked(row); + + /// + public bool IsBannerConditionUnlocked(BannerCondition row) + => this.playerStateService.IsBannerConditionUnlocked(row); + + /// + public bool IsBannerDecorationUnlocked(BannerDecoration row) + => this.playerStateService.IsBannerDecorationUnlocked(row); + + /// + public bool IsBannerFacialUnlocked(BannerFacial row) + => this.playerStateService.IsBannerFacialUnlocked(row); + + /// + public bool IsBannerFrameUnlocked(BannerFrame row) + => this.playerStateService.IsBannerFrameUnlocked(row); + + /// + public bool IsBannerTimelineUnlocked(BannerTimeline row) + => this.playerStateService.IsBannerTimelineUnlocked(row); + + /// + public bool IsBuddyActionUnlocked(BuddyAction row) + => this.playerStateService.IsBuddyActionUnlocked(row); + + /// + public bool IsBuddyEquipUnlocked(BuddyEquip row) + => this.playerStateService.IsBuddyEquipUnlocked(row); + + /// + public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) + => this.playerStateService.IsCharaMakeCustomizeUnlocked(row); + + /// + public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) + => this.playerStateService.IsChocoboTaxiUnlocked(row); + + /// + public bool IsCompanionUnlocked(Companion row) + => this.playerStateService.IsCompanionUnlocked(row); + + /// + public bool IsCraftActionUnlocked(CraftAction row) + => this.playerStateService.IsCraftActionUnlocked(row); + + /// + public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row) + => this.playerStateService.IsCSBonusContentTypeUnlocked(row); + + /// + public bool IsEmoteUnlocked(Emote row) + => this.playerStateService.IsEmoteUnlocked(row); + + /// + public bool IsGeneralActionUnlocked(GeneralAction row) + => this.playerStateService.IsGeneralActionUnlocked(row); + + /// + public bool IsGlassesUnlocked(Glasses row) + => this.playerStateService.IsGlassesUnlocked(row); + + /// + public bool IsHowToUnlocked(HowTo row) + => this.playerStateService.IsHowToUnlocked(row); + + /// + public bool IsInstanceContentUnlocked(InstanceContent row) + => this.playerStateService.IsInstanceContentUnlocked(row); + + /// + public bool IsItemUnlockable(Item item) + => this.playerStateService.IsItemUnlockable(item); + + /// + public bool IsItemUnlocked(Item item) + => this.playerStateService.IsItemUnlocked(item); + + /// + public bool IsMcGuffinUnlocked(McGuffin row) + => this.playerStateService.IsMcGuffinUnlocked(row); + + /// + public bool IsMJILandmarkUnlocked(MJILandmark row) + => this.playerStateService.IsMJILandmarkUnlocked(row); + + /// + public bool IsMountUnlocked(Mount row) + => this.playerStateService.IsMountUnlocked(row); + + /// + public bool IsNotebookDivisionUnlocked(NotebookDivision row) + => this.playerStateService.IsNotebookDivisionUnlocked(row); + + /// + public bool IsOrchestrionUnlocked(Orchestrion row) + => this.playerStateService.IsOrchestrionUnlocked(row); + + /// + public bool IsOrnamentUnlocked(Ornament row) + => this.playerStateService.IsOrnamentUnlocked(row); + + /// + public bool IsPerformUnlocked(Perform row) + => this.playerStateService.IsPerformUnlocked(row); + + /// + public bool IsPublicContentUnlocked(PublicContent row) + => this.playerStateService.IsPublicContentUnlocked(row); + + /// + public bool IsRowRefUnlocked(RowRef rowRef) + => this.playerStateService.IsRowRefUnlocked(rowRef); + + /// + public bool IsRowRefUnlocked(RowRef rowRef) where T : struct, IExcelRow + => this.playerStateService.IsRowRefUnlocked(rowRef); + + /// + public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) + => this.playerStateService.IsSecretRecipeBookUnlocked(row); + + /// + public bool IsTraitUnlocked(Trait row) + => this.playerStateService.IsTraitUnlocked(row); + + /// + public bool IsTripleTriadCardUnlocked(TripleTriadCard row) + => this.playerStateService.IsTripleTriadCardUnlocked(row); + + /// + public bool IsUnlockLinkUnlocked(uint unlockLink) + => this.playerStateService.IsUnlockLinkUnlocked(unlockLink); + + /// + public bool IsUnlockLinkUnlocked(ushort unlockLink) + => this.playerStateService.IsUnlockLinkUnlocked(unlockLink); + + /// + void IInternalDisposableService.DisposeService() + { + this.playerStateService.ClassJobChange -= this.OnClassJobChange; + this.playerStateService.LevelChange -= this.OnLevelChange; + this.playerStateService.Unlock -= this.OnUnlock; + } + + private void OnLevelChange(uint classJobId, uint level) + => this.LevelChange?.Invoke(classJobId, level); + + private void OnClassJobChange(uint classJobId) + => this.ClassJobChange?.Invoke(classJobId); + + private void OnUnlock(RowRef rowRef) + => this.Unlock?.Invoke(rowRef); +} diff --git a/Dalamud/Game/PlayerState/Sex.cs b/Dalamud/Game/PlayerState/Sex.cs new file mode 100644 index 000000000..e6ed6cc78 --- /dev/null +++ b/Dalamud/Game/PlayerState/Sex.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Game.PlayerState; + +/// +/// Represents the sex of a character. +/// +public enum Sex : byte +{ + /// + /// Male sex. + /// + Male = 0, + + /// + /// Female sex. + /// + Female = 1, +} diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 624b62253..fc34d8558 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -35,7 +35,7 @@ using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using AddonSheet = Lumina.Excel.Sheets.Addon; -using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; +using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; using StatusSheet = Lumina.Excel.Sheets.Status; namespace Dalamud.Game.Text.Evaluator; @@ -526,7 +526,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return false; // the game uses LocalPlayer here, but using PlayerState seems more safe. - return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse); + return this.ResolveStringExpression(in context, CSPlayerState.Instance()->EntityId == entityId ? eTrue : eFalse); } private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 60d8a17e2..7059e113f 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -9,19 +9,6 @@ namespace Dalamud.Plugin.Services; ///
public interface IClientState { - /// - /// A delegate type used for the event. - /// - /// The new ClassJob id. - public delegate void ClassJobChangeDelegate(uint classJobId); - - /// - /// A delegate type used for the event. - /// - /// The ClassJob id. - /// The level of the corresponding ClassJob. - public delegate void LevelChangeDelegate(uint classJobId, uint level); - /// /// A delegate type used for the event. /// @@ -37,31 +24,33 @@ public interface IClientState /// /// Event that fires when a characters ClassJob changed. /// - public event ClassJobChangeDelegate? ClassJobChanged; + [Obsolete("Use IPlayerState.ClassJobChange", true)] + public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged; /// /// Event that fires when any character level changes, including levels /// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL). /// - public event LevelChangeDelegate? LevelChanged; + [Obsolete("Use IPlayerState.LevelChange", true)] + public event IPlayerState.LevelChangeDelegate? LevelChanged; /// - /// Event that fires when a character is logging in, and the local character object is available. + /// Event that fires when the local player is logged in, and the local character object is available. /// public event Action Login; /// - /// Event that fires when a character is logging out. + /// Event that fires when the local player is logging out. /// public event LogoutDelegate Logout; /// - /// Event that fires when a character is entering PvP. + /// Event that fires when the local player is entering a PvP zone. /// public event Action EnterPvP; /// - /// Event that fires when a character is leaving PvP. + /// Event that fires when the local player is leaving a PvP zone. /// public event Action LeavePvP; @@ -93,6 +82,7 @@ public interface IClientState /// /// Gets the content ID of the local character. /// + [Obsolete("Use IPlayerState.ContentId", true)] public ulong LocalContentId { get; } /// @@ -101,12 +91,12 @@ public interface IClientState public bool IsLoggedIn { get; } /// - /// Gets a value indicating whether the user is playing PvP. + /// Gets a value indicating whether the user is in a PvP zone. /// public bool IsPvP { get; } /// - /// Gets a value indicating whether the user is playing PvP, excluding the Wolves' Den. + /// Gets a value indicating whether the user is in a PvP zone, excluding the Wolves' Den. /// public bool IsPvPExcludingDen { get; } diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs new file mode 100644 index 000000000..7c6fc13f0 --- /dev/null +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -0,0 +1,521 @@ +using Dalamud.Game.PlayerState; + +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. +/// +public interface IPlayerState +{ + /// + /// A delegate type used for the event. + /// + /// The new ClassJob id. + delegate void ClassJobChangeDelegate(uint classJobId); + + /// + /// A delegate type used for the event. + /// + /// The ClassJob id. + /// The level of the corresponding ClassJob. + delegate void LevelChangeDelegate(uint classJobId, uint level); + + /// + /// A delegate type used for the event. + /// + /// A RowRef of the unlocked thing. + delegate void UnlockDelegate(RowRef rowRef); + + /// + /// Event that fires when a characters ClassJob changed. + /// + event ClassJobChangeDelegate? ClassJobChange; + + /// + /// Event that fires when any character level changes, including levels + /// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL). + /// + event LevelChangeDelegate? LevelChange; + + /// + /// Event triggered when something was unlocked. + /// + event UnlockDelegate? Unlock; + + /// + /// Gets a value indicating whether the local character is loaded. + /// + /// + /// The actual GameObject will not immediately exist when this changes to true. + /// + bool IsLoaded { get; } + + /// + /// Gets the name of the local character. + /// + string CharacterName { get; } + + /// + /// Gets the entity ID of the local character. + /// + uint EntityId { get; } + + /// + /// Gets the content ID of the local character. + /// + ulong ContentId { get; } + + /// + /// Gets the World row for the local character's current world. + /// + RowRef CurrentWorld { get; } + + /// + /// Gets the World row for the local character's home world. + /// + RowRef HomeWorld { get; } + + /// + /// Gets the sex of the local character. + /// + Sex Sex { get; } + + /// + /// Gets the Race row for the local character. + /// + RowRef Race { get; } + + /// + /// Gets the Tribe row for the local character. + /// + RowRef Tribe { get; } + + /// + /// Gets the ClassJob row for the local character's current class/job. + /// + RowRef ClassJob { get; } + + /// + /// Gets the current class/job's level of the local character. + /// + short Level { get; } + + /// + /// Gets a value indicating whether the local character's level is synced. + /// + bool IsLevelSynced { get; } + + /// + /// Gets the effective level of the local character. + /// + short EffectiveLevel { get; } + + /// + /// Gets the GuardianDeity row for the local character. + /// + RowRef GuardianDeity { get; } + + /// + /// Gets the birth month of the local character. + /// + byte BirthMonth { get; } + + /// + /// Gets the birth day of the local character. + /// + byte BirthDay { get; } + + /// + /// Gets the ClassJob row for the local character's starting class. + /// + RowRef FirstClass { get; } + + /// + /// Gets the Town row for the local character's starting town. + /// + RowRef StartTown { get; } + + /// + /// Gets the base strength of the local character. + /// + int BaseStrength { get; } + + /// + /// Gets the base dexterity of the local character. + /// + int BaseDexterity { get; } + + /// + /// Gets the base vitality of the local character. + /// + int BaseVitality { get; } + + /// + /// Gets the base intelligence of the local character. + /// + int BaseIntelligence { get; } + + /// + /// Gets the base mind of the local character. + /// + int BaseMind { get; } + + /// + /// Gets the piety mind of the local character. + /// + int BasePiety { get; } + + /// + /// Gets the GrandCompany row for the local character's current Grand Company affiliation. + /// + RowRef GrandCompany { get; } + + /// + /// Gets the Aetheryte row for the local character's home aetheryte. + /// + RowRef HomeAetheryte { get; } + + /// + /// Gets a span of Aetheryte rows for the local character's favourite aetherytes. + /// + ReadOnlySpan> FavouriteAetherytes { get; } + + /// + /// Gets the Aetheryte row for the local character's free aetheryte. + /// + RowRef FreeAetheryte { get; } + + /// + /// Gets the amount of received player commendations of the local character. + /// + uint BaseRestedExperience { get; } + + /// + /// Gets the amount of received player commendations of the local character. + /// + short PlayerCommendations { get; } + + /// + /// Gets the Carrier Level of Delivery Moogle Quests of the local character. + /// + byte DeliveryLevel { get; } + + /// + /// Gets the mentor version of the local character. + /// + MentorVersion MentorVersion { get; } + + /// + /// Gets the value of an attribute of the local character. + /// + /// The attribute to check. + /// The value of the specific attribute. + int GetAttribute(PlayerAttribute attribute); + + /// + /// Gets the Grand Company rank of the local character. + /// + /// The Grand Company to check. + /// The Grand Company rank of the local character. + byte GetGrandCompanyRank(GrandCompany grandCompany); + + /// + /// Gets the level of the local character's class/job. + /// + /// The ClassJob row to check. + /// The level of the requested class/job. + short GetClassJobLevel(ClassJob classJob); + + /// + /// Gets the experience of the local character's class/job. + /// + /// The ClassJob row to check. + /// The experience of the requested class/job. + int GetClassJobExperience(ClassJob classJob); + + /// + /// Gets the desynthesis level of the local character's crafter job. + /// + /// The ClassJob row to check. + /// The desynthesis level of the requested crafter job. + float GetDesynthesisLevel(ClassJob classJob); + + /// + /// 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 ChocoboTaxi (Chocobokeeps of the Chocobo Porter service) is unlocked. + /// + /// The ChocoboTaxi row to check. + /// if unlocked; otherwise, . + bool IsChocoboTaxiUnlocked(ChocoboTaxi 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 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 item); + + /// + /// Determines whether the specified Item is unlocked. + /// + /// The Item row to check. + /// if unlocked; otherwise, . + bool IsItemUnlocked(Item item); + + /// + /// 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 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 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); +}