Skip to content

Advanced Detouring Guide

stormytuna edited this page Jan 15, 2024 · 2 revisions

Prerequisites

You should know how to look through vanilla code.

You should know what Events are.

What and why

Sometimes when trying to implement a feature, we won't be able to just override a hook provided by tModLoader. However, if we could somehow 'override' an existing vanilla method, we would be able to produce the effect we want. This is basically what detours are.

tModLoader provides an API for detouring vanilla methods, providing many events for us to subscribe to. They follow the pattern:

namespace.On_Class.MethodName_MethodSignature

examples:

Terraria.On_Player.GetItemGrabRange
Terraria.GameContent.ItemDropRules.On_CommonDrop
Terraria.On_Projectile.NewProjectile_IEntitySource_Vector2_Vector2_int_int_float_int_float_float_float
Terraria.On_NPC.HitModifiers.SetInstantKill

A few things to note here:

  • On_ only prefixes the class, never the namespace or method
  • Methods with overloads are affixed with the method signature to tell them apart, only when the method actually has overloads
  • Nested types, like NPC.HitModifiers, have their detour events on the parent

All of these On events are how we detour vanilla methods.

How

To detour one of them, we use a Load hook somewhere, we'll be using ModSystem for this example, but you may prefer to put it somewhere else, a ModPlayer or ModItem if it's related to the other code there.

So, we override Load

public class DetourExample : ModSystem 
{
    public override void Load() {
    
    }
}

Now, we just pick something to detour. You will usually find out what you need to detour by looking through vanilla code. For this example, we will write a detour that triples the buff time of any buff, excluding flask buffs and debuffs. If we search through vanilla code a little, we will find that the Player.AddBuff_DetermineBuffTimeToAdd method does this.

We're going to use a feature of Visual Studio, but any proper IDE should also have this functionality. If we type out our event followed by += to subscribe a method, we will see this:

image

This is a handy feature of (most) IDEs, which allows you to quickly generate a method with the correct parameters. If we go ahead and press tab, we will see this:

image

This allows us to rename our subscribed method to whatever we want, you can leave it if you want. I like naming my methods after what they do rather than what they're detouring, so we will use this for this example:

public class DetourExample : ModSystem 
{
    public override void Load() {
        Terraria.On_Player.AddBuff_DetermineBuffTimeToAdd += TripleBuffTime;
    }

    private int TripleBuffTime(On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig, Player self, int type, int time1) {
        throw new System.NotImplementedException();
    }
}

First thing's first, let's go ahead and make it static to avoid capturing any instanced stuff from our class (see here for more information). Now, we have a couple more parameters than the vanilla method. This On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig and Player self. This is a common pattern you'll see in detours, the first orig parameter is how we call the vanilla method and the second self parameter is the object this method is being invoked on (note: this won't be here when detouring static methods). As you may already be able to tell, these follow the pattern:

On_Class.orig_MethodName orig and Class self

Additionally, any parameters passed to the existing vanilla method are after these 2, in this example we have int type and int time1, but other methods you detour might have more or less.

So, let's actually implement something

public class DetourExample : ModSystem 
{
    public override void Load() {
        Terraria.On_Player.AddBuff_DetermineBuffTimeToAdd += TripleBuffTime;
    }

    private static int TripleBuffTime(On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig, Player self, int type, int time1) {
        int buffTime = orig(self, type, time1);

        return buffTime;
    }
}

All this will do is call orig, which will call the vanilla method for us, then returns the result. This is the exact same as not having the detour at all. Let's go ahead and actually do something

public class DetourExample : ModSystem 
{
    public override void Load() {
        Terraria.On_Player.AddBuff_DetermineBuffTimeToAdd += TripleBuffTime;
    }

    private static int TripleBuffTime(On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig, Player self, int type, int time1) {
        int buffTime = orig(self, type, time1);

        if (!Main.debuff[type] && !BuffID.Sets.IsAFlaskBuff[type] && !Main.buffNoTimeDisplay[type] && time1 != 2) {
            return buffTime * 3;
        }

        return buffTime;
    }
}

We've added a little bit of logic here - if our buff isn't a debuff, a flask buff or a buff that shouldn't have its time increased, return triple the result from orig.

And that's it... our detour is finished! If we go ahead and test this out, we'll see that a 10 minute mining potion provides 30 minutes of buff.

image image

This is just one example of how you can use detours, and it's an extremely basic one.

What about...

Unsubscribing

If you understand events, you'll know we usually need to unsubscribe from them, otherwise our methods stay subscribed when they shouldn't be. tModLoader handles unsubscribing for us behind the scenes, so we don't need to do it.

Multiple mods subscribing to the same event

You can actually test this out yourself - detour the same method multiple times, place some logs in there by calling Main.NewText("Hello, from the first detour!") and see what happens when you switch up the order of your detours. You should see that the last method subscribed is called first, and if you remove the orig call, only the last method is called. Basically, when you call orig it actually calls the next subscribed method, ending with the vanilla method. So, if your mod is loaded before another mod that detours the same method, their orig will call your detour, and your orig will call the vanilla logic. If it's the other way around, your orig calls their detour, their orig calls the vanilla logic.

Constructors

These are detourable too. ctor is the instance constructor, cctor is the static constructor.

Detouring methods without a provided event

This is common if you want to detour methods from other mods. It's out of scope for this guide, but this guide explains it in detail.

Common mistakes

Capturing objects

If we use instanced properties/fields in our detour, we open the door to accidentally capturing those objects. This can cause issues, as we capture these objects from the dummy instance created by tModLoader, then continue to use them each time our detour is run. If we capture the ModPlayer Player property, it won't be the same as the self we might be passed in our detour. The way around this is just making our detour method static, and using the provided self parameter to access instanced data. Ex:

    public class DetourExample : ModPlayer
    {
        public bool EpicAccessory { get; set; } // Assume this is reset properly in ResetEffects and assigned properly in an accessory item

        public override void Load() {
            Terraria.On_Player.AddBuff_DetermineBuffTimeToAdd += TripleBuffTime;
        }

        private int TripleBuffTime(On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig, Player self, int type, int time1) {
            int buffTime = orig(self, type, time1);

            if (!Main.debuff[type] && !BuffID.Sets.IsAFlaskBuff[type] && !Main.buffNoTimeDisplay[type] && time1 != 2) {
                int buffTimeMult = EpicAccessory ? 5 : 3;
                return buffTime * buffTimeMult;
            }

            return buffTime;
        }
    }

Here, we're accidentally capturing that EpicAccessory bool, allowed by our detour method being instanced. If we throw a static in there, we'll see this error:

image

So, let's fix it up to use the passed instance.

    public class DetourExample : ModPlayer
    {
        public bool EpicAccessory { get; set; } // Assume this is reset properly in ResetEffects and assigned properly in an accessory item

        public override void Load() {
            Terraria.On_Player.AddBuff_DetermineBuffTimeToAdd += TripleBuffTime;
        }

        private static int TripleBuffTime(On_Player.orig_AddBuff_DetermineBuffTimeToAdd orig, Player self, int type, int time1) {
            int buffTime = orig(self, type, time1);

            if (!Main.debuff[type] && !BuffID.Sets.IsAFlaskBuff[type] && !Main.buffNoTimeDisplay[type] && time1 != 2) {
                int buffTimeMult = self.GetModPlayer<DetourExample>().EpicAccessory ? 5 : 3;
                return buffTime * buffTimeMult;
            }

            return buffTime;
        }
    }
Clone this wiki locally