Calling The Game's Code
Sometimes, it is beneficial to ask the game itself to do something, rather than doing it yourself. In effect, this means using the game code as a library where any arbitrary function can be called and their results used freely. This allows plugins to perform calculations in the same way the game does, or otherwise take actions in the game just as it would if Dalamud weren't even there.
For example, a plugin might want to check if the player is a mentor:
public unsafe bool IsPlayerMentor() {
var playerStatePtr = PlayerState.Instance();
return playerStatePtr->IsMentor();
}
This method will grab the instance of PlayerState
from Client Structs, and
call the appropriate check.
Making Your Own Delegates
Sometimes, a method you're interested in might not be in Client Structs. When this happens, a developer can engage their reverse engineering prowess to generate a signature, which they can then use to create their own delegate:
public class GameFunctions {
private delegate byte IsQuestCompletedDelegate(ushort questId);
[Signature("E8 ?? ?? ?? ?? 41 88 84 2C")]
private readonly IsQuestCompletedDelegate? _isQuestCompleted = null;
public GameFunctions() {
Services.GameInteropProvider.InitializeFromAttributes(this);
}
public bool IsQuestCompleted(ushort questId) {
if (this._isQuestCompleted == null)
throw new InvalidOperationException("IsQuestCompleted signature wasn't found!");
return this._isQuestCompleted(questId) > 0;
}
}
This is a lot of code, so let's break it down a bit.
First, the developer declares a delegate for the function they
want to call. This informs the compiler and the code of the return type (in this
case, a byte
), as well as the arguments of the function. This line alone is
purely declaratory, and has no impact other than definition. If a specific
argument is a reference to an undocumented pointer (or the developer simply
doesn't care about accessing any data inside the struct target), the nint
type
will often be used.
Next, the developer declares a nullable instance of that delegate, with its
default value set to null
. This instance is then marked with the
[Signature(string signature)]
attribute. This attribute is provided by
Dalamud's game interop systems and specifies the signature that identifies the
function we're interested in.
Then, the class's constructor has a call to
IGameInteropProvider#InitializeFromAttributes
. This method will scan the
referenced object (in this case, this
) and use reflection to find all class
members with the [Signature()]
tag. It will then automatically resolve the
signature and inject the proper pointer into that variable. If a signature was
unable to be resolved, the delegate instance will be set to null
for handling
by the developer.
Lastly, the IsQuestCompleted()
method is defined. This exists in "managed
code" (so, in C#) and provides some ease of use around the raw method. For
example, our method will throw an exception if the delegate is null and will
convert the returned byte
into a bool
. These wrapper methods are generally
often kept simple, but will also often hold important safety or sanity checks to
ensure that there's a clean bridge between C# and the game's native code.
Another Way To Delegate
While looking at some plugins, you may instead notice a pattern that looks slightly different:
[Signature("E8 ?? ?? ?? ?? 41 88 84 2C")]
private readonly delegate* unmanaged<ushort, byte> _isQuestCompletedDelegate;
This is a shorter (but arguably slightly more complex) way of addressing the
same concept. Instead of having to declare the delegate and other information
ahead of time, all the information about the delegate's arguments and return
value is included up front in the unmanaged<>
segment. The
unmanaged
keyword means that this function is not part of the
plugin's C# code, but instead comes from a lower level. The part inside the <>
denotes the function's arguments and return type. The return type is always the
last type in the list, and all others are argument types, in the same order as
the arguments. For example, <uint, string, byte>
is a function like
MyFunction(uint someNumber, string someString)
that returns a byte
.
Everything else behaves as it does above, including nullability.
Delegating With SigScanner
Sometimes, developers may not want to use the [Signature]
attribute for one
reason or another. While it's generally best to use it, it may be too inflexible
or just not appropriate for a specific use case. In these cases, developers may
instead use SigScanner
service to resolve their signatures directly. This is
normally combined with the above method of defining signatures:
public class SomeSigWrapper {
private readonly delegate* unmanaged<ushort, byte> _isQuestCompletedDelegate;
public SomeSigWrapper() {
var fptr = Services.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 88 84 2C");
this._isQuestCompletedDelegate = (delegate* unmanaged<ushort, byte>) fptr;
}
}
The above code will scan the .text section (the portion of a PE32 binary that
normally contains code) for the specified signature, throwing an exception if it
cannot be found. The alternate TryScanText
method will return a boolean true
or false
depending on if the signature was resolved. After the signature has
been found and resolved to an address, it is cast to the appropriate function
pointer type, and assigned to the delegate. It may then be called in code
elsewhere, usually through a wrapper method like the one demonstrated above.
Some developers will combine this with something called the "Resolver" pattern, where a class is dedicated to just resolving these addresses, which are used elsewhere. This is less of a thing in newer plugins, but is still relevant enough to worth mentioning here.