Skip to content

The Callback System

The callback system is how Anomaly scripts communicate with the engine and with each other. Almost everything you'll ever do in a mod is triggered by a callback.


The callback API

RegisterScriptCallback(name, fn)    -- Subscribe to an event
UnregisterScriptCallback(name, fn)  -- Unsubscribe
AddScriptCallback(name)             -- Declare a new event (for mod authors)
SendScriptCallback(name, ...)       -- Fire an event (for mod authors)

How registration works

Internally, callbacks are stored in a table where the function itself is the key (from axr_main.script):

intercepts["on_game_load"][your_function] = true

This means: - Registering the same function twice is harmless — the second call just sets the same key to true again, no duplicate entry is created - You must pass the exact same function reference to unregister — this is why anonymous functions can never be unregistered - Order of execution among registered functions is not guaranteed

The intercepts table persists for the entire process lifetime — it is never automatically reset between game sessions. If you load a save, return to main menu, and load another save without quitting, the table from the first session is still in memory. This is why on_game_end unregistration matters: not because double-registration causes double-firing (it doesn't), but because callbacks that aren't unregistered remain active across session boundaries and can fire while your module's state is in an indeterminate condition — reset local variables, a nil db.actor, or stale flags from the previous session.

Unregistering in on_game_end is also used in advanced mods to replace or intercept base game callbacks: unregister the original function, then re-register a patched version that adds behaviour before or after the original call.


Two callback systems

Anomaly actually has two separate callback systems that serve different purposes. Understanding this distinction helps you pick the right one.

Script callbacks (Lua-side dispatch)

RegisterScriptCallback / SendScriptCallback is a purely Lua-side system, implemented in axr_main.script and dynamic_callbacks.lua. The engine doesn't know about it. Base game scripts and mods manually call SendScriptCallback(...) at the right moments, and all registered handlers are invoked.

This is the system you use for most modding: on_game_load, on_key_press, npc_on_death_callback, etc. These are the callbacks listed in the Callbacks Reference.

Object callbacks (engine-side dispatch)

game_object:set_callback(type, fn) is an engine-side callback system. You register a Lua function on a specific game_object, and the C++ engine calls it directly when the event fires on that object. These are per-object, not global.

In practice, set_callback is used exclusively inside object binders. The binder's net_spawn registers callbacks on self.object, and net_destroy clears them. Across the entire base game and all community mods we analysed, no mod uses set_callback outside of a binder context. If you need to react to events like death, hit, or item pickup, use RegisterScriptCallback — the base game's binders already relay these engine events into the script callback system via callbacks_gameobject.script.

-- Typical usage: inside an object binder
function my_binder:net_spawn(se_abstract)
    object_binder.net_spawn(self, se_abstract)
    self.object:set_callback(callback.hit, self.hit_callback, self)
    return true
end

function my_binder:hit_callback(obj, amount, direction, who, bone_id)
    -- handle hit on this specific object
end

function my_binder:net_destroy()
    self.object:set_callback(callback.hit, nil)  -- clear
    object_binder.net_destroy(self)
end

The callback.* enum values are defined in the engine. Some are vanilla, some were added by the modded exes:

Callback type Vanilla Description
callback.trade_start Yes Trade session opened
callback.trade_stop Yes Trade session closed
callback.trade_sell_buy_item Yes Item bought/sold
callback.zone_enter Yes Object entered a zone
callback.zone_exit Yes Object left a zone
callback.death Yes Entity died
callback.hit Yes Entity took damage
callback.sound Yes Sound event
callback.use_object Yes Object used/interacted
callback.on_item_take Yes Item picked up
callback.on_item_drop Yes Item dropped
callback.patrol_path_in_point Yes NPC reached patrol point
callback.script_animation Yes Script animation completed
callback.helicopter_on_point Yes Heli reached point
callback.helicopter_on_hit Yes Heli took damage
callback.weapon_no_ammo Yes No ammo available
callback.key_press Modded Key pressed (actor only)
callback.key_release Modded Key released (actor only)
callback.key_hold Modded Key held (actor only)
callback.mouse_move Modded Mouse moved
callback.mouse_wheel Modded Mouse scrolled
callback.item_to_belt Modded Item moved to belt
callback.item_to_slot Modded Item moved to slot
callback.item_to_ruck Modded Item moved to backpack
callback.weapon_zoom_in Modded Weapon aimed down sights
callback.weapon_zoom_out Modded Weapon lowered from aim
callback.weapon_jammed Modded Weapon jammed
callback.weapon_fired Modded Weapon fired
callback.weapon_magazine_empty Modded Magazine emptied
callback.actor_before_death Modded Before actor death (can be intercepted)
callback.on_foot_step Modded Footstep sound
callback.weapon_lowered Modded Weapon lowered stance
callback.weapon_raised Modded Weapon raised stance
callback.hud_animation_end Modded HUD animation completed

For mod work, use RegisterScriptCallback. The only reason to use set_callback directly is if you're writing a custom object binder and need to handle engine events on that specific object — and even then, most events you'd care about already have corresponding script callbacks that the base game binders relay for you.


The canonical pattern

Every mod that registers callbacks should follow this structure:

-- Declare local functions — the same references are used to register AND unregister
local function on_game_load()
    -- ...
end

local function on_key_press(key)
    -- ...
end

-- Register in on_game_start
function on_game_start()
    RegisterScriptCallback("on_game_load", on_game_load)
    RegisterScriptCallback("on_key_press", on_key_press)
end

-- Unregister in on_game_end
function on_game_end()
    UnregisterScriptCallback("on_game_load", on_game_load)
    UnregisterScriptCallback("on_key_press", on_key_press)
end

Anonymous functions break unregistration

This is a common mistake:

-- WRONG: each call creates a new function object; you can never unregister it
RegisterScriptCallback("on_game_load", function()
    printf("loaded")
end)
Always assign your callback to a named local variable first.


Callback arguments

Callbacks receive arguments depending on the event. Always check what arguments a callback provides before writing your handler.

-- on_key_press provides the key code
local function on_key_press(key)
    if key == DIK_keys.DIK_F5 then
        -- ...
    end
end

-- npc_on_death_callback provides victim and killer
local function npc_on_death_callback(victim, who)
    printf("NPC %s killed by %s", victim:name(), who and who:name() or "unknown")
end

-- actor_on_before_hit provides a hit object, bone id, and flags
local function actor_on_before_hit(shit, bone_id, flags)
    shit.power = shit.power * 0.5  -- halve incoming damage
    -- flags.ret_value = false   -- would cancel the hit entirely
end

See the Callbacks Reference for the full list with signatures.


Registering objects (not just functions)

The callback system also supports registering a table/object with a matching method name. This is used by object binders:

-- RegisterScriptCallback("save_state", self) will call self:save_state(m_data)
class "my_binder" (object_binder)

function my_binder:save_state(m_data)
    m_data[self.object:id()] = self.my_data
end

function my_binder:net_spawn(se_abstract)
    RegisterScriptCallback("save_state", self)
end

function my_binder:net_destroy()
    UnregisterScriptCallback("save_state", self)
end

For most mods you won't need this — function registration is sufficient.


Defining your own callbacks

If you're writing a mod that other mods should be able to hook into, you can create your own callbacks:

-- Declare the callback (do this at top level, before any registrations)
AddScriptCallback("my_mod_on_something_happened")

-- Fire it when the event occurs
local function do_something()
    -- ... do the thing ...
    SendScriptCallback("my_mod_on_something_happened", some_data)
end

Other mods can then:

local function on_my_mod_event(data)
    -- react to your event
end

function on_game_start()
    RegisterScriptCallback("my_mod_on_something_happened", on_my_mod_event)
end

This is how Anomaly's major systems (MCM, the body health system, etc.) expose their events to other mods.


Suppressing default behaviour

Some callbacks pass a flags table that you can modify to cancel the default engine action:

-- on_before_key_press: set flags.ret_value = false to swallow the keypress
local function on_before_key_press(key, bind, dis, flags)
    if key == DIK_keys.DIK_TAB and some_condition then
        flags.ret_value = false  -- prevent default TAB behaviour
    end
end

Flag field names vary by callback

There is no single convention. Key callbacks use flags.ret_value = false to suppress. Surge/psi storm callbacks use flags.allow = false. Save/load callbacks use flags.ret = true. Always check the Callbacks Reference for the correct field name — using the wrong one will silently do nothing.


See also

  • Callbacks Reference — full list of every callback with signatures and argument details
  • Object Binders — the per-object callback system (set_callback) used inside binders