Skip to content

Hooks reference

This page describes how Augment.Hook intercepts game functions.


Execution model#

When you register a hook on a symbol, Augment builds a chain for that function:

  caller invokes Foo::bar(...)
        ↓
  [before hooks], priority ascending, skip original if ctx.cancelled = true
        ↓
  original Foo::bar  (may be skipped)
        ↓
  [after hooks], priority ascending
        ↓
  return to caller

A replace hook runs instead of the original:

  caller invokes Foo::bar(...)
        ↓
  [before hooks]
        ↓
  [replace hook]
        ↓
  [after hooks]
        ↓
  return to caller

Hook phases#

Phase When it runs Can cancel? Typical use
before Before the original Yes, set ctx.cancelled = true Change arguments, skip the call
after After the original No Change the return value
replace Instead of the original N/A Replace the function

HookCtx#

Every hook callback gets a ctx table. Shape depends on the hooked function.

Field Type Description
ctx.self struct proxy this for methods. nil for free functions.
ctx.cancelled boolean Set true in a before hook to skip the original.
ctx.ret depends Return value. Readable in after, writable to change what the caller gets.
Named params depends Each argument by source name, e.g. ctx.speed.

Struct views#

Struct fields (including ctx.self) are view proxies. Read and write fields by name:

Hook["Enemy::takeDamage"]:before(function(ctx)
    local enemy = ctx.self
    local amt   = ctx.amount

    print(enemy.mHP, amt)
    ctx.amount = amt * 0.5
end)
import * as Hook from "Augment.Hook"

Hook.Enemy_takeDamage.before((ctx) => {
    const enemy = ctx.self
    const amt = ctx.amount

    print(enemy.mHP, amt)
    ctx.amount = amt * 0.5
})

Warning

Do not assign to a struct field slot like ctx.self. Write scalar fields on the proxy instead.

ctx only exposes names listed for that function. Invalid keys throw (ctx has no field '...'). See Debugging.


HookBuilder#

Augment.Hook["Symbol"] returns a HookBuilder. Chain phases on one builder:

local Hook = require("Augment.Hook")

Hook["Foo::bar"]
    :before(function(ctx)  print("before") end)
    :after (function(ctx)  print("after")  end)
import * as Hook from "Augment.Hook"

Hook.Foo_bar
    .before((ctx) => { print("before") })
    .after ((ctx) => { print("after")  })

.once(), fire once only#

Hook["Foo::init"]:once():before(function(ctx)
    -- first call only
end)
import * as Hook from "Augment.Hook"

Hook.Foo_init.once().before((ctx) => {
    // first call only
})

Overload disambiguation#

If "Foo::bar" has multiple overloads, Hook["Foo::bar"] returns an ambiguous handle. Calling :before, :after, or :replace on it errors and lists matches.

Pick one with :sig(args). Do not use :at(file). Source-file picks break when code moves or the symbol map is regenerated. Use Game Hooks to find the parameter list you need.

:sig(args), pick by parameter types#

local h = Hook["Foo::bar"]:sig("int, float")
h:before(function(ctx) ... end)
import * as Hook from "Augment.Hook"

const h = Hook.Foo_bar.sig("int, float")
h.before((ctx) => { /* ... */ })

The signature string matches each overload's parameter list.


Priorities#

Augment.Mixin.register accepts a priority. Lower numbers run first in the same phase. Default is 0.

local Mixin = require("Augment.Mixin")

Mixin.register("Foo::bar", "before", function(ctx)
    print("high priority")
end, -10, "my-tag")
import * as Mixin from "Augment.Mixin"

Mixin.register("Foo::bar", "before", (ctx) => {
    print("high priority")
}, -10, "my-tag")

Augment.Hook always uses priority 0. Use Augment.Mixin when you need ordering.


Tags#

Optional debug labels on Mixin.register:

Mixin.register("Foo::bar", "before", fn, 0, "my-mod:feature")
Mixin.register("Foo::bar", "before", fn, 0, "my-mod:feature")