How Patches Work
You can use Concord without knowing any of this. But if you want the mental model behind the API, here it is.
The simple model
A patched game method has three slots:
before the method
the original game method
after the method
A head patch runs in the first slot. A return patch runs in the last slot. Concord builds a single wrapper method containing the original method plus every patch that applies to it.
When the docs show an "after" version of a method, it's a readable sketch of the runtime behavior. Concord doesn't edit the game's C# source.
What happens at patch time
Concord changes the runtime behavior for the loaded process. It doesn't rewrite the game's DLL on disk, and it doesn't touch source code.
The flow:
- Concord finds the target method (say,
ShopItem.GetPrice()). - It builds a generated method called a wrapper.
- The wrapper contains the patch code plus the original method body.
- Concord installs a detour from the target method to the wrapper.
- Calls to
ShopItem.GetPrice()now get routed to the wrapper.
So after this patch:
abstract class PricePatch : ShopItem
{
[Inject(nameof(GetPrice), At.Return)]
void AfterGetPrice(CallbackInfo<int> ci)
{
ci.ReturnValue += 5;
}
}
the original method still exists:
public int GetPrice()
{
return 10;
}
but calls behave as if they go through this wrapper:
public int GetPrice__ConcordWrapper()
{
int result = 10;
result += 5;
return result;
}
That's why the patch affects normal callers. They still call GetPrice(), but the runtime entry point now sends them to the wrapper.
When the patch is removed, Concord takes down the detour. If no patches remain for that target, calls go back to the original method body.
What happens to the original method
The original method body isn't thrown away. Concord keeps a pristine clone so advanced features (like reverse patches) can call the unpatched behavior directly. Normal calls go through the patched wrapper; reverse patches call the pristine original.
Why patch classes extend game classes
Concord patches usually look like this:
abstract class PricePatch : ShopItem
{
}
The : ShopItem part makes the patch template compile in the context of the target type, so C# can bind visible target members naturally. Concord doesn't create a real PricePatch object while the game runs. The class is just a template. Concord copies the body of your patch method into a generated wrapper around the real target method.
Injection points
Where your patch runs depends on the injection point you pick.
| Point | Meaning | When to use it |
|---|---|---|
At.Head |
Before the original method | Validate input, cancel early, log state |
At.Return |
After the original method | Change a result, clean up, react to success |
At.Around |
Around the original method | Advanced control over when the original runs |
At.Invoke |
Around a call inside the method | Patch one specific call site |
Start with At.Head and At.Return. They cover most patches.
At.Invoke in detail
At.Invoke targets a method call inside the target method. It wraps that call. The patch gets an operation handle for the matched call and chooses when, whether, and how to invoke it.
This target has a call to PriceRules.ApplyMarkup inside GetFinalPrice:
public int GetFinalPrice(int basePrice)
{
int markedUp = PriceRules.ApplyMarkup(basePrice);
return markedUp + ShippingCost;
}
Run code before the call by putting it before original.Invoke(...):
[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);
}
Run code after by putting it after:
[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;
}
Change the call's arguments by passing different values to Invoke:
[Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
int AroundApplyMarkup(int basePrice, Operation<int> original)
{
return original.Invoke(basePrice - 5);
}
Skip the original call entirely by not calling Invoke:
[Inject(nameof(GetFinalPrice), At.Invoke(typeof(PriceRules), nameof(PriceRules.ApplyMarkup)))]
int ReplaceApplyMarkup(int basePrice, Operation<int> original)
{
return 20;
}
If Concord can't find the requested call site inside the target method, composition fails with CONC031. Matching starts with the declaring type and method name. For repeated calls, be explicit (an ordinal or an "all call sites" selection) so matching multiple sites is never surprising.
Non-call targets
At.Invoke is limited to invocations: method calls, property getter/setter calls, and eventually constructor calls. Other IL shapes need different tools:
| Target | Better fit |
|---|---|
| Method call | At.Invoke |
| Property getter/setter | At.Invoke on the generated getter/setter, or a future At.Property |
| Field read/write | A future At.FieldGet / At.FieldSet, or shadow-field access when patching around the containing method |
| Return instruction | Phase 2 advanced IL editing |
| Throw path | At.Throw |
| Object construction | A future At.New / constructor-call matcher |
| Local variable store/load | Phase 2 advanced IL editing |
Branches / if statements |
Phase 2 advanced IL editing, or patch a stable call inside the condition |
| Constants / literals | Phase 2 advanced IL editing |
Local variables, branch structure, and raw return instructions are compiler output, not stable source-level anchors. A small source change can renumber locals or reshape branches. Concord keeps the easy API on stable anchors and leaves arbitrary IL surgery to a phase-2 advanced tool.
CallbackInfo
CallbackInfo is the patch's control handle. For a void method:
void BeforeSave(CallbackInfo ci)
{
ci.Cancel();
}
For a method that returns a value:
void AfterGetPrice(CallbackInfo<int> ci)
{
ci.ReturnValue += 5;
}
The <int> means the target method returns an int. If it returns string, use CallbackInfo<string>. If it returns bool, use CallbackInfo<bool>.
Concord uses these types as markers. In the generated wrapper, they become simple local variables, not heap objects.
Shadows
If a game class has private int fuel, normal C# won't let your patch touch fuel from another type. A Concord shadow is a declaration that stands in for the private field:
private new int fuel;
Concord checks that the real field exists and has the same type, then rewrites your patch so reads and writes go to the real field.
Reverse patches
Most patches call the patched version of a method. A reverse patch calls a clean clone of the original. Useful when you need the vanilla answer:
int originalPrice = vanillaGetPrice(item);
Most mods won't need reverse patches.
What Concord handles for you
Concord's goal is to make these things boring: checking that patch method names and types are valid, composing patches from multiple mods, restoring the original method when a patch is removed, avoiding per-call allocations, and giving useful diagnostics when a patch no longer matches the game.