Skip to content

Hooks.md

ESTONlA edited this page May 27, 2026 · 2 revisions

Hooks

Hooks let a mod run code before, after, deferred after, or instead of selected vanilla script methods.

Use hooks when you want to add behavior without replacing an entire vanilla script. They are usually more compatible than full resource replacement or script overrides.

How Hooks Work

OrcKit generates a temporary hook pack. For selected vanilla methods, it renames the original method internally and inserts a wrapper method with the original name.

The wrapper can:

  1. Dispatch -pre callbacks.
  2. Run a replace hook or the vanilla method.
  3. Dispatch -post callbacks.
  4. Dispatch -callback callbacks deferred.

Only non-static functions are hookable.

Declare Hook Targets

Declare target scripts and methods in mod.txt:

[hooks]
res://scripts/player.gd="_ready,_process"

This tells OrcKit which vanilla scripts need wrappers.

Wrap all methods:

[hooks]
res://scripts/player.gd="*"

Prefer specific method names. * is convenient while investigating, but it creates a larger hook pack and can affect compatibility.

Register Hooks from an Autoload

extends Node

func _ready() -> void:
	var modlib = Engine.get_meta("OrcmodLib")
	if modlib._is_ready:
		_register_hooks(modlib)
	else:
		modlib.frameworks_ready.connect(func(): _register_hooks(modlib))

func _register_hooks(modlib) -> void:
	modlib.hook("player-_ready-pre", Callable(self, "_before_player_ready"), 100)

func _before_player_ready() -> void:
	print("Player is about to run _ready")

Hook Name Format

<script-stem>-<method-name>-<phase>

Example vanilla path:

res://scripts/player.gd

Script stem:

player

Method:

_ready

Hook names:

player-_ready-pre
player-_ready-post
player-_ready-callback
player-_ready

Hook names are lowercase for the script stem and method name.

Hook Phases

-pre

Runs before vanilla behavior.

modlib.hook("player-_process-pre", Callable(self, "_before_process"))

func _before_process(delta: float) -> void:
	pass

-post

Runs after vanilla behavior.

modlib.hook("player-_process-post", Callable(self, "_after_process"))

func _after_process(delta: float) -> void:
	pass

For methods with return values, a post hook can accept the current result as its last argument. If it returns a non-null value, that value becomes the new result.

func _after_get_damage(amount: int, current_result: int) -> int:
	return current_result + 5

-callback

Runs deferred after the wrapper.

modlib.hook("player-_ready-callback", Callable(self, "_after_ready_deferred"))

Use callback hooks when you want work to happen after the current call stack.

No suffix: replace hook

modlib.hook("player-_process", Callable(self, "_replace_process"))

func _replace_process(delta: float) -> void:
	var modlib = Engine.get_meta("OrcmodLib")
	modlib.skip_super()
	# Vanilla _process will not run.

Only one replace hook can own a hook name. If another replace hook already exists, hook() returns -1.

Hook Priority

Lower priority callbacks run earlier.

modlib.hook("player-_ready-pre", Callable(self, "_first"), 10)
modlib.hook("player-_ready-pre", Callable(self, "_second"), 100)

Use priority only when order matters. Most mods can use the default 100.

add_hook

add_hook() builds the hook name for you:

modlib.add_hook("res://scripts/player.gd", "_ready", Callable(self, "_before_ready"), true)
modlib.add_hook("res://scripts/player.gd", "_ready", Callable(self, "_after_ready"), false)

The last argument means:

  • true: pre hook
  • false: post hook

hook_many

var result = modlib.hook_many({
	"player-_ready-pre": Callable(self, "_before_ready"),
	"player-_ready-post": Callable(self, "_after_ready"),
}, 100)

if not result["ok"]:
	push_warning("Some hooks failed to register")

Game-Specific Helper Hooks

OrcKit also exposes stable helper functions for common Sir, We Have an Orc Problem events. These are easier to use than raw hook names.

Call these from an autoload after frameworks_ready:

extends Node

func _ready() -> void:
	var modlib = Engine.get_meta("OrcmodLib")
	if modlib._is_ready:
		_install(modlib)
	else:
		modlib.frameworks_ready.connect(func(): _install(modlib))

func _install(modlib) -> void:
	modlib.on_battle_start(Callable(self, "_on_battle_start"))
	modlib.on_enemy_killed(Callable(self, "_on_enemy_killed"))
	modlib.on_tower_placed(Callable(self, "_on_tower_placed"))

func _on_battle_start(battle: Node) -> void:
	print("Battle started")

func _on_enemy_killed(count: int, data: PackedByteArray, battle: Node) -> void:
	print("Killed ", count, " enemy/enemies")

func _on_tower_placed(tower: Node, battle: Node) -> void:
	print("Tower placed: ", tower.name)

Available helpers:

Helper Callback arguments
modlib.on_battle_start(callable, priority := 100) battle
modlib.on_battle_end(callable, priority := 100) health, total_enemies_spawned, total_enemies_killed, battle
modlib.on_enemy_spawned(callable, priority := 100) count, battle
modlib.on_enemy_killed(callable, priority := 100) count, data, battle
modlib.on_tower_placed(callable, priority := 100) tower, battle
modlib.on_tower_removed(callable, priority := 100) tower, battle
modlib.on_level_loaded(callable, priority := 100) selected_level, battle
modlib.on_tech_tree_opened(callable, priority := 100) menu
modlib.on_upgrade_purchased(callable, priority := 100) upgrade, new_level

Your callback may accept fewer arguments than listed. OrcKit only passes as many arguments as your callback declares.

These helper calls are statically scanned when they appear directly in your script, for example modlib.on_enemy_killed(...). If you register them dynamically through strings, call(), or generated code, add the matching raw target in [hooks].

Unhooking

var hook_id := -1

func _install(modlib) -> void:
	hook_id = modlib.hook("player-_ready-pre", Callable(self, "_before_ready"))

func _exit_tree() -> void:
	if hook_id != -1 and Engine.has_meta("OrcmodLib"):
		Engine.get_meta("OrcmodLib").unhook(hook_id)

Most mods register once and never unhook, but unhooking is useful for temporary systems.

Common Hook Mistakes

Hook registered too early

Register after frameworks_ready.

Missing [hooks] declaration

Direct .hook() calls are scanned and OrcKit tries to infer the target script, but explicit [hooks] declarations are more reliable.

Direct game-specific helper calls such as modlib.on_battle_start(...) are also scanned. If a helper hook does not fire, add an explicit [hooks] entry while debugging.

Wrong script stem

For res://scripts/player_controller.gd, the stem is:

player_controller

not:

PlayerController

Static method

Static methods are skipped by the wrapper generator.

Replacing when a pre or post hook would do

Replace hooks are powerful but conflict-prone. Prefer -pre or -post unless you need to suppress vanilla behavior.

Best Practices

  • Declare hook targets in mod.txt.
  • Register hooks from one autoload script.
  • Wait for frameworks_ready.
  • Keep hook callbacks small.
  • Avoid heavy work in _process and _physics_process hooks.
  • Use -post for result mutation.
  • Use replace hooks sparingly.
  • Document hook names in your mod release notes for compatibility authors.

Clone this wiki locally