532
edits
No edit summary |
(preliminary harmony tutorial) |
||
(19 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
{{ | Harmony is etc etc. This tutorial will assume a general familiarity with XML modding and C# scripting, | ||
as Harmony should be a last resort used only where those cannot be applied. | |||
== | I'm not including decompiled C# code directly in this tutorial, even in snippets; please look these parts and functions up in | ||
your own copy so you can follow along. | |||
In this tutorial, we'll be making it possible to bring statues (like those created by a {{f|lithofex}}) | |||
to life with a {{f|nano-neuro animator}} or {{f|Spray-a-Brain}}. Most walls and furniture can be animated, | |||
but statues are an exception. Stone statues of creatures are dynamically generated at runtime, whereas the | |||
specifications for animatable furniture are determined by their blueprints, so it's not trivial to | |||
}} | fit them into the existing system. We can't do it simply by adding new XML data to specify, or new code as with normal | ||
C# scripting. We'll need to use Harmony to patch existing methods. | |||
== Preliminaries == | |||
The first step here isn't to write a patch; it's to figure out where a patch needs to go. First, we look at | |||
the ObjectBlueprints file (Items.xml) for the {{f|nano-neuro animator}} to see how it animates an object. | |||
We see it has the AnimateObject part, which we can look up later in the decompiled game code. | |||
Next, we look at the blueprints (in Furniture.xml) for an object that can already be animated, the {{f|iron maiden}}. | |||
These are the relevant lines: | |||
<tag Name="AnimatedSkills" Value="Persuasion_Intimidate" /> | |||
<tag Name="BodyType" Value="IronMaiden" /> | |||
<tag Name="Animatable" /> | |||
We also check the blueprint in the same file for "Random Stone Statue", which is what we want to animate. | |||
It's missing all the tags we see in the iron maiden, but has the part "RandomStatue", which we'll look up | |||
in the decompiled game code. We could just add them to the blueprint via normal XML modding, and this would | |||
make the statue animatable. But we run into the question of what BodyType to assign to our random statue. | |||
We don't know whether it'll be a statue of a salthopper, a saw-hander, or Saad Amus the Sky-Bear. | |||
There's no single correct anatomy to assign. So we'll have to do it on the fly, at runtime, which means using | |||
C# rather than XML. | |||
Next, we check the decompiled game code, starting with the AnimateObject part. | |||
The important pieces here are the CanAnimate method, which checks if a GameObject frankenObject can be animated, | |||
and the Animate method, which takes frankenObject and a few less-relevant parameters and brings frankenObject to life. | |||
CanAnimate checks if frankenObject has either the blueprint tag or the string property "Animatable" (more on this later), | |||
while Animate goes through a long process of assigning frankenObject all the parts it needs to become a real live creature, | |||
including assigning the anatomy based on the blueprint tag BodyType (or Humanoid by default). | |||
We also check the decompiled code for RandomStatue. Here, the important bit is the method SetCreature, which | |||
modifies the object's properties (tile, description, etc.) to correspond with the creature it depicts. | |||
This is where we need to set up the information which AnimateObject.Animate will use to turn this statue | |||
into an appropriate creature. Since we need to modify an existing method, we'll need to use Harmony. | |||
Now we begin the actual modding. Begin by creating a mod folder with the usual basic files in it. | |||
Next, add a C# file. The name doesn't really matter, but we'll call it AnimateStatue.cs. | |||
Put a namespace declaration in it and import Harmony, like so: | |||
using HarmonyLib; | |||
namespace AnimateStatue.HarmonyPatches { | |||
//TODO | |||
} | |||
Next, we need to add a Harmony patch to RandomStatue.SetCreature to assign the proper BodyType. | |||
This is a simple modification, and it doesn't impact any of the other lines in the method, so | |||
we don't have to worry about the order in which it happens. This makes it a perfect candidate for | |||
a Harmony [https://harmony.pardeike.net/articles/patching-postfix.html postfix] patch. | |||
== Postfixes == | |||
A postfix patch modifies a function by adding some code which will always be run after the original function finishes. | |||
This code can have access to the parameters used to call the function, the value returned by the function, | |||
and (in the case of non-static methods) the instance whose method is being called. | |||
Even if another mod adds a postfix patch to the same function as us, | |||
we're guaranteed that our postfix code will be run. We're not guaranteed that it'll run *correctly*, but this | |||
patch is simple and discrete enough that it'd be hard for another mod to break it accidentally. | |||
We start our patch with a declaration of what function we're modifying, and some more imports we'll be using: | |||
//...earlier stuff | |||
using XRL.World; | |||
using XRL.World.Parts; | |||
namespace AnimateStatue.HarmonyPatches { | |||
[HarmonyPatch(typeof(RandomStatue), nameof(RandomStatue.SetCreature))] | |||
class RandomStatuePatch { | |||
//TODO | |||
} | |||
} | |||
This states that our patch, RandomStatuePatch, will modify the class RandomStatue's method SetCreature. | |||
We can then fill in the class definition with our actual postfix function: | |||
//...earlier stuff | |||
class RandomStatuePatch { | |||
[HarmonyPostfix] | |||
static void Postfix(RandomStatue __instance, GameObject creatureObject) { | |||
//TODO | |||
} | |||
} | |||
The HarmonyPostfix line tells Harmony that we're defining a postfix. The parameters of the Postfix function | |||
declare what information we want access to. Here, we're using the special variable <code>__instance</code> | |||
to get access to the RandomStatue part whose SetCreature method we're calling, like the keyword "this" | |||
if we were writing a method rather than patching one. We're also taking `creatureObject`, the sole parameter | |||
of the setCreature method. The name `creatureObject` is not arbitrary - if we're taking parameters | |||
from the original function, we have to name them to match. If the developers renamed SetCreature's parameter | |||
from `creatureObject` to `beingObject`, we would have to change the parameter here to `beingObject`. | |||
Now that we understand the class and function signatures for our patch, let's add the substance of it: | |||
//...earlier stuff | |||
static void Postfix(RandomStatue __instance, GameObject creatureObject) { | |||
__instance.ParentObject.SetStringProperty("Animatable", "Yes"); //this may be unnecessary | |||
__instance.ParentObject.SetStringProperty("BodyType", creatureObject.Body.Anatomy); | |||
} | |||
Here, __instance is the RandomStatue part. __instance.ParentObject is therefore the actual Random Stone Statue | |||
game object we're dealing with, as that object has the RandomStatue part. We're setting the statue's Animatable | |||
string property to "Yes", and setting the BodyType string property to the anatomy of the creatureObject. | |||
(The former is technically unnecessary as we could instead put Animatable in a blueprint tag, but | |||
we're doing the patch here anyway. Feel free to delete it and instead do some XML if you prefer.). | |||
Since BodyType is set according to the creature the statue depicts, we should be able to animate it | |||
with the correct body type. Load up the game, enable the mod, spawn in some random statue (or lithofexes), | |||
and try it for yourself! Use the swap [[wish]] | |||
to check the creature's anatomy. | |||
Unfortunately, as you'll have discovered if you tried it out, all statues are still humanoid. We're going to need more patching. | |||
The problem here is the split between blueprint tags and object properties. These are two very similar ways of | |||
assigning random bits of data to objects. Tags and properties have a name and a value. Some code will check only whether | |||
a name is present, while other code will check the value for a given name and do something with it. The difference | |||
is where they're defined. | |||
Blueprint tags are set directly on object blueprints, like <tag Name="Animatable"/> or <tag Name="BodyType" Value="IronMaiden"/>. | |||
Object properties, on the other hand, are set at runtime, such as by our calls to SetStringProperty. | |||
In many cases, the two are effectively interchangeable, as game objects will often use | |||
methods like GetPropertyOrTag(name), which checks for a property with that name and, if there's none, checks the blueprint | |||
for a tag with that name. For instance, the AnimateObject.CanAnimate method checks if the object to be animated | |||
has Animatable as either a tag or a property. | |||
The key issue is that the AnimateObject.Animate method sets the anatomy of the newly animated object by a call to GetTag("BodyType"), | |||
which exclusively checks tags, not properties. But we can't set BodyType on the blueprint, | |||
so we have to set it to check properties as well. | |||
This might be possible to do with another postfix, simply overriding the anatomy. In fact, that would probably | |||
be the safest route. Try doing it yourself as an exercise. | |||
There is, however, a certain elegance to the idea of just tweaking this one method call instead of | |||
adding a whole new one. An odd sort of elegance, to be fair, since we'll have to drag ourselves down | |||
into the mud to actually achieve it. But if we really do want to just modify this one function call, | |||
in the middle of the method, without touching anything else or duplicating the call, we'll | |||
have found ourselves a use case for a Harmony [https://harmony.pardeike.net/articles/patching-transpiler.html transpiler]. | |||
== Transpilers == | |||
Compared to standard C# scripting, Harmony patches are powerful but difficult and fragile, | |||
and should be used only as a last resort. | |||
Transpilers have the same relationship to the rest of Harmony as Harmony itself does to that standard scripting. | |||
Rather than hooking on to a function at the beginning or end to work with its arguments and values, transpilers | |||
directly manipulate the sequence of code instructions which make up the function. | |||
Here be dragons. | |||
These code instructions are not in C#. Instead, they're in the Common Intermediate Language (CIL or IL), a bytecode | |||
instruction set developed by Microsoft as a compilation target for C# (and other high-level languages). | |||
Each instruction in IL is a single, small operation on a stack-based virtual machine. For instance, | |||
calling a function with three parameters, a single line of C#, requires four instructions in IL: | |||
three instructions to put the parameters on the stack, plus an instruction to actually call the function. | |||
A detailed introduction to IL is beyond the scope of this tutorial, but the decompiler ILSpy has an option | |||
to decompile the code in "IL with C#" format, which will include the IL instructions annotated with their | |||
corresponding C# lines. This, in my opinion, is the most useful format for understanding the IL. In this case, | |||
we can easily find the C# line with the GetTag call, which will look roughly like this: | |||
IL_0126: callvirt instance string XRL.World.GameObject::GetTag(string, string) | |||
We want to change this to call GetTagOrStringProperty. | |||
We'll need to add some more imports, and a new patch class for AnimateObject.Animate: | |||
//...earlier stuff | |||
using System.Reflection; | |||
using System.Reflection.Emit; | |||
using System.Collections.Generic; | |||
namespace AnimateStatue.HarmonyPatches { | |||
//...earlier stuff | |||
[HarmonyPatch(typeof(AnimateObject), nameof(AnimateObject.Animate))] | |||
class AnimateObjectPatch { | |||
//TODO | |||
} | |||
} | |||
The Reflection imports will allow us to work with instructions more directly. | |||
Our transpiler will need to be annotated as such, and must be a function from a sequence of | |||
code instructions to a sequence of code instructions. Since we're working so abstractly here, | |||
we can't use normal collections, so we need to import System.Collections.Generic for the IEnumerable | |||
sequence type. Here's what the function signature will look like: | |||
//...earlier stuff | |||
class AnimateObjectPatch { | |||
[HarmonyTranspiler] | |||
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { | |||
//TODO | |||
} | |||
} | |||
We want this to find the code instruction that calls GetTag and swap it to call GetTagOrStringProperty. | |||
There are a couple calls to GetTag in this function, but luckily the one we care about is the first one, | |||
so we can just modify the first call to GetTag that we find. | |||
We yield rather than simply returning, because we're returning an IEnumerable sequence which will | |||
automatically collect the values we return: | |||
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { | |||
var found = false; | |||
foreach (var instruction in instructions) | |||
{ | |||
if (!found && false) //TODO should check if the instruction calls GetTag | |||
{ | |||
yield return null; //TODO should be the code instruction but for GetTagOrStringProperty instead | |||
found = true; | |||
} | |||
else { | |||
yield return instruction; | |||
} | |||
} | |||
} | |||
Now we need to be able to examine the instructions. For a full rundown on how to do this, see | |||
[[https://harmony.pardeike.net/articles/patching-transpiler-codes.html|the Harmony documentation here]]. | |||
Essentially, we get the information of the GameObject methods GetTag and GetPropertyOrTag (using System.Reflection), | |||
which we can then use to check and construct CodeInstructions (a class defined by Harmony to make it easier | |||
to work with IL in C#). | |||
CodeInstructions come with a handy Calls method, which takes a MethodInfo and checks whether the | |||
instruction is a call to the method with that info. This solves our first TODO. For the second TODO, we create a new CodeInstruction | |||
with the operation code Callvirt (from System.Reflection.Emit) which calls the method specified by another MethodInfo, | |||
in this case the MethodInfo m_getTagOrProp which refers to GameObject.GetPropertyOrTag. | |||
//...earlier stuff | |||
class AnimateObjectPatch { | |||
static MethodInfo m_getTagOrProp = typeof(GameObject).GetMethod("GetPropertyOrTag"); | |||
static MethodInfo m_getTag = typeof(GameObject).GetMethod("GetTag"); | |||
[HarmonyTranspiler] | |||
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { | |||
//...earlier stuff | |||
if (!found && instruction.Calls(m_getTag)) | |||
{ | |||
yield return new CodeInstruction(OpCodes.Callvirt, m_getTagOrProp); | |||
found = true; | |||
} | |||
//...earlier stuff | |||
} | |||
} | |||
In summary, our transpiler patch takes in the IL instructions for AnimateObject.Animate and | |||
passes them through unchanged until it finds an instruction that's a call to GetTag. | |||
It replaces that instruction with a call to GetPropertyOrTag, then passes all the | |||
remaining IL instructions through unchanged. We can clearly see how fiddly and fragile this is. | |||
If the call to GetTag is replaced with a call to something else, it will replace the next | |||
call to GetTag instead. If a new GetTag call | |||
is added before the BodyType one, it will replace that one instead and leave BodyType to | |||
exclusively pull from blueprint tags. | |||
Even so, this is almost a best-case scenario for a transpiler. | |||
We don't have to worry about branching or labels, we only have to replace a single instruction, | |||
and the replacement does essentially the same thing but more permissive, so it's unlikely to break | |||
too badly. If you take a crack at transpiling on your own, try to keep your patches likewise | |||
as minimal as possible - nothing good awaits in the deep swamps of assembly metaprogramming. |