From 4422622e1e3c0c8e2ec5b0a6f2dc046afbbdc661 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 13:30:34 +0200 Subject: [PATCH 01/28] Add IPlayerState service --- Dalamud/Game/ChatHandlers.cs | 2 +- .../ClientState/Aetherytes/AetheryteList.cs | 9 +- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 +- Dalamud/Game/ClientState/ClientState.cs | 14 +- Dalamud/Game/ClientState/Fates/Fate.cs | 10 +- Dalamud/Game/ClientState/Fates/FateTable.cs | 17 +- .../Game/ClientState/Objects/ObjectTable.cs | 15 +- .../ClientState/Objects/Types/GameObject.cs | 12 +- Dalamud/Game/ClientState/Party/PartyList.cs | 12 +- .../Game/ClientState/Statuses/StatusList.cs | 18 +- .../Game/Network/Internal/NetworkHandlers.cs | 30 +- Dalamud/Game/PlayerState/MentorVersion.cs | 27 + Dalamud/Game/PlayerState/PlayerAttribute.cs | 489 ++++++++++++++++++ Dalamud/Game/PlayerState/PlayerState.cs | 205 ++++++++ Dalamud/Game/PlayerState/Sex.cs | 17 + .../Game/Text/Evaluator/SeStringEvaluator.cs | 6 +- .../Windows/Data/Widgets/GaugeWidget.cs | 6 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 12 +- .../Windows/Data/Widgets/PluginIpcWidget.cs | 8 +- .../Windows/Data/Widgets/TargetWidget.cs | 6 +- .../Steps/SeStringEvaluatorSelfTestStep.cs | 5 +- Dalamud/Plugin/Services/IClientState.cs | 2 + Dalamud/Plugin/Services/IObjectTable.cs | 8 +- Dalamud/Plugin/Services/IPlayerState.cs | 212 ++++++++ Dalamud/Utility/Util.cs | 7 +- 25 files changed, 1043 insertions(+), 112 deletions(-) create mode 100644 Dalamud/Game/PlayerState/MentorVersion.cs create mode 100644 Dalamud/Game/PlayerState/PlayerAttribute.cs create mode 100644 Dalamud/Game/PlayerState/PlayerState.cs create mode 100644 Dalamud/Game/PlayerState/Sex.cs create mode 100644 Dalamud/Plugin/Services/IPlayerState.cs diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index c57dd70b8..8237219bf 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -77,7 +77,7 @@ internal partial class ChatHandlers : IServiceType } // For injections while logged in - if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) + if (clientState.IsLoggedIn && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); #if !DEBUG && false diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a3d44d423..4a6d011e9 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -22,7 +23,7 @@ namespace Dalamud.Game.ClientState.Aetherytes; internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList { [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly ObjectTable objectTable = Service.Get(); private readonly Telepo* telepoInstance = Telepo.Instance(); @@ -37,7 +38,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis { get { - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return 0; this.Update(); @@ -59,7 +60,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis return null; } - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return null; return new AetheryteEntry(this.telepoInstance->TeleportList[index]); @@ -69,7 +70,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis private void Update() { // this is very very important as otherwise it crashes - if (this.clientState.LocalPlayer == null) + if (this.objectTable.LocalPlayer == null) return; this.telepoInstance->UpdateAetheryteList(); diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 84cfd24a3..44774a574 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -24,7 +24,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList private const uint InvalidObjectID = 0xE0000000; [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly PlayerState.PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private BuddyList() @@ -105,10 +105,10 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList /// public IBuddyMember? CreateBuddyMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) + if (address == IntPtr.Zero) return null; - if (address == IntPtr.Zero) + if (!this.playerState.IsLoaded) return null; var buddy = new BuddyMember(address); diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index e92af21c3..64be5cc67 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -46,6 +46,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState [ServiceManager.ServiceDependency] private readonly NetworkHandlers networkHandlers = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ObjectTable objectTable = Service.Get(); + private Hook onLogoutHook; private bool initialized; private ushort territoryTypeId; @@ -184,10 +190,10 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } /// - public IPlayerCharacter? LocalPlayer => Service.GetNullable()?[0] as IPlayerCharacter; + public IPlayerCharacter? LocalPlayer => this.objectTable.LocalPlayer; /// - public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId; + public unsafe ulong LocalContentId => this.playerState.ContentId; /// public unsafe bool IsLoggedIn @@ -241,7 +247,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public bool IsClientIdle(out ConditionFlag blockingFlag) { blockingFlag = 0; - if (this.LocalPlayer is null) return true; + if (this.objectTable.LocalPlayer is null) return true; var condition = Service.GetNullable(); @@ -368,7 +374,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState if (condition == null || gameGui == null || data == null) return; - if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null) + if (condition.Any() && this.lastConditionNone && this.objectTable.LocalPlayer != null) { Log.Debug("Is login"); this.lastConditionNone = false; diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 504b690c3..5a82ef0c5 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -150,15 +150,11 @@ internal unsafe partial class Fate /// True or false. public static bool IsValid(Fate fate) { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) + if (fate == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 1bf557ad5..2266c762d 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -60,15 +60,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable /// public bool IsValid(IFate fate) { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) + if (fate == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// @@ -87,12 +83,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable /// public IFate? CreateFateReference(IntPtr offset) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (offset == IntPtr.Zero) return null; - if (offset == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new Fate(offset); diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 84c1b5693..0a5e900f0 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -31,16 +31,16 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { private static int objectTableLength; - private readonly ClientState clientState; + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly CachedEntry[] cachedObjectTable; private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; [ServiceManager.ServiceConstructor] - private unsafe ObjectTable(ClientState clientState) + private unsafe ObjectTable() { - this.clientState = clientState; - var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted; objectTableLength = nativeObjectTable.Length; @@ -66,6 +66,9 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable /// public int Length => objectTableLength; + /// + public IPlayerCharacter? LocalPlayer => this[0] as IPlayerCharacter; + /// public IEnumerable PlayerObjects => this.GetPlayerObjects(); @@ -142,10 +145,10 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable { ThreadSafety.AssertMainThread(); - if (this.clientState.LocalContentId == 0) + if (address == nint.Zero) return null; - if (address == nint.Zero) + if (!this.playerState.IsLoaded) return null; var obj = (CSGameObject*)address; diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index 829949c12..c37b72961 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -1,9 +1,7 @@ using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; namespace Dalamud.Game.ClientState.Objects.Types; @@ -170,15 +168,11 @@ internal partial class GameObject /// True or false. public static bool IsValid(IGameObject? actor) { - var clientState = Service.GetNullable(); - - if (actor is null || clientState == null) + if (actor == null) return false; - if (clientState.LocalContentId == 0) - return false; - - return true; + var playerState = Service.Get(); + return playerState.IsLoaded == true; } /// diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index a016a8211..0a81095c6 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -25,7 +25,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList private const int AllianceLength = 20; [ServiceManager.ServiceDependency] - private readonly ClientState clientState = Service.Get(); + private readonly PlayerState.PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private PartyList() @@ -91,10 +91,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreatePartyMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + if (address == IntPtr.Zero || !this.playerState.IsLoaded) return null; return new PartyMember(address); @@ -112,10 +109,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreateAllianceMemberReference(IntPtr address) { - if (this.clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + if (address == IntPtr.Zero || !this.playerState.IsLoaded) return null; return new PartyMember(address); diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index a38e45ea3..04d0d822c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -66,15 +66,14 @@ public sealed unsafe partial class StatusList /// The status object containing the requested data. public static StatusList? CreateStatusListReference(IntPtr address) { + if (address == IntPtr.Zero) + return null; + // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either // here or somewhere else. - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) - return null; - - if (address == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new StatusList(address); @@ -87,12 +86,11 @@ public sealed unsafe partial class StatusList /// The status object containing the requested data. public static Status? CreateStatusReference(IntPtr address) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (address == IntPtr.Zero) return null; - if (address == IntPtr.Zero) + var playerState = Service.Get(); + if (!playerState.IsLoaded) return null; return new Status(address); diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 9b85d0ff3..2f9276cc0 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -16,13 +16,12 @@ using Dalamud.Hooking; using Dalamud.Networking.Http; 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; namespace Dalamud.Game.Network.Internal; @@ -269,29 +268,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private static (ulong UploaderId, uint WorldId) GetUploaderInfo() { - var agentLobby = AgentLobby.Instance(); - - var uploaderId = agentLobby->LobbyData.ContentId; - if (uploaderId == 0) - { - var playerState = PlayerState.Instance(); - if (playerState->IsLoaded) - { - uploaderId = playerState->ContentId; - } - } - - var worldId = agentLobby->LobbyData.CurrentWorldId; - if (worldId == 0) - { - var localPlayer = Control.GetLocalPlayer(); - if (localPlayer != null) - { - worldId = localPlayer->CurrentWorld; - } - } - - return (uploaderId, worldId); + var playerState = Service.Get(); + return (playerState.ContentId, playerState.CurrentWorld.RowId); } private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData) 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.cs b/Dalamud/Game/PlayerState/PlayerState.cs new file mode 100644 index 000000000..cebdb0ef8 --- /dev/null +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -0,0 +1,205 @@ +using Dalamud.Data; +using Dalamud.IoC.Internal; +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. +/// +[ServiceManager.EarlyLoadedService] +[ResolveVia] +internal unsafe class PlayerState : IPlayerState, IServiceType +{ + /// + public bool IsLoaded => CSPlayerState.Instance()->IsLoaded; + + /// + public string CharacterName => this.IsLoaded ? CSPlayerState.Instance()->CharacterNameString : string.Empty; + + /// + public uint EntityId => this.IsLoaded ? CSPlayerState.Instance()->EntityId : default; + + /// + public ulong ContentId => this.IsLoaded ? CSPlayerState.Instance()->ContentId : default; + + /// + public RowRef CurrentWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.CurrentWorldId) + : default; + } + } + + /// + public RowRef HomeWorld + { + get + { + var agentLobby = AgentLobby.Instance(); + return agentLobby->IsLoggedIn + ? LuminaUtils.CreateRef(agentLobby->LobbyData.HomeWorldId) + : default; + } + } + + /// + public Sex Sex => this.IsLoaded ? (Sex)CSPlayerState.Instance()->Sex : default; + + /// + public RowRef Race => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Race) : default; + + /// + public RowRef Tribe => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->Tribe) : default; + + /// + public RowRef ClassJob => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->CurrentClassJobId) : default; + + /// + public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; + + /// + public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced; + + /// + public short EffectiveLevel => this.IsLoaded ? (this.IsLevelSynced ? CSPlayerState.Instance()->SyncedLevel : CSPlayerState.Instance()->CurrentLevel) : default; + + /// + public RowRef GuardianDeity => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GuardianDeity) : default; + + /// + public byte BirthMonth => this.IsLoaded ? CSPlayerState.Instance()->BirthMonth : default; + + /// + public byte BirthDay => this.IsLoaded ? CSPlayerState.Instance()->BirthDay : default; + + /// + public RowRef FirstClass => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FirstClass) : default; + + /// + public RowRef StartTown => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->StartTown) : default; + + /// + public int BaseStrength => this.IsLoaded ? CSPlayerState.Instance()->BaseStrength : default; + + /// + public int BaseDexterity => this.IsLoaded ? CSPlayerState.Instance()->BaseDexterity : default; + + /// + public int BaseVitality => this.IsLoaded ? CSPlayerState.Instance()->BaseVitality : default; + + /// + public int BaseIntelligence => this.IsLoaded ? CSPlayerState.Instance()->BaseIntelligence : default; + + /// + public int BaseMind => this.IsLoaded ? CSPlayerState.Instance()->BaseMind : default; + + /// + public int BasePiety => this.IsLoaded ? CSPlayerState.Instance()->BasePiety : default; + + /// + public RowRef GrandCompany => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->GrandCompany) : default; + + /// + public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; + + /// + public ReadOnlySpan> FavouriteAetherytes + { + get + { + var playerState = CSPlayerState.Instance(); + if (playerState->IsLoaded || 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; + } + } + + /// + public RowRef FreeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->FreeAetheryteId) : default; + + /// + public uint BaseRestedExperience => this.IsLoaded ? CSPlayerState.Instance()->BaseRestedExperience : default; + + /// + public short PlayerCommendations => this.IsLoaded ? CSPlayerState.Instance()->PlayerCommendations : default; + + /// + public byte DeliveryLevel => this.IsLoaded ? CSPlayerState.Instance()->DeliveryLevel : default; + + /// + public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default; + + /// + public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default; + + /// + public byte GetGrandCompanyRank(GrandCompany grandCompany) + { + if (!this.IsLoaded) + return default; + + return grandCompany.RowId switch + { + 1 => CSPlayerState.Instance()->GCRankMaelstrom, + 2 => CSPlayerState.Instance()->GCRankTwinAdders, + 3 => CSPlayerState.Instance()->GCRankImmortalFlames, + _ => default, + }; + } + + /// + public short GetClassJobLevel(ClassJob classJob) + { + if (classJob.ExpArrayIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->ClassJobLevels[classJob.ExpArrayIndex]; + } + + /// + public int GetClassJobExperience(ClassJob classJob) + { + if (classJob.ExpArrayIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->ClassJobExperience[classJob.ExpArrayIndex]; + } + + /// + public float GetDesynthesisLevel(ClassJob classJob) + { + if (classJob.DohDolJobIndex == -1) + return default; + + if (!this.IsLoaded) + return default; + + return CSPlayerState.Instance()->DesynthesisLevels[classJob.DohDolJobIndex] / 100f; + } +} 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 9f898bcca..018742271 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -35,7 +35,6 @@ using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using AddonSheet = Lumina.Excel.Sheets.Addon; -using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; using StatusSheet = Lumina.Excel.Sheets.Status; namespace Dalamud.Game.Text.Evaluator; @@ -68,6 +67,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator [ServiceManager.ServiceDependency] private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly ConcurrentDictionary, string> actStrCache = []; private readonly ConcurrentDictionary, string> objStrCache = []; @@ -564,7 +566,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, playerState.EntityId == entityId ? eTrue : eFalse); } private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index bf6800a53..7a5a9c89b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.JobGauge; using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Objects; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -29,10 +29,10 @@ internal class GaugeWidget : IDataWindowWidget /// public void Draw() { - var clientState = Service.Get(); + var objectTable = Service.Get(); var jobGauges = Service.Get(); - var player = clientState.LocalPlayer; + var player = objectTable.LocalPlayer; if (player == null) { ImGui.Text("Player is not present"u8); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index 290c7d9a2..9a2de7261 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -4,6 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; +using Dalamud.Game.PlayerState; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -39,12 +40,13 @@ internal class ObjectTableWidget : IDataWindowWidget var chatGui = Service.Get(); var clientState = Service.Get(); + var playerState = Service.Get(); var gameGui = Service.Get(); var objectTable = Service.Get(); var stateString = string.Empty; - if (clientState.LocalPlayer == null) + if (objectTable.LocalPlayer == null) { ImGui.Text("LocalPlayer null."u8); } @@ -55,10 +57,10 @@ internal class ObjectTableWidget : IDataWindowWidget else { stateString += $"ObjectTableLen: {objectTable.Length}\n"; - stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n"; - stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.ValueNullable?.Name : clientState.LocalPlayer.CurrentWorld.RowId.ToString())}\n"; - stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.ValueNullable?.Name : clientState.LocalPlayer.HomeWorld.RowId.ToString())}\n"; - stateString += $"LocalCID: {clientState.LocalContentId:X}\n"; + stateString += $"LocalPlayerName: {playerState.CharacterName}\n"; + stateString += $"CurrentWorldName: {(this.resolveGameData ? playerState.CurrentWorld.ValueNullable?.Name : playerState.CurrentWorld.RowId.ToString())}\n"; + stateString += $"HomeWorldName: {(this.resolveGameData ? playerState.HomeWorld.ValueNullable?.Name : playerState.HomeWorld.RowId.ToString())}\n"; + stateString += $"LocalCID: {playerState.ContentId:X}\n"; stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 6c581604e..0ca754a91 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -1,10 +1,10 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -111,12 +111,12 @@ internal class PluginIpcWidget : IDataWindowWidget if (ImGui.Button("Action GO"u8)) { - this.ipcSubGo.InvokeAction(Service.Get().LocalPlayer); + this.ipcSubGo.InvokeAction(Service.Get().LocalPlayer); } if (ImGui.Button("Func GO"u8)) { - this.callGateResponse = this.ipcSubGo.InvokeFunc(Service.Get().LocalPlayer); + this.callGateResponse = this.ipcSubGo.InvokeFunc(Service.Get().LocalPlayer); } if (!this.callGateResponse.IsNullOrEmpty()) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 081f3ec96..6caf3286d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Utility; @@ -33,7 +33,7 @@ internal class TargetWidget : IDataWindowWidget { ImGui.Checkbox("Resolve GameData"u8, ref this.resolveGameData); - var clientState = Service.Get(); + var objectTable = Service.Get(); var targetMgr = Service.Get(); if (targetMgr.Target != null) @@ -80,7 +80,7 @@ internal class TargetWidget : IDataWindowWidget if (ImGui.Button("Clear FT"u8)) targetMgr.FocusTarget = null; - var localPlayer = clientState.LocalPlayer; + var localPlayer = objectTable.LocalPlayer; if (localPlayer != null) { diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs index 9853e31d4..e32b6cd2a 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SeStringEvaluatorSelfTestStep.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.SeStringHandling.Payloads; @@ -51,8 +52,8 @@ internal class SeStringEvaluatorSelfTestStep : ISelfTestStep // that it returned the local players name by using its EntityId, // and that it didn't include the world name by checking the HomeWorldId against AgentLobby.Instance()->LobbyData.HomeWorldId. - var clientState = Service.Get(); - var localPlayer = clientState.LocalPlayer; + var objectTable = Service.Get(); + var localPlayer = objectTable.LocalPlayer; if (localPlayer is null) { ImGui.Text("You need to be logged in for this step."u8); diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 0342ea77c..de0c5dad8 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,11 +109,13 @@ public interface IClientState /// /// Gets the local player character, if one is present. /// + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if you need to.")] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] public ulong LocalContentId { get; } /// diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs index 4c5305513..36cd72ebe 100644 --- a/Dalamud/Plugin/Services/IObjectTable.cs +++ b/Dalamud/Plugin/Services/IObjectTable.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Plugin.Services; @@ -19,6 +20,11 @@ public interface IObjectTable : IEnumerable /// public int Length { get; } + /// + /// Gets the local player character, if one is present. + /// + public IPlayerCharacter? LocalPlayer { get; } + /// /// Gets an enumerator for accessing player objects. This will only contain BattleChara objects. /// Does not contain any mounts, minions, or accessories. diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs new file mode 100644 index 000000000..dc507e461 --- /dev/null +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -0,0 +1,212 @@ +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 +{ + /// + /// 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); +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index ff06618ab..a3e6e16ed 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -18,18 +18,21 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Support; + using Lumina.Excel.Sheets; + using Serilog; + using TerraFX.Interop.Windows; + using Windows.Win32.System.Memory; using Windows.Win32.System.Ole; using Windows.Win32.UI.WindowsAndMessaging; -using Dalamud.Interface.Internal; - using FLASHWINFO = Windows.Win32.UI.WindowsAndMessaging.FLASHWINFO; using HWND = Windows.Win32.Foundation.HWND; using MEMORY_BASIC_INFORMATION = Windows.Win32.System.Memory.MEMORY_BASIC_INFORMATION; From 8cac4862494bf2a9a31188310b5cb8dc39a42e50 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:36:55 +0200 Subject: [PATCH 02/28] Add PluginInterface attribute to PlayerState --- Dalamud/Game/PlayerState/PlayerState.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index cebdb0ef8..5e5528eca 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -1,4 +1,5 @@ using Dalamud.Data; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -15,9 +16,10 @@ namespace Dalamud.Game.PlayerState; /// /// This class contains the PlayerState wrappers. /// +[PluginInterface] [ServiceManager.EarlyLoadedService] [ResolveVia] -internal unsafe class PlayerState : IPlayerState, IServiceType +internal unsafe class PlayerState : IServiceType, IPlayerState { /// public bool IsLoaded => CSPlayerState.Instance()->IsLoaded; From c2fc04c3a80c93694584b3db9eed40963dc9507a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:37:11 +0200 Subject: [PATCH 03/28] Improve wording --- Dalamud/Plugin/Services/IClientState.cs | 2 +- Dalamud/Plugin/Services/IPlayerState.cs | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index de0c5dad8..36bf2e296 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,7 +109,7 @@ public interface IClientState /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if you need to.")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] public IPlayerCharacter? LocalPlayer { get; } /// diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index dc507e461..a119f231b 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -8,15 +8,16 @@ 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. +/// Interface for determining the players state. /// public interface IPlayerState { /// - /// Gets a value indicating whether the local character is loaded. + /// Gets a value indicating whether the local players data is loaded. /// /// - /// The actual GameObject will not immediately exist when this changes to true. + /// PlayerState is separate from , + /// and as such the game object might not exist when it's loaded. /// bool IsLoaded { get; } @@ -141,37 +142,37 @@ public interface IPlayerState RowRef GrandCompany { get; } /// - /// Gets the Aetheryte row for the local character's home aetheryte. + /// Gets the Aetheryte row for the local player's home aetheryte. /// RowRef HomeAetheryte { get; } /// - /// Gets a span of Aetheryte rows for the local character's favourite aetherytes. + /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. /// ReadOnlySpan> FavouriteAetherytes { get; } /// - /// Gets the Aetheryte row for the local character's free aetheryte. + /// Gets the Aetheryte row for the local player's free aetheryte. /// RowRef FreeAetheryte { get; } /// - /// Gets the amount of received player commendations of the local character. + /// Gets the amount of received player commendations of the local player. /// uint BaseRestedExperience { get; } /// - /// Gets the amount of received player commendations of the local character. + /// Gets the amount of received player commendations of the local player. /// short PlayerCommendations { get; } /// - /// Gets the Carrier Level of Delivery Moogle Quests of the local character. + /// Gets the Carrier Level of Delivery Moogle Quests of the local player. /// byte DeliveryLevel { get; } /// - /// Gets the mentor version of the local character. + /// Gets the mentor version of the local player. /// MentorVersion MentorVersion { get; } From 153870a053252f19e759301ead3a79fcc6b7a72e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:37:22 +0200 Subject: [PATCH 04/28] Add mentor states --- Dalamud/Game/PlayerState/PlayerState.cs | 15 +++++++++++++ Dalamud/Plugin/Services/IPlayerState.cs | 28 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 5e5528eca..7af067119 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -151,6 +151,21 @@ internal unsafe class PlayerState : IServiceType, IPlayerState /// public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default; + /// + public bool IsMentor => this.IsLoaded && CSPlayerState.Instance()->IsMentor(); + + /// + public bool IsBattleMentor => this.IsLoaded && CSPlayerState.Instance()->IsBattleMentor(); + + /// + public bool IsTradeMentor => this.IsLoaded && CSPlayerState.Instance()->IsTradeMentor(); + + /// + public bool IsNovice => this.IsLoaded && CSPlayerState.Instance()->IsNovice(); + + /// + public bool IsReturner => this.IsLoaded && CSPlayerState.Instance()->IsReturner(); + /// public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default; diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index a119f231b..98b0c36da 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -176,6 +176,34 @@ public interface IPlayerState /// MentorVersion MentorVersion { get; } + /// + /// Gets a value indicating whether the local player is any kind of Mentor (Battle or Trade Mentor). + /// + bool IsMentor { get; } + + /// + /// Gets a value indicating whether the local player is a Battle Mentor. + /// + bool IsBattleMentor { get; } + + /// + /// Gets a value indicating whether the local player is a Trade Mentor. + /// + bool IsTradeMentor { get; } + + /// + /// Gets a value indicating whether the local player is a novice (aka. Sprout or New Adventurer). + /// + /// + /// Can be if /nastatus was used to deactivate it. + /// + bool IsNovice { get; } + + /// + /// Gets a value indicating whether the local player is a returner. + /// + bool IsReturner { get; } + /// /// Gets the value of an attribute of the local character. /// From a55c8ca773cb0f82a2df0c0e7f934a1f7efe31a8 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 5 Oct 2025 14:38:50 +0200 Subject: [PATCH 05/28] Fix warning --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 018742271..a4efad488 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -566,7 +566,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.EntityId == entityId ? eTrue : eFalse); + return this.ResolveStringExpression(in context, this.playerState.EntityId == entityId ? eTrue : eFalse); } private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload) From bcf651b5c17e25f48fbc44d96f5837845fd4ee27 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 6 Oct 2025 01:38:35 +0200 Subject: [PATCH 06/28] Fix FavoriteAetherytes --- Dalamud/Game/PlayerState/PlayerState.cs | 9 ++++++--- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 7af067119..06d57133d 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -118,15 +118,18 @@ internal unsafe class PlayerState : IServiceType, IPlayerState public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; /// - public ReadOnlySpan> FavouriteAetherytes + public ReadOnlySpan> FavoriteAetherytes { get { var playerState = CSPlayerState.Instance(); - if (playerState->IsLoaded || playerState->FavouriteAetheryteCount == 0) - return []; + if (!playerState->IsLoaded) + return default; var count = playerState->FavouriteAetheryteCount; + if (count == 0) + return default; + var array = new RowRef[count]; for (var i = 0; i < count; i++) diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 98b0c36da..bf84227ef 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -149,7 +149,7 @@ public interface IPlayerState /// /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. /// - ReadOnlySpan> FavouriteAetherytes { get; } + ReadOnlySpan> FavoriteAetherytes { get; } /// /// Gets the Aetheryte row for the local player's free aetheryte. From 2cf869872d5bbf1045858367ae9c3954ec48397b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 6 Oct 2025 02:08:18 +0200 Subject: [PATCH 07/28] Return IReadOnlyList instead of ReadOnlySpan --- Dalamud/Game/PlayerState/PlayerState.cs | 4 +++- Dalamud/Plugin/Services/IPlayerState.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/PlayerState/PlayerState.cs index 06d57133d..c80166dd5 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/PlayerState/PlayerState.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Dalamud.Data; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -118,7 +120,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState public RowRef HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef(CSPlayerState.Instance()->HomeAetheryteId) : default; /// - public ReadOnlySpan> FavoriteAetherytes + public IReadOnlyList> FavoriteAetherytes { get { diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index bf84227ef..1a22f58d6 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + using Dalamud.Game.PlayerState; using Lumina.Excel; @@ -147,9 +149,9 @@ public interface IPlayerState RowRef HomeAetheryte { get; } /// - /// Gets a span of Aetheryte rows for the local player's favourite aetherytes. + /// Gets an array of Aetheryte rows for the local player's favourite aetherytes. /// - ReadOnlySpan> FavoriteAetherytes { get; } + IReadOnlyList> FavoriteAetherytes { get; } /// /// Gets the Aetheryte row for the local player's free aetheryte. From 6ade5b21cfad0d951ca92471906a1d21aca23c01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 1 Oct 2025 17:06:06 +0200 Subject: [PATCH 08/28] Add IUnlockState service --- Dalamud/Game/ItemActionType.cs | 76 +++ Dalamud/Game/UnlockState/UnlockState.cs | 798 ++++++++++++++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 297 +++++++++ 3 files changed, 1171 insertions(+) create mode 100644 Dalamud/Game/ItemActionType.cs create mode 100644 Dalamud/Game/UnlockState/UnlockState.cs create mode 100644 Dalamud/Plugin/Services/IUnlockState.cs 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/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs new file mode 100644 index 000000000..c84d1e73c --- /dev/null +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -0,0 +1,798 @@ +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.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; + +/// +/// 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.ServiceConstructor] + private UnlockState() + { + this.clientState.Login += this.UpdateUnlocks; + this.clientState.Logout += this.OnLogout; + this.gameGui.UnlocksUpdate += this.UpdateUnlocks; + } + + /// + public event IUnlockState.UnlockDelegate Unlock; + + private bool IsLoaded => PlayerState.Instance()->IsLoaded; + + /// + void IInternalDisposableService.DisposeService() + { + this.clientState.Login -= this.UpdateUnlocks; + this.clientState.Logout -= this.OnLogout; + this.gameGui.UnlocksUpdate -= this.UpdateUnlocks; + } + + /// + 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 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 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: + 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.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 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 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; + } + + /// + 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() + { + 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 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); + } + } + } + } + } +} + +/// +/// 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 IsChocoboTaxiUnlocked(ChocoboTaxi row) => this.unlockStateService.IsChocoboTaxiUnlocked(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 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 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 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..baee47115 --- /dev/null +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -0,0 +1,297 @@ +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 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 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 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 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); +} From ba159f8c5fd8e396535d87d361405c7c6d5564af Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 1 Oct 2025 23:59:01 +0200 Subject: [PATCH 09/28] Add IsRecipeUnlocked --- Dalamud/Game/UnlockState/RecipeData.cs | 257 ++++++++++++++++++++++++ Dalamud/Game/UnlockState/UnlockState.cs | 17 +- Dalamud/Plugin/Services/IUnlockState.cs | 7 + 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/UnlockState/RecipeData.cs diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs new file mode 100644 index 000000000..81c0b838b --- /dev/null +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -0,0 +1,257 @@ +using System.Linq; + +using CommunityToolkit.HighPerformance; + +using Dalamud.Data; +using Dalamud.Game.Gui; + +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; + + /// + /// 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.gameGui.UnlocksUpdate += this.Update; + } + + /// + void IInternalDisposableService.DisposeService() + { + this.clientState.Login -= this.Update; + this.clientState.Logout -= this.OnLogout; + this.gameGui.UnlocksUpdate -= this.Update; + } + + /// + /// 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; + } + + private void Update() + { + // Client::Game::UI::RecipeNote.InitializeStructs + + if (!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()->UnlockedSecretRecipeBooksBitmask.TryCheckBitInSpan(bitIndex, out var result) && result) + { + 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()->UnlockedSecretRecipeBooksBitmask.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) + { + this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.ToArray(); + changed |= true; + } + + if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinkBitmask.SequenceEqual(this.cachedUnlockLinks)) + { + this.cachedUnlockLinks = UIState.Instance()->UnlockLinkBitmask.ToArray(); + changed |= true; + } + + return changed; + } +} diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index c84d1e73c..d41dce2e4 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -39,6 +39,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly RecipeData recipeData = Service.Get(); + [ServiceManager.ServiceConstructor] private UnlockState() { @@ -346,6 +349,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.IsPublicContentUnlocked(row.RowId); } + /// + public bool IsRecipeUnlocked(Recipe row) + { + return this.recipeData.IsRecipeUnlocked(row); + } + /// public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) { @@ -495,6 +504,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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); @@ -584,6 +596,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -596,7 +609,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - FishingSpot // - Spearfishing // - Adventure (Sightseeing) - // - Recipes // - MinerFolkloreTome // - BotanistFolkloreTome // - FishingFolkloreTome @@ -767,6 +779,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// 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); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index baee47115..371af033c 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -245,6 +245,13 @@ public interface IUnlockState /// 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. /// From 62fdd2c60d252bb5a0558de7834678869c7546a1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 02:01:11 +0200 Subject: [PATCH 10/28] Fix IsChocoboTaxiStandUnlocked --- Dalamud/Game/UnlockState/UnlockState.cs | 10 +++++----- Dalamud/Plugin/Services/IUnlockState.cs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index d41dce2e4..ff1effdd1 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -167,12 +167,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState } /// - public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) + public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) { if (!this.IsLoaded) return false; - return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId); + return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId - 0x120000); } /// @@ -453,8 +453,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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 chocoboTaxiStandRow)) + return this.IsChocoboTaxiStandUnlocked(chocoboTaxiStandRow); if (rowRef.TryGetValue(out var companionRow)) return this.IsCompanionUnlocked(companionRow); @@ -723,7 +723,7 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row) => this.unlockStateService.IsCharaMakeCustomizeUnlocked(row); /// - public bool IsChocoboTaxiUnlocked(ChocoboTaxi row) => this.unlockStateService.IsChocoboTaxiUnlocked(row); + public bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row) => this.unlockStateService.IsChocoboTaxiStandUnlocked(row); /// public bool IsCompanionUnlocked(Companion row) => this.unlockStateService.IsCompanionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 371af033c..d3620ffe2 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -113,11 +113,11 @@ public interface IUnlockState bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row); /// - /// Determines whether the specified ChocoboTaxi (Chocobokeeps of the Chocobo Porter service) is unlocked. + /// Determines whether the specified ChocoboTaxiStand (Chocobokeeps of the Chocobo Porter service) is unlocked. /// - /// The ChocoboTaxi row to check. + /// The ChocoboTaxiStand row to check. /// if unlocked; otherwise, . - bool IsChocoboTaxiUnlocked(ChocoboTaxi row); + bool IsChocoboTaxiStandUnlocked(ChocoboTaxiStand row); /// /// Determines whether the specified Companion (Minions) is unlocked. From 5905afdf103b55fd2ff450579ec340722d562ca4 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 03:23:54 +0200 Subject: [PATCH 11/28] Cache completed Quests in RecipeData too --- Dalamud/Game/UnlockState/RecipeData.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 81c0b838b..4c89c2f3e 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -5,6 +5,7 @@ using CommunityToolkit.HighPerformance; using Dalamud.Data; using Dalamud.Game.Gui; +using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.Interop; @@ -33,6 +34,7 @@ internal unsafe class RecipeData : IInternalDisposableService private readonly ushort[,] noteBookDivisionIds; private byte[]? cachedUnlockedSecretRecipeBooks; private byte[]? cachedUnlockLinks; + private byte[]? cachedCompletedQuests; /// /// Initializes a new instance of the class. @@ -105,6 +107,7 @@ internal unsafe class RecipeData : IInternalDisposableService { this.cachedUnlockedSecretRecipeBooks = null; this.cachedUnlockLinks = null; + this.cachedCompletedQuests = null; } private void Update() @@ -252,6 +255,12 @@ internal unsafe class RecipeData : IInternalDisposableService changed |= true; } + if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuestsBitmask.SequenceEqual(this.cachedCompletedQuests)) + { + this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuestsBitmask.ToArray(); + changed |= true; + } + return changed; } } From c4dd75bdda5f5d0c375801cd54161b96ee9b57c5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 19:36:52 +0200 Subject: [PATCH 12/28] Update RecipeData when levels changed --- Dalamud/Game/UnlockState/RecipeData.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 4c89c2f3e..9a2f95b53 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -53,6 +53,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login += this.Update; this.clientState.Logout += this.OnLogout; + this.clientState.LevelChanged += this.OnlevelChanged; this.gameGui.UnlocksUpdate += this.Update; } @@ -61,6 +62,7 @@ internal unsafe class RecipeData : IInternalDisposableService { this.clientState.Login -= this.Update; this.clientState.Logout -= this.OnLogout; + this.clientState.LevelChanged -= this.OnlevelChanged; this.gameGui.UnlocksUpdate -= this.Update; } @@ -110,9 +112,18 @@ internal unsafe class RecipeData : IInternalDisposableService 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 Update() { - // Client::Game::UI::RecipeNote.InitializeStructs + // based on Client::Game::UI::RecipeNote.InitializeStructs if (!this.NeedsUpdate()) return; From 3746c47a84331538e5ec3cf9cbb130bf30627987 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 2 Oct 2025 19:39:49 +0200 Subject: [PATCH 13/28] Ignore RecipeData updates when not logged in Just to be safe... --- Dalamud/Game/UnlockState/RecipeData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 9a2f95b53..48409248a 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -125,7 +125,7 @@ internal unsafe class RecipeData : IInternalDisposableService { // based on Client::Game::UI::RecipeNote.InitializeStructs - if (!this.NeedsUpdate()) + if (!this.clientState.IsLoggedIn || !this.NeedsUpdate()) return; Array.Clear(this.unlockedNoteBookDivisionsCount, 0, this.unlockedNoteBookDivisionsCount.Length); From 986dfa04d0145a5596e8284fce461041c383f723 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 7 Oct 2025 17:42:16 +0200 Subject: [PATCH 14/28] Mark IUnlockState as experimental --- Dalamud/Game/UnlockState/UnlockState.cs | 2 ++ Dalamud/Plugin/Services/IUnlockState.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index ff1effdd1..b60e9ccdf 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -20,6 +20,8 @@ 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. /// diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index d3620ffe2..22ef94eb2 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using Lumina.Excel; using Lumina.Excel.Sheets; @@ -8,6 +10,7 @@ namespace Dalamud.Plugin.Services; /// /// Interface for determining unlock state of various content in the game. /// +[Experimental("UnlockState")] public interface IUnlockState { /// From 878080d66076c3b2b96c86668f8409548a617584 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 16 Oct 2025 00:56:45 +0200 Subject: [PATCH 15/28] Fix IsChocoboTaxiStandUnlocked call --- Dalamud/Game/UnlockState/UnlockState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index b60e9ccdf..ae8fc4f86 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -174,7 +174,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded) return false; - return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId - 0x120000); + return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId); } /// From 68c02caf37495a14bcd78fea6d58245274746772 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 01:10:35 +0200 Subject: [PATCH 16/28] Fix obsolete warnings --- Dalamud/Game/UnlockState/RecipeData.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 48409248a..593b7fd38 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -212,7 +212,7 @@ internal unsafe class RecipeData : IInternalDisposableService _ => default, }; - if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.TryCheckBitInSpan(bitIndex, out var result) && result) + if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex)) { secretRecipeBookUnlocked = true; break; @@ -254,15 +254,15 @@ internal unsafe class RecipeData : IInternalDisposableService } } - if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) + if (this.cachedUnlockedSecretRecipeBooks == null || !PlayerState.Instance()->UnlockedSecretRecipeBooks.SequenceEqual(this.cachedUnlockedSecretRecipeBooks)) { - this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooksBitmask.ToArray(); + this.cachedUnlockedSecretRecipeBooks = PlayerState.Instance()->UnlockedSecretRecipeBooks.ToArray(); changed |= true; } - if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinkBitmask.SequenceEqual(this.cachedUnlockLinks)) + if (this.cachedUnlockLinks == null || !UIState.Instance()->UnlockLinks.SequenceEqual(this.cachedUnlockLinks)) { - this.cachedUnlockLinks = UIState.Instance()->UnlockLinkBitmask.ToArray(); + this.cachedUnlockLinks = UIState.Instance()->UnlockLinks.ToArray(); changed |= true; } From 6e8efabc3b88f396c66e66556244255afa8e5436 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:32:43 +0200 Subject: [PATCH 17/28] Add support for Occult Record items --- Dalamud/Game/ItemActionType.cs | 76 ----------------- Dalamud/Game/UnlockState/ItemActionType.cs | 97 ++++++++++++++++++++++ Dalamud/Game/UnlockState/UnlockState.cs | 26 +++--- 3 files changed, 111 insertions(+), 88 deletions(-) delete mode 100644 Dalamud/Game/ItemActionType.cs create mode 100644 Dalamud/Game/UnlockState/ItemActionType.cs diff --git a/Dalamud/Game/ItemActionType.cs b/Dalamud/Game/ItemActionType.cs deleted file mode 100644 index 3f2ac5f17..000000000 --- a/Dalamud/Game/ItemActionType.cs +++ /dev/null @@ -1,76 +0,0 @@ -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/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionType.cs new file mode 100644 index 000000000..741dcd31b --- /dev/null +++ b/Dalamud/Game/UnlockState/ItemActionType.cs @@ -0,0 +1,97 @@ +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.UnlockState; + +// TODO: Switch to FFXIVClientStructs.FFXIV.Client.Enums.ItemActionType. + +/// +/// 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/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index ae8fc4f86..e2528c7e7 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -263,6 +263,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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(): @@ -387,18 +388,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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; + 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; } /// From 193d321103f78f23e79c6c3ddf5d864830d7fd30 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:33:06 +0200 Subject: [PATCH 18/28] Add support for Soul Shard items --- Dalamud/Game/UnlockState/UnlockState.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index e2528c7e7..14ea9e2c9 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Component.Exd; @@ -284,6 +285,10 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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; } @@ -400,7 +405,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState or ItemActionType.FramersKit or ItemActionType.Ornament or ItemActionType.Glasses - or ItemActionType.OccultRecords; + or ItemActionType.OccultRecords + or ItemActionType.SoulShards; } /// @@ -625,6 +631,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - 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(bool fireEvent = true) where T : struct, IExcelRow From 880add5ab374afa7fa7203c9002346fd8265715d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 02:50:38 +0200 Subject: [PATCH 19/28] Add support for MKDLore rows --- Dalamud/Game/UnlockState/UnlockState.cs | 13 +++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 14ea9e2c9..a016b91cb 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -309,6 +309,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink); } + /// + public bool IsMKDLoreUnlocked(MKDLore row) + { + return this.IsUnlockLinkUnlocked(row.Unknown2); + } + /// public bool IsMountUnlocked(Mount row) { @@ -493,6 +499,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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); @@ -599,6 +608,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -772,6 +782,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// 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); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 22ef94eb2..79f68416e 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -206,6 +206,13 @@ public interface IUnlockState /// 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. /// From a06c0e3ed2497823a168c322b82ed5b7e40e2cd2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 03:51:38 +0200 Subject: [PATCH 20/28] Add support for EmjVoiceNpc rows --- Dalamud/Game/UnlockState/UnlockState.cs | 10 ++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index a016b91cb..0d4a8859e 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -205,6 +205,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink); } + /// + public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) + { + return this.IsUnlockLinkUnlocked(row.Unknown26); + } + /// public bool IsGeneralActionUnlocked(GeneralAction row) { @@ -601,6 +607,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); + this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); this.UpdateUnlocksForSheet(fireEvent); @@ -758,6 +765,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public bool IsEmoteUnlocked(Emote row) => this.unlockStateService.IsEmoteUnlocked(row); + /// + public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) => this.unlockStateService.IsEmjVoiceNpcUnlocked(row); + /// public bool IsGeneralActionUnlocked(GeneralAction row) => this.unlockStateService.IsGeneralActionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 79f68416e..50167812d 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -150,6 +150,13 @@ public interface IUnlockState /// 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 GeneralAction is unlocked. /// From 69caffeb97e31d5ec5c58e75ef34ca4de5f733f5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 18 Oct 2025 03:52:31 +0200 Subject: [PATCH 21/28] Add support for EmjCostume rows --- Dalamud/Game/UnlockState/UnlockState.cs | 15 +++++++++++++++ Dalamud/Plugin/Services/IUnlockState.cs | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 0d4a8859e..f86b89efb 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -8,6 +8,7 @@ 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; @@ -211,6 +212,14 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState 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) { @@ -644,6 +653,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - 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 @@ -768,6 +780,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// 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); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 50167812d..00f2df190 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -157,6 +157,13 @@ public interface IUnlockState /// 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. /// From 700aaa4a5d647a74f657088f0d38a85694a3a28f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 19 Oct 2025 17:20:30 +0200 Subject: [PATCH 22/28] Fix Unlock event not firing --- Dalamud/Game/UnlockState/UnlockState.cs | 111 ++++++++++-------------- 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index f86b89efb..846be8294 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -577,65 +577,53 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink); } - private void UpdateUnlocks() - { - 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 void UpdateUnlocks(bool fireEvent) + private void UpdateUnlocks() { 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); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); - this.UpdateUnlocksForSheet(fireEvent); + 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 @@ -663,7 +651,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } - private void UpdateUnlocksForSheet(bool fireEvent = true) where T : struct, IExcelRow + private void UpdateUnlocksForSheet() where T : struct, IExcelRow { var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []); @@ -679,20 +667,17 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState unlockedRowIds.Add(row.RowId); - if (fireEvent) - { - Log.Verbose("Unlock detected: {row}", $"{typeof(T).Name}#{row.RowId}"); + Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); - foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + { + try { - try - { - action((RowRef)rowRef); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } + action((RowRef)rowRef); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } From af8b61f08a4759512444f0d11c0e89e2b973c699 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:47:31 +0100 Subject: [PATCH 23/28] Update to use AgentUpdate event --- Dalamud/Game/UnlockState/RecipeData.cs | 10 ++++++++-- Dalamud/Game/UnlockState/UnlockState.cs | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index 593b7fd38..b96cab951 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -54,7 +54,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login += this.Update; this.clientState.Logout += this.OnLogout; this.clientState.LevelChanged += this.OnlevelChanged; - this.gameGui.UnlocksUpdate += this.Update; + this.gameGui.AgentUpdate += this.OnAgentUpdate; } /// @@ -63,7 +63,7 @@ internal unsafe class RecipeData : IInternalDisposableService this.clientState.Login -= this.Update; this.clientState.Logout -= this.OnLogout; this.clientState.LevelChanged -= this.OnlevelChanged; - this.gameGui.UnlocksUpdate -= this.Update; + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } /// @@ -121,6 +121,12 @@ internal unsafe class RecipeData : IInternalDisposableService } } + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) + { + if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate)) + this.Update(); + } + private void Update() { // based on Client::Game::UI::RecipeNote.InitializeStructs diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 846be8294..a4b9381cc 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -49,9 +49,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceConstructor] private UnlockState() { - this.clientState.Login += this.UpdateUnlocks; + this.clientState.Login += this.OnLogin; this.clientState.Logout += this.OnLogout; - this.gameGui.UnlocksUpdate += this.UpdateUnlocks; + this.gameGui.AgentUpdate += this.OnAgentUpdate; } /// @@ -62,9 +62,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// void IInternalDisposableService.DisposeService() { - this.clientState.Login -= this.UpdateUnlocks; + this.clientState.Login -= this.OnLogin; this.clientState.Logout -= this.OnLogout; - this.gameGui.UnlocksUpdate -= this.UpdateUnlocks; + this.gameGui.AgentUpdate -= this.OnAgentUpdate; } /// @@ -577,12 +577,23 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink); } + private void OnLogin() + { + this.Update(); + } + private void OnLogout(int type, int code) { this.cachedUnlockedRowIds.Clear(); } - private void UpdateUnlocks() + private void OnAgentUpdate(AgentUpdateFlag agentUpdateFlag) + { + if (agentUpdateFlag.HasFlag(AgentUpdateFlag.UnlocksUpdate)) + this.Update(); + } + + private void Update() { if (!this.IsLoaded) return; From 5cc327c5f9abd1439e81ec2eac486a1e6070081b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:49:23 +0100 Subject: [PATCH 24/28] Fix obsolete --- Dalamud/Game/UnlockState/RecipeData.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index b96cab951..c419ba4fd 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -272,9 +272,9 @@ internal unsafe class RecipeData : IInternalDisposableService changed |= true; } - if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuestsBitmask.SequenceEqual(this.cachedCompletedQuests)) + if (this.cachedCompletedQuests == null || !QuestManager.Instance()->CompletedQuests.SequenceEqual(this.cachedCompletedQuests)) { - this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuestsBitmask.ToArray(); + this.cachedCompletedQuests = QuestManager.Instance()->CompletedQuests.ToArray(); changed |= true; } From 497e61f699e9ccff29a48a3a37ea0f8f2fc7de00 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 8 Nov 2025 11:58:54 +0100 Subject: [PATCH 25/28] Remove comment about removed CS enum --- Dalamud/Game/UnlockState/ItemActionType.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionType.cs index 741dcd31b..8e3d79b84 100644 --- a/Dalamud/Game/UnlockState/ItemActionType.cs +++ b/Dalamud/Game/UnlockState/ItemActionType.cs @@ -2,8 +2,6 @@ using Lumina.Excel.Sheets; namespace Dalamud.Game.UnlockState; -// TODO: Switch to FFXIVClientStructs.FFXIV.Client.Enums.ItemActionType. - /// /// Enum for . /// From 750fa58147b8a68cd0110b084e93f0742cc38411 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 12:52:07 +0000 Subject: [PATCH 26/28] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index f6c479b3f..0afa6b672 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f6c479b3fa0b452b44403c8ea53d592bec415e1e +Subproject commit 0afa6b67288e5e667da74c1d3ad582e6c964644c From 64d4f7061ad2973d48f8f7cb8108dd70bd358fd1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 19:29:48 +0100 Subject: [PATCH 27/28] Rename namespace PlayerState to Player --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 8 +++++--- Dalamud/Game/ClientState/ClientState.cs | 7 ++++--- Dalamud/Game/ClientState/Fates/Fate.cs | 3 ++- Dalamud/Game/ClientState/Fates/FateTable.cs | 5 +++-- Dalamud/Game/ClientState/Objects/ObjectTable.cs | 3 ++- Dalamud/Game/ClientState/Objects/Types/GameObject.cs | 3 ++- Dalamud/Game/ClientState/Party/PartyList.cs | 3 ++- Dalamud/Game/ClientState/Statuses/StatusList.cs | 6 ++++-- Dalamud/Game/Network/Internal/NetworkHandlers.cs | 3 ++- Dalamud/Game/{PlayerState => Player}/MentorVersion.cs | 2 +- Dalamud/Game/{PlayerState => Player}/PlayerAttribute.cs | 2 +- Dalamud/Game/{PlayerState => Player}/PlayerState.cs | 2 +- Dalamud/Game/{PlayerState => Player}/Sex.cs | 2 +- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 3 ++- .../Internal/Windows/Data/Widgets/ObjectTableWidget.cs | 2 +- Dalamud/Plugin/Services/IPlayerState.cs | 2 +- 16 files changed, 34 insertions(+), 22 deletions(-) rename Dalamud/Game/{PlayerState => Player}/MentorVersion.cs (95%) rename Dalamud/Game/{PlayerState => Player}/PlayerAttribute.cs (99%) rename Dalamud/Game/{PlayerState => Player}/PlayerState.cs (99%) rename Dalamud/Game/{PlayerState => Player}/Sex.cs (86%) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 44774a574..dbac76518 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -2,11 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.UI; +using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState.Buddy; @@ -24,7 +26,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList private const uint InvalidObjectID = 0xE0000000; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private BuddyList() @@ -69,7 +71,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy; /// public IBuddyMember? this[int index] diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 64be5cc67..caf307683 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal; +using Dalamud.Game.Player; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -15,7 +16,6 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -23,6 +23,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.Sheets; using Action = System.Action; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState; @@ -47,7 +48,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly NetworkHandlers networkHandlers = Service.Get(); [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); @@ -285,7 +286,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; this.MapId = AgentMap.Instance()->CurrentMapId; - this.Instance = UIState.Instance()->PublicInstance.InstanceId; + this.Instance = CSUIState.Instance()->PublicInstance.InstanceId; this.initialized = true; diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 5a82ef0c5..f82109fd0 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Data; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory; @@ -153,7 +154,7 @@ internal unsafe partial class Fate if (fate == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 2266c762d..30b0f4102 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -63,7 +64,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable if (fate == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } @@ -86,7 +87,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable if (offset == IntPtr.Zero) return null; - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 0a5e900f0..b66dd4775 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -32,7 +33,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private static int objectTableLength; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); private readonly CachedEntry[] cachedObjectTable; diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index c37b72961..4b331a479 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Game.ClientState.Objects.Types; @@ -171,7 +172,7 @@ internal partial class GameObject if (actor == null) return false; - var playerState = Service.Get(); + var playerState = Service.Get(); return playerState.IsLoaded == true; } diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 0a81095c6..9618b679c 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Game.Player; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -25,7 +26,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList private const int AllianceLength = 20; [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceConstructor] private PartyList() diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index 04d0d822c..410ae9d7c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Game.Player; + namespace Dalamud.Game.ClientState.Statuses; /// @@ -72,7 +74,7 @@ public sealed unsafe partial class StatusList // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either // here or somewhere else. - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; @@ -89,7 +91,7 @@ public sealed unsafe partial class StatusList if (address == IntPtr.Zero) return null; - var playerState = Service.Get(); + var playerState = Service.Get(); if (!playerState.IsLoaded) return null; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 2f9276cc0..6a6d73b33 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; +using Dalamud.Game.Player; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Networking.Http; @@ -268,7 +269,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private static (ulong UploaderId, uint WorldId) GetUploaderInfo() { - var playerState = Service.Get(); + var playerState = Service.Get(); return (playerState.ContentId, playerState.CurrentWorld.RowId); } diff --git a/Dalamud/Game/PlayerState/MentorVersion.cs b/Dalamud/Game/Player/MentorVersion.cs similarity index 95% rename from Dalamud/Game/PlayerState/MentorVersion.cs rename to Dalamud/Game/Player/MentorVersion.cs index 701eda112..e856e1169 100644 --- a/Dalamud/Game/PlayerState/MentorVersion.cs +++ b/Dalamud/Game/Player/MentorVersion.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Specifies the mentor certification version for a player. diff --git a/Dalamud/Game/PlayerState/PlayerAttribute.cs b/Dalamud/Game/Player/PlayerAttribute.cs similarity index 99% rename from Dalamud/Game/PlayerState/PlayerAttribute.cs rename to Dalamud/Game/Player/PlayerAttribute.cs index 4db8af107..9d9954817 100644 --- a/Dalamud/Game/PlayerState/PlayerAttribute.cs +++ b/Dalamud/Game/Player/PlayerAttribute.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Represents a player's attribute. diff --git a/Dalamud/Game/PlayerState/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs similarity index 99% rename from Dalamud/Game/PlayerState/PlayerState.cs rename to Dalamud/Game/Player/PlayerState.cs index c80166dd5..316b09e2f 100644 --- a/Dalamud/Game/PlayerState/PlayerState.cs +++ b/Dalamud/Game/Player/PlayerState.cs @@ -13,7 +13,7 @@ using Lumina.Excel.Sheets; using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; using GrandCompany = Lumina.Excel.Sheets.GrandCompany; -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// This class contains the PlayerState wrappers. diff --git a/Dalamud/Game/PlayerState/Sex.cs b/Dalamud/Game/Player/Sex.cs similarity index 86% rename from Dalamud/Game/PlayerState/Sex.cs rename to Dalamud/Game/Player/Sex.cs index e6ed6cc78..0981cb9a4 100644 --- a/Dalamud/Game/PlayerState/Sex.cs +++ b/Dalamud/Game/Player/Sex.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.PlayerState; +namespace Dalamud.Game.Player; /// /// Represents the sex of a character. diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index a4efad488..58bcdbd0b 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Config; +using Dalamud.Game.Player; using Dalamud.Game.Text.Evaluator.Internal; using Dalamud.Game.Text.Noun; using Dalamud.Game.Text.Noun.Enums; @@ -68,7 +69,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator private readonly SheetRedirectResolver sheetRedirectResolver = Service.Get(); [ServiceManager.ServiceDependency] - private readonly PlayerState.PlayerState playerState = Service.Get(); + private readonly PlayerState playerState = Service.Get(); private readonly ConcurrentDictionary, string> actStrCache = []; private readonly ConcurrentDictionary, string> objStrCache = []; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index 9a2de7261..71fb18352 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -4,7 +4,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; -using Dalamud.Game.PlayerState; +using Dalamud.Game.Player; using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 1a22f58d6..425ffc963 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Dalamud.Game.PlayerState; +using Dalamud.Game.Player; using Lumina.Excel; using Lumina.Excel.Sheets; From cb441631e1c51941b5b0d6a1e5f27c37ad4879ca Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 17 Nov 2025 20:58:14 +0100 Subject: [PATCH 28/28] Add empty ServiceConstructor to PlayerState --- Dalamud/Game/Player/PlayerState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/Player/PlayerState.cs b/Dalamud/Game/Player/PlayerState.cs index 316b09e2f..917c946db 100644 --- a/Dalamud/Game/Player/PlayerState.cs +++ b/Dalamud/Game/Player/PlayerState.cs @@ -23,6 +23,11 @@ namespace Dalamud.Game.Player; [ResolveVia] internal unsafe class PlayerState : IServiceType, IPlayerState { + [ServiceManager.ServiceConstructor] + private PlayerState() + { + } + /// public bool IsLoaded => CSPlayerState.Instance()->IsLoaded;