Debugging a script
The Script page, the log, common errors, faults and an offline check.
A script cannot print. You debug it with four things: the Script page, the Mission state page, the log, and an offline check before you launch.
The fast loop
- Save the file.
- Press Reload script on the Script page. A mod built in Sunrise Studio can use Update running game instead. It writes the scripts and reloads that mod’s programs.
- Read Program and Error on the Script page.
- Read your variables on the Mission state page.
- Repeat.
The reload waits until the mission has no request, client report or timer in flight, then reopens the script. The button shows “reload waits for a safe point” until then, and “reopened N programs” when it is done. A script that does not compile, or an on_load that fails, replaces nothing. The button shows the error and the old script keeps running. The full rules are in “A script reload” in How a script runs.
Turn on the Mission Script overlay on the HUD page to see the status and last error while you play.
Show values without print
Write what you want to see into a variable. Use a debug. prefix, so you can find and remove them later.
local function note(context, key, value)
context:set_variable("debug." .. key, value)
end
return {
on_event_player_trigger = function(context, state, event)
note(context, "last_trigger", event.slot and event.slot.name or "unknown")
note(context, "trigger_count", (state:variable("debug.trigger_count") or 0) + 1)
end,
}
Remember the limits: 512 variables, names up to 63 bytes, strings up to 127 bytes. A debug note that fails its own limits faults the script.
To see why an event did not match, store its raw fields:
note(context, "slot_index", event.slot_index or -1)
note(context, "registry_key", event.registry_key or -1)
The log
Turn on the file and debug level first. See Step 1: turn on the script log. The file is Sunrise/logs/sunrise.log. The Logs page shows the same lines in game, with a text filter.
t= is milliseconds since the log opened. session= is the activity session. activity_row= is the activity row, counting from 1.
Every script line looks like this:
server level=<level> t=<ms> ev=mission_script stage=<stage> result=<result> session=<id> activity_row=<n> [fields] [error="<text>"]
Filter on ev=mission_script.
Stages you will see
stage
result
meaning
initialize
enabled
scripting is on
initialize
path_error, sdk_lua_path_error
the Sunrise folder was not found
attach
ready
the script attached
attach
no_script, no_activity_link, activity_join_pending, catalog_unavailable, capacity, program_error, or an SDK or world status name
why it did not attach
open
ready
the script loaded and started; reason=state_reattached after a reopen
open
no_script
no file at the expected path
open
file_error, source_too_large
the file could not be read, or is empty or over 128 KiB
open
invalid_sdk_view, invalid_program_key
the SDK data for this activity could not be read
open
compile_error
a syntax error; see error=
open
runtime_error
the top-level code failed; see error=
open
out_of_memory
over 64 MiB while loading
open
invalid_program
the file did not return a table, an entry is not a function, the table has on_event, or initial_state is not a valid state; see error=
initial_state
publication_pending
waiting for the first state to reach the client
initial_state
a status
the first state was refused
start, load
script_error, instruction_budget, out_of_memory
on_start or on_load failed
event
committed
a callback ran; detail= names the event
event
script_error, instruction_budget, out_of_memory
a callback failed; the program is now faulted
intent
a delivery step, such as squad_enqueued
a request entered delivery
delivery
a result, such as squad_staged
a request finished delivery
intent_refused
the refused request, such as checkpoint_refused
a request was refused; error= says why
state_restore
faulted
the saved mission state is faulted; no callback runs
events
binding_gap, initial_binding_gap
client reports arrived with a gap; the program is faulted
player_trigger
resolved or a status
a trigger report was matched to its slot, or not
cinematic
a signal or a status
a cinematic report was matched, or not
trigger, squad, scene, objective, device_state
watch_capacity
too many watched slots of that kind
scene
output_capacity
one scene run reported more than 32 outputs; later ones raise no event
source
ok, fail
a mod’s script was read, or found but not readable; mod= names the mod
reload
answered
a reload request finished; see below
rebind
generation
the client’s generation changed; the program keeps running
close
stale_generation
the script was closed; it reopens on the next tick
A reload answered line carries status= and reopened=, the number of programs that now run the new code. On failure, error= holds the message the button shows.
status
meaning
ok
done
compile_error
a new script does not compile
failed
the reload was refused; nothing was replaced
cancelled
the request was canceled
Two answers write no log line. With scripting off, the button shows “mission scripting is off”. When a newer request replaces this one, the button shows “replaced by a newer reload”.
Examples
A syntax error:
server level=warn ... ev=mission_script stage=open result=compile_error ... error="[string 'act/0125/87d9ca16']:42: '}' expected near 'end'"
The main file is named after the activity id. The number after it is the line. The log turns double quotes in the message into single quotes. An error inside a required module names that module’s file instead.
A refused checkpoint:
server level=warn ... ev=mission_script stage=intent_refused result=checkpoint_refused ... error="party_or_spawn_unavailable"
Common problems
problem
cause
fix
open no_script
wrong folder or file name
match the path on the Script page
runtime_error with module 'x' not found
a bad require name
use missions.X; check the folder name
runtime_error with missing mission constant
a key that does not exist
search the module for the right key
attempt to call a nil value (global 'pairs')
pairs is removed
walk arrays with ipairs
attempt to call a nil value (field 'match')
pattern functions are removed
use string.sub and ==
bad argument #1 to 'slot' (string expected, got userdata)
you passed a slot handle to context:slot
pass the id string, or use the handle directly
instruction_budget
a loop ran too long
walk less; store results; split work over events
a trigger never reports
it was never armed, or was armed before its area loaded
arm it in region_changed for its region
a squad never counts as cleared
no report at full strength yet, or the placement was refused
check effect_result; check the Squads page
a goal shows no marker
the marker’s object was not registered yet
send the goal again after its area loads
reload says “no started mission uses this script”
the top-level code or on_start failed, so the mission never started; reload skips it
fix the script, then leave the activity and launch it again
reload says “only started, bound missions can reload”
the mission stopped or restarted while the reload waited
press Reload script again
reload says “reload source is missing, unreadable or too large”
a started mission’s script cannot be read; Reload script reloads every started mission
fix or restore that file, or leave that activity
reload keeps saying “reload waits for a safe point”
a request, report or timer is still in flight
wait; a request ends within 60 seconds
reload says “mod X is not loaded”
Update running game named a mod the game did not load
restart the game with the mod enabled
nothing happens after an effect
results wait for the request queue to empty
do not wait for a result inside the same callback
Errors that an API call raises, such as effect argument is not declared, are in Errors you will see.
After a fault
A faulted program runs no more callbacks. Reopening it faults again.
- Read the error on the Script page or in the log.
- Fix the script.
- Press Reload script. It clears the fault and keeps your variables and timers as of the last good callback. Then it runs
on_load.
A reload skips a mission that never started, because its top-level code or on_start failed. It does not clear that fault. A reload also cannot clear a fault while the saved state still holds requests made before it. The button then shows “replacement mission state could not be prepared”. In both cases, leave the activity and launch it again. A new launch is assumed to start with fresh mission state.
Client reports that arrived while the program was faulted are not replayed. Your on_load must bring the mission back to a sane state from its variables.
Check a script offline
You can load a script with a stock Lua 5.4 interpreter before you start the game. This catches syntax errors, bad require names, missing keys, and errors in on_start. It does not prove the game accepts your requests.
Save this as check_script.lua in the Sunrise folder:
-- Offline check for one mission script. Run it from the Sunrise folder:
-- lua54 check_script.lua my_mission
local name = assert(arg and arg[1], "usage: lua54 check_script.lua <script name>")
package.path = "sdk/lua/?.lua;scripts/?.lua"
local out, open_file = print, loadfile
local calls = {}
-- A fake handle: listed fields answer; any other key is a method that records its call.
local function fake(label, fields)
return setmetatable(fields or {}, {__index = function(_, key)
return function()
calls[#calls + 1] = label .. ":" .. key
return fake(label .. ":" .. key, {value = tostring(#calls)})
end
end})
end
local sdk = fake("sdk", {
device_transitions = {open = "open", close = "close", power_on = "power_on",
power_off = "power_off", lock = "lock", unlock = "unlock"},
device_channels = {position = "position", power = "power", lock = "lock"},
squad_modes = {reinforce = "reinforce", replace = "replace", reserve = "reserve"},
atom_kinds = {face = "face", sequence = "sequence", sleep = "sleep", move_to = "move_to",
trivial = "trivial", control_flag = "control_flag", snap_to = "snap_to",
set_temperament = "set_temperament", set_channel = "set_channel", ability = "ability"},
client_teleport_reset = 0,
unit = function(x) return x end,
})
local variables = {}
local function selector(kind)
return function(_, id)
assert(type(id) == "string" or math.type(id) == "integer", kind .. " selector is invalid")
return fake(kind .. " " .. tostring(id), {id = id})
end
end
local context = fake("context", {
sdk = sdk,
attempt_generation = "1",
activity_role = "private",
player_key = "1",
mission_complete = false,
slot = selector("slot"),
squad = selector("squad"),
scene = selector("scene"),
cohort = function() return {cleared = false, observed_full = false, size = 1} end,
set_variable = function(_, key, value) variables[key] = value end,
clear_variable = function(_, key)
local had = variables[key] ~= nil
variables[key] = nil
return had
end,
start_timer = function() return "1" end,
cancel_timer = function() return false end,
set_phase = function() end,
})
local state = {
phase = 0,
revision = "0",
variable = function(_, key) return variables[key] end,
has_variable = function(_, key) return variables[key] ~= nil end,
timer = function() return nil end,
}
-- Remove what the runtime sandbox removes, so the script cannot use it here either.
for _, key in ipairs({"collectgarbage", "dofile", "getmetatable", "load", "loadfile", "next",
"pairs", "print", "rawequal", "rawlen", "warn", "xpcall"}) do
_G[key] = nil
end
for _, key in ipairs({"dump", "find", "match", "gmatch", "gsub"}) do
string[key] = nil
end
math.random, math.randomseed = nil, nil
local chunk = assert(open_file("scripts/" .. name .. "/" .. name .. ".lua", "t"))
local program = chunk()
assert(type(program) == "table", "the script must return a table")
if program.on_start ~= nil then
program.on_start(context, state)
end
out("requests made by on_start:")
for _, call in ipairs(calls) do
out(" " .. call)
end
What it checks and what it does not:
checked
not checked
the file compiles
whether a slot has the right type for a method
every require resolves
argument names and value ranges
every lib.one key exists
whether the client accepts a request
on_start runs without a Lua error
events; you can call handlers with your own fake events
no use of removed functions
the instruction and memory budgets
io, os and the other libraries still exist in a stock interpreter. Do not use them in a script. The runtime does not have them.
To test an event handler, build a fake event and call it:
local event = {kind = 40, region_index = 16, attempt_generation = "1"}
program.on_event_region_changed(context, state, event)
Ask for help with the right facts
When you report a problem, include:
- the script path from the Script page
- the
ev=mission_scriptlines around the problem, witherror= - the variables from the Mission state page
- what you did in game right before it happened
