Concepts
Objects and reflection
Every UnrealScript object is reachable from Lua as an Object proxy: a handle to the live UObject. Properties resolve through the game's own reflection (UClass → UProperty), so pc.Pawn looks up the Pawn property on the controller's class at runtime and reads the right offset. Nothing about game classes is baked into the runtime, which is why it survives patches that move fields around.
Reads give you Lua values for simple kinds and live proxies for compound ones:
| UnrealScript | Lua |
|---|---|
int, byte | integer |
float | number |
bool | boolean |
name, string | string |
| object reference | Object proxy, or nil when none |
Vector | table { x, y, z } (a copy) |
Rotator | table { pitch, yaw, roll } (a copy) |
| any other struct | Struct proxy (live) |
array<T> | Array proxy (live) |
Map{K, V} | Map proxy (live) |
Writes go the other way with the same rules. An unknown property reads as nil and logs once; writing one raises a Lua error, so typos fail loudly.
Functions resolve the same way: pc.ClientMessage is a bound function and pc:ClientMessage("hi") calls it. Script and native functions both work.
Hooks
A hook attaches to a function by name:
tempest.before("TgGame.TgGame.PostLogin", function(game, params) ... end)
tempest.on("TgGame.TgGame.PostLogin", function(game, params) ... end)before runs before the game's own code, on after it. Both receive the object the function ran on and a Params proxy over the live argument frame. A before handler can change arguments by assigning to params, and can return false to cancel the call entirely (set params.ReturnValue first if the caller expects one). An on handler can read params.ReturnValue.
The spec decides what matches:
"Package.Class.Func"binds to that exact function and fires whenever its body runs, including inside aSuperchain. Use this form for natives; it is the only way to hook them."Func"matches any function with that name, on any class, and fires once per logical call (the outermost frame of aSuperchain). Handy for exploring, less precise.
Under the hood three entry points are hooked so that nothing is missed: ProcessEvent for events the engine raises, ProcessInternal for every UnrealScript function body, and a per-function replacement for natives. Calls made from Lua bypass the first but still reach the other two, so a hooked native sees calls made with obj:Func() too.
Threads
The game runs its logic on one thread, and scripts should touch game objects only from there. Hook handlers already run on it. Code that starts elsewhere, such as the DLL's console REPL, is queued and executes at the next event; tempest.queue(fn) does the same for your own code, and tempest.on_game_thread() tells you where you are. Key bindings and console commands dispatch on the game thread as well.
Mods and sandboxes
At startup the runtime finds every enabled mod next to its own and runs each one's scripts/ in a private environment. Globals a mod sets are invisible to other mods; reads fall through to the API and the standard library. MOD holds the mod's id, and require("a.b") loads scripts/a/b.lua from the same mod into the same sandbox.
To share something, use the shared table or the explicit pair:
-- in mod "my-lib"
tempest.export("greet", function(pc) pc:ClientMessage("hi") end)
-- in another mod, any time later
local greet = tempest.import("greet")Mods load alphabetically by id. A mod that needs another one loaded first says so in its manifest.toml:
[scripting]
after = ["my-lib"]Hooks, commands and key bindings remember which mod registered them. Errors in a handler are logged with the mod's id, and tempest.reload("my-lib") drops only that mod's registrations before running it again.
Memory ownership
Strings and arrays written from Lua are allocated with the engine's allocator, so the game can free or grow them later. The runtime does not run a garbage collector for game objects: an object you construct with tempest.new stays alive only while something in the game references it, so assign it to a property right away. memory.* is the escape hatch for anything reflection does not describe; it reads and writes raw addresses with no checks beyond page readability.
