An On.* hook inserts a delegate into a call chain. The delegate receives an orig continuation. Calling orig enters the next hook or, at the end of the chain, the underlying game method.
private void PlayerUpdate(On.Player.orig_Update orig, Player self, bool eu)
{
orig(self, eu);
RunAdditiveBehavior(self);
}Code before orig runs on the way into the chain. Code after it runs while the chain unwinds. Another hook may wrap yours, so returning from orig does not mean every hook has finished. The installed MonoMod evidence did not establish a global order across mods. Preserve the arguments and call orig once in a simple additive hook unless the behavior has a documented reason to replace or repeat the original call.
Subscription lifetime
Subscribe from the plugin’s OnEnable callback and remove the same named method from OnDisable for an ordinary gameplay hook:
private void OnEnable()
{
On.Player.Update += PlayerUpdate;
}
private void OnDisable()
{
On.Player.Update -= PlayerUpdate;
}Named methods make the removal target explicit. A new lambda expression in OnDisable would be a different delegate and would leave the original subscription in place. First plugin covers the BepInEx component that owns these callbacks.
Hook attachment only establishes that the delegate is registered. A global::Player may lack a room, be entering a shortcut, be slated for deletion, or represent an NPC. Hooks that run every frame need state checks at the point of use. Object interaction applies those checks to a small rock marker.
Rain World mod initialization
Rain World finalizes enabled mods during a process transition in global::ProcessManager.Update(System.Single). At step zero, that method calls global::ModManager.WrapModInitHooks(). The source-verified order for build 22785462 is:
flowchart TD A[ProcessManager.Update transition] --> B["global::ModManager.WrapModInitHooks()"] B --> C[RainWorld.PreModsDisabledEnabled] C --> D[RainWorld.OnModsDisabled] D --> E[RainWorld.PreModsInit] E --> F[RainWorld.OnModsInit] F --> G[RainWorld.PostModsInit] G --> H[Initial user data read, configs, short translations]
global::RainWorld.PreModsInit() initializes global::Menu.Remix.MixedUI.MachineConnector. global::RainWorld.OnModsInit() initializes ExtEnum types and conditionally registers module extensions. global::RainWorld.PostModsInit() begins the initial user data read, loads configurations, and loads short translation strings.
An On.RainWorld.OnModsInit hook that runs code after orig(self) therefore runs after the base OnModsInit continuation, but before PostModsInit has completed its work. Code that needs loaded configuration or user data belongs at a later verified point.
The finalization state is reset during later process transitions. Treat mod initialization as repeatable. A guard for permanent registration can sit after orig:
private bool registrationsAdded;
private void RainWorldOnModsInit(On.RainWorld.orig_OnModsInit orig, RainWorld self)
{
orig(self);
if (registrationsAdded)
return;
AddPermanentRegistrations();
registrationsAdded = true;
}AddPermanentRegistrations() is an illustrative placeholder, so this excerpt is not a standalone compilable handler. The flag changes only after registration returns successfully. Keep that guard separate from reloadable resources. A permanent flag around atlas loading or another resource refresh can leave stale data after a later finalization pass.
Wrapped initialization hooks
global::ModManager.WrapModsInit() replaces foreign initialization delegates with wrappers. A wrapper records exceptions and whether the delegate called orig. If a hook omits orig, the wrapper reports an initialization issue. It does not call the omitted continuation on the hook’s behalf.
global::ModManager.WrapModInitHooks() skips this wrapping when the hook assembly is absent or when noinitwrap.txt exists. The behavior described here is the inspected default with the installed hook assembly present. Removing the original initialization delegate later may also fail to remove the replacement wrapper. Hot disable and re-enable behavior for wrapped initialization hooks remains untested, so use ordinary gameplay hooks for the basic subscription example.
Choosing the side of orig
Put validation that decides whether your additive work can run after orig when the work depends on the game’s updated state. The rock marker reads current player input, room membership, and object positions after global::Player.Update(System.Boolean) returns.
Pre-orig code is appropriate only when the mod must inspect or alter inputs before the game sees them. Replacement hooks that omit orig assume responsibility for all behavior in the original method and all later hooks in that continuation. That scope requires a reason specific to the method and compatibility testing.
Object lifetime adds another boundary. A valid global::PhysicalObject reference may lose its room when its abstract room unloads. Abstract and realized objects explains why a hook should revalidate physical references rather than retain them across room transitions. The exact build and hook assembly identities are in Runtime reference.