Skip to content

Augment.Hook

Augment.Hook is the main module for gameplay hooks.

Loading#

local Hook = require("Augment.Hook")
import * as Hook from "Augment.Hook"

Indexing by symbol name#

Augment.Hook is a proxy table. A flat symbol name returns a HookBuilder.

local builder = Hook["ClassName::methodName"]

-- or dot syntax when the symbol has no `::`:
local builder = Hook.ClassName_methodName
import * as Hook from "Augment.Hook"

const builder = Hook.ClassName_methodName

Symbol names:

  • Use the C++ flat name: "ClassName::methodName".
  • :: is allowed. Template args and return types are omitted.
  • In TypeScript types, :: becomes _ on the Hook module (Hook.ClassName_methodName).

HookBuilder methods#

All methods return the builder itself, so calls can be chained.

:before(fn)#

Registers fn to run before the original function. If fn sets ctx.cancelled = true, the original is skipped.

Hook["Foo::bar"]:before(function(ctx)
    ctx.cancelled = true  -- skip the original
end)
Hook.Foo_bar.before((ctx) => {
    ctx.cancelled = true  // skip the original
})

:after(fn)#

Registers fn to run after the original function (or after a replace hook).

Hook["Foo::bar"]:after(function(ctx)
    print("original returned:", ctx.ret)
end)
Hook.Foo_bar.after((ctx) => {
    print("original returned:", ctx.ret)
})

:replace(fn)#

Registers fn to run instead of the original function. The original is not called.

Hook["Foo::bar"]:replace(function(ctx)
    -- completely replace bar's behaviour
    ctx.ret = 42
end)
Hook.Foo_bar.replace((ctx) => {
    // completely replace bar's behaviour
    ctx.ret = 42
})

:once()#

Modifies the builder so registered hooks fire only on the first call, then are ignored.

Hook["GameMgr::init"]:once():before(function(ctx)
    print("game initialised!")
end)
Hook.GameMgr_init.once().before((ctx) => {
    print("game initialised!")
})

Overload disambiguation#

When a symbol is ambiguous (multiple overloads exist), indexing Hook returns a special handle. Calling :before, :after, or :replace on it directly raises an error.

Disambiguate with :sig(signature). Do not use :at(file). File-based picks are brittle across builds and refactors. Look up the correct overload on Game Hooks and match its parameter list.

:sig(signature)#

Select the overload by its parameter types:

Hook["Foo::bar"]:sig("int, float"):before(function(ctx) ... end)
Hook.Foo_bar.sig("int, float").before((ctx) => { /* ... */ })

HookCtx fields#

The ctx argument passed to every hook callback has the following fields. Exact parameter names and types depend on the hooked function.

Field Type Available in Description
ctx.self struct proxy / nil before, after, replace this pointer for C++ methods only. Always nil for free functions. Use ctx.<paramName> instead (such as ctx.i_this)
ctx.cancelled boolean before Set true to skip the original call
ctx.ret scalar / struct proxy after Return value, write to change what the caller receives
ctx.<paramName> scalar / struct proxy / NativePtr before, replace Each parameter by its source name. For free functions this is the only way to access the receiver
ctx.__fields string[] before, after, replace Returns a sorted list of all valid field names on ctx. Useful for debugging unknown struct layouts

Opaque pointer parameters#

When a parameter is declared as a struct pointer but the hook manifest has no field list for that view type, ctx.<paramName> returns the raw NativePtr instead of a field proxy.

Use Hook.readCString for char const*, or Augment.Mixin memory helpers when you need to read/write bytes at an offset. If you expected a field proxy, print ctx.__fields. When the view is opaque, the parameter will not expose nested keys.

Free functions vs methods#

For C++ methods, ctx.self is the this pointer. For free functions (such as daNi_Execute(ni_class* i_this, ...)), ctx.self is always nil. Access the actor through the named parameter instead:

