Skip to content

Detouring

UnlimitedHugs edited this page May 11, 2017 · 5 revisions

NOTICE: Since the 3.0.0 version of HugsLib (A17) all detouring functionality has been replaced by patching. See Introduction to Patching for information on how to migrate your existing detours to the new system.


Detouring for fun and profit

Detouring is way to redirect all calls from one method or property to another. This allows mods to modify functionality in the base game that would otherwise not be modable.
Detouring destroys the original method, so the destination method will have to reproduce its original functionality, in addition to whatever else it intends to do.


Here be dragons

Detouring is an advanced modding feature that is only to be used used as a last resort, when there is no other reasonable way to implement the functionality you are looking for. There may be better solutions- to name a few, a mod can add custom Comps to Things, use Reflection to access private members, and replace objects with versions that use inheritance to override original functionality. Modifying Defs at load time is also an option, which allows to alter ThinkTrees, among other things. There are two main issues with taking the detouring approach:

  • Each method may only be detoured once.
    This means, that two mods that detour the same method will be incompatible with each other. The library will raise an error in the console when an attempt is made to detour an already detoured method. This may be easy to diagnose in Dev mode, but regular players would not even see the error.
  • Updating your mod will require additional effort.
    With each update to Rimworld, mod authors that use detours will have to look through each method they detoured in the base game. This is needed to make sure that the functionality they duplicated in the target method still matches that of the original.

If you're still reading, the library provides some facilities to make detouring easier. Note, that if the Community Core Library is also loaded, HugsLib will forward any detouring requests to it for improved compatibility.

Detour by attribute

Decorating your target method or property with the DetourMethod and DetourProperty respectively will allow the library to apply the appropriate detours automatically when the declaring mod is being loaded.

Detouring static methods

To ensure that a detour works properly, the argument count, argument types and the return type of the target method must match that of the original. It is not a requirement, but generally a good idea, to declare your target methods in a separate static class.
This example detours Widgets.ButtonText, which is called to draw most buttons in the game, and replaces the button text. Note, that a different method than the one being detoured is used to do the actual drawing work.

[DetourMethod(typeof(Widgets), "ButtonText")]
public static bool ButtonText(Rect rect, string label, bool drawBackground, bool doMouseoverSound, bool active) {
	const string newLabel = "Test";
	return Widgets.ButtonText(rect, newLabel, drawBackground, doMouseoverSound, Widgets.NormalOptionColor, active);
}

Detouring instance methods

Instance methods receive an additional argument, which is the reference to the object the original method is a member of. This corresponds to the this keyword you would use in a regular instance method. To receive this argument, the target method must be static and declared as an extension method of the original method's parent type. Any additional arguments must come after the self-referencing argument. This example changes the string displayed in the description panel when any pawn is selected.

[DetourMethod(typeof(Pawn), "GetInspectString")]
private static string GetInspectString(this Pawn self) {
	return self.Label + " test";
}

Detouring properties

Detouring a property is just syntactic sugar for detouring either the getter, setter or both property methods into an equivalent property. When detouring a member property, it is often useful to have access to the object the property vas invoked on. To that end we can create a class that extends the type containing the source property. When using the this keyword in the destination getter or setter within that class, it will refer to the object the detoured property was invoked on. Do not create new instance fields in that class to to access them from the new property code, as this will result in memory corruption.
In this example the Thing.HitPoints property is detoured, preventing the hitpoints of any Thing from going below 1, thus making everything indestructible by damage. This example is inefficient performance-wise, because of the numerous reflection calls. The member field is not an issue because it is static.

public class Indestructible : Thing {
	public static FieldInfo hitPointsIntField = typeof(Thing).GetField("hitPointsInt", Helpers.AllBindingFlags); 
	[DetourProperty(typeof (Thing), "HitPoints", DetourProperty.Both)]
	public int _HitPoints {
		get { return (int)hitPointsIntField.GetValue(this); }
		set {
			if (value < 1) value = 1;
			hitPointsIntField.SetValue(this, value);
		}
	}
}

Handling detour failures

When two mods detour the same method or property, the detour will only be applied for the one loaded first. The detour in the second mod will generate an error. This can be difficult to debug, since errors are silently written to the log when the game is not in Dev mode.
It's a good idea to account for the possibility that your detour may not go through, and display an error dialog to the player or disable some part of your mod. Since version 2.2.0 there is a way to do that by using the DetourFallback attribute.
The attribute must be applied to a static method with a specific signature, that exists within the same class as your destination method or property.
Note, that detours are applied and fallback handlers are invoked before any ModBase descendants are instantiated or initialized.
This example displays an error dialog box to the player when the detour fails due to being already taken:

public static class DetourFallbackTest {
	[DetourMethod(typeof(Pawn_RelationsTracker), "Notify_RescuedBy")]
	internal static void _Notify_RescuedBy(this Pawn_RelationsTracker t, Pawn rescuer) {
		// do usual detour stuff
	}

	[DetourFallback("_Notify_RescuedBy")]
	public static void DetourFallbackHandler(MemberInfo attemptedDestination, MethodInfo existingDestination, Exception detourException) {
		if (existingDestination != null) {
			LongEventHandler.QueueLongEvent(() => {
				Find.WindowStack.Add(new Dialog_MessageBox(string.Format("ModName: a required method was already detoured to {0}", existingDestination.FullName())));
			}, null, false, null);
		}
	}
}

The attemptedDestination is the destination method or property of the attempted detour. This can be a MethodInfo or a PropertyInfo.
The existingDestination parameter is a reference to the method your source method was already detoured to. Will be null if the failure is not due to the detour already being taken.
The detourException parameter contains the exception that was generated during the detour attempt. You can use detourException.InnerException.Message to read the details.

The attribute can be provided with multiple method names it should accept failures from, or none at all- in which case is becomes the fallback for all detours in that class.
Fallbacks will also handle other detouring failures- most notably a missing source method. This can be useful when detouring methods in other mods.
If you are applying your detours manually, you can call DetourProvider.TryGetExistingDetourDestination to know if your source method has already been detoured.

Classic detouring

A detour can also be requested directly, by providing the source and destination MethodInfo. DetourProvider.CompatibleDetour will throw an exception if the detour fails, while DetourProvider.TryCompatibleDetour returns a bool to indicate the success of failure of the operation.

var source = typeof(Widgets).GetMethod("ButtonText", Helpers.AllBindingFlags, null, new[]{typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool)}, null);
var destination = typeof(Detours).GetMethod("ButtonText", Helpers.AllBindingFlags);
DetourProvider.CompatibleDetour(source, destination);