Making Code Mods - EverestAPI/Resources GitHub Wiki

Before you can add features to your code mod, make sure to follow the Code Mod Setup Guide.

Table Of Contents

Reading the game's code

Reading the code for the vanilla game is made possible through the use of Decompilers 🔗. There are a number of decompilers and ways to decompile available, with different benefits for each.

dnSpy (Windows)

https://github.com/dnSpyEx/dnSpy

  • An unofficial continuation of the discontinued dnSpy project
  • Allows for directly modifying the assembly (not recommended for Celeste modding except for testing)
  • Includes a debugger that can be attached to the Celeste process
  • Uses the ILSpy decompilation engine, but will not always be fully up to date.

ILSpy (Cross-Platform)

https://github.com/icsharpcode/ILSpy (Windows only)
https://github.com/icsharpcode/AvaloniaILSpy (cross-platform)

All of the above programs (dnSpy, ILSpy, ILSpyCMD) can be used to export the decompiled source for the game, which can then be opened and browsed with any editor. This code is unlikely to compile without modifications, as the decompilation process is not perfect and some compiler-generated types are included directly.

⚠️ DO NOT UPLOAD THE DECOMPILED SOURCE CODE ANYWHERE ⚠️

Executing code when specific events occur

You can use Everest events to execute some actions when an event such as a new level starting, etc. happens.
Everest Events Reference

For example, to call the onPlayerSpawn method when the player spawns, use this:

Everest.Events.Player.OnSpawn += onPlayerSpawn;

Modifying the game's code

If you want to modify the game's behaviour, you can check the game's code with ILSpy 🔗 or dnSpy 🔗. ILSpy gives better decompiled code, and dnSpy provides a debugger.

Once you find the method you want to modify, you can use On.Celeste or IL.Celeste to do this. Each of these namespaces allows you to create a type of hook - On hooks or IL hooks, respectively. (However, you should not import these namespaces in your file, as they will conflict with the main Celeste namespace, which contains the actual Celeste types.) Hooks allow you to alter, or even replace, the behaviour of existing Celeste code.

Note: On.Celeste and IL.Celeste come from MMHOOK_Celeste.dll, which is auto-generated by MonoMod HookGen 🔗 when Everest is installed. You need to install Everest on the OpenGL / FNA version of the game to auto-generate a working .dll, otherwise you'll need the Windows-only and obsolete XNA Framework to even compile your mod.

If you want to hook a method not provided by MMHOOK_Celeste.dll, you can manually construct a hook. See IL.* hooks for a description of the process for IL hooks, or ExampleMod 🔗 for commented examples of both kinds of manually constructed hooks.

On.Celeste hooks

Those hooks allow to "replace" a method in vanilla with your own method. You can call the original method when/if you want, by using the orig parameter passed to the hook.

For example, Extended Variants 🔗 use the following to make the game think wall-jumping is always impossible:

public void Load() {
    On.Celeste.Player.WallJumpCheck += modPlayerWallJumpCheck;
}

public void Unload() {
    On.Celeste.Player.WallJumpCheck -= modPlayerWallJumpCheck;
}

private bool modPlayerWallJumpCheck(On.Celeste.Player.orig_WallJumpCheck orig, Player self, int dir) {
    if(Settings.DisableWallJumping) {
        // instead of running the vanilla method, return false all the time.
        return false;
    }

    // call the vanilla method by calling the "orig" method.
    return orig(self, dir);
}

When Settings.DisableWallJumping is true, the vanilla code for Player.WallJumpCheck() won't run, and the method will instead always return false. Otherwise, the method will behave like vanilla.

IL.Celeste hooks

Those hooks allow modifying the contents of a method. Those are useful when you want to inject or modify code at a specific point in a big method, and don't want to copy-paste the entirety of it in your mod.

When you add an IL hook to a method, the hook is immediately called with an ILContext object. For example:

IL.Celeste.Player.DashBegin += modDashLength;

private void modDashLength(ILContext il) { ... }

This object allows you to modify the IL code for the method directly, and the way you want. CIL stands for Common Intermediate Language 🔗, and is a lower-level language. For instance, this code:

