From 1ba18e54bf14fa6475001fa55bbcfab6c42249a2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 30 Jan 2026 12:28:05 +0100 Subject: [PATCH] Add support for Titles to IUnlockState --- Dalamud/Game/UnlockState/UnlockState.cs | 48 +++++++++++++++++++++++-- Dalamud/Plugin/Services/IUnlockState.cs | 13 +++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 8496c898a..273a0228e 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -50,6 +50,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; private readonly Hook setAchievementCompletedHook; + private readonly Hook setTitleUnlockedHook; [ServiceManager.ServiceConstructor] private UnlockState() @@ -62,17 +63,27 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"), this.SetAchievementCompletedDetour); + this.setTitleUnlockedHook = Hook.FromAddress( + this.sigScanner.ScanText("B8 ?? ?? ?? ?? 66 3B D0 73 ?? 44 0F B7 C2 49 C1 E8 ?? 4C 03 C1 0F B7 C2 83 E0 ?? 41 0F B6 48 ?? 0F AB C1"), + this.SetTitleUnlockedDetour); + this.setAchievementCompletedHook.Enable(); + this.setTitleUnlockedHook.Enable(); } private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id); + private delegate void SetTitleUnlockedDelegate(TitleList* thisPtr, ushort id); + /// public event IUnlockState.UnlockDelegate Unlock; /// public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded(); + /// + public bool IsTitleListLoaded => UIState.Instance()->TitleList.DataReceived; + private bool IsLoaded => PlayerState.Instance()->IsLoaded; /// @@ -448,6 +459,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); } + /// + public bool IsTitleUnlocked(Title row) + { + // Only check for login state here as individual Titles + // may be flagged as complete when you unlock them, regardless + // of whether the full Titles list was loaded or not. + + if (!this.IsLoaded) + return false; + + return UIState.Instance()->TitleList.IsTitleUnlocked((ushort)row.RowId); + } + /// public bool IsTraitUnlocked(Trait row) { @@ -608,6 +632,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var secretRecipeBookRow)) return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); + if (rowRef.TryGetValue(out var titleRow)) + return this.IsTitleUnlocked(titleRow); + if (rowRef.TryGetValue<Trait>(out var traitRow)) return this.IsTraitUnlocked(traitRow); @@ -667,14 +694,24 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<AchievementSheet>(id)); } + private void SetTitleUnlockedDetour(TitleList* thisPtr, ushort id) + { + this.setTitleUnlockedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<Title>(id)); + } + private void Update() { if (!this.IsLoaded) return; Log.Verbose("Checking for new unlocks..."); - - // Do not check for Achievements here! + + // Do not check for Achievements or Titles here! this.UpdateUnlocksForSheet<ActionSheet>(); this.UpdateUnlocksForSheet<AetherCurrent>(); @@ -736,7 +773,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - EmjCostume // Probably not happening, because it requires fetching data from server: - // - Titles // - Bozjan Field Notes // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } @@ -806,6 +842,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded; + /// <inheritdoc/> + public bool IsTitleListLoaded => this.unlockStateService.IsTitleListLoaded; + /// <inheritdoc/> public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row); @@ -932,6 +971,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); + /// <inheritdoc/> + public bool IsTitleUnlocked(Title row) => this.unlockStateService.IsTitleUnlocked(row); + /// <inheritdoc/> public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index 29e5186bc..4554aa318 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -26,6 +26,11 @@ public interface IUnlockState : IDalamudService /// </summary> bool IsAchievementListLoaded { get; } + /// <summary> + /// Gets a value indicating whether the full Titles list was received. + /// </summary> + bool IsTitleListLoaded { get; } + /// <summary> /// Determines whether the specified Achievement is completed.<br/> /// Requires that the player requested the Achievements list (can be chcked with <see cref="IsAchievementListLoaded"/>). @@ -322,6 +327,14 @@ public interface IUnlockState : IDalamudService /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> bool IsSecretRecipeBookUnlocked(SecretRecipeBook row); + /// <summary> + /// Determines whether the specified Title is unlocked.<br/> + /// Requires that the player requested the Titles list (can be chcked with <see cref="IsTitleListLoaded"/>). + /// </summary> + /// <param name="row">The Title row to check.</param> + /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> + bool IsTitleUnlocked(Title row); + /// <summary> /// Determines whether the specified Trait is unlocked. /// </summary>