diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index c01ab2af0..7ce4697cb 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
///
/// Configuration to store settings for a dalamud plugin.
///
-[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
+[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index a411883d5..2d32b8e8a 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
+using Dalamud.Hooking.Internal.Verification;
using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
@@ -73,6 +74,11 @@ internal sealed unsafe class Dalamud : IServiceType
scanner,
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
+ using (Timings.Start("HookVerifier Init"))
+ {
+ HookVerifier.Initialize(scanner);
+ }
+
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index 4fc81632a..d81d262bf 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -55,7 +55,9 @@ public class AddonRefreshArgs : AddonArgs
AtkValuePtr ptr;
unsafe
{
+#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
+#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr;
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index e0b2defbf..1cc0eacf3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -55,7 +55,9 @@ public class AddonSetupArgs : AddonArgs
AtkValuePtr ptr;
unsafe
{
+#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
+#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr;
diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
index af85f9228..138484580 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs
@@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
///
- [Api14ToDo("Maybe make this config scoped to internal name?")]
+ [Api15ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
///
diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs
index 1cd3ef91d..b8fd78b4f 100644
--- a/Dalamud/Hooking/Hook.cs
+++ b/Dalamud/Hooking/Hook.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Hooking.Internal;
+using Dalamud.Hooking.Internal.Verification;
namespace Dalamud.Hooking;
@@ -230,6 +231,8 @@ public abstract class Hook : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
+ HookVerifier.Verify(procAddress);
+
procAddress = HookManager.FollowJmp(procAddress);
if (useMinHook)
return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly());
diff --git a/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs
new file mode 100644
index 000000000..c43b5d540
--- /dev/null
+++ b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+
+namespace Dalamud.Hooking.Internal.Verification;
+
+///
+/// Exception thrown when a provided delegate for a hook does not match a known delegate.
+///
+public class HookVerificationException : Exception
+{
+ private HookVerificationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Create a new exception.
+ ///
+ /// The address of the function that is being hooked.
+ /// The delegate passed by the user.
+ /// The delegate we think is correct.
+ /// Additional context to show to the user.
+ /// The created exception.
+ internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message)
+ {
+ return new HookVerificationException(
+ $"Hook verification failed for address 0x{address.ToInt64():X}\n\n" +
+ $"Why: {message}\n" +
+ $"Passed Delegate: {GetSignature(passed)}\n" +
+ $"Correct Delegate: {GetSignature(enforced)}\n\n" +
+ "The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals.");
+ }
+
+ private static string GetSignature(Type delegateType)
+ {
+ var method = delegateType.GetMethod("Invoke");
+ if (method == null) return delegateType.Name;
+
+ var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name));
+ return $"{method.ReturnType.Name} ({parameters})";
+ }
+}
diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs
new file mode 100644
index 000000000..ad68ae38e
--- /dev/null
+++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs
@@ -0,0 +1,107 @@
+using System.Linq;
+
+using Dalamud.Game;
+using Dalamud.Logging.Internal;
+
+namespace Dalamud.Hooking.Internal.Verification;
+
+///
+/// Global utility that can verify whether hook delegates are correctly declared.
+/// Initialized out-of-band, since Hook is instantiated all over the place without a service, so this cannot be
+/// a service either.
+///
+internal static class HookVerifier
+{
+ private static readonly ModuleLog Log = new("HookVerifier");
+
+ private static readonly VerificationEntry[] ToVerify =
+ [
+ new(
+ "ActorControlSelf",
+ "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
+ typeof(ActorControlSelfDelegate),
+ "Signature changed in Patch 7.4") // 7.4 (new parameters)
+ ];
+
+ private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Process to scan in.
+ public static void Initialize(TargetSigScanner scanner)
+ {
+ foreach (var entry in ToVerify)
+ {
+ if (!scanner.TryScanText(entry.Signature, out var address))
+ {
+ Log.Error("Could not resolve signature for hook {Name} ({Sig})", entry.Name, entry.Signature);
+ continue;
+ }
+
+ entry.Address = address;
+ }
+ }
+
+ ///
+ /// Verify the hook with the provided address and exception.
+ ///
+ /// The address of the function we are hooking.
+ /// The delegate type passed by the creator of the hook.
+ /// Exception thrown when we think the hook is not correctly declared.
+ public static void Verify(IntPtr address) where T : Delegate
+ {
+ var entry = ToVerify.FirstOrDefault(x => x.Address == address);
+
+ // Nothing to verify for this hook?
+ if (entry == null)
+ {
+ return;
+ }
+
+ var passedType = typeof(T);
+
+ // Directly compare delegates
+ if (passedType == entry.TargetDelegateType)
+ {
+ return;
+ }
+
+ var passedInvoke = passedType.GetMethod("Invoke")!;
+ var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
+
+ // Compare Return Type
+ var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType;
+
+ // Compare Parameter Count
+ var passedParams = passedInvoke.GetParameters();
+ var enforcedParams = enforcedInvoke.GetParameters();
+
+ if (passedParams.Length != enforcedParams.Length)
+ {
+ mismatch = true;
+ }
+ else
+ {
+ // Compare Parameter Types
+ for (var i = 0; i < passedParams.Length; i++)
+ {
+ if (passedParams[i].ParameterType != enforcedParams[i].ParameterType)
+ {
+ mismatch = true;
+ break;
+ }
+ }
+ }
+
+ if (mismatch)
+ {
+ throw HookVerificationException.Create(address, passedType, entry.TargetDelegateType, entry.Message);
+ }
+ }
+
+ private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message)
+ {
+ public nint Address { get; set; }
+ }
+}
diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs
index cc1f48ce7..a9dfad1f0 100644
--- a/Dalamud/Interface/Animation/Easing.cs
+++ b/Dalamud/Interface/Animation/Easing.cs
@@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic.
///
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
- [Api14ToDo("Map this field to ValueClamped, probably.")]
+ [Api15ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped;
///
diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs
index 1211b505d..82554995b 100644
--- a/Dalamud/Interface/Internal/UiDebug.cs
+++ b/Dalamud/Interface/Internal/UiDebug.cs
@@ -420,13 +420,13 @@ internal unsafe class UiDebug
ImGui.SameLine();
Service.Get().Draw(textInputComponent->AtkComponentInputBase.RawString);
- ImGui.Text("Text1: "u8);
- ImGui.SameLine();
- Service.Get().Draw(textInputComponent->UnkText01);
-
- ImGui.Text("Text2: "u8);
- ImGui.SameLine();
- Service.Get().Draw(textInputComponent->UnkText02);
+ // ImGui.Text("Text1: "u8);
+ // ImGui.SameLine();
+ // Service.Get().Draw(textInputComponent->UnkText01);
+ //
+ // ImGui.Text("Text2: "u8);
+ // ImGui.SameLine();
+ // Service.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("AvailableLines: "u8);
ImGui.SameLine();
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
index a35195498..922d226b6 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
@@ -89,20 +89,14 @@ internal unsafe class ComponentNodeTree : ResNodeTree
{
case TextInput:
var textInputComponent = (AtkComponentTextInput*)this.Component;
- ImGui.Text(
- $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}");
- ImGui.Text(
- $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}");
- ImGui.Text(
- $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
- ImGui.Text(
- $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
- ImGui.Text(
- $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
- ImGui.Text(
- $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
- ImGui.Text(
- $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
+ ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}");
+ ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}");
+ // TODO: Reenable when unknowns have been unprivated / named
+ // ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
+ // ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
+ ImGui.Text($"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
+ ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
+ ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
break;
case List:
case TreeList:
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
index e177abab7..8bd631b0e 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
@@ -149,16 +149,27 @@ internal class CallGateChannel
return (TRet)result;
}
+ ///
+ /// Set the context for the invocations through this channel.
+ ///
+ /// The context to set.
internal void SetInvocationContext(IpcContext ipcContext)
{
this.ipcExecutionContext.Value = ipcContext;
}
+ ///
+ /// Get the context for invocations through this channel.
+ ///
+ /// The context, if one was set.
internal IpcContext? GetInvocationContext()
{
return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null;
}
+ ///
+ /// Clear the context for this channel.
+ ///
internal void ClearInvocationContext()
{
this.ipcExecutionContext.Value = null;
diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs
index 2555b3b30..9e7453c25 100644
--- a/Dalamud/Plugin/Services/IClientState.cs
+++ b/Dalamud/Plugin/Services/IClientState.cs
@@ -2,6 +2,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
+using Dalamud.Utility;
namespace Dalamud.Plugin.Services;
@@ -109,12 +110,14 @@ public interface IClientState : IDalamudService
///
/// Gets the local player character, if one is present.
///
+ [Api15ToDo("Remove")]
[Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")]
public IPlayerCharacter? LocalPlayer { get; }
///
/// Gets the content ID of the local character.
///
+ [Api15ToDo("Remove")]
[Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")]
public ulong LocalContentId { get; }
diff --git a/Dalamud/Utility/Api14ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs
deleted file mode 100644
index 945b6e4db..000000000
--- a/Dalamud/Utility/Api14ToDoAttribute.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Dalamud.Utility;
-
-///
-/// Utility class for marking something to be changed for API 13, for ease of lookup.
-///
-[AttributeUsage(AttributeTargets.All, Inherited = false)]
-internal sealed class Api14ToDoAttribute : Attribute
-{
- ///
- /// Marks that this should be made internal.
- ///
- public const string MakeInternal = "Make internal.";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The explanation.
- /// The explanation 2.
- public Api14ToDoAttribute(string what, string what2 = "")
- {
- _ = what;
- _ = what2;
- }
-}
diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs
index 5f718bcee..b632d14d7 100644
--- a/Dalamud/Utility/ItemUtil.cs
+++ b/Dalamud/Utility/ItemUtil.cs
@@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Text;
+
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.ReadOnly;
@@ -125,10 +126,15 @@ public static class ItemUtil
if (IsEventItem(itemId))
{
+ // Only English, German, and French have a Name field.
+ // For other languages, the Name is an empty string, and the Singular field should be used instead.
+ language ??= dataManager.Language;
+ var useSingular = language is not (ClientLanguage.English or ClientLanguage.German or ClientLanguage.French);
+
return dataManager
.GetExcelSheet(language)
.TryGetRow(itemId, out var eventItem)
- ? eventItem.Name
+ ? (useSingular ? eventItem.Singular : eventItem.Name)
: default;
}
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 90168316b..a88271426 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 90168316b4c5e3af2746a1bdea52fb10f9113862
+Subproject commit a8827142678d35e62ab0c1bafe94d607271af010