Table of Contents

Common Tasks

The patch shapes you'll reach for most often. Assumes you're comfortable reading C#.

Note

These examples show the authoring API as designed. Some surface pieces are still in progress, see Implementation Status.

Run code before the game method

Use At.Head:

abstract class DoorPatch : Door
{
    [Inject(nameof(Open), At.Head)]
    void BeforeOpen()
    {
        Logger.Info("A door is opening.");
    }
}

Good for logging, setting up state, or gating the method before the game does its thing. At runtime:

public void Open()
{
    Logger.Info("A door is opening.");
    IsOpen = true;
}

Stop the game method

Add CallbackInfo and call Cancel():

abstract class DoorPatch : Door
{
    [Inject(nameof(Open), At.Head)]
    void BeforeOpen(CallbackInfo ci)
    {
        if (IsLocked)
        {
            ci.Cancel();
        }
    }
}

Cancelling a void method is straightforward. If the method returns a value, you also need to set ReturnValue.

At runtime:

public void Open()
{
    if (IsLocked)
        return;

    IsOpen = true;
}

Replace a return value

CallbackInfo<T> where T is the method's return type:

abstract class PricePatch : ShopItem
{
    [Inject(nameof(GetPrice), At.Return)]
    void AfterGetPrice(CallbackInfo<int> ci)
    {
        ci.ReturnValue = 1;
    }
}

Runs after the game calculates the price and swaps the result to 1. At runtime:

public int GetPrice()
{
    int result = BasePrice;
    result = 1;
    return result;
}

Cancel and return a value

For a non-void target, set ReturnValue before or when you cancel:

abstract class PricePatch : ShopItem
{
    [Inject(nameof(GetPrice), At.Head)]
    void BeforeGetPrice(CallbackInfo<int> ci)
    {
        if (IsFree)
        {
            ci.ReturnValue = 0;
            ci.Cancel();
        }
    }
}

If you cancel a non-void method without setting a return value, Concord reports CONC012. At runtime:

public int GetPrice()
{
    if (IsFree)
        return 0;

    return BasePrice;
}

Read or write a private field

Declare a shadow field with the same name and type:

abstract class HealthPatch : Pawn
{
    private new int hitPoints;

    [Inject(nameof(TakeDamage), At.Return)]
    void AfterTakeDamage()
    {
        if (hitPoints < 1)
        {
            hitPoints = 1;
        }
    }
}

Use shadow fields sparingly. Prefer public or protected members when the game already exposes what you need. At runtime:

public void TakeDamage(int amount)
{
    hitPoints -= amount;

    if (hitPoints < 1)
        hitPoints = 1;
}

Wrap a call inside the target

Sometimes the moment you care about isn't the start or end of a method. It's a specific call inside it. At.Invoke wraps the matched call. Your patch gets an Operation<T> handle for the original call and decides when, whether, and how to run it.

abstract class PricePatch : ShopItem
{
    [Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
    int AroundApplyMarkup(int basePrice, Operation<int> original)
    {
        Logger.Info("Applying markup.");
        return original.Invoke(basePrice);
    }
}

Reach for this only when At.Head or At.Return isn't precise enough. At runtime:

public int GetFinalPrice(int basePrice)
{
    Logger.Info("Applying markup.");
    int markedUp = PriceRules.ApplyMarkup(basePrice);
    return markedUp + ShippingCost;
}

After a call inside the target

Since At.Invoke wraps the call, "after" is just code after original.Invoke(...):

[Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
int AroundApplyMarkup(int basePrice, Operation<int> original)
{
    int markedUp = original.Invoke(basePrice);
    Logger.Info($"Marked up price: {markedUp}");
    return markedUp;
}

At runtime:

public int GetFinalPrice(int basePrice)
{
    int markedUp = PriceRules.ApplyMarkup(basePrice);
    Logger.Info($"Marked up price: {markedUp}");
    return markedUp + ShippingCost;
}

Change a call's argument

Pass different arguments to original.Invoke(...):

[Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
int WrapApplyMarkup(int basePrice, Operation<int> original)
{
    int discountedBase = basePrice - 5;
    return original.Invoke(discountedBase);
}

At runtime:

public int GetFinalPrice(int basePrice)
{
    int discountedBase = basePrice - 5;
    int markedUp = PriceRules.ApplyMarkup(discountedBase);
    return markedUp + ShippingCost;
}

Replace a call entirely

Don't call original.Invoke(...). Just return your own value:

[Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
int ReplaceApplyMarkup(int basePrice, Operation<int> original)
{
    if (UseFlatPrice)
    {
        return 20;
    }

    return original.Invoke(basePrice);
}

Be careful here. Skipping the original call means skipping its side effects too. At runtime:

public int GetFinalPrice(int basePrice)
{
    int markedUp = UseFlatPrice
        ? 20
        : PriceRules.ApplyMarkup(basePrice);

    return markedUp + ShippingCost;
}

Call the unpatched original

A reverse patch calls the original game method directly, bypassing any patches:

var original = Patcher.ReversePatch(typeof(ShopItem).GetMethod("GetPrice"));

int vanillaPrice = original(item);

Useful when your patch needs to compare patched behavior against the game's unmodified output.

Pick good patch names

Patch class names should say what they change. FreeStarterItemsPatch is good. Patch1 is not. Patch names show up in debugging and diagnostics.