Patching from C#
Everything you can do in a Lua patch you can also do from a C# mod. PatchManager exposes a C# patching API that mirrors the Lua surface: the same passes, buckets, gates, dependencies, and the same "remove" behavior, expressed as C#. Use it when your mod is already a compiled assembly and you would rather keep your patches in C# alongside the rest of your code, with full type safety on the asset wrappers.
The same value wrappers you use in Lua (PartUserData, CelestialBodyUserData, and so on) are the types you get in C#, with the same helpers.
One difference from Lua: field access
In a Lua patch you read and write an asset's JSON fields as dynamic members: part.crashTolerance. That dynamic member access is a Lua feature and does not exist in C#. In C# you reach the same fields through a string indexer instead:
part["crashTolerance"] = 30.0; // not part.crashTolerance
double m = part["mass"]; // implicit conversion to double
bool heavy = part["mass"] > 5.0;The indexer returns a wrapper that converts implicitly to double, float, int, long, bool, and string, and accepts those types on assignment, so writing a number or reading one just works. The typed helper methods (AddModule, engineModes, and the rest) are real C# methods and members, so those you call normally. It is only the raw JSON fields that move from .field in Lua to ["field"] in C#.
The fluent style
Most C# mods derive from KerbalMod, which gives you a PM property: a patch scope already bound to your mod's id. A domain method on it starts a patch you chain on, the C# equivalent of PM.Parts:Patch(...) in Lua. Register your patches from OnPreInitialized:
using Redux.ExtraModTypes;
public class MyMod : KerbalMod
{
public override void OnPreInitialized()
{
PM.PatchPart("KerbalImpactToleranceFix")
.Named("eva_kerbal")
.Do(part => part["crashTolerance"] = 30.0);
}
}Register from **OnPreInitialized**, not later. By the time OnInitialized runs, PatchManager has already flushed its queue and run the patch flow, so a patch registered after that throws. This is the same rule as Lua, where registration happens once, up front.
If your patch code is not inside a KerbalMod (a static helper, a Harmony patch, some other entry point), get a scope from Patching instead: Patching.Mod binds to the calling assembly's mod id, and Patching.ForMod("YourModId") names it explicitly. KerbalMod's PM is just Patching.ForMod with your id filled in, so the chain after it is identical either way.
The domain methods mirror the Lua submodules:
C# | Lua equivalent |
|---|---|
|
|
|
|
|
|
| the planets override methods |
|
|
| the science methods |
|
|
|
|
The fluent chain
PatchBuilder<T> carries the same methods as the Lua chain, typed to the asset wrapper T:
.Early(),.Late(),.First(),.Last()for pass and bucket..Named(...),.NotNamed(...)for targeting..Needs(...),.Conflicts(...),.NeedsPatch(...),.ConflictsPatch(...)for dependencies..Feature(...),.NoFeature(...)for features. The patch runs only in feature sets that have all.Featureids on and all.NoFeatureids off. An id without a mod prefix is a feature of your mod..After(...),.Before(...),.AfterPatch(...),.BeforePatch(...)for ordering..Requires(predicate, message),.Has(key, ...),.HasNo(key, ...)for gates..Do(...)for the change.
The predicate in .Requires is typed: it receives the wrapper, so there is no cast.
PM.PatchPart("BuffHeavyEngines")
.Named("engine_*")
.Requires(part => part["mass"] > 5.0, "not heavy enough")
.Do(part => { /* ... */ });Note .Named("engine_*"): the C# and Lua targeting both support * and ? wildcards.
Keeping and removing
.Do comes in two forms. The void form keeps the asset, like a Lua callback that returns nothing:
.Do(part => part["mass"] = 0.0);The form returning PatchResult lets you delete the asset, the equivalent of returning "remove" in Lua:
.Do(part => SomeCondition(part) ? PatchResult.Remove : PatchResult.Keep);The attribute style
If you prefer declarative registration, mark a class with [PMPatch] and its patch methods with a per-domain attribute. PatchManager discovers them at load and registers each one. The modifiers (pass, targeting, dependencies, gates) are attributes too:
using PatchManager.CSharpPatching;
using PatchManager.CSharpPatching.Attributes;
using PatchManager.Parts;
using PatchManager.Parts.UserData;
[PMPatch]
public class MyPatches
{
[PatchPart]
[Named("eva_kerbal")]
public void FixKerbalTolerance(PartUserData part)
{
part["crashTolerance"] = 30.0;
}
[PatchPart]
[Named("engine_*")]
[Requires(nameof(IsHeavy), "not heavy enough")]
public PatchResult BuffHeavyEngines(PartUserData part)
{
// ...
return PatchResult.Keep;
}
private bool IsHeavy(PartUserData part) => part["mass"] > 5.0;
}[PMPatch] derives your mod id from the assembly, or pass [PMPatch("YourModId")] to set it. The method takes the typed wrapper and returns void (keep) or PatchResult. A [Requires] or [Has] attribute names a sibling predicate method with nameof. Each available modifier attribute ([Early], [Late], [First], [Last], [Named], [NotNamed], [Needs], [Conflicts], [Before], [After], [NeedsFeature], [NoFeature], and the patch variants) has a fluent method with the same function. [NeedsFeature("HardMode")] is the attribute form of .Feature("HardMode").
Features
As with a Lua patch, each C# patch needs the default feature of your mod. Thus the patch runs only in campaigns that have your mod on. The patch scope also declares features and registers run callbacks. These are the C# equivalents of PM:DefineFeature, PM:OnBeginRun and PM:OnEndRun:
public override void OnPreInitialized()
{
PM.DefineFeature("HardMode", requires: new[] { "Ksp2Redux:Parts" });
PM.OnBeginRun(set => _heavyParts.Clear());
PM.PatchPart("HalveThrust")
.Feature("HardMode")
.Has("Module_Engine")
.Do(part => { /* ... */ });
}OnBeginRun and OnEndRun receive the set that PatchManager builds. They return an action that accepts .Feature(id) and .NoFeature(id). Put the gate on the patch, not in an if around the patch. Features gives the reason.
Which style
Both produce identical patches: the attribute style is just declarative sugar over the fluent builder. Use the fluent style when you want patches built conditionally or in a loop, and the attribute style when your patches are static and you like them read as a list of declarations. Mix them freely.