diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 5ccd7fadb..8496c898a 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Data; using Dalamud.Game.Gui; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -16,7 +17,9 @@ using FFXIVClientStructs.FFXIV.Component.Exd; using Lumina.Excel; using Lumina.Excel.Sheets; +using AchievementSheet = Lumina.Excel.Sheets.Achievement; using ActionSheet = Lumina.Excel.Sheets.Action; +using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; @@ -30,7 +33,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { private static readonly ModuleLog Log = new(nameof(UnlockState)); - private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + [ServiceManager.ServiceDependency] + private readonly TargetSigScanner sigScanner = Service.Get(); [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -44,17 +48,31 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceDependency] private readonly RecipeData recipeData = Service.Get(); + private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + private readonly Hook setAchievementCompletedHook; + [ServiceManager.ServiceConstructor] private UnlockState() { this.clientState.Login += this.OnLogin; this.clientState.Logout += this.OnLogout; this.gameGui.AgentUpdate += this.OnAgentUpdate; + + this.setAchievementCompletedHook = Hook.FromAddress( + this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"), + this.SetAchievementCompletedDetour); + + this.setAchievementCompletedHook.Enable(); } + private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id); + /// public event IUnlockState.UnlockDelegate Unlock; + /// + public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded(); + private bool IsLoaded => PlayerState.Instance()->IsLoaded; /// @@ -63,6 +81,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.clientState.Login -= this.OnLogin; this.clientState.Logout -= this.OnLogout; this.gameGui.AgentUpdate -= this.OnAgentUpdate; + + this.setAchievementCompletedHook.Dispose(); + } + + /// + public bool IsAchievementComplete(AchievementSheet row) + { + // Only check for login state here as individual Achievements + // may be flagged as complete when you unlock them, regardless + // of whether the full Achievements list was loaded or not. + + if (!this.IsLoaded) + return false; + + return CSAchievement.Instance()->IsComplete((int)row.RowId); } /// @@ -464,6 +497,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded || rowRef.IsUntyped) return false; + if (rowRef.TryGetValue(out var achievementRow)) + return this.IsAchievementComplete(achievementRow); + if (rowRef.TryGetValue(out var actionRow)) return this.IsActionUnlocked(actionRow); @@ -621,6 +657,16 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.Update(); } + private void SetAchievementCompletedDetour(CSAchievement* thisPtr, uint id) + { + this.setAchievementCompletedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef(id)); + } + private void Update() { if (!this.IsLoaded) @@ -628,6 +674,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState Log.Verbose("Checking for new unlocks..."); + // Do not check for Achievements here! + this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); this.UpdateUnlocksForSheet(); @@ -688,7 +736,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - EmjCostume // Probably not happening, because it requires fetching data from server: - // - Achievements // - Titles // - Bozjan Field Notes // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 @@ -712,16 +759,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); - foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + this.RaiseUnlockSafely((RowRef)rowRef); + } + } + + private void RaiseUnlockSafely(RowRef rowRef) + { + 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); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } @@ -751,6 +803,12 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// public event IUnlockState.UnlockDelegate? Unlock; + /// + public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded; + + /// + public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row); + /// public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index f51222ba1..29e5186bc 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using Lumina.Excel; using Lumina.Excel.Sheets; @@ -23,6 +21,19 @@ public interface IUnlockState : IDalamudService /// event UnlockDelegate? Unlock; + /// + /// Gets a value indicating whether the full Achievements list was received. + /// + bool IsAchievementListLoaded { get; } + + /// + /// Determines whether the specified Achievement is completed.
+ /// Requires that the player requested the Achievements list (can be chcked with ). + ///
+ /// The Achievement row to check. + /// if completed; otherwise, . + bool IsAchievementComplete(Achievement row); + /// /// Determines whether the specified Action is unlocked. ///