Luau
Courage Reborn uses Luau, Roblox's typed superset of Lua 5.1. Plain Lua mostly works. Type annotations are optional.
Info
If you're new to Lua/Luau, the Luau language reference is a good starting point.
Type checking#
Put --!nocheck on the first line of a mod entry file to skip Luau's type checker for that module. Useful for quick scripts and examples without type annotations:
Other file modes: --!nonstrict (default for new files) and --!strict. See the Luau type checking docs.
Entry point#
When Petrichor loads a luau mod it:
- Reads the file named by
entryinmod.json(default:main.luau). - Compiles it to Luau bytecode and runs it once in the shared Luau VM.
- Expects the chunk to
returna mod class frommod(). - Calls
new(), thentick(delta)each frame andshutdown()on teardown.
Register hooks in init (or code called from init). See Script entry point.
require() resolution#
The built-in require resolves names in order:
- Built-in Petrichor modules:
"Augment.Hook","Augment.Mixin","Augment.Class","Petrichor.Log","Petrichor.Mod","Petrichor.Events","Petrichor.Task","Petrichor.Timer","Petrichor.Net","Petrichor.Storage","Petrichor.Json","Petrichor.Random","Petrichor.Hash","Petrichor.Bits","Petrichor.Math" - Courage host modules:
"Courage.Shaders","Courage.Assets","Courage.Game","Courage.World","Courage.Player","Courage.Save","Courage.Hooks","Courage.Bmg","Courage.Settings","Courage.Config","Courage.Timer","Courage.UI","Courage.Layout","Courage.Audio" - File-based modules: dots become
/,.luauis appended, path is relative to the active mod folder.
Petrichor.Standard (newclass, mod, log, Levels) is preloaded as globals, not via require().
Async, timers, and networking#
Petrichor ships a task scheduler on the main Luau thread. Use it for waiting on hooks without blocking the game:
See Petrichor.Task, Petrichor.Timer, Petrichor.Net, and Petrichor.Storage.
Hook lifecycle#
Hooks registered at load stay for the whole session. You cannot unregister a Luau hook at runtime today.
The execution order for a hooked function is:
[ before hooks, sorted by priority (ascending) ]
↓
[ original function ] # skipped if a before hook sets ctx.cancelled = true
↓
[ after hooks, sorted by priority (ascending) ]
A replace hook entirely replaces the original function slot, and the original does not run.
Struct views#
When ctx.self or a parameter is a pointer to a game struct, the hook runtime wraps it in a view proxy that lets you read and write fields by name:
Hook["daCow_c::action_move"]:before(function(ctx)
local cow = ctx.self -- proxy for daCow_c*
-- read a field
local mode = cow.mMode -- returns a number
-- write a field
cow.mMode = 3
-- access an embedded struct field (returns another proxy)
local pos = cow.mCurrentPos
print(pos.x, pos.y, pos.z)
end)
Field names and types come from the game symbol map (augment.bin). Accessing a field that doesn't exist raises an error.
Module pattern#
For larger mods, split logic across helper modules and call them from the entry file:
-- patches/cow.luau
local Hook = require("Augment.Hook")
local Log = require("Petrichor.Log")
return function()
Hook["daCow_c::Execute"]:before(function(ctx)
Log.info("cow-patch", "tick")
end)
end
-- main.luau
local registerCow = require("patches.cow")
local registerLink = require("patches.link")
registerCow()
registerLink()
// src/patches/cow.ts
import * as Hook from "Augment.Hook"
import * as Log from "Petrichor.Log"
export = function () {
Hook.daCow_c_Execute.before((ctx) => {
Log.info("cow-patch", "tick")
})
}
// src/main.ts
import registerCow from "./patches/cow"
import registerLink from "./patches/link"
registerCow()
registerLink()
Type annotations (optional)#
Luau supports gradual typing. You can annotate your callbacks for better editor support:
For full type safety, you can use the TypeScript workflow as well.