Your First Patch
A small patch, end to end. The example isn't tied to a real game class, it's just here to show you the shape of the API.
The game code
Imagine the game has a campfire class:
public class Campfire
{
private int fuel = 10;
public int GetWarmth()
{
return fuel * 2;
}
public void BurnFuel(int amount)
{
fuel -= amount;
}
}
Patching a return value
This patch makes every campfire 5 points warmer:
abstract class CampfireWarmthPatch : Campfire
{
[Inject(nameof(GetWarmth), At.Return)]
void AfterGetWarmth(CallbackInfo<int> ci)
{
ci.ReturnValue += 5;
}
}
The patch extends the target type so C# can bind target members naturally. Concord never actually creates a CampfireWarmthPatch object during gameplay (the class is just a template). [Inject(nameof(GetWarmth), At.Return)] patches GetWarmth and runs after the original method has produced a return value. The target returns an int, so CallbackInfo<int> lets the patch read or replace that value. ci.ReturnValue += 5 grabs the game's result and adds 5.
At runtime, calls behave as if the method were:
public int GetWarmth()
{
int result = fuel * 2;
result += 5;
return result;
}
Concord doesn't touch the Campfire source.
Stopping the original method
This patch blocks invalid fuel burns:
abstract class CampfireBurnPatch : Campfire
{
[Inject(nameof(BurnFuel), At.Head)]
void BeforeBurnFuel(int amount, CallbackInfo ci)
{
if (amount <= 0)
{
ci.Cancel();
}
}
}
At.Head runs before the game method. ci.Cancel() skips it. Since BurnFuel returns void, the patch uses CallbackInfo (not CallbackInfo<int>).
At runtime:
public void BurnFuel(int amount)
{
if (amount <= 0)
return;
fuel -= amount;
}
Using a private field
The game class has a private fuel field. Concord lets you declare a shadow field that stands in for it:
abstract class CampfireFuelPatch : Campfire
{
private new int fuel;
[Inject(nameof(BurnFuel), At.Return)]
void AfterBurnFuel(int amount)
{
if (fuel < 0)
{
fuel = 0;
}
}
}
private new int fuel; is a shadow declaration. Concord verifies the real target has a field named fuel with the same type, then rewrites field access in the copied patch body to hit the real field. If the field doesn't exist, you get CONC001. Wrong type? CONC002.
At runtime:
public void BurnFuel(int amount)
{
fuel -= amount;
if (fuel < 0)
fuel = 0;
}
Applying the patch
Patches go through a Patcher:
var patcher = new Patcher("example.campfires");
patcher.Apply<CampfireWarmthPatch>();
patcher.Apply<CampfireBurnPatch>();
patcher.Apply<CampfireFuelPatch>();
Pick a stable mod id, something like author.mod-name.
What to remember
- Extend the class you want to patch.
At.Headruns before the original,At.Returnruns after.CallbackInfo<T>when the target returns a value,CallbackInfowhen it returnsvoid.- Shadow fields are for private target state, use them only when you have to.