Add IPlayerState service

This commit is contained in:
Haselnussbomber 2025-07-07 19:54:24 +02:00
parent d3bd5f1dce
commit 752ed8181c
No known key found for this signature in database
GPG key ID: BB905BB49E7295D1
14 changed files with 2535 additions and 84 deletions

View file

@ -16,13 +16,12 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
using Action = System.Action;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
namespace Dalamud.Game.ClientState;
@ -37,7 +36,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook<EventFramework.Delegates.SetTerritoryTypeId> setupTerritoryTypeHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<LogoutCallbackInterface.Delegates.OnLogout> onLogoutHook;
[ServiceManager.ServiceDependency]
@ -63,14 +61,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}");
this.setupTerritoryTypeHook = Hook<EventFramework.Delegates.SetTerritoryTypeId>.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.onLogoutHook = Hook<LogoutCallbackInterface.Delegates.OnLogout>.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
}
@ -80,10 +76,10 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
public event IPlayerState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/>
public event Action? Login;
@ -120,7 +116,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter;
/// <inheritdoc/>
public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
public ulong LocalContentId => 0;
/// <inheritdoc/>
public unsafe bool IsLoggedIn
@ -173,7 +169,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
void IInternalDisposableService.DisposeService()
{
this.setupTerritoryTypeHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
@ -211,54 +206,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
}
private unsafe void UIModuleHandlePacketDetour(
UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{
this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet);
switch (type)
{
case UIModulePacketType.ClassJobChange:
{
var classJobId = uintParam;
foreach (var action in Delegate.EnumerateInvocationList(this.ClassJobChanged))
{
try
{
action(classJobId);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
case UIModulePacketType.LevelChange:
{
var classJobId = *(uint*)packet;
var level = *(ushort*)((nint)packet + 4);
foreach (var action in Delegate.EnumerateInvocationList(this.LevelChanged))
{
try
{
action(classJobId, level);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
}
}
private void FrameworkOnOnUpdateEvent(IFramework framework1)
{
var condition = Service<Conditions.Condition>.GetNullable();
@ -356,10 +303,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
public event IPlayerState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/>
public event Action? Login;

View file

@ -0,0 +1,76 @@
using Lumina.Excel.Sheets;
namespace Dalamud.Game;
/// <summary>
/// Enum for <see cref="ItemAction.Type"/>.
/// </summary>
public enum ItemActionType : ushort
{
/// <summary>
/// Used to unlock a companion (minion).
/// </summary>
Companion = 853,
/// <summary>
/// Used to unlock a chocobo companion barding.
/// </summary>
BuddyEquip = 1013,
/// <summary>
/// Used to unlock a mount.
/// </summary>
Mount = 1322,
/// <summary>
/// Used to unlock recipes from a crafting recipe book.
/// </summary>
SecretRecipeBook = 2136,
/// <summary>
/// Used to unlock various types of content (e.g. Riding Maps, Blue Mage Totems, Emotes, Hairstyles).
/// </summary>
UnlockLink = 2633,
/// <summary>
/// Used to unlock a Triple Triad Card.
/// </summary>
TripleTriadCard = 3357,
/// <summary>
/// Used to unlock gathering nodes of a Folklore Tome.
/// </summary>
FolkloreTome = 4107,
/// <summary>
/// Used to unlock an Orchestrion Roll.
/// </summary>
OrchestrionRoll = 25183,
/// <summary>
/// Used to unlock portrait designs.
/// </summary>
FramersKit = 29459,
/// <summary>
/// Used to unlock Bozjan Field Notes. These are server-side but are cached client-side.
/// </summary>
FieldNotes = 19743,
/// <summary>
/// Used to unlock an Ornament (fashion accessory).
/// </summary>
Ornament = 20086,
/// <summary>
/// Used to unlock glasses.
/// </summary>
Glasses = 37312,
/// <summary>
/// Used for Company Seal Vouchers, which convert the item into Company Seals when used.<br/>
/// Can be used only if in a Grand Company.<br/>
/// IsUnlocked always returns false.
/// </summary>
CompanySealVouchers = 41120,
}

View file

@ -18,13 +18,14 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Network;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.Sheets;
using Serilog;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
namespace Dalamud.Game.Network.Internal;
/// <summary>
@ -269,12 +270,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private static (ulong UploaderId, uint WorldId) GetUploaderInfo()
{
// TODO: switch to use Dalamuds PlayerState service
var agentLobby = AgentLobby.Instance();
var uploaderId = agentLobby->LobbyData.ContentId;
if (uploaderId == 0)
{
var playerState = PlayerState.Instance();
var playerState = CSPlayerState.Instance();
if (playerState->IsLoaded == 1)
{
uploaderId = playerState->ContentId;

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.PlayerState;
/// <summary>
/// Specifies the mentor certification version for a player.
/// </summary>
public enum MentorVersion : byte
{
/// <summary>
/// Indicates that the player has never held mentor status in any expansion.
/// </summary>
None = 0,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Shadowbringers</c> expansion.
/// </summary>
Shadowbringers = 1,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Endwalker</c> expansion.
/// </summary>
Endwalker = 2,
/// <summary>
/// Indicates that the player was last a mentor during the <c>Dawntrail</c> expansion.
/// </summary>
Dawntrail = 3,
}

View file

@ -0,0 +1,489 @@
namespace Dalamud.Game.PlayerState;
/// <summary>
/// Represents a player's attribute.
/// </summary>
public enum PlayerAttribute
{
/// <summary>
/// Strength.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
Strength = 1,
/// <summary>
/// Dexterity.
/// </summary>
/// <remarks>
/// Affects physical damage dealt by rogue's arms, viper's arms, archer's arms, machinist's arms, and dancer's arms.
/// </remarks>
Dexterity = 2,
/// <summary>
/// Vitality.
/// </summary>
/// <remarks>
/// Affects maximum HP.
/// </remarks>
Vitality = 3,
/// <summary>
/// Intelligence.
/// </summary>
/// <remarks>
/// Affects attack magic potency when role is DPS.
/// </remarks>
Intelligence = 4,
/// <summary>
/// Mind.
/// </summary>
/// <remarks>
/// Affects healing magic potency. Also affects attack magic potency when role is Healer.
/// </remarks>
Mind = 5,
/// <summary>
/// Piety.
/// </summary>
/// <remarks>
/// Affects MP regeneration. Regeneration rate is determined by piety. Only applicable when in battle and role is Healer.
/// </remarks>
Piety = 6,
/// <summary>
/// Health Points.
/// </summary>
HP = 7,
/// <summary>
/// Mana Points.
/// </summary>
MP = 8,
/// <summary>
/// Tactical Points.
/// </summary>
TP = 9,
/// <summary>
/// Gathering Point.
/// </summary>
GP = 10,
/// <summary>
/// Crafting Points.
/// </summary>
CP = 11,
/// <summary>
/// Physical Damage.
/// </summary>
PhysicalDamage = 12,
/// <summary>
/// Magic Damage.
/// </summary>
MagicDamage = 13,
/// <summary>
/// Delay.
/// </summary>
Delay = 14,
/// <summary>
/// Additional Effect.
/// </summary>
AdditionalEffect = 15,
/// <summary>
/// Attack Speed.
/// </summary>
AttackSpeed = 16,
/// <summary>
/// Block Rate.
/// </summary>
BlockRate = 17,
/// <summary>
/// Block Strength.
/// </summary>
BlockStrength = 18,
/// <summary>
/// Tenacity.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
Tenacity = 19,
/// <summary>
/// Attack Power.
/// </summary>
/// <remarks>
/// Affects amount of damage dealt by physical attacks. The higher the value, the more damage dealt.
/// </remarks>
AttackPower = 20,
/// <summary>
/// Defense.
/// </summary>
/// <remarks>
/// Affects the amount of damage taken by physical attacks. The higher the value, the less damage taken.
/// </remarks>
Defense = 21,
/// <summary>
/// Direct Hit Rate.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
DirectHitRate = 22,
/// <summary>
/// Evasion.
/// </summary>
Evasion = 23,
/// <summary>
/// Magic Defense.
/// </summary>
/// <remarks>
/// Affects the amount of damage taken by magic attacks. The higher the value, the less damage taken.
/// </remarks>
MagicDefense = 24,
/// <summary>
/// Critical Hit Power.
/// </summary>
CriticalHitPower = 25,
/// <summary>
/// Critical Hit Resilience.
/// </summary>
CriticalHitResilience = 26,
/// <summary>
/// Critical Hit.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
CriticalHit = 27,
/// <summary>
/// Critical Hit Evasion.
/// </summary>
CriticalHitEvasion = 28,
/// <summary>
/// Slashing Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by slashing attacks.
/// </remarks>
SlashingResistance = 29,
/// <summary>
/// Piercing Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by piercing attacks.
/// </remarks>
PiercingResistance = 30,
/// <summary>
/// Blunt Resistance.
/// </summary>
/// <remarks>
/// Decreases damage done by blunt attacks.
/// </remarks>
BluntResistance = 31,
/// <summary>
/// Projectile Resistance.
/// </summary>
ProjectileResistance = 32,
/// <summary>
/// Attack Magic Potency.
/// </summary>
/// <remarks>
/// Affects the amount of damage dealt by magic attacks.
/// </remarks>
AttackMagicPotency = 33,
/// <summary>
/// Healing Magic Potency.
/// </summary>
/// <remarks>
/// Affects the amount of HP restored via healing magic.
/// </remarks>
HealingMagicPotency = 34,
/// <summary>
/// Enhancement Magic Potency.
/// </summary>
EnhancementMagicPotency = 35,
/// <summary>
/// Elemental Bonus.
/// </summary>
ElementalBonus = 36,
/// <summary>
/// Fire Resistance.
/// </summary>
/// <remarks>
/// Decreases fire-aspected damage.
/// </remarks>
FireResistance = 37,
/// <summary>
/// Ice Resistance.
/// </summary>
/// <remarks>
/// Decreases ice-aspected damage.
/// </remarks>
IceResistance = 38,
/// <summary>
/// Wind Resistance.
/// </summary>
/// <remarks>
/// Decreases wind-aspected damage.
/// </remarks>
WindResistance = 39,
/// <summary>
/// Earth Resistance.
/// </summary>
/// <remarks>
/// Decreases earth-aspected damage.
/// </remarks>
EarthResistance = 40,
/// <summary>
/// Lightning Resistance.
/// </summary>
/// <remarks>
/// Decreases lightning-aspected damage.
/// </remarks>
LightningResistance = 41,
/// <summary>
/// Water Resistance.
/// </summary>
/// <remarks>
/// Decreases water-aspected damage.
/// </remarks>
WaterResistance = 42,
/// <summary>
/// Magic Resistance.
/// </summary>
MagicResistance = 43,
/// <summary>
/// Determination.
/// </summary>
/// <remarks>
/// Affects the amount of damage dealt by both physical and magic attacks, as well as the amount of HP restored by healing spells.
/// </remarks>
Determination = 44,
/// <summary>
/// Skill Speed.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
SkillSpeed = 45,
/// <summary>
/// Spell Speed.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
SpellSpeed = 46,
/// <summary>
/// Haste.
/// </summary>
Haste = 47,
/// <summary>
/// Morale.
/// </summary>
/// <remarks>
/// In PvP, replaces physical and magical defense in determining damage inflicted by other players. Also influences the amount of damage dealt to other players.
/// </remarks>
Morale = 48,
/// <summary>
/// Enmity.
/// </summary>
Enmity = 49,
/// <summary>
/// Enmity Reduction.
/// </summary>
EnmityReduction = 50,
/// <summary>
/// Desynthesis Skill Gain.
/// </summary>
DesynthesisSkillGain = 51,
/// <summary>
/// EXP Bonus.
/// </summary>
EXPBonus = 52,
/// <summary>
/// Regen.
/// </summary>
Regen = 53,
/// <summary>
/// Special Attribute.
/// </summary>
SpecialAttribute = 54,
/// <summary>
/// Main Attribute.
/// </summary>
MainAttribute = 55,
/// <summary>
/// Secondary Attribute.
/// </summary>
SecondaryAttribute = 56,
/// <summary>
/// Slow Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of slow.
/// </remarks>
SlowResistance = 57,
/// <summary>
/// Petrification Resistance.
/// </summary>
PetrificationResistance = 58,
/// <summary>
/// Paralysis Resistance.
/// </summary>
ParalysisResistance = 59,
/// <summary>
/// Silence Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of silence.
/// </remarks>
SilenceResistance = 60,
/// <summary>
/// Blind Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of blind.
/// </remarks>
BlindResistance = 61,
/// <summary>
/// Poison Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of poison.
/// </remarks>
PoisonResistance = 62,
/// <summary>
/// Stun Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of stun.
/// </remarks>
StunResistance = 63,
/// <summary>
/// Sleep Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of sleep.
/// </remarks>
SleepResistance = 64,
/// <summary>
/// Bind Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of bind.
/// </remarks>
BindResistance = 65,
/// <summary>
/// Heavy Resistance.
/// </summary>
/// <remarks>
/// Shortens the duration of heavy.
/// </remarks>
HeavyResistance = 66,
/// <summary>
/// Doom Resistance.
/// </summary>
DoomResistance = 67,
/// <summary>
/// Reduced Durability Loss.
/// </summary>
ReducedDurabilityLoss = 68,
/// <summary>
/// Increased Spiritbond Gain.
/// </summary>
IncreasedSpiritbondGain = 69,
/// <summary>
/// Craftsmanship.
/// </summary>
/// <remarks>
/// Affects the amount of progress achieved in a single synthesis step.
/// </remarks>
Craftsmanship = 70,
/// <summary>
/// Control.
/// </summary>
/// <remarks>
/// Affects the amount of quality improved in a single synthesis step.
/// </remarks>
Control = 71,
/// <summary>
/// Gathering.
/// </summary>
/// <remarks>
/// Affects the rate at which items are gathered.
/// </remarks>
Gathering = 72,
/// <summary>
/// Perception.
/// </summary>
/// <remarks>
/// Affects item yield when gathering as a botanist or miner, and the size of fish when fishing or spearfishing.
/// </remarks>
Perception = 73,
}

View file

@ -0,0 +1,588 @@
using Dalamud.Data;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Component.Exd;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
namespace Dalamud.Game.PlayerState;
/// <summary>
/// This class represents the state of the players unlocks.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState
{
/// <inheritdoc/>
public bool IsActionUnlocked(Lumina.Excel.Sheets.Action row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
}
/// <inheritdoc/>
public bool IsAetherCurrentUnlocked(AetherCurrent row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsAetherCurrentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsAetherCurrentZoneComplete(row.RowId);
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public bool IsBannerBgUnlocked(BannerBg row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public bool IsBannerDecorationUnlocked(BannerDecoration row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerFacialUnlocked(BannerFacial row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerFrameUnlocked(BannerFrame row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBannerTimelineUnlocked(BannerTimeline row)
{
return row.UnlockCondition.IsValid && this.IsBannerConditionUnlocked(row.UnlockCondition.Value);
}
/// <inheritdoc/>
public bool IsBuddyActionUnlocked(BuddyAction row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsBuddyEquipUnlocked(BuddyEquip row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row)
{
return row.IsPurchasable && this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsChocoboTaxiUnlocked(ChocoboTaxi row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsChocoboTaxiStandUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCompanionUnlocked(Companion row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsCompanionUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsCraftActionUnlocked(CraftAction row)
{
return this.IsUnlockLinkUnlocked(row.QuestRequirement.RowId);
}
/// <inheritdoc/>
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsEmoteUnlocked(Emote row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsGeneralActionUnlocked(GeneralAction row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsGlassesUnlocked(Glasses row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsGlassesUnlocked((ushort)row.RowId);
}
/// <inheritdoc/>
public bool IsHowToUnlocked(HowTo row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsHowToUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsInstanceContentUnlocked(Lumina.Excel.Sheets.InstanceContent row)
{
if (!this.IsLoaded)
return false;
return UIState.IsInstanceContentUnlocked(row.RowId);
}
/// <inheritdoc/>
public unsafe bool IsItemUnlocked(Item item)
{
if (item.ItemAction.RowId == 0)
return false;
if (!this.IsLoaded)
return false;
// To avoid the ExdModule.GetItemRowById call, which can return null if the excel page
// is not loaded, we're going to imitate the IsItemActionUnlocked call first:
switch ((ItemActionType)item.ItemAction.Value.Type)
{
case ItemActionType.Companion:
return UIState.Instance()->IsCompanionUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.BuddyEquip:
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.Mount:
return CSPlayerState.Instance()->IsMountUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.SecretRecipeBook:
return CSPlayerState.Instance()->IsSecretRecipeBookUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.UnlockLink:
return UIState.Instance()->IsUnlockLinkUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.TripleTriadCard when item.AdditionalData.Is<TripleTriadCard>():
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)item.AdditionalData.RowId);
case ItemActionType.FolkloreTome:
return CSPlayerState.Instance()->IsFolkloreBookUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.OrchestrionRoll when item.AdditionalData.Is<Orchestrion>():
return CSPlayerState.Instance()->IsOrchestrionRollUnlocked(item.AdditionalData.RowId);
case ItemActionType.FramersKit:
return CSPlayerState.Instance()->IsFramersKitUnlocked(item.AdditionalData.RowId);
case ItemActionType.Ornament:
return CSPlayerState.Instance()->IsOrnamentUnlocked(item.ItemAction.Value.Data[0]);
case ItemActionType.Glasses:
return CSPlayerState.Instance()->IsGlassesUnlocked((ushort)item.AdditionalData.RowId);
case ItemActionType.CompanySealVouchers:
return false;
}
var row = ExdModule.GetItemRowById(item.RowId);
return row != null && UIState.Instance()->IsItemActionUnlocked(row) == 1;
}
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row)
{
return CSPlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsMJILandmarkUnlocked(MJILandmark row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsMountUnlocked(Mount row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsMountUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsNotebookDivisionUnlocked(NotebookDivision row)
{
return this.IsUnlockLinkUnlocked(row.QuestUnlock.RowId);
}
/// <inheritdoc/>
public bool IsOrchestrionUnlocked(Orchestrion row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsOrchestrionRollUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsOrnamentUnlocked(Ornament row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsOrnamentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsPerformUnlocked(Perform row)
{
return this.IsUnlockLinkUnlocked((uint)row.UnlockLink);
}
/// <inheritdoc/>
public bool IsPublicContentUnlocked(PublicContent row)
{
if (!this.IsLoaded)
return false;
return UIState.IsPublicContentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row)
{
if (!this.IsLoaded)
return false;
return CSPlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsTraitUnlocked(Trait row)
{
return this.IsUnlockLinkUnlocked(row.Quest.RowId);
}
/// <inheritdoc/>
public bool IsTripleTriadCardUnlocked(TripleTriadCard row)
{
if (!this.IsLoaded)
return false;
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.RowId);
}
/// <inheritdoc/>
public bool IsItemUnlockable(Item item)
{
if (item.ItemAction.RowId == 0)
return false;
return (ItemActionType)item.ItemAction.Value.Type is
ItemActionType.Companion or
ItemActionType.BuddyEquip or
ItemActionType.Mount or
ItemActionType.SecretRecipeBook or
ItemActionType.UnlockLink or
ItemActionType.TripleTriadCard or
ItemActionType.FolkloreTome or
ItemActionType.OrchestrionRoll or
ItemActionType.FramersKit or
ItemActionType.Ornament or
ItemActionType.Glasses;
}
/// <inheritdoc/>
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>
{
return this.IsRowRefUnlocked((RowRef)rowRef);
}
/// <inheritdoc/>
public bool IsRowRefUnlocked(RowRef rowRef)
{
if (!this.IsLoaded || rowRef.IsUntyped)
return false;
if (rowRef.TryGetValue<Lumina.Excel.Sheets.Action>(out var actionRow))
return this.IsActionUnlocked(actionRow);
if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow))
return this.IsAetherCurrentUnlocked(aetherCurrentRow);
if (rowRef.TryGetValue<AetherCurrentCompFlgSet>(out var aetherCurrentCompFlgSetRow))
return this.IsAetherCurrentCompFlgSetUnlocked(aetherCurrentCompFlgSetRow);
if (rowRef.TryGetValue<AozAction>(out var aozActionRow))
return this.IsAozActionUnlocked(aozActionRow);
if (rowRef.TryGetValue<BannerBg>(out var bannerBgRow))
return this.IsBannerBgUnlocked(bannerBgRow);
if (rowRef.TryGetValue<BannerCondition>(out var bannerConditionRow))
return this.IsBannerConditionUnlocked(bannerConditionRow);
if (rowRef.TryGetValue<BannerDecoration>(out var bannerDecorationRow))
return this.IsBannerDecorationUnlocked(bannerDecorationRow);
if (rowRef.TryGetValue<BannerFacial>(out var bannerFacialRow))
return this.IsBannerFacialUnlocked(bannerFacialRow);
if (rowRef.TryGetValue<BannerFrame>(out var bannerFrameRow))
return this.IsBannerFrameUnlocked(bannerFrameRow);
if (rowRef.TryGetValue<BannerTimeline>(out var bannerTimelineRow))
return this.IsBannerTimelineUnlocked(bannerTimelineRow);
if (rowRef.TryGetValue<BuddyAction>(out var buddyActionRow))
return this.IsBuddyActionUnlocked(buddyActionRow);
if (rowRef.TryGetValue<BuddyEquip>(out var buddyEquipRow))
return this.IsBuddyEquipUnlocked(buddyEquipRow);
if (rowRef.TryGetValue<CSBonusContentType>(out var csBonusContentTypeRow))
return this.IsCSBonusContentTypeUnlocked(csBonusContentTypeRow);
if (rowRef.TryGetValue<CharaMakeCustomize>(out var charaMakeCustomizeRow))
return this.IsCharaMakeCustomizeUnlocked(charaMakeCustomizeRow);
if (rowRef.TryGetValue<ChocoboTaxi>(out var chocoboTaxiRow))
return this.IsChocoboTaxiUnlocked(chocoboTaxiRow);
if (rowRef.TryGetValue<Companion>(out var companionRow))
return this.IsCompanionUnlocked(companionRow);
if (rowRef.TryGetValue<CraftAction>(out var craftActionRow))
return this.IsCraftActionUnlocked(craftActionRow);
if (rowRef.TryGetValue<Emote>(out var emoteRow))
return this.IsEmoteUnlocked(emoteRow);
if (rowRef.TryGetValue<GeneralAction>(out var generalActionRow))
return this.IsGeneralActionUnlocked(generalActionRow);
if (rowRef.TryGetValue<Glasses>(out var glassesRow))
return this.IsGlassesUnlocked(glassesRow);
if (rowRef.TryGetValue<HowTo>(out var howToRow))
return this.IsHowToUnlocked(howToRow);
if (rowRef.TryGetValue<Lumina.Excel.Sheets.InstanceContent>(out var instanceContentRow))
return this.IsInstanceContentUnlocked(instanceContentRow);
if (rowRef.TryGetValue<Item>(out var itemRow))
return this.IsItemUnlocked(itemRow);
if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow))
return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
if (rowRef.TryGetValue<McGuffin>(out var mcGuffinRow))
return this.IsMcGuffinUnlocked(mcGuffinRow);
if (rowRef.TryGetValue<Mount>(out var mountRow))
return this.IsMountUnlocked(mountRow);
if (rowRef.TryGetValue<NotebookDivision>(out var notebookDivisionRow))
return this.IsNotebookDivisionUnlocked(notebookDivisionRow);
if (rowRef.TryGetValue<Orchestrion>(out var orchestrionRow))
return this.IsOrchestrionUnlocked(orchestrionRow);
if (rowRef.TryGetValue<Ornament>(out var ornamentRow))
return this.IsOrnamentUnlocked(ornamentRow);
if (rowRef.TryGetValue<Perform>(out var performRow))
return this.IsPerformUnlocked(performRow);
if (rowRef.TryGetValue<PublicContent>(out var publicContentRow))
return this.IsPublicContentUnlocked(publicContentRow);
if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow))
return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
if (rowRef.TryGetValue<Trait>(out var traitRow))
return this.IsTraitUnlocked(traitRow);
if (rowRef.TryGetValue<TripleTriadCard>(out var tripleTriadCardRow))
return this.IsTripleTriadCardUnlocked(tripleTriadCardRow);
return false;
}
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(ushort unlockLink)
{
if (!this.IsLoaded)
return false;
if (unlockLink == 0)
return false;
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink);
}
/// <inheritdoc/>
public bool IsUnlockLinkUnlocked(uint unlockLink)
{
if (!this.IsLoaded)
return false;
if (unlockLink == 0)
return false;
return UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(unlockLink);
}
private void UpdateUnlocks(bool fireEvent)
{
if (!this.IsLoaded)
return;
this.UpdateUnlocksForSheet<Lumina.Excel.Sheets.Action>(fireEvent);
this.UpdateUnlocksForSheet<AetherCurrent>(fireEvent);
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(fireEvent);
this.UpdateUnlocksForSheet<AozAction>(fireEvent);
this.UpdateUnlocksForSheet<BannerBg>(fireEvent);
this.UpdateUnlocksForSheet<BannerCondition>(fireEvent);
this.UpdateUnlocksForSheet<BannerDecoration>(fireEvent);
this.UpdateUnlocksForSheet<BannerFacial>(fireEvent);
this.UpdateUnlocksForSheet<BannerFrame>(fireEvent);
this.UpdateUnlocksForSheet<BannerTimeline>(fireEvent);
this.UpdateUnlocksForSheet<BuddyAction>(fireEvent);
this.UpdateUnlocksForSheet<BuddyEquip>(fireEvent);
this.UpdateUnlocksForSheet<CSBonusContentType>(fireEvent);
this.UpdateUnlocksForSheet<CharaMakeCustomize>(fireEvent);
this.UpdateUnlocksForSheet<ChocoboTaxi>(fireEvent);
this.UpdateUnlocksForSheet<Companion>(fireEvent);
this.UpdateUnlocksForSheet<CraftAction>(fireEvent);
this.UpdateUnlocksForSheet<Emote>(fireEvent);
this.UpdateUnlocksForSheet<GeneralAction>(fireEvent);
this.UpdateUnlocksForSheet<Glasses>(fireEvent);
this.UpdateUnlocksForSheet<HowTo>(fireEvent);
this.UpdateUnlocksForSheet<Lumina.Excel.Sheets.InstanceContent>(fireEvent);
this.UpdateUnlocksForSheet<Item>(fireEvent);
this.UpdateUnlocksForSheet<MJILandmark>(fireEvent);
this.UpdateUnlocksForSheet<McGuffin>(fireEvent);
this.UpdateUnlocksForSheet<Mount>(fireEvent);
this.UpdateUnlocksForSheet<NotebookDivision>(fireEvent);
this.UpdateUnlocksForSheet<Orchestrion>(fireEvent);
this.UpdateUnlocksForSheet<Ornament>(fireEvent);
this.UpdateUnlocksForSheet<Perform>(fireEvent);
this.UpdateUnlocksForSheet<PublicContent>(fireEvent);
this.UpdateUnlocksForSheet<SecretRecipeBook>(fireEvent);
this.UpdateUnlocksForSheet<Trait>(fireEvent);
this.UpdateUnlocksForSheet<TripleTriadCard>(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<T>(bool fireEvent = true) where T : struct, IExcelRow<T>
{
var unlockedRowIds = this.cachedUnlockedRowIds.GetOrAdd(typeof(T), _ => []);
foreach (var row in this.dataManager.GetExcelSheet<T>())
{
if (unlockedRowIds.Contains(row.RowId))
continue;
var rowRef = LuminaUtils.CreateRef<T>(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);
}
}
}
}
}
}

View file

@ -0,0 +1,281 @@
using Dalamud.Data;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using GrandCompany = Lumina.Excel.Sheets.GrandCompany;
namespace Dalamud.Game.PlayerState;
/// <summary>
/// This class contains the PlayerState wrappers.
/// </summary>
internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState
{
/// <summary>
/// Gets a value indicating whether the local character is loaded.
/// </summary>
/// <remarks>
/// This is equivalent with being logged in.<br/>
/// The actual GameObject will not immediately exist when this changes to true.
/// </remarks>
public bool IsLoaded => CSPlayerState.Instance()->IsLoaded == 1;
/// <summary>
/// Gets the name of the local character.
/// </summary>
public string CharacterName => this.IsLoaded ? CSPlayerState.Instance()->CharacterNameString : string.Empty;
/// <summary>
/// Gets the entity ID of the local character.
/// </summary>
public uint EntityId => this.IsLoaded ? CSPlayerState.Instance()->EntityId : default;
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
public ulong ContentId => this.IsLoaded ? CSPlayerState.Instance()->ContentId : default;
/// <summary>
/// Gets the World row for the local character's current world.
/// </summary>
public RowRef<World> CurrentWorld
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby->IsLoggedIn
? LuminaUtils.CreateRef<World>(agentLobby->LobbyData.CurrentWorldId)
: default;
}
}
/// <summary>
/// Gets the World row for the local character's home world.
/// </summary>
public RowRef<World> HomeWorld
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby->IsLoggedIn
? LuminaUtils.CreateRef<World>(agentLobby->LobbyData.HomeWorldId)
: default;
}
}
/// <summary>
/// Gets the sex of the local character.
/// </summary>
public Sex Sex => this.IsLoaded ? (Sex)CSPlayerState.Instance()->Sex : default;
/// <summary>
/// Gets the Race row for the local character.
/// </summary>
public RowRef<Race> Race => this.IsLoaded ? LuminaUtils.CreateRef<Race>(CSPlayerState.Instance()->Race) : default;
/// <summary>
/// Gets the Tribe row for the local character.
/// </summary>
public RowRef<Tribe> Tribe => this.IsLoaded ? LuminaUtils.CreateRef<Tribe>(CSPlayerState.Instance()->Tribe) : default;
/// <summary>
/// Gets the ClassJob row for the local character's current class/job.
/// </summary>
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
/// <summary>
/// Gets the current class/job's level of the local character.
/// </summary>
public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default;
/// <summary>
/// Gets a value indicating whether the local character's level is synced.
/// </summary>
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced == 1;
/// <summary>
/// Gets the effective level of the local character.
/// </summary>
public short EffectiveLevel => this.IsLoaded ? (this.IsLevelSynced ? CSPlayerState.Instance()->SyncedLevel : CSPlayerState.Instance()->CurrentLevel) : default;
/// <summary>
/// Gets the GuardianDeity row for the local character.
/// </summary>
public RowRef<GuardianDeity> GuardianDeity => this.IsLoaded ? LuminaUtils.CreateRef<GuardianDeity>(CSPlayerState.Instance()->GuardianDeity) : default;
/// <summary>
/// Gets the birth month of the local character.
/// </summary>
public byte BirthMonth => this.IsLoaded ? CSPlayerState.Instance()->BirthMonth : default;
/// <summary>
/// Gets the birth day of the local character.
/// </summary>
public byte BirthDay => this.IsLoaded ? CSPlayerState.Instance()->BirthDay : default;
/// <summary>
/// Gets the ClassJob row for the local character's starting class.
/// </summary>
public RowRef<ClassJob> FirstClass => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->FirstClass) : default;
/// <summary>
/// Gets the Town row for the local character's starting town.
/// </summary>
public RowRef<Town> StartTown => this.IsLoaded ? LuminaUtils.CreateRef<Town>(CSPlayerState.Instance()->StartTown) : default;
/// <summary>
/// Gets the base strength of the local character.
/// </summary>
public int BaseStrength => this.IsLoaded ? CSPlayerState.Instance()->BaseStrength : default;
/// <summary>
/// Gets the base dexterity of the local character.
/// </summary>
public int BaseDexterity => this.IsLoaded ? CSPlayerState.Instance()->BaseDexterity : default;
/// <summary>
/// Gets the base vitality of the local character.
/// </summary>
public int BaseVitality => this.IsLoaded ? CSPlayerState.Instance()->BaseVitality : default;
/// <summary>
/// Gets the base intelligence of the local character.
/// </summary>
public int BaseIntelligence => this.IsLoaded ? CSPlayerState.Instance()->BaseIntelligence : default;
/// <summary>
/// Gets the base mind of the local character.
/// </summary>
public int BaseMind => this.IsLoaded ? CSPlayerState.Instance()->BaseMind : default;
/// <summary>
/// Gets the piety mind of the local character.
/// </summary>
public int BasePiety => this.IsLoaded ? CSPlayerState.Instance()->BasePiety : default;
/// <summary>
/// Gets the GrandCompany row for the local character's current Grand Company affiliation.
/// </summary>
public RowRef<GrandCompany> GrandCompany => this.IsLoaded ? LuminaUtils.CreateRef<GrandCompany>(CSPlayerState.Instance()->GrandCompany) : default;
/// <summary>
/// Gets the Aetheryte row for the local character's home aetheryte.
/// </summary>
public RowRef<Aetheryte> HomeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef<Aetheryte>(CSPlayerState.Instance()->HomeAetheryteId) : default;
/// <summary>
/// Gets a span of Aetheryte rows for the local character's favourite aetherytes.
/// </summary>
public ReadOnlySpan<RowRef<Aetheryte>> FavouriteAetherytes
{
get
{
var playerState = CSPlayerState.Instance();
if (playerState->IsLoaded != 1 || playerState->FavouriteAetheryteCount == 0)
return [];
var count = playerState->FavouriteAetheryteCount;
var array = new RowRef<Aetheryte>[count];
for (var i = 0; i < count; i++)
array[i] = LuminaUtils.CreateRef<Aetheryte>(playerState->FavouriteAetherytes[i]);
return array;
}
}
/// <summary>
/// Gets the Aetheryte row for the local character's free aetheryte.
/// </summary>
public RowRef<Aetheryte> FreeAetheryte => this.IsLoaded ? LuminaUtils.CreateRef<Aetheryte>(CSPlayerState.Instance()->FreeAetheryteId) : default;
/// <summary>
/// Gets the amount of received player commendations of the local character.
/// </summary>
public uint BaseRestedExperience => this.IsLoaded ? CSPlayerState.Instance()->BaseRestedExperience : default;
/// <summary>
/// Gets the amount of received player commendations of the local character.
/// </summary>
public short PlayerCommendations => this.IsLoaded ? CSPlayerState.Instance()->PlayerCommendations : default;
/// <summary>
/// Gets the Carrier Level of Delivery Moogle Quests of the local character.
/// </summary>
public byte DeliveryLevel => this.IsLoaded ? CSPlayerState.Instance()->DeliveryLevel : default;
/// <summary>
/// Gets the mentor version of the local character.
/// </summary>
public MentorVersion MentorVersion => this.IsLoaded ? (MentorVersion)CSPlayerState.Instance()->MentorVersion : default;
/// <summary>
/// Gets the value of an attribute of the local character.
/// </summary>
/// <param name="attribute">The attribute to check.</param>
/// <returns>The value of the specific attribute.</returns>
public int GetAttribute(PlayerAttribute attribute) => this.IsLoaded ? CSPlayerState.Instance()->Attributes[(int)attribute] : default;
/// <summary>
/// Gets the Grand Company rank of the local character.
/// </summary>
/// <param name="grandCompany">The Grand Company to check.</param>
/// <returns>The Grand Company rank of the local character.</returns>
public byte GetGrandCompanyRank(GrandCompany grandCompany)
{
var playerState = CSPlayerState.Instance();
if (playerState->IsLoaded != 1)
return default;
return grandCompany.RowId switch
{
1 => playerState->GCRankMaelstrom,
2 => playerState->GCRankTwinAdders,
3 => playerState->GCRankImmortalFlames,
_ => default,
};
}
/// <summary>
/// Gets the level of the local character's class/job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The level of the requested class/job.</returns>
public short GetClassJobLevel(ClassJob classJob) => this.IsLoaded ? CSPlayerState.Instance()->ClassJobLevels[classJob.ExpArrayIndex] : default;
/// <summary>
/// Gets the experience of the local character's class/job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The experience of the requested class/job.</returns>
public int GetClassJobExperience(ClassJob classJob)
{
var playerState = CSPlayerState.Instance();
if (playerState->IsLoaded != 1)
return default;
return playerState->ClassJobExperience[classJob.ExpArrayIndex];
}
/// <summary>
/// Gets the desynthesis level of the local character's crafter job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The desynthesis level of the requested crafter job.</returns>
public float GetDesynthesisLevel(ClassJob classJob)
{
if (classJob.ExpArrayIndex == -1)
return default;
var playerState = CSPlayerState.Instance();
if (playerState->IsLoaded != 1)
return default;
return playerState->DesynthesisLevels[classJob.DohDolJobIndex] / 100f;
}
}

View file

@ -0,0 +1,153 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace Dalamud.Game.PlayerState;
/// <summary>
/// This class represents the state of the local player.
/// </summary>
internal unsafe partial class PlayerState : IInternalDisposableService, IPlayerState
{
private static readonly ModuleLog Log = new("PlayerState");
private readonly PlayerStateAddressResolver address;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<PerformMateriaActionMigrationDelegate> performMateriaActionMigrationDelegateHook;
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceConstructor]
private PlayerState(TargetSigScanner sigScanner)
{
this.address = new PlayerStateAddressResolver();
this.address.Setup(sigScanner);
this.clientState.Login += this.OnLogin;
this.clientState.Logout += this.OnLogout;
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress(
(nint)UIModule.StaticVirtualTablePointer->HandlePacket,
this.UIModuleHandlePacketDetour);
this.performMateriaActionMigrationDelegateHook = Hook<PerformMateriaActionMigrationDelegate>.FromAddress(
this.address.PerformMateriaActionMigration,
this.PerformMateriaActionMigrationDetour);
this.uiModuleHandlePacketHook.Enable();
this.performMateriaActionMigrationDelegateHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PerformMateriaActionMigrationDelegate(RaptureHotbarModule* thisPtr);
/// <inheritdoc/>
public event IPlayerState.ClassJobChangeDelegate? ClassJobChange;
/// <inheritdoc/>
public event IPlayerState.LevelChangeDelegate? LevelChange;
/// <inheritdoc/>
public event IPlayerState.UnlockDelegate Unlock;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.clientState.Login -= this.OnLogin;
this.clientState.Logout -= this.OnLogout;
this.uiModuleHandlePacketHook.Dispose();
this.performMateriaActionMigrationDelegateHook.Dispose();
}
private void OnLogin()
{
try
{
this.UpdateUnlocks(false);
}
catch (Exception ex)
{
Log.Error(ex, "Error during initial unlock check");
}
}
private void OnLogout(int type, int code)
{
this.cachedUnlockedRowIds.Clear();
}
private unsafe void UIModuleHandlePacketDetour(
UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{
this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet);
switch (type)
{
case UIModulePacketType.ClassJobChange:
{
var classJobId = uintParam;
foreach (var action in Delegate.EnumerateInvocationList(this.ClassJobChange))
{
try
{
action(classJobId);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
case UIModulePacketType.LevelChange:
{
var classJobId = *(uint*)packet;
var level = *(ushort*)((nint)packet + 4);
foreach (var action in Delegate.EnumerateInvocationList(this.LevelChange))
{
try
{
action(classJobId, level);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
}
}
private void PerformMateriaActionMigrationDetour(RaptureHotbarModule* thisPtr)
{
try
{
this.UpdateUnlocks(true);
}
catch (Exception ex)
{
Log.Error(ex, "Error during unlock check");
}
this.performMateriaActionMigrationDelegateHook.Original(thisPtr);
}
}

View file

@ -0,0 +1,24 @@
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.PlayerState;
/// <summary>
/// Unlock state memory address resolver.
/// </summary>
internal class PlayerStateAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of a method which is called when <see cref="RaptureAtkModule.AgentUpdateFlag"/> has <see cref="RaptureAtkModule.AgentUpdateFlags.UnlocksUpdate"/>.
/// </summary>
public nint PerformMateriaActionMigration { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
// RaptureHotbarModule.PerformMateriaActionMigration
this.PerformMateriaActionMigration = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 8B 01");
}
}

View file

@ -0,0 +1,335 @@
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.PlayerState;
/// <summary>
/// This class represents the state of the local player.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IPlayerState>]
#pragma warning restore SA1015
internal unsafe class PlayerStatePluginScoped : IInternalDisposableService, IPlayerState
{
[ServiceManager.ServiceDependency]
private readonly PlayerState playerStateService = Service<PlayerState>.Get();
private readonly LocalPlugin plugin;
/// <summary>
/// Initializes a new instance of the <see cref="PlayerStatePluginScoped"/> class.
/// </summary>
/// <param name="plugin">The plugin.</param>
internal PlayerStatePluginScoped(LocalPlugin plugin)
{
this.plugin = plugin;
this.playerStateService.ClassJobChange += this.OnClassJobChange;
this.playerStateService.LevelChange += this.OnLevelChange;
this.playerStateService.Unlock += this.OnUnlock;
}
/// <inheritdoc />
public event IPlayerState.ClassJobChangeDelegate? ClassJobChange;
/// <inheritdoc />
public event IPlayerState.LevelChangeDelegate? LevelChange;
/// <inheritdoc />
public event IPlayerState.UnlockDelegate? Unlock;
/// <inheritdoc />
public bool IsLoaded => this.playerStateService.IsLoaded;
/// <inheritdoc />
public string CharacterName => this.playerStateService.CharacterName;
/// <inheritdoc />
public uint EntityId => this.playerStateService.EntityId;
/// <inheritdoc />
public ulong ContentId => this.playerStateService.ContentId;
/// <inheritdoc />
public RowRef<World> CurrentWorld => this.playerStateService.CurrentWorld;
/// <inheritdoc />
public RowRef<World> HomeWorld => this.playerStateService.HomeWorld;
/// <inheritdoc />
public Sex Sex => this.playerStateService.Sex;
/// <inheritdoc />
public RowRef<Race> Race => this.playerStateService.Race;
/// <inheritdoc />
public RowRef<Tribe> Tribe => this.playerStateService.Tribe;
/// <inheritdoc />
public RowRef<ClassJob> ClassJob => this.playerStateService.ClassJob;
/// <inheritdoc />
public short Level => this.playerStateService.Level;
/// <inheritdoc />
public bool IsLevelSynced => this.playerStateService.IsLevelSynced;
/// <inheritdoc />
public short EffectiveLevel => this.playerStateService.EffectiveLevel;
/// <inheritdoc />
public RowRef<GuardianDeity> GuardianDeity => this.playerStateService.GuardianDeity;
/// <inheritdoc />
public byte BirthMonth => this.playerStateService.BirthMonth;
/// <inheritdoc />
public byte BirthDay => this.playerStateService.BirthDay;
/// <inheritdoc />
public RowRef<ClassJob> FirstClass => this.playerStateService.FirstClass;
/// <inheritdoc />
public RowRef<Town> StartTown => this.playerStateService.StartTown;
/// <inheritdoc />
public int BaseStrength => this.playerStateService.BaseStrength;
/// <inheritdoc />
public int BaseDexterity => this.playerStateService.BaseDexterity;
/// <inheritdoc />
public int BaseVitality => this.playerStateService.BaseVitality;
/// <inheritdoc />
public int BaseIntelligence => this.playerStateService.BaseIntelligence;
/// <inheritdoc />
public int BaseMind => this.playerStateService.BaseMind;
/// <inheritdoc />
public int BasePiety => this.playerStateService.BasePiety;
/// <inheritdoc />
public RowRef<GrandCompany> GrandCompany => this.playerStateService.GrandCompany;
/// <inheritdoc />
public RowRef<Aetheryte> HomeAetheryte => this.playerStateService.HomeAetheryte;
/// <inheritdoc />
public ReadOnlySpan<RowRef<Aetheryte>> FavouriteAetherytes => this.playerStateService.FavouriteAetherytes;
/// <inheritdoc />
public RowRef<Aetheryte> FreeAetheryte => this.playerStateService.FreeAetheryte;
/// <inheritdoc />
public uint BaseRestedExperience => this.playerStateService.BaseRestedExperience;
/// <inheritdoc />
public short PlayerCommendations => this.playerStateService.PlayerCommendations;
/// <inheritdoc />
public byte DeliveryLevel => this.playerStateService.DeliveryLevel;
/// <inheritdoc />
public MentorVersion MentorVersion => this.playerStateService.MentorVersion;
/// <inheritdoc />
public int GetAttribute(PlayerAttribute attribute)
=> this.playerStateService.GetAttribute(attribute);
/// <inheritdoc />
public int GetClassJobExperience(ClassJob classJob)
=> this.playerStateService.GetClassJobExperience(classJob);
/// <inheritdoc />
public short GetClassJobLevel(ClassJob classJob)
=> this.playerStateService.GetClassJobLevel(classJob);
/// <inheritdoc />
public float GetDesynthesisLevel(ClassJob classJob)
=> this.playerStateService.GetDesynthesisLevel(classJob);
/// <inheritdoc />
public byte GetGrandCompanyRank(GrandCompany grandCompany)
=> this.playerStateService.GetGrandCompanyRank(grandCompany);
/// <inheritdoc />
public bool IsActionUnlocked(Lumina.Excel.Sheets.Action row)
=> this.playerStateService.IsActionUnlocked(row);
/// <inheritdoc />
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row)
=> this.playerStateService.IsAetherCurrentCompFlgSetUnlocked(row);
/// <inheritdoc />
public bool IsAetherCurrentUnlocked(AetherCurrent row)
=> this.playerStateService.IsAetherCurrentUnlocked(row);
/// <inheritdoc />
public bool IsAozActionUnlocked(AozAction row)
=> this.playerStateService.IsAozActionUnlocked(row);
/// <inheritdoc />
public bool IsBannerBgUnlocked(BannerBg row)
=> this.playerStateService.IsBannerBgUnlocked(row);
/// <inheritdoc />
public bool IsBannerConditionUnlocked(BannerCondition row)
=> this.playerStateService.IsBannerConditionUnlocked(row);
/// <inheritdoc />
public bool IsBannerDecorationUnlocked(BannerDecoration row)
=> this.playerStateService.IsBannerDecorationUnlocked(row);
/// <inheritdoc />
public bool IsBannerFacialUnlocked(BannerFacial row)
=> this.playerStateService.IsBannerFacialUnlocked(row);
/// <inheritdoc />
public bool IsBannerFrameUnlocked(BannerFrame row)
=> this.playerStateService.IsBannerFrameUnlocked(row);
/// <inheritdoc />
public bool IsBannerTimelineUnlocked(BannerTimeline row)
=> this.playerStateService.IsBannerTimelineUnlocked(row);
/// <inheritdoc />
public bool IsBuddyActionUnlocked(BuddyAction row)
=> this.playerStateService.IsBuddyActionUnlocked(row);
/// <inheritdoc />
public bool IsBuddyEquipUnlocked(BuddyEquip row)
=> this.playerStateService.IsBuddyEquipUnlocked(row);
/// <inheritdoc />
public bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row)
=> this.playerStateService.IsCharaMakeCustomizeUnlocked(row);
/// <inheritdoc />
public bool IsChocoboTaxiUnlocked(ChocoboTaxi row)
=> this.playerStateService.IsChocoboTaxiUnlocked(row);
/// <inheritdoc />
public bool IsCompanionUnlocked(Companion row)
=> this.playerStateService.IsCompanionUnlocked(row);
/// <inheritdoc />
public bool IsCraftActionUnlocked(CraftAction row)
=> this.playerStateService.IsCraftActionUnlocked(row);
/// <inheritdoc />
public bool IsCSBonusContentTypeUnlocked(CSBonusContentType row)
=> this.playerStateService.IsCSBonusContentTypeUnlocked(row);
/// <inheritdoc />
public bool IsEmoteUnlocked(Emote row)
=> this.playerStateService.IsEmoteUnlocked(row);
/// <inheritdoc />
public bool IsGeneralActionUnlocked(GeneralAction row)
=> this.playerStateService.IsGeneralActionUnlocked(row);
/// <inheritdoc />
public bool IsGlassesUnlocked(Glasses row)
=> this.playerStateService.IsGlassesUnlocked(row);
/// <inheritdoc />
public bool IsHowToUnlocked(HowTo row)
=> this.playerStateService.IsHowToUnlocked(row);
/// <inheritdoc />
public bool IsInstanceContentUnlocked(InstanceContent row)
=> this.playerStateService.IsInstanceContentUnlocked(row);
/// <inheritdoc />
public bool IsItemUnlockable(Item item)
=> this.playerStateService.IsItemUnlockable(item);
/// <inheritdoc />
public bool IsItemUnlocked(Item item)
=> this.playerStateService.IsItemUnlocked(item);
/// <inheritdoc />
public bool IsMcGuffinUnlocked(McGuffin row)
=> this.playerStateService.IsMcGuffinUnlocked(row);
/// <inheritdoc />
public bool IsMJILandmarkUnlocked(MJILandmark row)
=> this.playerStateService.IsMJILandmarkUnlocked(row);
/// <inheritdoc />
public bool IsMountUnlocked(Mount row)
=> this.playerStateService.IsMountUnlocked(row);
/// <inheritdoc />
public bool IsNotebookDivisionUnlocked(NotebookDivision row)
=> this.playerStateService.IsNotebookDivisionUnlocked(row);
/// <inheritdoc />
public bool IsOrchestrionUnlocked(Orchestrion row)
=> this.playerStateService.IsOrchestrionUnlocked(row);
/// <inheritdoc />
public bool IsOrnamentUnlocked(Ornament row)
=> this.playerStateService.IsOrnamentUnlocked(row);
/// <inheritdoc />
public bool IsPerformUnlocked(Perform row)
=> this.playerStateService.IsPerformUnlocked(row);
/// <inheritdoc />
public bool IsPublicContentUnlocked(PublicContent row)
=> this.playerStateService.IsPublicContentUnlocked(row);
/// <inheritdoc />
public bool IsRowRefUnlocked(RowRef rowRef)
=> this.playerStateService.IsRowRefUnlocked(rowRef);
/// <inheritdoc />
public bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>
=> this.playerStateService.IsRowRefUnlocked(rowRef);
/// <inheritdoc />
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row)
=> this.playerStateService.IsSecretRecipeBookUnlocked(row);
/// <inheritdoc />
public bool IsTraitUnlocked(Trait row)
=> this.playerStateService.IsTraitUnlocked(row);
/// <inheritdoc />
public bool IsTripleTriadCardUnlocked(TripleTriadCard row)
=> this.playerStateService.IsTripleTriadCardUnlocked(row);
/// <inheritdoc />
public bool IsUnlockLinkUnlocked(uint unlockLink)
=> this.playerStateService.IsUnlockLinkUnlocked(unlockLink);
/// <inheritdoc />
public bool IsUnlockLinkUnlocked(ushort unlockLink)
=> this.playerStateService.IsUnlockLinkUnlocked(unlockLink);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.playerStateService.ClassJobChange -= this.OnClassJobChange;
this.playerStateService.LevelChange -= this.OnLevelChange;
this.playerStateService.Unlock -= this.OnUnlock;
}
private void OnLevelChange(uint classJobId, uint level)
=> this.LevelChange?.Invoke(classJobId, level);
private void OnClassJobChange(uint classJobId)
=> this.ClassJobChange?.Invoke(classJobId);
private void OnUnlock(RowRef rowRef)
=> this.Unlock?.Invoke(rowRef);
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.PlayerState;
/// <summary>
/// Represents the sex of a character.
/// </summary>
public enum Sex : byte
{
/// <summary>
/// Male sex.
/// </summary>
Male = 0,
/// <summary>
/// Female sex.
/// </summary>
Female = 1,
}

View file

@ -35,7 +35,7 @@ using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using AddonSheet = Lumina.Excel.Sheets.Addon;
using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using CSPlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using StatusSheet = Lumina.Excel.Sheets.Status;
namespace Dalamud.Game.Text.Evaluator;
@ -526,7 +526,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return false;
// the game uses LocalPlayer here, but using PlayerState seems more safe.
return this.ResolveStringExpression(in context, PlayerState.Instance()->EntityId == entityId ? eTrue : eFalse);
return this.ResolveStringExpression(in context, CSPlayerState.Instance()->EntityId == entityId ? eTrue : eFalse);
}
private bool TryResolveColor(in SeStringContext context, in ReadOnlySePayloadSpan payload)

View file

@ -9,19 +9,6 @@ namespace Dalamud.Plugin.Services;
/// </summary>
public interface IClientState
{
/// <summary>
/// A delegate type used for the <see cref="ClassJobChanged"/> event.
/// </summary>
/// <param name="classJobId">The new ClassJob id.</param>
public delegate void ClassJobChangeDelegate(uint classJobId);
/// <summary>
/// A delegate type used for the <see cref="LevelChanged"/> event.
/// </summary>
/// <param name="classJobId">The ClassJob id.</param>
/// <param name="level">The level of the corresponding ClassJob.</param>
public delegate void LevelChangeDelegate(uint classJobId, uint level);
/// <summary>
/// A delegate type used for the <see cref="Logout"/> event.
/// </summary>
@ -37,31 +24,33 @@ public interface IClientState
/// <summary>
/// Event that fires when a characters ClassJob changed.
/// </summary>
public event ClassJobChangeDelegate? ClassJobChanged;
[Obsolete("Use IPlayerState.ClassJobChange", true)]
public event IPlayerState.ClassJobChangeDelegate? ClassJobChanged;
/// <summary>
/// Event that fires when <em>any</em> character level changes, including levels
/// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL).
/// </summary>
public event LevelChangeDelegate? LevelChanged;
[Obsolete("Use IPlayerState.LevelChange", true)]
public event IPlayerState.LevelChangeDelegate? LevelChanged;
/// <summary>
/// Event that fires when a character is logging in, and the local character object is available.
/// Event that fires when the local player is logged in, and the local character object is available.
/// </summary>
public event Action Login;
/// <summary>
/// Event that fires when a character is logging out.
/// Event that fires when the local player is logging out.
/// </summary>
public event LogoutDelegate Logout;
/// <summary>
/// Event that fires when a character is entering PvP.
/// Event that fires when the local player is entering a PvP zone.
/// </summary>
public event Action EnterPvP;
/// <summary>
/// Event that fires when a character is leaving PvP.
/// Event that fires when the local player is leaving a PvP zone.
/// </summary>
public event Action LeavePvP;
@ -93,6 +82,7 @@ public interface IClientState
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
[Obsolete("Use IPlayerState.ContentId", true)]
public ulong LocalContentId { get; }
/// <summary>
@ -101,12 +91,12 @@ public interface IClientState
public bool IsLoggedIn { get; }
/// <summary>
/// Gets a value indicating whether the user is playing PvP.
/// Gets a value indicating whether the user is in a PvP zone.
/// </summary>
public bool IsPvP { get; }
/// <summary>
/// Gets a value indicating whether the user is playing PvP, excluding the Wolves' Den.
/// Gets a value indicating whether the user is in a PvP zone, excluding the Wolves' Den.
/// </summary>
public bool IsPvPExcludingDen { get; }

View file

@ -0,0 +1,521 @@
using Dalamud.Game.PlayerState;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Plugin.Services;
#pragma warning disable SA1400 // Access modifier should be declared: Interface members are public by default
/// <summary>
/// Interface for determining unlock state of various content in the game.
/// </summary>
public interface IPlayerState
{
/// <summary>
/// A delegate type used for the <see cref="ClassJobChange"/> event.
/// </summary>
/// <param name="classJobId">The new ClassJob id.</param>
delegate void ClassJobChangeDelegate(uint classJobId);
/// <summary>
/// A delegate type used for the <see cref="LevelChange"/> event.
/// </summary>
/// <param name="classJobId">The ClassJob id.</param>
/// <param name="level">The level of the corresponding ClassJob.</param>
delegate void LevelChangeDelegate(uint classJobId, uint level);
/// <summary>
/// A delegate type used for the <see cref="Unlock"/> event.
/// </summary>
/// <param name="rowRef">A RowRef of the unlocked thing.</param>
delegate void UnlockDelegate(RowRef rowRef);
/// <summary>
/// Event that fires when a characters ClassJob changed.
/// </summary>
event ClassJobChangeDelegate? ClassJobChange;
/// <summary>
/// Event that fires when <em>any</em> character level changes, including levels
/// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL).
/// </summary>
event LevelChangeDelegate? LevelChange;
/// <summary>
/// Event triggered when something was unlocked.
/// </summary>
event UnlockDelegate? Unlock;
/// <summary>
/// Gets a value indicating whether the local character is loaded.
/// </summary>
/// <remarks>
/// The actual GameObject will not immediately exist when this changes to true.
/// </remarks>
bool IsLoaded { get; }
/// <summary>
/// Gets the name of the local character.
/// </summary>
string CharacterName { get; }
/// <summary>
/// Gets the entity ID of the local character.
/// </summary>
uint EntityId { get; }
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
ulong ContentId { get; }
/// <summary>
/// Gets the World row for the local character's current world.
/// </summary>
RowRef<World> CurrentWorld { get; }
/// <summary>
/// Gets the World row for the local character's home world.
/// </summary>
RowRef<World> HomeWorld { get; }
/// <summary>
/// Gets the sex of the local character.
/// </summary>
Sex Sex { get; }
/// <summary>
/// Gets the Race row for the local character.
/// </summary>
RowRef<Race> Race { get; }
/// <summary>
/// Gets the Tribe row for the local character.
/// </summary>
RowRef<Tribe> Tribe { get; }
/// <summary>
/// Gets the ClassJob row for the local character's current class/job.
/// </summary>
RowRef<ClassJob> ClassJob { get; }
/// <summary>
/// Gets the current class/job's level of the local character.
/// </summary>
short Level { get; }
/// <summary>
/// Gets a value indicating whether the local character's level is synced.
/// </summary>
bool IsLevelSynced { get; }
/// <summary>
/// Gets the effective level of the local character.
/// </summary>
short EffectiveLevel { get; }
/// <summary>
/// Gets the GuardianDeity row for the local character.
/// </summary>
RowRef<GuardianDeity> GuardianDeity { get; }
/// <summary>
/// Gets the birth month of the local character.
/// </summary>
byte BirthMonth { get; }
/// <summary>
/// Gets the birth day of the local character.
/// </summary>
byte BirthDay { get; }
/// <summary>
/// Gets the ClassJob row for the local character's starting class.
/// </summary>
RowRef<ClassJob> FirstClass { get; }
/// <summary>
/// Gets the Town row for the local character's starting town.
/// </summary>
RowRef<Town> StartTown { get; }
/// <summary>
/// Gets the base strength of the local character.
/// </summary>
int BaseStrength { get; }
/// <summary>
/// Gets the base dexterity of the local character.
/// </summary>
int BaseDexterity { get; }
/// <summary>
/// Gets the base vitality of the local character.
/// </summary>
int BaseVitality { get; }
/// <summary>
/// Gets the base intelligence of the local character.
/// </summary>
int BaseIntelligence { get; }
/// <summary>
/// Gets the base mind of the local character.
/// </summary>
int BaseMind { get; }
/// <summary>
/// Gets the piety mind of the local character.
/// </summary>
int BasePiety { get; }
/// <summary>
/// Gets the GrandCompany row for the local character's current Grand Company affiliation.
/// </summary>
RowRef<GrandCompany> GrandCompany { get; }
/// <summary>
/// Gets the Aetheryte row for the local character's home aetheryte.
/// </summary>
RowRef<Aetheryte> HomeAetheryte { get; }
/// <summary>
/// Gets a span of Aetheryte rows for the local character's favourite aetherytes.
/// </summary>
ReadOnlySpan<RowRef<Aetheryte>> FavouriteAetherytes { get; }
/// <summary>
/// Gets the Aetheryte row for the local character's free aetheryte.
/// </summary>
RowRef<Aetheryte> FreeAetheryte { get; }
/// <summary>
/// Gets the amount of received player commendations of the local character.
/// </summary>
uint BaseRestedExperience { get; }
/// <summary>
/// Gets the amount of received player commendations of the local character.
/// </summary>
short PlayerCommendations { get; }
/// <summary>
/// Gets the Carrier Level of Delivery Moogle Quests of the local character.
/// </summary>
byte DeliveryLevel { get; }
/// <summary>
/// Gets the mentor version of the local character.
/// </summary>
MentorVersion MentorVersion { get; }
/// <summary>
/// Gets the value of an attribute of the local character.
/// </summary>
/// <param name="attribute">The attribute to check.</param>
/// <returns>The value of the specific attribute.</returns>
int GetAttribute(PlayerAttribute attribute);
/// <summary>
/// Gets the Grand Company rank of the local character.
/// </summary>
/// <param name="grandCompany">The Grand Company to check.</param>
/// <returns>The Grand Company rank of the local character.</returns>
byte GetGrandCompanyRank(GrandCompany grandCompany);
/// <summary>
/// Gets the level of the local character's class/job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The level of the requested class/job.</returns>
short GetClassJobLevel(ClassJob classJob);
/// <summary>
/// Gets the experience of the local character's class/job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The experience of the requested class/job.</returns>
int GetClassJobExperience(ClassJob classJob);
/// <summary>
/// Gets the desynthesis level of the local character's crafter job.
/// </summary>
/// <param name="classJob">The ClassJob row to check.</param>
/// <returns>The desynthesis level of the requested crafter job.</returns>
float GetDesynthesisLevel(ClassJob classJob);
/// <summary>
/// Determines whether the specified Action is unlocked.
/// </summary>
/// <param name="row">The Action row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsActionUnlocked(Lumina.Excel.Sheets.Action row);
/// <summary>
/// Determines whether the specified AetherCurrentCompFlgSet is unlocked.
/// </summary>
/// <param name="row">The AetherCurrentCompFlgSet row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row);
/// <summary>
/// Determines whether the specified AetherCurrent is unlocked.
/// </summary>
/// <param name="row">The AetherCurrent row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsAetherCurrentUnlocked(AetherCurrent row);
/// <summary>
/// Determines whether the specified AozAction (Blue Mage Action) is unlocked.
/// </summary>
/// <param name="row">The AozAction row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsAozActionUnlocked(AozAction row);
/// <summary>
/// Determines whether the specified BannerBg (Portrait Backgrounds) is unlocked.
/// </summary>
/// <param name="row">The BannerBg row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerBgUnlocked(BannerBg row);
/// <summary>
/// Determines whether the specified BannerCondition is unlocked.
/// </summary>
/// <param name="row">The BannerCondition row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerConditionUnlocked(BannerCondition row);
/// <summary>
/// Determines whether the specified BannerDecoration (Portrait Accents) is unlocked.
/// </summary>
/// <param name="row">The BannerDecoration row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerDecorationUnlocked(BannerDecoration row);
/// <summary>
/// Determines whether the specified BannerFacial (Portrait Expressions) is unlocked.
/// </summary>
/// <param name="row">The BannerFacial row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerFacialUnlocked(BannerFacial row);
/// <summary>
/// Determines whether the specified BannerFrame (Portrait Frames) is unlocked.
/// </summary>
/// <param name="row">The BannerFrame row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerFrameUnlocked(BannerFrame row);
/// <summary>
/// Determines whether the specified BannerTimeline (Portrait Poses) is unlocked.
/// </summary>
/// <param name="row">The BannerTimeline row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBannerTimelineUnlocked(BannerTimeline row);
/// <summary>
/// Determines whether the specified BuddyAction (Action of the players Chocobo Companion) is unlocked.
/// </summary>
/// <param name="row">The BuddyAction row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBuddyActionUnlocked(BuddyAction row);
/// <summary>
/// Determines whether the specified BuddyEquip (Equipment of the players Chocobo Companion) is unlocked.
/// </summary>
/// <param name="row">The BuddyEquip row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsBuddyEquipUnlocked(BuddyEquip row);
/// <summary>
/// Determines whether the specified CharaMakeCustomize (Hairstyles and Face Paint patterns) is unlocked.
/// </summary>
/// <param name="row">The CharaMakeCustomize row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsCharaMakeCustomizeUnlocked(CharaMakeCustomize row);
/// <summary>
/// Determines whether the specified ChocoboTaxi (Chocobokeeps of the Chocobo Porter service) is unlocked.
/// </summary>
/// <param name="row">The ChocoboTaxi row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsChocoboTaxiUnlocked(ChocoboTaxi row);
/// <summary>
/// Determines whether the specified Companion (Minions) is unlocked.
/// </summary>
/// <param name="row">The Companion row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsCompanionUnlocked(Companion row);
/// <summary>
/// Determines whether the specified CraftAction is unlocked.
/// </summary>
/// <param name="row">The CraftAction row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsCraftActionUnlocked(CraftAction row);
/// <summary>
/// Determines whether the specified CSBonusContentType is unlocked.
/// </summary>
/// <param name="row">The CSBonusContentType row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsCSBonusContentTypeUnlocked(CSBonusContentType row);
/// <summary>
/// Determines whether the specified Emote is unlocked.
/// </summary>
/// <param name="row">The Emote row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsEmoteUnlocked(Emote row);
/// <summary>
/// Determines whether the specified GeneralAction is unlocked.
/// </summary>
/// <param name="row">The GeneralAction row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsGeneralActionUnlocked(GeneralAction row);
/// <summary>
/// Determines whether the specified Glasses is unlocked.
/// </summary>
/// <param name="row">The Glasses row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsGlassesUnlocked(Glasses row);
/// <summary>
/// Determines whether the specified HowTo is unlocked.
/// </summary>
/// <param name="row">The HowTo row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsHowToUnlocked(HowTo row);
/// <summary>
/// Determines whether the specified InstanceContent is unlocked.
/// </summary>
/// <param name="row">The InstanceContent row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsInstanceContentUnlocked(InstanceContent row);
/// <summary>
/// Determines whether the specified Item is considered unlockable.
/// </summary>
/// <param name="item">The Item row to check.</param>
/// <returns><see langword="true"/> if unlockable; otherwise, <see langword="false"/>.</returns>
bool IsItemUnlockable(Item item);
/// <summary>
/// Determines whether the specified Item is unlocked.
/// </summary>
/// <param name="item">The Item row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsItemUnlocked(Item item);
/// <summary>
/// Determines whether the specified McGuffin is unlocked.
/// </summary>
/// <param name="row">The McGuffin row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsMcGuffinUnlocked(McGuffin row);
/// <summary>
/// Determines whether the specified MJILandmark (Island Sanctuary landmark) is unlocked.
/// </summary>
/// <param name="row">The MJILandmark row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsMJILandmarkUnlocked(MJILandmark row);
/// <summary>
/// Determines whether the specified Mount is unlocked.
/// </summary>
/// <param name="row">The Mount row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsMountUnlocked(Mount row);
/// <summary>
/// Determines whether the specified NotebookDivision (Categories in Crafting/Gathering Log) is unlocked.
/// </summary>
/// <param name="row">The NotebookDivision row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsNotebookDivisionUnlocked(NotebookDivision row);
/// <summary>
/// Determines whether the specified Orchestrion roll is unlocked.
/// </summary>
/// <param name="row">The Orchestrion row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsOrchestrionUnlocked(Orchestrion row);
/// <summary>
/// Determines whether the specified Ornament (Fashion Accessories) is unlocked.
/// </summary>
/// <param name="row">The Ornament row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsOrnamentUnlocked(Ornament row);
/// <summary>
/// Determines whether the specified Perform (Performance Instruments) is unlocked.
/// </summary>
/// <param name="row">The Perform row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsPerformUnlocked(Perform row);
/// <summary>
/// Determines whether the specified PublicContent is unlocked.
/// </summary>
/// <param name="row">The PublicContent row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsPublicContentUnlocked(PublicContent row);
/// <summary>
/// Determines whether the underlying RowRef type is unlocked.
/// </summary>
/// <param name="rowRef">The RowRef to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsRowRefUnlocked(RowRef rowRef);
/// <summary>
/// Determines whether the underlying RowRef type is unlocked.
/// </summary>
/// <typeparam name="T">The type of the Excel row.</typeparam>
/// <param name="rowRef">The RowRef to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsRowRefUnlocked<T>(RowRef<T> rowRef) where T : struct, IExcelRow<T>;
/// <summary>
/// Determines whether the specified SecretRecipeBook (Master Recipe Books) is unlocked.
/// </summary>
/// <param name="row">The SecretRecipeBook row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsSecretRecipeBookUnlocked(SecretRecipeBook row);
/// <summary>
/// Determines whether the specified Trait is unlocked.
/// </summary>
/// <param name="row">The Trait row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsTraitUnlocked(Trait row);
/// <summary>
/// Determines whether the specified TripleTriadCard is unlocked.
/// </summary>
/// <param name="row">The TripleTriadCard row to check.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsTripleTriadCardUnlocked(TripleTriadCard row);
/// <summary>
/// Determines whether the specified unlock link is unlocked or quest is completed.
/// </summary>
/// <param name="unlockLink">The unlock link id or quest id (quest ids in this case are over 65536).</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsUnlockLinkUnlocked(uint unlockLink);
/// <summary>
/// Determines whether the specified unlock link is unlocked.
/// </summary>
/// <param name="unlockLink">The unlock link id.</param>
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsUnlockLinkUnlocked(ushort unlockLink);
}