Add support for Achievements to IUnlockState

This commit is contained in:
Haselnussbomber 2026-01-30 12:18:39 +01:00
parent 33a7cdefa8
commit 595dad4a7b
No known key found for this signature in database
GPG key ID: BB905BB49E7295D1
2 changed files with 82 additions and 13 deletions

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -16,7 +17,9 @@ using FFXIVClientStructs.FFXIV.Component.Exd;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using AchievementSheet = Lumina.Excel.Sheets.Achievement;
using ActionSheet = Lumina.Excel.Sheets.Action; using ActionSheet = Lumina.Excel.Sheets.Action;
using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement;
using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; 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 static readonly ModuleLog Log = new(nameof(UnlockState));
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = []; [ServiceManager.ServiceDependency]
private readonly TargetSigScanner sigScanner = Service<TargetSigScanner>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get(); private readonly DataManager dataManager = Service<DataManager>.Get();
@ -44,17 +48,31 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly RecipeData recipeData = Service<RecipeData>.Get(); private readonly RecipeData recipeData = Service<RecipeData>.Get();
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
private readonly Hook<SetAchievementCompletedDelegate> setAchievementCompletedHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private UnlockState() private UnlockState()
{ {
this.clientState.Login += this.OnLogin; this.clientState.Login += this.OnLogin;
this.clientState.Logout += this.OnLogout; this.clientState.Logout += this.OnLogout;
this.gameGui.AgentUpdate += this.OnAgentUpdate; this.gameGui.AgentUpdate += this.OnAgentUpdate;
this.setAchievementCompletedHook = Hook<SetAchievementCompletedDelegate>.FromAddress(
this.sigScanner.ScanText("81 FA ?? ?? ?? ?? 0F 87 ?? ?? ?? ?? 53"),
this.SetAchievementCompletedDetour);
this.setAchievementCompletedHook.Enable();
} }
private delegate void SetAchievementCompletedDelegate(CSAchievement* thisPtr, uint id);
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate Unlock; public event IUnlockState.UnlockDelegate Unlock;
/// <inheritdoc/>
public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded();
private bool IsLoaded => PlayerState.Instance()->IsLoaded; private bool IsLoaded => PlayerState.Instance()->IsLoaded;
/// <inheritdoc/> /// <inheritdoc/>
@ -63,6 +81,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.clientState.Login -= this.OnLogin; this.clientState.Login -= this.OnLogin;
this.clientState.Logout -= this.OnLogout; this.clientState.Logout -= this.OnLogout;
this.gameGui.AgentUpdate -= this.OnAgentUpdate; this.gameGui.AgentUpdate -= this.OnAgentUpdate;
this.setAchievementCompletedHook.Dispose();
}
/// <inheritdoc/>
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);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -464,6 +497,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (!this.IsLoaded || rowRef.IsUntyped) if (!this.IsLoaded || rowRef.IsUntyped)
return false; return false;
if (rowRef.TryGetValue<AchievementSheet>(out var achievementRow))
return this.IsAchievementComplete(achievementRow);
if (rowRef.TryGetValue<ActionSheet>(out var actionRow)) if (rowRef.TryGetValue<ActionSheet>(out var actionRow))
return this.IsActionUnlocked(actionRow); return this.IsActionUnlocked(actionRow);
@ -621,6 +657,16 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.Update(); this.Update();
} }
private void SetAchievementCompletedDetour(CSAchievement* thisPtr, uint id)
{
this.setAchievementCompletedHook.Original(thisPtr, id);
if (!this.IsLoaded)
return;
this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<AchievementSheet>(id));
}
private void Update() private void Update()
{ {
if (!this.IsLoaded) if (!this.IsLoaded)
@ -628,6 +674,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
Log.Verbose("Checking for new unlocks..."); Log.Verbose("Checking for new unlocks...");
// Do not check for Achievements here!
this.UpdateUnlocksForSheet<ActionSheet>(); this.UpdateUnlocksForSheet<ActionSheet>();
this.UpdateUnlocksForSheet<AetherCurrent>(); this.UpdateUnlocksForSheet<AetherCurrent>();
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(); this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
@ -688,7 +736,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// - EmjCostume // - EmjCostume
// Probably not happening, because it requires fetching data from server: // Probably not happening, because it requires fetching data from server:
// - Achievements
// - Titles // - Titles
// - Bozjan Field Notes // - Bozjan Field Notes
// - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0
@ -712,11 +759,17 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); // Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}");
this.RaiseUnlockSafely((RowRef)rowRef);
}
}
private void RaiseUnlockSafely(RowRef rowRef)
{
foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) foreach (var action in Delegate.EnumerateInvocationList(this.Unlock))
{ {
try try
{ {
action((RowRef)rowRef); action(rowRef);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -725,7 +778,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
} }
} }
} }
}
/// <summary> /// <summary>
/// Plugin-scoped version of a <see cref="UnlockState"/> service. /// Plugin-scoped version of a <see cref="UnlockState"/> service.
@ -751,6 +803,12 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate? Unlock; public event IUnlockState.UnlockDelegate? Unlock;
/// <inheritdoc/>
public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded;
/// <inheritdoc/>
public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);

View file

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -23,6 +21,19 @@ public interface IUnlockState : IDalamudService
/// </summary> /// </summary>
event UnlockDelegate? Unlock; event UnlockDelegate? Unlock;
/// <summary>
/// Gets a value indicating whether the full Achievements list was received.
/// </summary>
bool IsAchievementListLoaded { 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"/>).
/// </summary>
/// <param name="row">The Achievement row to check.</param>
/// <returns><see langword="true"/> if completed; otherwise, <see langword="false"/>.</returns>
bool IsAchievementComplete(Achievement row);
/// <summary> /// <summary>
/// Determines whether the specified Action is unlocked. /// Determines whether the specified Action is unlocked.
/// </summary> /// </summary>