User:Illuminatiswag/Sandbox: Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
(update)
(preliminary harmony tutorial)
 
Line 1: Line 1:
{{Missing info|May need to be ordered properly, also needs TODOs filled in}}
Harmony is etc etc. This tutorial will assume a general familiarity with XML modding and C# scripting,
{{tocright}}
as Harmony should be a last resort used only where those cannot be applied.
{{Qud dialogue|nodetitle=Early (Only available if you haven't completed [[A Call to Arms]])
I'm not including decompiled C# code directly in this tutorial, even in snippets; please look these parts and functions up in  
|text=Should you be here, =player.offspringTerm=?}}
your own copy so you can follow along.
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=End
|text=My apologies, Barathrum.}}}}
{{Qud dialogue|nodetitle=Recame (Only available if you've completed [[Tomb of the Eaters#Disable the Spindle's Magnetic Field|Disable the Spindle's Magnetic Field]] and you haven't completed [[Tomb of the Eaters]])
|text=Is that you, nestling =name=? Are you returned from gate and Tomb? Come closer, I say.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=RecameMe
|text=It is, Barathrum. I've returned.}}}}
{{Qud dialogue|nodetitle=Welcome
|text=Come closer, nestling. Let me look upon your countenance.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Old
|text=You are so... old.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Quest
|text=Otho said you wished to speak with me.
|comment=Only available if you haven't accepted [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Euclid
|text=What's with the houseplant over there?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if [[Pax Klanq, I Presume?]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if you've completed [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PaxComplete
|text=I convinced Pax Klanq to construct the climber.
|comment=Only available if [[Pax Klanq, I Presume?]] is active and you've completed [[Pax Klanq, I Presume?#Convince Pax Klanq to Construct the Climber|Convince Pax Klanq to Construct the Climber]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombIntro
|text=Pax Klanq has agreed to build the climber. What now?
|comment=Only available if you've completed [[Pax Klanq, I Presume?]] and you haven't accepted [[Tomb of the Eaters]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombExplain1
|text=Could you tell me again about the Tomb of the Eaters?
|comment=Only available if [[Tomb of the Eaters]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem
|text=The magnetic field has been disable and the Spindle is free to ascend. What now?
|comment=Only available if you've completed [[Tomb of the Eaters]] and you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem4
|text=Tell me again of the plan to mold a giant creature to ascend the Spindle.
|comment=Only available if [[The Golem]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemQuestDone
|text=The creature has been made. What now?
|comment=Only available if you've completed [[The Golem]] and the Golem is not done}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemInfo
|text=Tell me again how the giant creature functions.
|comment=Only available if you've completed [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=Live and drink, eldest Barathrum.}}}}
{{Qud dialogue|nodetitle=RecameMe
|text=You appear... refreshed. My old eyes spy the swim lines of the waking world and to you they cede precedence. A newness obtains.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Brightsheol1
|text=Outside of time I found myself in a place of light. I chose to return and was made new.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Brightsheol2
|text=I found Brightsheol, where it lies beyond the Tomb. I spoke to the Shomer there and arranged for the disablement of the Spindle's magnets.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreKlanq
|text=Over there, by your workbench. Is that... Pax Klanq?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreJunk
|text=There is junk about.}}}}
{{Qud dialogue|nodetitle=PreKlanq
|text={{Qud shader no parse|emote|*Barathrum pauses.*}}


It is. For reasons I will explain shortly, for reasons beyond any of our remits, Klanq has returned to Grit Gate.}}
In this tutorial, we'll be making it possible to bring statues (like those created by a {{f|lithofex}})
{{Qud dialogue:choice|
to life with a {{f|nano-neuro animator}} or {{f|Spray-a-Brain}}. Most walls and furniture can be animated,
{{Qud dialogue:choice row
but statues are an exception. Stone statues of creatures are dynamically generated at runtime, whereas the
|tonode=RecameMe
specifications for animatable furniture are determined by their blueprints, so it's not trivial to
|text=I've something else to say.}}
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.
{{Qud dialogue:choice row
|tonode=Brightsheol1
|text=Outside of time I found myself in a place of light. I chose to return and was made new.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Brightsheol2
|text=I found Brightsheol, where it lies beyond the Tomb. I spoke to the Shomer there and arranged for the disablement of the Spindle's magnets.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreKlanq
|text=Over there, by your workbench. Is that... Pax Klanq?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreJunk
|text=There is junk about.}}}}
{{Qud dialogue|nodetitle=PreJunk
|text={{Qud shader no parse|emote|*Barathrum pauses.*}}


Unusual circumstances have put my study in disarray. Lamentable, this is, but necessary.}}
== Preliminaries ==
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=RecameMe
|text=I've something else to say.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Brightsheol1
|text=Outside of time I found myself in a place of light. I chose to return and was made new.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Brightsheol2
|text=I found Brightsheol, where it lies beyond the Tomb. I spoke to the Shomer there and arranged for the disablement of the Spindle's magnets.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreKlanq
|text=Over there, by your workbench. Is that... Pax Klanq?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreJunk
|text=There is junk about.}}}}
{{Qud dialogue|nodetitle=Brightsheol1
|text={{Qud shader no parse|emote|*Barathrum pauses.*}}


I am confident there is a richness of wisdom in the poetry of that statement. One day, I will be eager to discover it. For now, what of the Spindle?}}
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
{{Qud dialogue:choice|
the ObjectBlueprints file (Items.xml) for the {{f|nano-neuro animator}} to see how it animates an object.
{{Qud dialogue:choice row
We see it has the AnimateObject part, which we can look up later in the decompiled game code.
|tonode=Brightsheol1
Next, we look at the blueprints (in Furniture.xml) for an object that can already be animated, the {{f|iron maiden}}.
|text=Outside of time I found myself in a place of light. I chose to return and was made new.}}
These are the relevant lines:
{{!}}-
{{Qud dialogue:choice row
|tonode=Brightsheol2
|text=I found Brightsheol, where it lies beyond the Tomb. I spoke to the Shomer there and arranged for the disablement of the Spindle's magnets.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreKlanq
|text=Over there, by your workbench. Is that... Pax Klanq?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PreJunk
|text=There is junk about.}}}}
{{Qud dialogue|nodetitle=Brightsheol2
|text={{Qud shader no parse|emote|*Barathrum breathes a deep and relieved sigh.*}}


You are ever a courier of bright tidings, =name=. I am fond of this fact.
    <tag Name="AnimatedSkills" Value="Persuasion_Intimidate" />
    <tag Name="BodyType" Value="IronMaiden" />
    <tag Name="Animatable" />


Categories and types are decohering in these historic times. Nonetheless, the ritual nourishes us. You are raised to Meyvn, =name=. The one who understands.}}
We also check the blueprint in the same file for "Random Stone Statue", which is what we want to animate.
{{Qud dialogue:choice|
It's missing all the tags we see in the iron maiden, but has the part "RandomStatue", which we'll look up
{{Qud dialogue:choice row
in the decompiled game code. We could just add them to the blueprint via normal XML modding, and this would
|tonode=TombReward
make the statue animatable. But we run into the question of what BodyType to assign to our random statue.
|text=*continue listening*}}}}
We don't know whether it'll be a statue of a salthopper, a saw-hander, or Saad Amus the Sky-Bear.  
{{Qud dialogue|nodetitle=TombReward
There's no single correct anatomy to assign. So we'll have to do it on the fly, at runtime, which means using
|text=Take this, too. A chute of morphing gel from my personal collection. You've arrayed yourself in the artifacts of time, and so we have little left to give you. But still, this may be useful.}}
C# rather than XML.
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=GolemPreface
|text=*continue listening*
|quest=Tomb of the Eaters|step=reward}}}}
{{Qud dialogue|nodetitle=GolemPreface
|text=Now, matters have... evolved... in your absence. Do you choose now to learn more?}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Golem
|text=I do.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=First, I need rest.}}}}
{{Qud dialogue|nodetitle=Golem
|text=High on the mount above Omonporch, the Putus Templar watch us through their war lenses and prepare an attack. The nephilim, too, stir from their cradles on the Moon Stair and slump with infernal purpose. The short of this is, due to a dearth of prep time and inadequate armament, Pax Klanq no longer believes in the viability of Q Girl's climber design. We must take Klanq at its word, here...}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Golem2
|text=*continue listening*}}}}
{{Qud dialogue|nodetitle=Golem2
|text=This disheartens us, but there is another hope. Klanq has offered up a new design, something unconventional at all angles. And, despite our early misgivings, Q Girl and I now work in assistance to the project...}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Golem3
|text=*continue listening*}}}}
{{Qud dialogue|nodetitle=Golem3
|text=A new creature struggles to be born, =name=, and you will help birth it. Then, it will carry us, you and I, to the top of the Spindle.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Golem4
|text=...}}}}
{{Qud dialogue|nodetitle=Golem4
|text=Pax Klanq will mold the creature from raw bioelectrical materials. You must gather these entrails: the body as a model, the catalyst to charge the sanguine fluid, the atzmus as deistic direction, the armament for protection, and the hamsa for personality. You will bond with the creature, too, and act as its pilot. So you must speak an incantation to inscribe the connection.


You'll also need to procure the sanguine fluid itself. And, too, a power source shielded from electromagnetic attacks.}}
Next, we check the decompiled game code, starting with the AnimateObject part.
{{Qud dialogue:choice|
The important pieces here are the CanAnimate method, which checks if a GameObject frankenObject can be animated,  
{{Qud dialogue:choice row
and the Animate method, which takes frankenObject and a few less-relevant parameters and brings frankenObject to life.
|tonode=Entrails
CanAnimate checks if frankenObject has either the blueprint tag or the string property "Animatable" (more on this later),
|text=These entrails, what are they? Where do I find them?
while Animate goes through a long process of assigning frankenObject all the parts it needs to become a real live creature,
|comment=Only available if you haven't accepted [[The Golem]]}}
including assigning the anatomy based on the blueprint tag BodyType (or Humanoid by default).
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=Entrails
|text=They are the components that we will grow, shape, and solder into the ascendant being. We can delve into specifics once you begin, at which point Pax Klanq can also serve as a source of knowledge.


As you acquire components, bring them to the mound of scrap and clay over by Klanq.}}
We also check the decompiled code for RandomStatue. Here, the important bit is the method SetCreature, which
{{Qud dialogue:choice|
modifies the object's properties (tile, description, etc.) to correspond with the creature it depicts.  
{{Qud dialogue:choice row
This is where we need to set up the information which AnimateObject.Animate will use to turn this statue
|tonode=Entrails
into an appropriate creature. Since we need to modify an existing method, we'll need to use Harmony.
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=EntrailsAlternate
|text=They are the components that we will grow, shape, and solder into the ascendant being. Ask Pax Klanq for details.


As you acquire components, bring them to the mound of scrap and clay over by Klanq.}}
Now we begin the actual modding. Begin by creating a mod folder with the usual basic files in it.
{{Qud dialogue:choice|
Next, add a C# file. The name doesn't really matter, but we'll call it AnimateStatue.cs.  
{{Qud dialogue:choice row
Put a namespace declaration in it and import Harmony, like so:
|tonode=Entrails
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=Pilot
|text=Correct. The creature will be shaped broad enough to contain two, but only you will be bonded to it. Only you will pilot.


Don't fear over an inescapable tether, though. The creature will be mobile unpiloted, if slower to act.}}
    using HarmonyLib;
{{Qud dialogue:choice|
    namespace AnimateStatue.HarmonyPatches {
{{Qud dialogue:choice row
        //TODO
|tonode=Entrails
    }
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=Fluid
|text=Primordial soup, to be catalyzed and ran through its arteries as the animating substance.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Entrails
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=Power
|text=Yes, the question of power is a trickier one. Our standard power sources might suffice, but they would be vulnerable to electromagnetic attacks on the ascent, and we believe it within the competency of the Putus Templar to execute such an attack.


This leaves us two options. Klanq and Q Girl have worked out a design for a neutronic battery, free from vulnerabilities on the EM spectrum. But it requires 3 drams of neutron flux. The alternative is remote power, but to carry us across  the Spindle's length the source would have to be extraordinary. I would look to Chavvah, the Tree of Life, for a psychic quickening. Reports tell of the resumption of their roaming across the Moon Stair. Seek the trunk at Eyn Roj to begin your investigation, if that's the path you choose.}}
Next, we need to add a Harmony patch to RandomStatue.SetCreature to assign the proper BodyType.
{{Qud dialogue:choice|
This is a simple modification, and it doesn't impact any of the other lines in the method, so
{{Qud dialogue:choice row
we don't have to worry about the order in which it happens. This makes it a perfect candidate for
|tonode=Entrails
a Harmony [https://harmony.pardeike.net/articles/patching-postfix.html postfix] patch.  
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=EntrailsAlternate
|text=These entrails, what are they? Where do I find them?
|comment=Only available if you've accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Pilot
|text=I'm to pilot the creature? And bond with it?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Fluid
|text=Sanguine fluid?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Power
|text=A power source?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=AcceptGolem
|text=Let us begin, then.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time and space from this.
|comment=Only available if you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Start
|text=I wish to ask about something else.
|comment=Only available if you've accepted [[The Golem]]}}}}
{{Qud dialogue|nodetitle=AcceptGolem
|text=Yes, =name=! Begin the gathering, and the molding will follow. The materials you choose will influence the being we create, but any such being will be suitable for ascent.


This is our last task before we make our historic climb, =name=. Be safe and well in doing it. If you have more questions, return here. Klanq and I will be available.}}
== Postfixes ==
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=End
|text=OK.
|quest=The Golem|step=accept}}}}
{{Qud dialogue|nodetitle=GolemQuestDone
|text=The creature is born, and you are bonded? What sterling work you do, =name=. Now, we must wait until our operators at Omonporch finalize the mechanical provisions for our ascension. Until then, continue as you do, and get to know your creature.


Live and drink, meyvn.}}
A postfix patch modifies a function by adding some code which will always be run after the original function finishes.  
{{Qud dialogue:choice|
This code can have access to the parameters used to call the function, the value returned by the function,
{{Qud dialogue:choice row
and (in the case of non-static methods) the instance whose method is being called.
|tonode=None
Even if another mod adds a postfix patch to the same function as us,
|text=Live and drink, Barathrum.}}}}
we're guaranteed that our postfix code will be run. We're not guaranteed that it'll run *correctly*, but this
{{Qud dialogue|nodetitle=GolemInfo
patch is simple and discrete enough that it'd be hard for another mod to break it accidentally.
|text=Klanq is able to answer your questions. You received an operating manual, too, no? Perhaps you could reference it.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Old
|text=You are so... old.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Quest
|text=Otho said you wished to speak with me.
|comment=Only available if you haven't accepted [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Euclid
|text=What's with the houseplant over there?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if [[Pax Klanq, I Presume?]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if you've completed [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PaxComplete
|text=I convinced Pax Klanq to construct the climber.
|comment=Only available if [[Pax Klanq, I Presume?]] is active and you've completed [[Pax Klanq, I Presume?#Convince Pax Klanq to Construct the Climber|Convince Pax Klanq to Construct the Climber]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombIntro
|text=Pax Klanq has agreed to build the climber. What now?
|comment=Only available if you've completed [[Pax Klanq, I Presume?]] and you haven't accepted [[Tomb of the Eaters]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombExplain1
|text=Could you tell me again about the Tomb of the Eaters?
|comment=Only available if [[Tomb of the Eaters]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem
|text=The magnetic field has been disable and the Spindle is free to ascend. What now?
|comment=Only available if you've completed [[Tomb of the Eaters]] and you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem4
|text=Tell me again of the plan to mold a giant creature to ascend the Spindle.
|comment=Only available if [[The Golem]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemQuestDone
|text=The creature has been made. What now?
|comment=Only available if you've completed [[The Golem]] and the Golem is not done}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemInfo
|text=Tell me again how the giant creature functions.
|comment=Only available if you've completed [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=Live and drink, eldest Barathrum.}}}}
{{Qud dialogue|nodetitle=Questions
|text=What can I offer, nestling?}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Old
|text=You are so... old.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Quest
|text=Otho said you wished to speak with me.
|comment=Only available if you haven't accepted [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Euclid
|text=What's with the houseplant over there?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if [[Pax Klanq, I Presume?]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Signal
|text=Please tell me about the signal again.
|comment=Only available if you've completed [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=PaxComplete
|text=I convinced Pax Klanq to construct the climber.
|comment=Only available if [[Pax Klanq, I Presume?]] is active and you've completed [[Pax Klanq, I Presume?#Convince Pax Klanq to Construct the Climber|Convince Pax Klanq to Construct the Climber]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombIntro
|text=Pax Klanq has agreed to build the climber. What now?
|comment=Only available if you've completed [[Pax Klanq, I Presume?]] and you haven't accepted [[Tomb of the Eaters]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=TombExplain1
|text=Could you tell me again about the Tomb of the Eaters?
|comment=Only available if [[Tomb of the Eaters]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem
|text=The magnetic field has been disable and the Spindle is free to ascend. What now?
|comment=Only available if you've completed [[Tomb of the Eaters]] and you haven't accepted [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Golem4
|text=Tell me again of the plan to mold a giant creature to ascend the Spindle.
|comment=Only available if [[The Golem]] is active}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemQuestDone
|text=The creature has been made. What now?
|comment=Only available if you've completed [[The Golem]] and the Golem is not done}}
{{!}}-
{{Qud dialogue:choice row
|tonode=GolemInfo
|text=Tell me again how the giant creature functions.
|comment=Only available if you've completed [[The Golem]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=Live and drink, eldest Barathrum.}}}}
{{Qud dialogue|nodetitle=TombComplete
|text=You look... refreshed. Thank you, =name=. Take this polygel we recently recovered from the Palladium Reef.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=End
|text=Live and drink, Barathrum.
|quest=Tomb of the Eaters|step=Return to Grit Gate}}}}
{{Qud dialogue|nodetitle=PaxComplete
|text=What welcome news, =pluralizeifplayerplural==stringgamestate:BarathrumitesRank:delver=! Please, take this as a token of my gratitude.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=TombIntro
|text=Thank you.
|quest=Pax Klanq, I Presume?|step=Return to Grit Gate}}}}
{{Qud dialogue|nodetitle=TombIntro
|text=The flywheel of our scheme is spinning, but we must keep our paws on the treadle. Are you ready to learn what comes next?}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=TombBody1
|text=Yes.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I need time on my own first.}}}}
{{Qud dialogue|nodetitle=TombBody1
|text=The climber design relies on the use of electromagnets to interface with the planet's magnetic field and generate torque. Unfortunately, the Spindle generates its own interfering field, perhaps as a mechanism to prevent unwanted ascension.


Our aim is to access the Spindle's control unit and disable the field. How we accomplish this, however, is an enigma. From disparate bits across the archives of the digitum, Ereshkigal has stitched together a cryptic but instructional brocade. She's learned the field can be turned off from a place called Brightsheol, located in the Thin World and accessible only through Resheph's tomb inside the Tomb of the Eaters.}}
We start our patch with a declaration of what function we're modifying, and some more imports we'll be using:
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=TombBody2
|text=...}}}}
{{Qud dialogue|nodetitle=TombBody2
|text=Strange, this, and imprecise, but as tinkers we know: a crude blueprint is better than no blueprint. You must return to Omonporch and enter the Tomb of the Eaters. Once inside, trace the errant paths cut into the mausoleum by robbers and vandals, and ascend to Resheph's burial chamber.


As for gaining entrance to the Tomb, Resheph sealed the gates a thousand years ago, but there's a flaw in the seal. The ancient Mark of Death has been lost to time, but if you were to recover it and incise the mark on your body, the Death Gate would open for you, as it did for countless Eater cadavers in the long-blurred past.
    //...earlier stuff
    using XRL.World;
    using XRL.World.Parts;
    namespace AnimateStatue.HarmonyPatches {
        [HarmonyPatch(typeof(RandomStatue), nameof(RandomStatue.SetCreature))]
        class RandomStatuePatch {
            //TODO
        }
    }


Recover the Mark of Death, =name=, enter the Tomb, and cross into Brightsheol. Place your paws once again on the dial and drum.}}
This states that our patch, RandomStatuePatch, will modify the class RandomStatue's method SetCreature.
{{Qud dialogue:choice|
We can then fill in the class definition with our actual postfix function:
{{Qud dialogue:choice row
|tonode=TombBody3
|text=I will enter the Tomb of the Eaters as you ask.}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=You ask so much. I must consider it.}}}}
{{Qud dialogue|nodetitle=TombBody3
|text=As I'd hoped you would, =name=. Take this disk with the signal encoded, just in case. And take this tattoo gun for when you recover the Mark of Death. You'll want to bring a pickaxe, too, or some other instrument for digging through stone. And beware: the Tomb is a vast and ancient space, and sacred to many. I cannot speak to what you will experience there.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=End
|text=I heed your warning. Farewell, Barathrum.
|quest=Tomb of the Eaters|step=accept}}}}
{{Qud dialogue|nodetitle=TombExplain1
|text=The climber design relies on the use of electromagnets to interface with the planet's magnetic field and generate torque. Unfortunately, the Spindle generates its own interfering field, perhaps as a mechanism to prevent unwanted ascension.


Our aim is to access the Spindle's control unit and disable the field. How we accomplish this, however, is an enigma. From disparate bits across the archives of the digitum, Ereshkigal has stitched together a cryptic but instructional brocade. She's learned the field can be turned off from a place called Brightsheol, located in the Thin World and accessible only through Resheph's tomb inside the Tomb of the Eaters.}}
    //...earlier stuff
{{Qud dialogue:choice|
    class RandomStatuePatch {
{{Qud dialogue:choice row
        [HarmonyPostfix]
|tonode=TombExplain2
        static void Postfix(RandomStatue __instance, GameObject creatureObject) {
|text=...}}}}
            //TODO
{{Qud dialogue|nodetitle=TombExplain2
        }
|text=Strange, this, and imprecise, but as tinkers we know: a crude blueprint is better than no blueprint. You must return to Omonporch and enter the Tomb of the Eaters. Once inside, trace the errant paths cut into the mausoleum by robbers and vandals, and ascend to Resheph's burial chamber.
    }


As for gaining entrance to the Tomb, Resheph sealed the gates a thousand years ago, but there's a flaw in the seal. The ancient Mark of Death has been lost to time, but if you were to recover it and incise the mark on your body, the Death Gate would open for you, as it did for countless Eater cadavers in the long-blurred past.
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`.


You're to recover the Mark of Death, =name=, enter the Tomb, and cross into Brightsheol.}}
Now that we understand the class and function signatures for our patch, let's add the substance of it:
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=TombExplain3
|text=...}}}}
{{Qud dialogue|nodetitle=TombExplain3
|text=Use the tattoo gun I've given you when you recover the Mark of Death. You'll want to bring a pickaxe, too, or some other instrument for digging through stone. And beware: the Tomb is a vast and ancient space, and sacred to many. I cannot speak to what you will experience there.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Questions
|text=I have more to ask, Barathrum.}}}}
{{Qud dialogue|nodetitle=Old
|text=So I am, nestling. For a millennium now I've watched the river of Time turn the gears of Qud. I even played my part in diverting the river where I could. Oh, but there have been so many players through the years! So many players.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Before
|text=And before Qud? Beyond the 1,000 years?}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Questions
|text=I have more to ask, Barathrum.}}}}
{{Qud dialogue|nodetitle=Before
|text=Little do I remember now, for I was but a cub, a nestling, when my kin and I crossed the Homs Delta into Qud. We were urged on by the flooding of a glacial river that flowed through our hearth-cave. The world was just unthawing then, and I recall the roar of the freezing water as it rushed through the grotto, and the frigid spray on my face as I watched in awe.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Questions
|text=I have more to ask, Barathrum.}}}}
{{Qud dialogue|nodetitle=Euclid
|text=That's my dear friend, Euclid. It's a prattleplant. It stores every phrase it hears in its neuroot network, and it mimics speech by flicking its leaves against one another. What comes out is gibberish as often as not, but even so, it's agreeable company for my nights spent tinkering. It is old, perhaps older than I am, and it hasn't yet run out of things to say.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Questions
|text=I have more to ask, Barathrum.}}}}
{{Qud dialogue|nodetitle=Quest
|text=I do. Your service to the guild has been laudable, =pluralizeifplayerplural==stringgamestate:BarathrumitesRank:delver=. You braved the vaporous depths of Bethesda Susa and decoded the signal, you secured the Spindlegrounds and handled the self-appointed Earl, and perhaps most materially, you defended our enclave from the Putus Templar.


Since you returned from Bethesda, you've no doubt wondered at the signal's contents. Verily, I hid them to shield you from the weight of the truth. Only Otho, Q Girl, and I bear that burden, but circumstances have changed. The river of Time powers the gear train of our schemes and devices, and it rushes forth. From here, I must share the burden. You must know.}}
    //...earlier stuff
{{Qud dialogue:choice|
        static void Postfix(RandomStatue __instance, GameObject creatureObject) {
{{Qud dialogue:choice row
            __instance.ParentObject.SetStringProperty("Animatable", "Yes"); //this may be unnecessary
|tonode=Signal
            __instance.ParentObject.SetStringProperty("BodyType", creatureObject.Body.Anatomy);
|text=What is it? Where does the signal come from, and what does it say?}}}}
        }
{{Qud dialogue|nodetitle=Signal
|text=The signal is a beacon of welcoming, and it originates from the top of the Spindle.}}
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=Spindle
|text=And what is the Spindle, truly?}}}}
{{Qud dialogue|nodetitle=Spindle
|text=The beacon confirmed my hypothesis. The Spindle is an elevator, engineered by the Eaters to convey freight to and from the vault of heaven. You see, nestling, in the earliest aurora of our past, the Eaters of Earth were joined by a great coven of beings that spanned the firmament. But the Eaters succumbed to some terrible temptation, and an injunction was placed on our world.


Since then, the stars were silent to us, and our world was left to molder and decay. But something changed. The signal affirms that an entity roosts atop the Spindle, and invites us to join it.
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.


Dare I say, is the injunction at an end? It's too early to tell, but alas, there could be hope for our world.}}
Unfortunately, as you'll have discovered if you tried it out, all statues are still humanoid. We're going to need more patching.
{{Qud dialogue:choice|
{{Qud dialogue:choice row
|tonode=WhatNow
|text=And what now?
|comment=Only available if you haven't accepted [[Pax Klanq, I Presume?]]}}
{{!}}-
{{Qud dialogue:choice row
|tonode=Questions
|text=I have more to ask, Barathrum.
|comment=Only available if you've accepted [[Pax Klanq, I Presume?]]}}}}
{{Qud dialogue|nodetitle=WhatNow
|text=I intend to ascend the Spindle and answer the call, =pluralizeifplayerplural==stringgamestate:BarathrumitesRank:delver=.


My protege &mQ Girl&y has designed a climber for the ascent. We intended to construct it piecewise at the enclave and the site itself, but the engineering feat exceeds even our capacity. With time, I am confident we could accomplish it, but the Putus Templar rob us of our patience. We need to act now, and so as loath as I am to admit it, we need Pax Klanq.}}
The problem here is the split between blueprint tags and object properties. These are two very similar ways of
{{Qud dialogue:choice|
assigning random bits of data to objects. Tags and properties have a name and a value. Some code will check only whether
{{Qud dialogue:choice row
a name is present, while other code will check the value for a given name and do something with it. The difference
|tonode=PaxKlanq
is where they're defined.
|text=Who is Pax Klanq?}}}}
Blueprint tags are set directly on object blueprints, like <tag Name="Animatable"/> or <tag Name="BodyType" Value="IronMaiden"/>.
{{Qud dialogue|nodetitle=PaxKlanq
Object properties, on the other hand, are set at runtime, such as by our calls to SetStringProperty.
|text=Pax Klanq is an eccentric mushroom prodigy. All their faults aside, they are a brilliant scientist and engineer, and I have little doubt that they could build the climber faster than we could. Several years have passed since we were last in contact, but they owe us a debt, and we must now collect.
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.


Unfortunately, no one knows Pax Klanq's whereabouts. Through contacts I made over the years, I inquired as to their location. The most I was able to garner were these enigmatic instructions:


"Seek the heart of the rainbow, eat the god's flesh, and follow the Coral Path."}}
The key issue is that the AnimateObject.Animate method sets the anatomy of the newly animated object by a call to GetTag("BodyType"),
{{Qud dialogue:choice|
which exclusively checks tags, not properties. But we can't set BodyType on the blueprint,  
{{Qud dialogue:choice row
so we have to set it to check properties as well.
|tonode=PaxKlanq2
This might be possible to do with another postfix, simply overriding the anatomy. In fact, that would probably
|text=*continue listening*}}}}
be the safest route. Try doing it yourself as an exercise.  
{{Qud dialogue|nodetitle=PaxKlanq2
|text=I must ask you to decipher this enigma, find Pax Klanq, and convince them to construct the climber. Remind Pax of the debt they owes us.


It strikes me as likely for this 'rainbow' to refer to the Rainbow Wood, where Klanq's kin consort, so I suggest you start there.}}
There is, however, a certain elegance to the idea of just tweaking this one method call instead of
{{Qud dialogue:choice|
adding a whole new one. An odd sort of elegance, to be fair, since we'll have to drag ourselves down
{{Qud dialogue:choice row
into the mud to actually achieve it. But if we really do want to just modify this one function call,
|tonode=Accept
in the middle of the method, without touching anything else or duplicating the call, we'll
|text=I will find Pax Klanq and deliver Q Girl's design.
have found ourselves a use case for a Harmony [https://harmony.pardeike.net/articles/patching-transpiler.html transpiler].
|quest=Pax Klanq, I Presume?|step=accept}}
{{!}}-
{{Qud dialogue:choice row
|tonode=End
|text=I must weigh everything you've told me.}}}}
{{Qud dialogue|nodetitle=Accept
|text=As I had hoped, =pluralizeifplayerplural==stringgamestate:BarathrumitesRank:delver=. First, speak with Q Girl. She'll hand over her design.


I await your return.}}
== Transpilers ==
{{Qud dialogue:choice|
 
{{Qud dialogue:choice row
Compared to standard C# scripting, Harmony patches are powerful but difficult and fragile,
|tonode=End
and should be used only as a last resort.
|text=Live and drink, Barathrum.}}}}
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.

Latest revision as of 05:18, 26 September 2023

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 lithofex) to life with a nano-neuro animator or 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 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 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 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 __instance 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 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 [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.