TypeScript
TypeScript mods add types for game structs, hook contexts, and runtime modules. The workflow is:
Project structure#
my-mod/
├── mod.json
├── package.json
├── tsconfig.json
├── src/
│ └── main.ts # TypeScript source
└── out/
└── main.luau # compiled Luau
mod.json#
Point entry at the compiled Luau output:
{
"id": "my-ts-mod",
"name": "My TypeScript Mod",
"type": "luau",
"entry": "out/main.luau",
"version": "1.0.0",
"hostVersion": "0.8.0",
"engineVersion": "1.0.0"
}
package.json#
{
"name": "my-ts-mod",
"version": "1.0.0",
"devDependencies": {
"@courage/types": "file:path-to-crxts-types"
}
}
Point file: at your clone of crxts-types. The path is relative to your mod directory (for example from mods/my-ts-mod/, go three levels up to the repo root, then into crxts-types).
Install dependencies:
tsconfig.json#
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"moduleDetection": "force",
"moduleResolution": "Node",
"noLib": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"rootDir": "src",
"outDir": "out",
"typeRoots": ["node_modules/@courage"]
},
"rbxts": {
"type": "standalone"
}
}
Key options:
| Option | Reason |
|---|---|
"noLib": true | Suppress the built-in TS lib, Luau has its own stdlib |
"typeRoots": ["node_modules/@courage"] | Pick up @courage/types declarations |
"rbxts": { "type": "standalone" } | Required by ts-luau's roblox-ts heritage |
Write your mod#
-- main.luau
local Log = require("Petrichor.Log")
local MyMod = mod("my-ts-mod")
function MyMod:init()
self:before("daCow_c::Execute", function(ctx)
local cow = ctx.self
Log.info("my-ts-mod", "Cow mode before Execute: " .. tostring(cow.mMode))
end)
self:before("daCow_c::action_move", function(ctx)
local cow = ctx.self
if cow.mProcess ~= nil then
ctx.cancelled = true
end
end)
end
return MyMod
// src/main.ts
import * as Log from "Petrichor.Log"
declare function mod(name: string): any
const MyMod = mod("my-ts-mod")
MyMod.init = function () {
this.before("daCow_c::Execute", (ctx: HookCtx) => {
const cow = ctx.self
Log.info("my-ts-mod", `Cow mode before Execute: ${cow.mMode}`)
})
this.before("daCow_c::action_move", (ctx: HookCtx) => {
const cow = ctx.self
if (cow.mProcess !== undefined) {
ctx.cancelled = true
}
})
}
export = MyMod
Petrichor runs the compiled Luau once and uses the export = value as the returned mod class (same as return MyMod in Luau). See Script entry point.
Info
Hook symbols are exposed as properties on the Augment.Hook module, with :: replaced by _. So daCow_c::Execute becomes Hook.daCow_c_Execute. All overloads and contexts are inferred from the game manifest.
Compile#
Or via npx tsx if you haven't built ts-luau yet:
This writes compiled .luau files to out/ mirroring the structure of src/.
Type overview#
| Declaration file | Contents |
|---|---|
courage.d.ts | Entry point, hand-maintained types (cXyz, csXyz, NativePtr, Mtx) |
lua.d.ts | Luau standard library |
generated/courage_bindings.d.ts | All game structs, enums, globals, and Augment.Hook module |
generated/hooks.d.ts | HookCtx and HookBuilder interfaces |
generated/runtime_modules.d.ts | Augment.Mixin and Petrichor.Log module declarations |