if (SaveData.Instance.Assists.SuperDashing) {
    dashAttackTimer += 0.15f;
}

translates to:

IL_009f: ldsfld class Celeste.SaveData Celeste.SaveData::Instance   <= load SaveData.Instance
IL_00a4: ldflda valuetype Celeste.Assists Celeste.SaveData::Assists <= load the Assists field in it
IL_00a9: ldfld bool Celeste.Assists::SuperDashing                   <= load the SuperDashing field in it
IL_00ae: brfalse.s IL_00c2                              <= if this is false, jump over the contents of the if

IL_00b0: ldarg.0                                        <= load "this"
IL_00b1: ldarg.0                                        <= load "this" again
IL_00b2: ldfld float32 Celeste.Player::dashAttackTimer  <= load the dashAttackTimer in this
IL_00b7: ldc.r4 0.15                                    <= load 0.15 to the stack
IL_00bc: add                                    <= this adds the 2 latest loaded things, so dashAttackTimer + 0.15
IL_00bd: stfld float32 Celeste.Player::dashAttackTimer  <= save the result to dashAttackTimer

IL_00c2: [...]

List of all existing instructions 🔗
A reference for what Operand type corresponds to each OpCode can be downloaded here 🔗, as described in this stackoverflow post 🔗

In ILSpy and dnSpy, you can view the IL code by using this combo box on the top-left: ILSpy screenshot for the combobox allowing to switch between IL and C# :link:

In dnSpy, you can also right-click a line of code to view its IL equivalent.

IL hooks allow you to add, remove or modify those IL instructions. For example:

private void modDashLength(ILContext il) {
    ILCursor cursor = new ILCursor(il);

    // jump where 0.3 or 0.15f are loaded (those are dash times)
    while (cursor.TryGotoNext(MoveType.After, instr => instr.MatchLdcR4(0.3f) || instr.MatchLdcR4(0.15f))) {
        Logger.Log("ExtendedVariantMode/DashLength", $"Applying dash length to constant at {cursor.Index} in CIL code for {cursor.Method.FullName}");

        cursor.EmitDelegate<Func<float>>(determineDashLengthFactor);
        cursor.Emit(OpCodes.Mul);
    }
}

private static float determineDashLengthFactor() {
    return Settings.DashLength / 10f;
}

This code looks up for every ldc.r4 0.3 or ldc.r4 0.15 in the code (that is, each time 0.3f and 0.15f are used), and multiply them with the value returned by determineDashLengthFactor().

Here is a what the IL code looks like before patching:

dashAttackTimer = 0.3f;
=>
ldarg.0
ldc.r4 0.3
stfld float32 Celeste.Player::dashAttackTimer

and here is a simplified view of what the code looks like after patching:

ldarg.0
ldc.r4 0.3
call float32 determineDashLengthFactor()
mul                               <= multiplies 0.3 and the result from determineDashLengthFactor()
stfld float32 Celeste.Player::dashAttackTimer
=>
dashAttackTimer = 0.3f * determineDashLengthFactor();

The dash attack timer (determining dash length) is now multiplied with an arbitrary factor, pulled from mod settings.

Extended Variants 🔗 rely a lot on IL hooks, to slightly alter game mechanics (like gravity and maximum fall speed, for example), so it has a lot of examples of these.

Please note that IL code is slightly different between the XNA and FNA versions, at least on Steam. Testing IL hooks against both versions is highly recommended.

Hooking coroutines

Coroutines are methods returning IEnumerator and containing yield return xxx in them. Their name usually ends with "Routine".

  • When yield return [number] is called, the coroutine pauses for this amount of time (in seconds).
  • When yield return null is called, the coroutine pauses for one frame.

Hooking them behaves particularly:

On.* Hooks

When hooking Coroutines, it is required to wrap any yield return orig() in a SwapImmediately object as shown here:

private static IEnumerator onFileSelectLeave(On.Celeste.OuiFileSelect.orig_Leave orig, OuiFileSelect self, Oui next) {
    yield return new SwapImmediately(orig(self, next));

    Logger.Log("TestMod", "I left file select!");
}

