diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs
index 971e91b2b..f6372ecfd 100644
--- a/Dalamud/Plugin/Internal/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/LocalPlugin.cs
@@ -11,6 +11,7 @@ using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
+using Dalamud.Utility.Signatures;
namespace Dalamud.Plugin.Internal
{
@@ -338,6 +339,8 @@ namespace Dalamud.Plugin.Internal
return;
}
+ SignatureHelper.Initialise(this.instance);
+
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
if (this.instance.Name != this.Manifest.Name)
{
diff --git a/Dalamud/Utility/Signatures/Fallibility.cs b/Dalamud/Utility/Signatures/Fallibility.cs
new file mode 100755
index 000000000..1e5c502cf
--- /dev/null
+++ b/Dalamud/Utility/Signatures/Fallibility.cs
@@ -0,0 +1,24 @@
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// The fallibility of a signature.
+ ///
+ public enum Fallibility
+ {
+ ///
+ /// The fallibility of the signature is determined by the field/property's
+ /// nullability.
+ ///
+ Auto,
+
+ ///
+ /// The signature is fallible.
+ ///
+ Fallible,
+
+ ///
+ /// The signature is infallible.
+ ///
+ Infallible,
+ }
+}
diff --git a/Dalamud/Utility/Signatures/NullabilityUtil.cs b/Dalamud/Utility/Signatures/NullabilityUtil.cs
new file mode 100755
index 000000000..83749c6ec
--- /dev/null
+++ b/Dalamud/Utility/Signatures/NullabilityUtil.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+
+namespace Dalamud.Utility.Signatures
+{
+ internal static class NullabilityUtil
+ {
+ internal static bool IsNullable(PropertyInfo property) => IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes);
+
+ internal static bool IsNullable(FieldInfo field) => IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes);
+
+ internal static bool IsNullable(ParameterInfo parameter) => IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes);
+
+ private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable customAttributes)
+ {
+ if (memberType.IsValueType)
+ {
+ return Nullable.GetUnderlyingType(memberType) != null;
+ }
+
+ var nullable = customAttributes
+ .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
+ if (nullable != null && nullable.ConstructorArguments.Count == 1)
+ {
+ var attributeArgument = nullable.ConstructorArguments[0];
+ if (attributeArgument.ArgumentType == typeof(byte[]))
+ {
+ var args = (ReadOnlyCollection)attributeArgument.Value!;
+ if (args.Count > 0 && args[0].ArgumentType == typeof(byte))
+ {
+ return (byte)args[0].Value! == 2;
+ }
+ }
+ else if (attributeArgument.ArgumentType == typeof(byte))
+ {
+ return (byte)attributeArgument.Value! == 2;
+ }
+ }
+
+ for (var type = declaringType; type != null; type = type.DeclaringType)
+ {
+ var context = type.CustomAttributes
+ .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
+ if (context != null &&
+ context.ConstructorArguments.Count == 1 &&
+ context.ConstructorArguments[0].ArgumentType == typeof(byte))
+ {
+ return (byte)context.ConstructorArguments[0].Value! == 2;
+ }
+ }
+
+ // Couldn't find a suitable attribute
+ return false;
+ }
+ }
+}
diff --git a/Dalamud/Utility/Signatures/ScanType.cs b/Dalamud/Utility/Signatures/ScanType.cs
new file mode 100755
index 000000000..5ede63ed7
--- /dev/null
+++ b/Dalamud/Utility/Signatures/ScanType.cs
@@ -0,0 +1,20 @@
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// The type of scan to perform with a signature.
+ ///
+ public enum ScanType
+ {
+ ///
+ /// Scan the text section of the executable. Uses
+ /// .
+ ///
+ Text,
+
+ ///
+ /// Scans the text section of the executable in order to find a data section
+ /// address. Uses
+ ///
+ StaticAddress,
+ }
+}
diff --git a/Dalamud/Utility/Signatures/SignatureAttribute.cs b/Dalamud/Utility/Signatures/SignatureAttribute.cs
new file mode 100755
index 000000000..ac6e6fbf8
--- /dev/null
+++ b/Dalamud/Utility/Signatures/SignatureAttribute.cs
@@ -0,0 +1,70 @@
+using System;
+
+using JetBrains.Annotations;
+
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// The main way to use SignatureHelper. Apply this attribute to any field/property
+ /// that should make use of a signature. See the field documentation for more
+ /// information.
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ [MeansImplicitUse(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Itself)]
+// ReSharper disable once ClassNeverInstantiated.Global
+ public sealed class SignatureAttribute : Attribute
+ {
+ ///
+ /// The memory signature for this field/property.
+ ///
+ public readonly string Signature;
+
+ ///
+ /// The way this signature should be used. By default, this is guessed using
+ /// simple heuristics, but it can be manually specified if SignatureHelper can't
+ /// figure it out.
+ ///
+ ///
+ ///
+ public SignatureUseFlags UseFlags = SignatureUseFlags.Auto;
+
+ ///
+ /// The type of scan to perform. By default, this scans the text section of
+ /// the executable, but this should be set to StaticAddress for static
+ /// addresses.
+ ///
+ public ScanType ScanType = ScanType.Text;
+
+ ///
+ /// The detour name if this signature is for a hook. SignatureHelper will search
+ /// the type containing this field/property for a method that matches the
+ /// hook's delegate type, but if it doesn't find one or finds more than one,
+ /// it will fail. You can specify the name of the method here to avoid this.
+ ///
+ public string? DetourName;
+
+ ///
+ /// When is set to Offset, this is the offset from
+ /// the signature to read memory from.
+ ///
+ public int Offset;
+
+ ///
+ /// When a signature is fallible, any errors while resolving it will be
+ /// logged in the Dalamud log and the field/property will not have its value
+ /// set. When a signature is not fallible, any errors will be thrown as
+ /// exceptions instead. If fallibility is not specified, it is inferred
+ /// based on if the field/property is nullable.
+ ///
+ public Fallibility Fallibility = Fallibility.Auto;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// signature to scan for, see
+ public SignatureAttribute(string signature)
+ {
+ this.Signature = signature;
+ }
+ }
+}
diff --git a/Dalamud/Utility/Signatures/SignatureException.cs b/Dalamud/Utility/Signatures/SignatureException.cs
new file mode 100755
index 000000000..b8b2a12ba
--- /dev/null
+++ b/Dalamud/Utility/Signatures/SignatureException.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// An exception for signatures.
+ ///
+ public class SignatureException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message.
+ internal SignatureException(string message)
+ : base(message) { }
+ }
+}
diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs
new file mode 100755
index 000000000..192ee56ee
--- /dev/null
+++ b/Dalamud/Utility/Signatures/SignatureHelper.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game;
+using Dalamud.Hooking;
+using Dalamud.Logging;
+using Dalamud.Utility.Signatures.Wrappers;
+
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// A utility class to help reduce signature boilerplate code.
+ ///
+ public static class SignatureHelper
+ {
+ private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
+
+ ///
+ /// Initialises an object's fields and properties that are annotated with a
+ /// .
+ ///
+ /// The object to initialise.
+ /// If warnings should be logged using .
+ public static void Initialise(object self, bool log = true)
+ {
+ var scanner = Service.Get();
+ var selfType = self.GetType();
+ var fields = selfType.GetFields(Flags).Select(field => (IFieldOrPropertyInfo)new FieldInfoWrapper(field))
+ .Concat(selfType.GetProperties(Flags).Select(prop => new PropertyInfoWrapper(prop)))
+ .Select(field => (field, field.GetCustomAttribute()))
+ .Where(field => field.Item2 != null);
+
+ foreach (var (info, sig) in fields)
+ {
+ var wasWrapped = false;
+ var actualType = info.ActualType;
+ if (actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ // unwrap the nullable
+ actualType = actualType.GetGenericArguments()[0];
+ wasWrapped = true;
+ }
+
+ var fallibility = sig!.Fallibility;
+ if (fallibility == Fallibility.Auto)
+ {
+ fallibility = info.IsNullable || wasWrapped
+ ? Fallibility.Fallible
+ : Fallibility.Infallible;
+ }
+
+ var fallible = fallibility == Fallibility.Fallible;
+
+ void Invalid(string message, bool prepend = true)
+ {
+ var errorMsg = prepend
+ ? $"Invalid Signature attribute for {selfType.FullName}.{info.Name}: {message}"
+ : message;
+ if (fallible)
+ {
+ PluginLog.Warning(errorMsg);
+ }
+ else
+ {
+ throw new SignatureException(errorMsg);
+ }
+ }
+
+ IntPtr ptr;
+ var success = sig.ScanType == ScanType.Text
+ ? scanner.TryScanText(sig.Signature, out ptr)
+ : scanner.TryGetStaticAddressFromSig(sig.Signature, out ptr);
+ if (!success)
+ {
+ if (log)
+ {
+ Invalid($"Failed to find {sig.ScanType} signature \"{info.Name}\" for {selfType.FullName} ({sig.Signature})", false);
+ }
+
+ continue;
+ }
+
+ switch (sig.UseFlags)
+ {
+ case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)):
+ case SignatureUseFlags.Pointer:
+ {
+ if (actualType.IsAssignableTo(typeof(Delegate)))
+ {
+ info.SetValue(self, Marshal.GetDelegateForFunctionPointer(ptr, actualType));
+ }
+ else
+ {
+ info.SetValue(self, ptr);
+ }
+
+ break;
+ }
+
+ case SignatureUseFlags.Auto when actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Hook<>):
+ case SignatureUseFlags.Hook:
+ {
+ if (!actualType.IsGenericType || actualType.GetGenericTypeDefinition() != typeof(Hook<>))
+ {
+ Invalid($"{actualType.Name} is not a Hook");
+ continue;
+ }
+
+ var hookDelegateType = actualType.GenericTypeArguments[0];
+
+ Delegate? detour;
+ if (sig.DetourName == null)
+ {
+ var matches = selfType.GetMethods(Flags)
+ .Select(method => method.IsStatic
+ ? Delegate.CreateDelegate(hookDelegateType, method, false)
+ : Delegate.CreateDelegate(hookDelegateType, self, method, false))
+ .Where(del => del != null)
+ .ToArray();
+ if (matches.Length != 1)
+ {
+ Invalid("Either found no matching detours or found more than one: specify a detour name");
+ continue;
+ }
+
+ detour = matches[0]!;
+ }
+ else
+ {
+ var method = selfType.GetMethod(sig.DetourName, Flags);
+ if (method == null)
+ {
+ Invalid($"Could not find detour \"{sig.DetourName}\"");
+ continue;
+ }
+
+ var del = method.IsStatic
+ ? Delegate.CreateDelegate(hookDelegateType, method, false)
+ : Delegate.CreateDelegate(hookDelegateType, self, method, false);
+ if (del == null)
+ {
+ Invalid($"Method {sig.DetourName} was not compatible with delegate {hookDelegateType.Name}");
+ continue;
+ }
+
+ detour = del;
+ }
+
+ var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType });
+ if (ctor == null)
+ {
+ PluginLog.Error("Error in SignatureHelper: could not find Hook constructor");
+ continue;
+ }
+
+ var hook = ctor.Invoke(new object?[] { ptr, detour });
+ info.SetValue(self, hook);
+
+ break;
+ }
+
+ case SignatureUseFlags.Auto when actualType.IsPrimitive:
+ case SignatureUseFlags.Offset:
+ {
+ var offset = Marshal.PtrToStructure(ptr + sig.Offset, actualType);
+ info.SetValue(self, offset);
+
+ break;
+ }
+
+ default:
+ {
+ if (log)
+ {
+ Invalid("could not detect desired signature use, set SignatureUseFlags manually");
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Dalamud/Utility/Signatures/SignatureUseFlags.cs b/Dalamud/Utility/Signatures/SignatureUseFlags.cs
new file mode 100755
index 000000000..759352906
--- /dev/null
+++ b/Dalamud/Utility/Signatures/SignatureUseFlags.cs
@@ -0,0 +1,40 @@
+using System;
+
+using Dalamud.Hooking;
+
+namespace Dalamud.Utility.Signatures
+{
+ ///
+ /// Use flags for a signature attribute. This tells SignatureHelper how to use the
+ /// result of the signature.
+ ///
+ public enum SignatureUseFlags
+ {
+ ///
+ /// SignatureHelper will use simple heuristics to determine the best signature
+ /// use for the field/property.
+ ///
+ Auto,
+
+ ///
+ /// The signature should be used as a plain pointer. This is correct for
+ /// static addresses, functions, or anything else that's an
+ /// at heart.
+ ///
+ Pointer,
+
+ ///
+ /// The signature should be used as a hook. This is correct for
+ /// fields/properties.
+ ///
+ Hook,
+
+ ///
+ /// The signature should be used to determine an offset. This is the default
+ /// for all primitive types. SignatureHelper will read from the memory at this
+ /// signature and store the result in the field/property. An offset from the
+ /// signature can be specified in the .
+ ///
+ Offset,
+ }
+}
diff --git a/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs b/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs
new file mode 100755
index 000000000..ed99d6b19
--- /dev/null
+++ b/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Reflection;
+
+namespace Dalamud.Utility.Signatures.Wrappers
+{
+ internal sealed class FieldInfoWrapper : IFieldOrPropertyInfo
+ {
+ public FieldInfoWrapper(FieldInfo info)
+ {
+ this.Info = info;
+ }
+
+ public string Name => this.Info.Name;
+
+ public Type ActualType => this.Info.FieldType;
+
+ public bool IsNullable => NullabilityUtil.IsNullable(this.Info);
+
+ private FieldInfo Info { get; }
+
+ public void SetValue(object? self, object? value)
+ {
+ this.Info.SetValue(self, value);
+ }
+
+ public T? GetCustomAttribute() where T : Attribute
+ {
+ return this.Info.GetCustomAttribute();
+ }
+ }
+}
diff --git a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs
new file mode 100755
index 000000000..60e4fbc24
--- /dev/null
+++ b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Dalamud.Utility.Signatures.Wrappers
+{
+ internal interface IFieldOrPropertyInfo
+ {
+ string Name { get; }
+
+ Type ActualType { get; }
+
+ bool IsNullable { get; }
+
+ void SetValue(object? self, object? value);
+
+ T? GetCustomAttribute() where T : Attribute;
+ }
+}
diff --git a/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs b/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs
new file mode 100755
index 000000000..e90e3ae9f
--- /dev/null
+++ b/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Reflection;
+
+namespace Dalamud.Utility.Signatures.Wrappers
+{
+ internal sealed class PropertyInfoWrapper : IFieldOrPropertyInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// PropertyInfo.
+ public PropertyInfoWrapper(PropertyInfo info)
+ {
+ this.Info = info;
+ }
+
+ public string Name => this.Info.Name;
+
+ public Type ActualType => this.Info.PropertyType;
+
+ public bool IsNullable => NullabilityUtil.IsNullable(this.Info);
+
+ private PropertyInfo Info { get; }
+
+ public void SetValue(object? self, object? value)
+ {
+ this.Info.SetValue(self, value);
+ }
+
+ public T? GetCustomAttribute() where T : Attribute
+ {
+ return this.Info.GetCustomAttribute();
+ }
+ }
+}