Hooks and patches
The advanced tier: detours, mid hooks, memory patches and symbols on game code, from C# or a native plugin, through Kiln's one hook registry.
Changed in 0.2.0-dev.21. Every hook Kiln places on the game exe now goes through one registry, kiln.hooks v2. It was tested with unit tests, Wine host tests and dedicated-server labs, not yet in a real client. See the client checklist.
Hooks are for the rare mod that blocks, data packs and services can’t cover. A hook runs your code inside a game function. It depends on the game build, so prefer the typed APIs where they exist.
Opt in
Only a mod marked advanced may hook, patch or look up symbols. Set <KilnAdvanced>true</KilnAdvanced> in the project,
or "advanced": true in kiln.mod.json:
{ "id": "mycoremod", "version": "1.0.0", "apiVersion": "0.1", "entryAssembly": "MyCoremod.dll",
"side": "server", "advanced": true }
Without it, the first hook call throws InvalidOperationException, and the build fails with
KILN107. Every hook, patch and symbol load is logged in kiln/logs/kiln.log with the mod id.
Detours
Hooks.Detour(symbolOrAddress, handler, priority = 0) runs your handler when the game calls the function. The handler
either calls the original or replaces it:
var h = Hooks.Detour("GetComponent", call =>
{
Interlocked.Increment(ref _calls);
return call.CallOriginal(); // run the rest of the chain and the game's function
});
h.Remove(); // or let Kiln remove it when the mod unloads
return call.CallOriginal();runs the rest of the chain and the game’s function. You may change arguments first.return someValue;replaces the function. The rest of the chain and the original don’t run.- The handler sees the first four integer or pointer arguments. Since dev.21 the original receives every argument the game passed, so a function with more than four arguments works. You can’t read or change the fifth and later.
- For float arguments, or to read every argument, use a mid hook or a native plugin (below).
Mid hooks
Hooks.Mid(symbolOrAddress, (ref Registers r) => ..., priority = 0) runs at one address with every register. Writes
take effect when the game resumes.
Hooks.Mid("GetEntityTransform", (ref Registers r) => Log.Info($"entity arg r8 = {r.R8}"));
r.Rax,r.Rcx…r.Rip, andr.XmmD(n)/r.Xmm(n)for vector registers.r.Rspis read-only.r.TrampolineRspstarts atr.Rsp - 8. The game resumes at the address on top of that stack.- At a function’s first instruction, return early with
r.Rax = value; r.TrampolineRsp = r.Rsp;.
Memory and patches
Memory.Read<T>(addr),TryRead,ReadBytes(addr, n): a bad address throws instead of crashing.Memory.Write<T>(addr, value): not undone at unload.Memory.Patch(addr, bytes...): Kiln keeps the original bytes and puts them back when you remove the handle or the mod unloads.
Symbols
Symbols.Get(name),TryGet: a function’s address by Kiln’s name for it.Symbols.Find(pattern),FindAll,Hole(pattern, match, hole): scan the game for a byte pattern. They match the exe file’s code, so another mod’s hooks don’t hide a match.- Your own names: ship
sigdb/server.jsonandsigdb/client.jsonwith every name prefixed<modid>/, and callSymbols.LoadShipped()once inOnLoad. Kiln finds them by pattern on each game build, so you hard-code no offsets.
public override void OnLoad()
{
Symbols.LoadShipped();
Hooks.Detour("mycoremod/DoThing", OnDoThing);
}
One registry for every hook
Since dev.21, Kiln places one detour per game function. Every handler on that function is a link in one chain:
your C# hooks, native plugins, Kiln’s own services and core tick, EMF mods’ hook, hook_pattern and hook_address,
Shroudtopia’s CreateDetour, and ShroudForge’s engine hooks when Kiln hosts it. Only hooks on system DLL exports
(LoadLibraryExW, Vulkan, XInput) and mod hooks outside the game exe still use plain safetyhook.
Order on one function: before/after constraints first, then priority (higher first), then registration order.
- C#
Hooks.DetourandHooks.Midare links at their priority. They need no change. - EMF and Shroudtopia mod detours run newest first, before Kiln’s own links. Their owners are
emf:<id>andshroudtopia:<mod>. - Kiln’s core tick links stay innermost.
Native plugins: kiln_hook.hpp
A native plugin uses the C table in native/core/abi/services/kiln_hooks.h, or the header-only C++ client
native/core/abi/kiln_hook.hpp:
#include "kiln_hook.hpp"
static kiln::RawHook g_add;
static void hk_chat_add(void* chat, int type, const char* text) {
if (g_add.caller() == g_receive_call_return) { /* a player's line */ } // read caller() first
g_add.call<void>(chat, type, text);
}
// in kiln_plugin_main:
if (!kiln::hooks_v2(api)) return KILN_ERR_UNAVAILABLE; // a Kiln without v2: turn the feature off
if (!g_add.install(api, owner, target, reinterpret_cast<void*>(&hk_chat_add), "chat/ChatLogAdd"))
return KILN_ERR_UNAVAILABLE; // g_add.error() says why
| Call | Use |
|---|---|
raw_detour (kiln::RawHook) |
any signature: floats, stack arguments, any return type. The handler has the target’s own signature |
detour2, call_original2 |
up to 12 integer or pointer arguments, collected for you |
mid2 (kiln::MidHook2) |
registers at one address, with safetyhook’s stack rules |
caller() |
the game’s return address for this call. Use it instead of _ReturnAddress() |
map_json |
the same text as state/hooks.json |
Rules:
- Read
caller()before you call the original or another hooked function. - Use
g.install(...)on a global.create()returns the hook by value, and a game thread can run the handler before your global is set. - Pass
call<R>(...)the game’s exact argument types (uint64_t, notint, for a 64-bit parameter). - Installing the same raw handler on a function again replaces the earlier link in one step.
- A
MidHook2installed again while live can run its handler twice for one call at that moment. - Dropping a
RawHookobject does not remove its link. Callrevoke(), orrevoke_owner(owner). - Order:
KilnHookOrdertakes a priority,beforeandafter(comma-separated owner names, for examplekiln.chat). Withflags = KILN_HOOK_NEWEST_FIRST, flagged links at one priority run newest first.
The full contract is in Kiln’s repository, docs/kiln/services/hooks.md, on GitHub.
The hook map
kiln/state/hooks.json lists every hooked function, its links in the order they run, and other modules’ hooks found
before or over Kiln. Kiln writes it at boot end, at the first world, and when links change.
kiln.log says when another DLL hooked first, or hooked over Kiln:
[kiln] hooks: chat/ChatLogAdd chained over enshwick.dll+0x11ea80
[kiln] hooks: xhl.dll+0x4410 hooked over Kiln's chat/ChatLogAdd (Kiln's detour stays armed; it is reached through their jump)
Kiln keeps working in both cases. When a DLL mod’s hooks get in the way, see closed-source DLL mods.
Threads and cost
- A handler runs on whatever thread the game calls the function on, often a job thread. Keep it short and thread-safe.
Don’t call tick-thread APIs (most of
World,Items…) from it. - The first hook on a function stops the game’s other threads for a moment. Place it in
OnLoador on the tick thread. Later hooks on the same function stop nothing. - Cost, measured on a lab server under Wine on
GetComponent: about 6 ns per call through an empty chain and 59 ns through a C# handler. dev.20 measured about 65 ns and 100 ns.
Limits
- 512 hooked functions and 65,536 link slots per game process. Links are never freed.
- A native handler installed again reuses its slot. C#
Hooks.DetourandHooks.Midtake a new slot on every install, so a mod that installs and removes one per command uses them up.linkSlotsinhooks.jsonshows the use; kiln.log warns at 75% and when full. - Reordering links that are already installed is not atomic for calls in flight. A call at that moment can skip a link or run one twice. It is built not to crash. Wine stress tests cover it; no real game session has.
- Under Wine, hot-reloading one of two mods that both hook the same very hot function (
GetComponent) can stall. Don’t stack several mods’ hot-path detours on one function.
Changes from dev.20
Hooks.Detourand the native v1detourpass every argument to the original, not only the first four.- The order between Kiln’s own services follows the registry: priority, then registration.
- EMF and Shroudtopia mod hooks run before Kiln’s own links on the same function.
- One hook function installed twice on the same game function is one link: the second install replaces the first.