Skip to content

Adding fields

Zetrith edited this page Feb 25, 2023 · 13 revisions

Accessor methods

To request a new field, the magic incantation is:

[PrepatcherField]
public static extern ref int MyInt(this TargetClass target);

This definition adds an int field to class TargetClass accessible using target.MyInt().

The method above is called the accessor of the new field. Let's go through its parts:

  • [PrepatcherField] (required), the attribute by which Prepatcher finds the accessor
  • public (optional), any access modifier is allowed
  • static (required), an instance method wouldn't make much sense
  • extern (optional), makes it so the definition doesn't need a body
  • ref (optional), allows the method to act as a setter, you should always need it unless you are doing injections
  • int (example), the field type
  • MyInt (example), accessor name
  • this (optional), makes the method an extension method, not required but very convenient
  • TargetClass (example), the target class where the new field is added (struct targets should be byref: ref TargetStruct)

Accessor methods are currently searched for only in static classes.

Generated field

Following the example above, the new field generated by Prepatcher would look something like this:

public class TargetClass {
    // ...
    int MyIntField;
    // ...
}

With the accessor patched into looking like:

public static ref int MyInt(this TargetClass target) {
    return ref target.MyIntField.
}

The name of the generated field is an implementation detail. There are no guarantees; it might change between different Prepatcher versions or even game sessions. There currently isn't a way to specify the name.

Generics

Adding generic fields is supported and works as expected. It's best to look at some examples.

For target classes:

public class TargetGeneric<T> { }
public class TargetGeneric3<T, U, W> { }

You can define:

[PrepatcherField]
private static extern ref List<T> MyList<T>(this TargetGeneric<T> target);

[PrepatcherField]
private static extern ref (A, C) MyPair<A, B, C>(this TargetGeneric3<A, B, C> target);

These are invalid:

// Target cannot be a generic instance type
[PrepatcherField]
private static extern ref List<T> GenericInstance<T>(TargetGeneric<int> target);

// The generic parameter list of the accessor method must equal the generic argument list of the target type
[PrepatcherField]
private static extern ref int GenericsDontMatch<T, U>(TargetGeneric3<T, U, U> target);

Default values

The DefaultValue and ValueInitializer attributes can be used to specify the initial value assigned to a field during object construction.

[PrepatcherField]
[DefaultValue(1)]
public static extern ref int MyInt(this TargetClass target);

[PrepatcherField]
[ValueInitializer(nameof(MyObjectInitializer))]
public static extern ref SomeObject MyObject(this TargetClass target);

private static SomeObject MyObjectInitializer(TargetClass target) {
    return new SomeObject(target);
}

DefaultValue is useful for simple constant defaults. It accepts numerical primitives and strings.

ValueInitializer takes the name of a method whose result becomes the initial value of the field. The method can be parameterless or have one parameter accepting the target object. The method is searched for in the class of the accessor.

Initial values are assigned only once, essentially by postfixing every constructor in the target class (unless it delegates to another constructor using this(...) so the value isn't assigned twice).

Injections

New fields can be automatically populated with components of the target type using injections.

[PrepatcherField]
[InjectComponent]
public static extern MyComp NewCompField(this TargetWithComps target);

The accessor must be getter-only (no ref).

If you are familiar with RimWorld's component system(s), target.NewCompField() above is equivalent to target.GetComp<MyComp>() but much faster.

Under the hood, Prepatcher essentially patches the component initialization method adding:

public void InitializeComps() {
    // ...
    this.NewCompField() = this.GetComp<MyComp>();
}

So the field is set once and quickly accessible using the accessor. The actual implementation is a bit more complicated to handle inheritance properly.

Small caveat: at the moment, the assignment operations are added at the end of the respective component initialization methods by Prepatcher during its patching cycle. Importantly, this means that they effectively run before any Harmony postfixes. Any components added "manually" by postfixing (and not in XML or a transpiler) won't get injected.

Currently supported components:

  • Verse.ThingComp on Verse.ThingWithComps
  • Verse.MapComponent on Verse.Map
  • Verse.GameComponent on Verse.Game
  • RimWorld.Planet.WorldComponent on RimWorld.Planet.World

Compatibility

Field addition is purely additive and poses almost no compatibility issues.

It can only create problems in very specific situations. For example, if another piece of code depends on the exact number of fields in a class or somehow indexes fields by their position from the end of a class's field list.

Clone this wiki locally