This page gives a brief overview over how to test a mod. There is no console in the game: a mod is tested by watching the log and by running races, and the dev boot runs a race without the front end, which is how the API itself was checked.

Required knowledge. Please familiarize yourself with these pages before reading further.

Reading the log#

Everything a script logs, and everything the hook says about it, goes to the Windows debug output (OutputDebugStringA). DebugView from Sysinternals shows it live; filtering on sshook drops the game's own output. On the translated builds the same lines go to the runtime's log file.

A script's lines read sshook: lua: [<mod>] <text>, with the mod's name in the brackets. An error in a handler is logged the same way with a traceback, file and line included, and the game carries on. A handler that fails fifty times in a row is dropped with a line saying so. The hook's own lines (sshook: ...) report which mods were found and in what order, whether lua51.dll loaded, and which race params a race runs. A release build leaves the hook's own lines out. A script's lines and its errors are in every build. A fatal error raised by the game itself comes through as sshook: game: ..., which is the line to look for when the game closes.

A dev-boot race#

game_setup/skip_frontend in SplitSecond.params (what the map plugin's "Run in Game" writes) loads straight into a race with no menus. The intro scene then waits for a button, and the game pauses its race logic while its window is not in the foreground, so a dev-boot race started from a script needs a press of Enter with the window focused before the countdown runs.

A dev-boot race is not a normal race, and a mod tested only there sees less than it would from the front end. The mode name needs the .params extension (game_setup/game_mode = "Nemesis.params", not "Nemesis"), or the game runs no mode file at all. Even with one loaded, a dev-boot race has no mode params of its own:

  • There is no ranking, so every position is derived from progress.
  • There are no end rules, so nobody reaches finished, and the AIs keep lapping past the last lap on a cool-down lap where the lap timer sits at 0.
  • None of the mode timers run, so there are no timer or player_finished events.
  • ss.race.mode reads nil.

The hook writes one line when a race controller appears (race params: mode=... endgame=... rules ...) that reports which rules the race actually runs. Finish, elimination and timer behaviour requires a race started from the front end.

Running a race unattended#

From a script outside the game: launch it, wait for the log to show race_state 3 (the intro), bring the window to the foreground, press Enter, and the countdown starts. The window should not be touched while the game boots or loads, as a focus change mid-load has crashed the loader. The AIs lap the airport test track (4322 m) in about 80 s, so a three-lap race needs some 330 s before the log is complete. A mod can do its own part from inside: a race_state handler that waits for the intro is the moment to set things up, and race_start is the green light.

Probing#

The quickest way to see what a race produces is a mod that logs every event with its fields. The following script registers a handler for each event name it knows and prints the payload sorted by key, so that the lines diff well between runs:

Mods\RaceProbe\Scripts\main.lua
local events = {
  "state", "race_state", "race_start", "race_finish", "race_results", "race_end",
  "race_event", "lap", "finish_line", "position", "score", "player_state",
  "player_lap", "vehicle_state", "wreck", "respawn", "powerplay", "checkpoint",
  "lap_time", "countdown", "timer", "player_finished",
}

local function describe(ev)
  local keys = {}
  for k in pairs(ev) do keys[#keys + 1] = k end
  table.sort(keys)
  local parts = {}
  for _, k in ipairs(keys) do
    local v = ev[k]
    if type(v) == "number" then v = string.format("%.3f", v) end
    parts[#parts + 1] = k .. "=" .. tostring(v)
  end
  return table.concat(parts, " ")
end

for _, name in ipairs(events) do
  ss.on(name, function(ev)
    ss.log("%8.2f %-16s %s", ss.race.time, name, describe(ev))
  end)
end

tick, input and draw are left out on purpose, as they fire every frame. They can be added with a counter when a frame-level question comes up. The Events page lists what each field means.