Table of Contents

Hooking the Game

You need this when neither Flow nor an interceptor covers what you want, and you have to patch a method directly.

When the game exposes no extension point for what you want, you patch it. ShapezShifter wraps MonoMod in DetourHelper, which expresses the target method as a lambda instead of a reflection string — so a rename becomes a compile error rather than a runtime crash.

Postfix — run after

using ShapezShifter.SharpDetour;
using MonoMod.RuntimeDetour;

Hook hook = DetourHelper.CreatePostfixHook<MapDrawer, FrameDrawOptionsNoLOD>(
    (drawer, options) => drawer.Draw(options),      // target, as a call expression
    (drawer, options) => Overlay.Draw(options));    // your code, same parameters

The first lambda is never executed — it is parsed as an expression tree to identify the method. Write it as a normal call on the parameters.

Overloads exist for zero to seven arguments, for void and for value-returning methods, and for static methods (CreateStaticPrefixHook):

CreatePostfixHook<TObject>(Expression<Action<TObject>>, Action<TObject>)
CreatePostfixHook<TObject, TArg0>(Expression<Action<TObject, TArg0>>, Action<TObject, TArg0>)
CreatePostfixHook<TObject, TArg0, TArg1>(…)
// …up to TArg6

Prefix — run before, and optionally rewrite the arguments

A prefix returns the arguments the original will receive. With one argument you return the new value; with several you return a tuple:

DetourHelper.CreatePrefixHook<SomeType, float>(
    (target, amount) => target.DoThing(amount),
    (target, amount) => amount * 2.0f);          // the original now sees double

DetourHelper.CreatePrefixHook<SomeType, int, string>(
    (target, count, label) => target.DoThing(count, label),
    (target, count, label) => (count + 1, label.ToUpper()));

For a void-target with no arguments the prefix is a plain Action<TObject> — no return value to rewrite.

Choosing a target

The method you hook is a contract you are inventing, so pick a stable one:

  • Prefer methods with meaningful signatures. MapDrawer.Draw(FrameDrawOptionsNoLOD) hands you everything you need; a private helper with three primitive parameters tells you nothing and moves between versions.
  • Prefer things called once per frame or once per session over things called per item. A hook on a hot path multiplies its cost by the size of the player's factory.
  • Hook the widest point that works. One hook on the draw entry point beats twenty on individual drawers.
  • Avoid compiler-generated members — lambdas, iterator state machines, local functions (<>c__DisplayClass…). Their names are not stable across builds.

Generic methods cannot be hooked at all

This one is a hard stop, not a preference. MonoMod refuses any method whose declaring type is generic:

System.ArgumentException: Source method is generic, generic hooks are not supported
  at MonoMod.RuntimeDetour.Hook.CheckSupported()

Being instantiated over a value type makes no difference — Foo<ShapeId>.Bar() is rejected exactly like Foo<T>.Bar(), even though a struct instantiation has its own native code and is intuitively "a concrete method". Hook.CheckSupported looks at the declaring type, not the instantiation.

It fails at construction, so a hook set up in a mod constructor throws during mod load and takes the game's startup with it. It also compiles perfectly — typeof(Foo<ShapeId>) .GetMethod("Bar") is a valid MethodBase, so nothing warns you until launch.

The way round it is to find a non-generic choke point the calls already pass through. Game code that is generic at one layer is usually reached through something that is not: a lane, a dispatcher, a factory. For example, an item entering a train station passes DummyLane.CanAcceptItem — non-generic — on its way to the generic TrainBeltToCargoFillingContainer<T> behind it, so a guard belongs on the lane.

Two costs to accept when you relocate a hook this way: the choke point is usually hotter than the method you wanted, so order your type tests so the common case falls through cheaply; and it is broader, so the hook must check it is looking at the case it cares about rather than assuming.

Unwind a partial set of hooks

Applying several related hooks is not atomic. If the third throws, the first two are live and the mod is half-patched — sometimes worse than not patching at all, because the pieces were designed to work together. Wrap the set and dispose what you applied:

try
{
    Add(target1, hook1);
    Add(target2, hook2);
    Add(target3, hook3);   // throws
}
catch
{
    Dispose();             // drop the ones that did apply
    throw;
}

Interceptors and rewirers

Before writing a detour, check whether Hijack already covers the structure you want to extend. The pattern is a pair: an interceptor that hooks the game's construction of something, and a rewirer interface you implement to contribute to it.

public interface ITickRewirer : IRewirer
{
    void Tick(float deltaTime);
}

Register with GameRewirers.AddRewirer(...), which returns a disposable RewirerHandle. The full list of interception points is in Architecture.

This is worth preferring over a raw detour whenever it fits: interceptors are part of ShapezShifter's API surface, so they are maintained across game updates, whereas your detour target is not.

Ticks have a one-line wrapper, which is all most mods need:

using ShapezShifter.Flow;
RewirerHandle handle = this.OnTick(dt => Update(dt));

Chaining, not replacing

Whenever you take over a delegate the game owns — a lane's AcceptHook, a DrawHooks delegate, an event — save the previous value and call it:

AcceptHookDelegate saved = lane.AcceptHook;
lane.AcceptHook = delegate(IItemReceiver receiver, ref IBeltItem item, ref Ticks remaining_T)
{
    Observe(item);
    saved?.Invoke(receiver, ref item, ref remaining_T);
};

Replacing outright is the most common way to break a machine while your own code looks correct — the cutter that no longer cuts, the belt that no longer moves.

Disposing

Every hook is disposable and every rewirer registration hands back a handle. Undo both in IMod.Dispose():

public void Dispose()
{
    DrawHook?.Dispose();
    TickHandle?.Dispose();
}

Staying compatible

The game's internals are not a public API, and the shipped assemblies carry no compatibility promise. Practical mitigations:

  • Keep hooks few and central. A mod with two hooks survives an update that a mod with twenty will not.
  • Isolate them. One file that installs every hook, so a version bump is one file to review.
  • Never assume a hook installed. Wrap installation in try/catch, log the failure, and let the rest of the mod carry on degraded rather than dying at load.
  • Watch for [Obsolete]. The game marks types it intends to replace — DrawHooks, IGameSessionManagers, IslandModel.LayoutQuery. They work today, and they are exactly what will change tomorrow.
  • Declare your range in manifest.json (GameVersionSupportRange) rather than letting the game load your mod into a version you have never tested.