This is due to the one frame delay that is present when switching between IEnumerators in a Coroutine.
ℹ️ To be able to use yield return new SwapImmediately(orig(self)) without issues, you need to depend on Everest 2781 or later in your everest.yaml.

⚠️ This one-line method might not work in all cases, especially if the vanilla routine is a "state machine" routine that changes the state (StateMachine.State = xx). In this case, any code you insert after the coroutine ends isn't run. To fix that, you can use this instead:

IEnumerator origEnum = orig(self);
while (origEnum.MoveNext()) yield return origEnum.Current;

IL.* hooks

The actual code of the coroutine is not in the method itself. For example, the IL code for Player.DashCoroutine() is:

IL_0000: ldc.i4.0
IL_0001: newobj instance void Celeste.Player/'<DashCoroutine>d__423'::.ctor(int32)
IL_0006: dup
IL_0007: ldarg.0
IL_0008: stfld class Celeste.Player Celeste.Player/'<DashCoroutine>d__423'::'<>4__this'
IL_000d: ret

⬆️ This is not the actual code for the method, this only instantiates a Celeste.Player/'<DashCoroutine>d__423' object and returns it. This is what IL.Celeste.Player.DashCoroutine += ... will hook, so using that will lead to unexpected results.

The code you see in the C# view in ILSpy is actually located in Celeste.Player/'<DashCoroutine>d__423'::MoveNext(), so if you want to IL hook it, this is the method you want to hook.

You can do so by building an IL hook manually:

ILHook dashCoroutineHook = new ILHook(
    typeof(Player).GetMethod("DashCoroutine", BindingFlags.NonPublic | BindingFlags.Instance).GetStateMachineTarget(),
    modDashSpeed);

GetStateMachineTarget() is what allows to turn Celeste.Player::DashCoroutine() into Celeste.Player/'<DashCoroutine>d__423'::MoveNext().

To undo this IL hook, you can do:

dashCoroutineHook.Dispose();

Note that building an IL hook manually is also useful to hook orig_* methods and other methods that are not made available through IL.Celeste.*.

Accessing private fields/properties/methods

In order to access a private field/property/method in a class, you can use the CelesteMod.Publicizer package 🔗. Simply update your reference to the Celeste.dll inside your .csproj and rebuild the project.

Important

Your IDE might complain about not finding the Celeste types. This can usually be resolved by just restarting it / invalidating it's cache.

Then you can simply access everything as if it were public:

Sprite vanillaSprite = someStrawberrySeedObject.sprite;
someStrawberrySeedObject.sprite = modSprite;

You might realize that some parts are still not public. Those are part of Everest and are intentionally left alone, to avoid mods depending on Everest-internal behaviour.

Alternativly, you can use DynamicData 🔗. For that, create or retrieve the cached DynamicData object, passing it the object you want the access the fields of:

DynamicData strawberrySeedData = new DynamicData(someStrawberrySeedObject);

// For performance, it is recommended to use DynamicData.For() to create the dynamicData once, and reuse it each time it is required.
DynamicData strawberrySeedData = DynamicData.For(someStrawberrySeedObject);

if you just want to access a static field/property/method

DynamicData inputData = new DynamicData(typeof(Input));

After doing that, you can access and set the fields/properties you want on someStrawberrySeedObject by doing this:

Sprite vanillaSprite = strawberrySeedData.Get<Sprite>("sprite"); // gets someStrawberrySeedObject.sprite
strawberrySeedData.Set("sprite", modSprite);    // changes someStrawberrySeedObject.sprite

You can also invoke private methods:

strawberrySeedData.Invoke("OnGainLeader");
strawberrySeedData.Invoke("OnPlayer", player);

DynamicData can also be used to "attach" data to any object. This is done by simply writing to a field that doesn't actually exist in the object strawberrySeedData.Set("nonExistentField", 42). You can then get this field back with strawberrySeedData.Get<int>("nonExistentField"). This can be used, for example, to save some data in a hook on an entity, and get it back later when the hook is executed again / in another hook.

Note

DynamicData is slower performance-wise and more annoying to use, so you'll probably want to use the publicizer in most cases.

⚠️ **GitHub.com Fallback** ⚠️