GUI Injection - UnlimitedHugs/RimworldHugsLib GitHub Wiki


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


When adding modded functionality to the game, it's often necessary to add a button or two to an existing window. Usually, this would require a detour of the window's drawing method, which comes with its own set of issues.

The new GUI injection system does away with that and adds a clean way to inject code into any window in the game. In fact, multiple mods can add injections for the same window and even replace the window contents entirely.

Basic usage

The easiest way to add an injection is by using the WindowInjection attribute. The attribute must be added to a static method with Window and Rect parameters. The argument of the attribute specifies the exact type of Window that will cause your method to be called.

This example adds a button to the "Load game" dialog:

[WindowInjection(typeof(Dialog_SaveFileList_Load))]
private static void DrawSaveDialogButton(Window window, Rect inRect) {
	var buttonSize = new Vector2(120f, 40f);
	if (Widgets.ButtonText(new Rect(0, inRect.height - buttonSize.y, buttonSize.x, buttonSize.y), "Hello")) {
		// do stuff				
	}
}

The first parameter is a reference to the window being drawn, while the second is the content area of the window.

If the injection method causes an exception, it will produce an error message and be removed to keep the performance of the game from tanking due to repeated errors.

Note, that an injection must not necessarily draw GUI elements- it could be used as a hook to execute other code, including closing that window and opening a new one of your choice.

InjectMode

The WindowInjection attribute has an optional Mode parameter that allows to specify how the injection should execute. The possible values are BeforeContents, AfterContents and ReplaceContents. The default value is AfterContents, which means that your code will execute after the normal drawing code of the window.

ReplaceContents can be used to prevent the usual window contents from being drawn, and can be useful if the intention is to completely rework the functionality of a window. This is generally not recommended, since it's easy to break existing functionality and alter a window that other injections rely on. ReplaceContents will not prevent other injections on the same window from being called.

Replacing GUI elements

Sometimes it is necessary to change what an existing button does. While it is possible to use ReplaceContents and take over the drawing of all the contents of that window, that is usually overkill.

There is an easier approach- just drawing the new element over the existing element. If the size and position of the elements match, only the element drawn last will be interactable.

Labels can be replaced in the same way- a background-colored texture can be drawn on top of the existing label, and the new label can be drawn after that.

var prevGuiColor = GUI.color;
GUI.color = new ColorInt(21, 25, 29).ToColor; // window background color
GUI.DrawTexture(targetRect, BaseContent.WhiteTex);
GUI.color = prevGuiColor;

ImmediateWindow

ImmediateWindow is a special case of window, but since it extends the Window class, it can have injections applied to it just as easily.
The caveat is that all immediate windows have the same type, so it's necessary to check its ID field in your method to make sure you are drawing into the correct window. Without the check the injection would draw into all immediate windows in the game.
The numeric ID can be found by looking at the relevant part of the decompiled source, but must come with a negative sign to properly match the window.

This example writes "Hello" in the bottom right corner of the personal shield status mini-window:

[WindowInjection(typeof(ImmediateWindow))]
private static void DrawImmediateWindowLabel(Window window, Rect inRect) {
	var win = window as ImmediateWindow;
	if(window.ID != -984688) return;
	GenUI.SetLabelAlign(TextAnchor.LowerRight);
	Widgets.Label(new Rect(inRect.width-300, inRect.height-40, 300, 40), "Hello");
	GenUI.ResetLabelAlign();
}

By the same logic of checking a window's members, other window types using the same type for different purposes can also be matched. This applies to Dialog_MessageBox, Dialog_NodeTree, and so on.

Manual injection

If you're not fond of attributes, injections are also easy to add using a direct call to WindowInjectionManager.AddInjection. This will require giving the injection a unique injectionId. Something like "namespace.typeName.methodName" is a good choice, since it allows to easier identify the owner of an injection when trying to diagnose issues.