script
Script lifecycle, update callbacks, timers, and cleanup.
Lifecycle reference
script.name
Entry filename as a string.
text
script.namescript.api_version
Integer 2 in this implementation.
text
script.api_versionscript.on_tick
Registers a recurring callback with zero delay; returns nothing. Multiple registrations accumulate.
text
script.on_tick(callback)script.every
Registers a recurring callback; returns nothing.
text
script.every(delay_ms, callback)script.after
Registers a one-shot callback; returns nothing.
text
script.after(delay_ms, callback)script.on_stop
Sets the single cleanup callback, replacing any previous one; returns nothing.
text
script.on_stop(callback)script.stop
Requests shutdown of this script; returns nothing.
text
script.stop()All callbacks above receive no arguments. Delay values are integer milliseconds in 0..86,400,000, inclusive. A zero-delay recurring callback runs at most once per script update. Timers are driven by game updates; they are not independent threads, exact wall-clock alarms, or a promise of a particular frame rate.
The initial chunk, menu callbacks, timer callbacks, native calls, and Social Club completion callbacks execute on Harmony's script owner. Rendering and network hooks do not execute Lua callbacks directly. Work from those systems is copied or queued for the script owner.
Timer behavior
Recurring timers schedule their next deadline from the current update time. They do not replay every missed interval after a pause. Timers created inside a timer callback become eligible on a later update. A zero-delay timer registered during initial loading or a menu callback can run during the later timer pass of that same update.
There is no timer handle or public timer-cancellation function. Use a boolean to make an existing recurring callback idle; use script.stop() to stop the entire script. Do not register a new recurring callback every frame.
lua
local page = menu.root("Timer example")
local enabled = false
local elapsed = 0
page:toggle("Count seconds", false, function(value)
enabled = value
end)
script.every(1000, function()
if enabled then
elapsed = elapsed + 1
util.log("Active seconds: " .. tostring(elapsed))
end
end)
script.after(3000, function()
util.log("The one-shot timer has run")
end)Use these scheduling functions instead of busy loops, sleeps, or BUILTIN.WAIT. The WAIT native is explicitly rejected by the Lua native bridge.
Stop, reload, and errors
script.stop() sets a shutdown request; it does not abort the currently executing Lua function. Return from your callback after requesting a stop. Reload tears down the previous instance and creates a fresh VM with fresh menu state.
An uncaught initial-chunk or callback error fails that script. Other scripts continue running. Cleanup removes its menu controls, draw commands, timers, callback references, network rules, native hooks, event subscriptions, HTTP callbacks, and owned input/account requests. Pending Social Club work may finish in the shared service, but its discarded callback is no longer delivered to the stopped script.
script.on_stop() is the place to restore resources or game changes your script owns. Native effects and ordinary Pointer:write_*() operations are not automatically undone. Tracked memory.patch() operations have separate conditional restoration behavior, described below. Network rules and listeners are removed before the stop callback. Creating new menu entries, timers, or requests while stopping is rejected.
lua
local page = menu.root("Lifecycle example")
script.on_stop(function()
util.log("Stopped " .. script.name)
-- Restore any game state or resources this script explicitly owns here.
end)
page:button("Stop this script", function()
script.stop()
return
end)Each protected Lua invocation has an accidental-runaway budget of 1,000,000 instructions or 20 ms, checked every 10,000 Lua instructions. This is cooperative protection: it cannot interrupt a blocking native/C++ call and is not a hard real-time or security guarantee. Keep callbacks short and divide work across updates.