Skip to content
Vexxing-Your-Files edited this page Dec 5, 2022 · 44 revisions

SpirePatch allows mods to patch their own code into Slay The Spire. When loading a mod, ModTheSpire searches through every class in the mod for any that have the SpirePatch annotation. For each method you want to patch with your mod, you must create a new class and annotate it with SpirePatch.

There is also a newer SpirePatch2, which changes how patch parameters are handled. You are advised to already know how SpirePatch works before reading up on SpirePatch2.

ModTheSpire currently supports the follow patch types:

General Rules

  • If a patch class is a nested class, it must be a static class.
  • A patch method must be a static method.
  • Use the @SpirePatch annotation on the patch class.
  • Patch methods are passed all the arguments of the original method as well as the instance if original method is not static (instance first, then parameters).
public static void [PatchMethod]([InstanceType] __instance, [parameters]...) { ... }

@SpirePatch Parameters

  • clz Defines the class that contains the method to be patched.
  • cls (Old way) Defines the class that contains the method to be patched. Must be the complete class path and class name.
  • method Defines the method to be patched.
    • Use SpirePatch.CONSTRUCTOR to target a constructor.
    • Use SpirePatch.STATICINITIALIZER to target a static initializer.
  • paramtypez Defines the parameter types of the method to be patched (only necessary if multiple methods with the same name exist).
  • paramtypes (Old way) Defines the parameter types of the method to be patched (only necessary if multiple methods with the same name exist). Type names must be complete class path and class name.
  • requiredModId Defines a Mod ID for a mod that must be loaded for this patch to apply.
    • Used for cross-mod patching.
    • Will generate an error if the mod is loaded but the patch fails.
  • optional When set to true, if the class+method to be patched does not exist (e.g. patching another mod that isn't loaded) the patch will be ignored.
    • Will NOT generate an error if the patch fails.
@SpirePatch(
	clz=AbstractPlayer.class,
	method="useCard",
	paramtypez={
		AbstractCard.class,
		AbstractMonster.class,
		int.class
	}
)
public class ExamplePatch
{
	...
}

Patching Order

Patches are applied first in order of type, then in order of mod. Patch type is ordered: Insert, Instrument, Replace, Prefix, Postfix, Raw. This means all Insert patches will be applied before any Instrument patches are applied, and so on. If two or more mods are loaded and define patches of the same type, those patches will be loaded in the order the mods were ordered in the launcher. If a single mod defines multiple patches of the same type, they will be applied in an arbitrary order.

Prefix

Prefix patching inserts a call to your Prefix method at the start of the method you are patching.

You may also use the @SpirePrefixPatch annotation to denote a method to be a Prefix.

public static void Prefix(Ironclad __instance)
{
	...
}

@SpirePrefixPatch
public static void Foobar(Ironclad __instance)
{
	...
}

Features

Postfix

Postfix patching inserts a call to your Postfix method at the end of the method you are patching. Postfix patches can also change the return value of the patched method. You can do this by adding a return value to your Postfix method. If you also add another parameter as the first parameter to your Postfix method of the same type as your Postfix returns, you will be passed the original return value of the patched method.

You may also use the @SpirePostfixPatch annotation to denote a method to be a Postfix.

public static void Postfix(Ironclad __instance)
// or
public static ArrayList<String> Postfix(ArrayList<String> __result, Ironclad __instance)
{
	__result.add("Example Card");
	return __result;
}

@SpirePostfixPatch
public static void Foobar(Ironclad __instance)

Features

Insert

Insert patching inserts a call to your Insert method in middle of the method you are patching. An Insert method must be accompanied by the @SpireInsertPatch.

You may also use the @SpireInsertPatch annotation to denote a method to be an Insert.

@SpireInsertPatch Parameters

Either loc or rloc or locator must be given. The patch method will be called directly before the line number specified. You can find the line numbers by using a java decompiler. JD-GUI seems to work best for getting correct line numbers. If JD-GUI can't decompile a class, use Luyten and turn on debug line numbers (Settings > Show Debug Line Numbers). The line numbers you want appear as comments at the start of each line in Luyten (ex: /*SL:27*/).

Example, with loc=121:

120:  System.out.println("A");
      // Code inserted here
121:  System.out.println("A");
122:  System.out.println("A");

If you use rloc there are a few things to keep in mind. A patch with rloc=0 inserts before the first line with a debug line number in the body of the method you are patching. So, to get a rloc line number you can simply subtract the debug line number that would get rloc=0 from the debug line number of the position you want to insert at.

Example (122 - 120 = rloc=2):

      public void print() {
        // rloc=0 would insert a patch here
120:    System.out.println("A");

        // rloc=2 would insert a patch here
122:    System.out.println("B");
      }

If you want the same patch to be inserted at multiple places you have the option of specifying locs or rlocs which are arrays of line numbers that your patch will inserted at. Example with locs = {121, 123}. If you specify locs or rlocs you do not have to specify loc or rloc.

120:  System.out.println("A");
      // Code inserted here
121:  System.out.println("A");
122:  System.out.println("A");
			// Code **also** inserted here
123:  System.out.println("A");
  • loc Defines the absolute line number to insert at, absolute to the start of the file.
  • rloc Defines the line number to insert at, relative to the start of the method to be patched.
  • locs Defines an additional array of line numbers to insert at, absolute to the start of the file.
  • rlocs Defines an additional array of line numbers to insert at, relative to the start of the method to be patched.
  • localvars Used to capture any local variables and pass them to the patch method. Captured variables are passed as arguments, appearing the in parameter list after the original method's parameters.
@SpireInsertPatch(
	loc=123,
	localvars={"example"}
)
// or
@SpireInsertPatch(
	rloc=4,
	localvars={"example"}
)
public static void Insert(Ironclad __instance, String param1, String param2, int example)
{
	...
}

Features

Locator

Since Slay The Spire is on a weekly update schedule, line numbers are subject to a lot of change. As such rloc is almost always preferable to loc since it is resilient to other areas of the file being patched changing. However, if you want a patch that is even more resilient to the weekly patches than rloc, you can use a Locator. A Locator is a function that is passed the raw CtBehavior from the Javassist API for the method your @SpireInsertPatch is patching. The Locator returns an array of line numbers that indicate where the patch should be applied. To specify a Locator, use the locator parameter of @SpireInsertPatch. When using a Locator you should not specify loc, rloc, locs, or rlocs on your @SpireInsertPatch because the Locator will handle finding the line number.

The reason why Locator is preferable to rloc is because it can patch based on the game logic. For example, the fact that the game creates a new AbstractDungeon to start the game isn't likely to change ever, however, the location of the line where the game creates the new AbstractDungeon could change if the developers make some unrelated changes to the calling method. This change by the devs shouldn't prevent the patch from working but if the patch was done with rloc is very likely to fail now. Instead, with a Locator you can explicitly specify that you want to patch the line before the creation of new AbstractDungeon so no matter where that line changes to, the patch will always find it.

ModTheSpire provides an API that can assist you in finding the lines that match your expected location. The LineFinder provides two methods findInOrder and findAllInOrder that can be used to find the first or find all the lines matching a description specified by a List<Matcher> expectedMatches and Matcher finalMatch. This uses the Matcher type which is a simple way to express that you want to for example match a new declaration or a method call. Example for finding the line at which the end method is called on the SpireBatch in the render method on CardCrawlGame.

@SpirePatch(
  clz=CardCrawlGame.class,
  method="render"
)
public class PostRenderHook {

  @SpireInsertPatch(
    locator=Locator.class,
    localvars={"sb"}
  )
  public static void Insert(CardCrawlGame __instance, SpriteBatch sb) {
    // draw things right before the SpriteBatch has `end` called
  }

  // ModTheSpire searches for a nested class that extends SpireInsertLocator
  // This class will be the Locator for the @SpireInsertPatch
  // When a Locator is not specified, ModTheSpire uses the default behavior for the @SpireInsertPatch
  private static class Locator extends SpireInsertLocator {
    // This is the abstract method from SpireInsertLocator that will be used to find the line
    // numbers you want this patch inserted at
    public int[] Locate(CtBehavior ctMethodToPatch) throws CannotCompileException, PatchingException {
      // finalMatcher is the line that we want to insert our patch before -
      // in this example we are using a `MethodCallMatcher` which is a type
      // of matcher that matches a method call based on the type of the calling
      // object and the name of the method being called. Here you can see that
      // we're expecting the `end` method to be called on a `SpireBatch`
      Matcher finalMatcher = new Matcher.MethodCallMatcher(SpireBatch.class, "end");

      // the `new ArrayList<Matcher>()` specifies the prerequisites before the line can be matched -
      // the LineFinder will search for all the prerequisites in order before it will match the finalMatcher -
      // since we didn't specify any prerequisites here, the LineFinder will simply find the first expression
      // that matches the finalMatcher.
      return LineFinder.findInOrder(ctMethodToPatch, new ArrayList<Matcher>(), finalMatcher);
    }
  }

}

If you want to write your own line finders to find more specific scenarios, like for example to find the last time a method is called, take a look at the com.evacipated.cardcrawl.modthespire.finders.MatchFinderExprEditor API and look at the sample implementations of InOrderFinder and InOrderMultiFinder here on Github.

Instrument

Instrument patching is much more powerful that the previous types of patching, but also more complex. Instrument patching gives you more access to javassist's API, allowing you to alter code in the method you are patching. For example, removing or replacing all method calls inside the method you are patching. See the javassist tutorial and documentation: https://www.javassist.org/tutorial/tutorial2.html#alter

import javassist.expr.ExprEditor;

public static ExprEditor Instrument()
{
	return new ExprEditor() {
		...
	};
}
// or
@SpireInstrumentPatch
public static ExprEditor Foobar()
{
	return new ExprEditor() {
		...
	};
}

Replace

A Replace patch will completely replace a method with your own. None of the original method's code will be called, calling your replace patch's code instead. For example, the following patch will replace the method body of CardLibary.getCardList:

@SpirePatch(
    clz=CardLibrary.class,
    method="getCardList"
)
public class GetCardList
{
    public static Object Replace(LibraryType type)
    {
        ...
    }
}

Note Because of the order patches are applied, Replace patching a method will override any Insert or Instrument patches on the same method.

Warning: DO NOT use a Replace patch unless absolutely necessary. The destructive nature of Replace patches mean you override any other patches applied to the method you're patching.

Raw

Raw patches give you access to the underlying Javassist API to make lower-level changes. This gives you much more flexibility in the changes you can make.

Raw patches are passed the CtBehavior object for the method you're patching. It is then up to you to use the Javassist API to make any changes you want. Starting places to learn Javassist: CtBehavior and Javassist tutorial.

public static void Raw(CtBehavior ctMethodToPatch)
{
    ...
}
// or
@SpireRawPatch
public static void fooBar(CtBehavior ctMethodToPatch)
{
    ...
}