-- WRONG for free functions
Hook.daNi_Execute:before(function(ctx)
    ctx.self.mAction = 1  -- ctx.self is nil, this will error
end)

-- CORRECT
Hook.daNi_Execute:before(function(ctx)
    ctx.i_this.mAction = 1  -- i_this is the first parameter by source name
end)
import * as Hook from "Augment.Hook"

// WRONG for free functions
Hook.daNi_Execute.before((ctx) => {
    ctx.self.mAction = 1  // ctx.self is undefined, this will error
})

// CORRECT
Hook.daNi_Execute.before((ctx) => {
    ctx.i_this.mAction = 1  // i_this is the first parameter by source name
})

If you're unsure what parameters are available, print ctx.__fields:

Hook.daNi_Execute:before(function(ctx)
    print(table.concat(ctx.__fields, ", "))
    -- prints: cancelled, i_this, ret, self
end)
import * as Hook from "Augment.Hook"

Hook.daNi_Execute.before((ctx) => {
    print(ctx.__fields.join(", "))
    // prints: cancelled, i_this, ret, self
})

Reading a scalar parameter#

Hook["Enemy::takeDamage"]:before(function(ctx)
    local dmg = ctx.amount  -- f32 parameter
    ctx.amount = dmg * 0.1  -- reduce damage to 10%
end)
Hook.Enemy_takeDamage.before((ctx) => {
    const dmg = ctx.amount  // f32 parameter
    ctx.amount = dmg * 0.1  // reduce damage to 10%
})

Reading a struct parameter#

Hook["Stage::load"]:before(function(ctx)
    local info = ctx.stageInfo  -- proxy for StageInfo*
    print(info.stageName)       -- read a string field
end)
Hook.Stage_load.before((ctx) => {
    const info = ctx.stageInfo  // proxy for StageInfo*
    print(info.stageName)       // read a string field
})

Hook.readCString(value, maxLen?)#

Some parameters are exposed as NativePtr (for example char const* stage names). Use Hook.readCString to read them as a Luau string:

local Hook = require("Augment.Hook")

Hook["dComIfGp_setNextStage"]:before(function(ctx)
    local stage = Hook.readCString(ctx.i_stage, 16)
    if stage then
        print("warp to", stage)
    end
end)
import * as Hook from "Augment.Hook"

Hook.dComIfGp_setNextStage.before((ctx) => {
    const stage = Hook.readCString(ctx.i_stage, 16)
    if (stage) {
        print("warp to", stage)
    }
})

If value is already a string, it is returned unchanged. See also Courage.Hooks.readStageName.


Hook.View(structName, ptr)#

Creates a struct view proxy over a raw native pointer ptr (lightuserdata or NativePtr). This is the same proxy mechanism used automatically for ctx.self and struct hook parameters.

local Hook = require("Augment.Hook")

local function inspectActor(ptr)
    local actor = Hook.View("fopAc_ac_c", ptr)
    if actor then
        print("Actor ID:", actor.mProcId)
    end
end
import * as Hook from "Augment.Hook"

function inspectActor(ptr: any) {
    const actor = Hook.View("fopAc_ac_c", ptr)
    if (actor) {
        console.log("Actor ID:", actor.mProcId)
    }
}

Typed hooks in TypeScript#

In TypeScript, hook symbols appear as properties of the Augment.Hook module. Each property is a HookBuilder<TCtx> where TCtx is the generated context interface for that function.

Hook["daCow_c::Execute"]:before(function(ctx)
    local cow = ctx.self
    cow.mMode = 0
end)
import * as Hook from "Augment.Hook"

// ctx is typed as the generated HookCtx for daCow_c::Execute
Hook.daCow_c_Execute.before((ctx) => {
    const cow = ctx.self  // typed as daCow_c
    cow.mMode = 0
})

See the TypeScript guide for the full workflow.


Debugging hook mistakes#

Wrong ctx field names throw ctx has no field '...' and print a valid: list. Look up the symbol on Game Hooks, print ctx.__fields, or read Debugging.