Skip to content

Making a Gamemode

Liam Robinson edited this page Nov 8, 2026 · 1 revision

Making a Gamemode

A gamemode gives maps their rules: what kind of event they are ridden as (a race against the clock, a trick contest, survival …), the time limit, what the end of the time means, and the medal targets of each map. It may also bring server scripts that add to the rules: notices, splits, records. Gamemodes appear on the game's Maps page next to the built-in modes, and run on servers.

The rules themselves are data: the game and a server apply them without a script. Scripts add to them (notices, records, ending a round early); they cannot change a run's time or points.

An example

sprint is a race against the clock on the template course (Making a Map), with medals and a script that announces each round, tells a rider its splits and keeps the best finish.

sprint/
  resource.toml
  map.toml
  maps/course.gltf
  gamemodes/sprint.toml
  server/main.luau

resource.toml:

[resource]
name = "sprint"
version = "1.0.0"
title = "Sprint"
description = "A race against the clock with medals, on the template course"
api = 1
type = "gamemode"

[add]
"map/course" = "map.toml"
"gamemode/sprint" = "gamemodes/sprint.toml"

[scripts]
server = ["server/*.luau"]

map.toml:

[map]
title = "Sprint course"
scene = "maps/course.gltf"
modes = ["race", "freeride"]

gamemodes/sprint.toml:

[gamemode]
title = "Sprint"
event = "race_clock"
time_up = "fail"            # a race can only fail at its time limit

[[gamemode.map]]
id = "sprint:map/course"
targets = [13.0, 15.0, 20.0]   # gold, silver, bronze: seconds, best first
time_limit_s = 90.0

server/main.luau:

-- Announces each round, tells a rider each split, and keeps the best finish.
addEventHandler("onRoundStart", root, function(round, drop)
    print("sprint: round " .. round .. " on " .. drop)
    showNotice(source, "Sprint", "Gold under 13 s", 5)
end)

addEventHandler("onRiderCheckpoint", root, function(checkpoint, index, clock)
    local player = getRiderPlayer(source)
    if player then
        showNotice(player, "Split " .. index, string.format("%.2f s", clock / 1000), 3)
    end
end)

addEventHandler("onRiderFinish", root, function(clock, points, failed)
    local best = storage.get("best")
    if not failed and (best == nil or clock < best) then
        storage.set("best", clock)
        print("sprint: a new best, " .. clock .. " ms")
    end
end)

addEventHandler("onRoundEnd", root, function(round)
    print("sprint: round " .. round .. " ended")
end)

-- `endround` in the console ends every round under way
addCommandHandler("endround", function(player, command)
    for _, instance in getInstances() do
        print("sprint: endRound " .. tostring(endRound(instance, "ended by the operator")))
    end
end)

On a server with

[game]
mode = "race"
map = "sprint:map/course"
gamemode = "sprint:gamemode/sprint"
lobby_wait = 5
countdown = 3

[resources]
start = ["sprint"]

one player rode three rounds; the operator typed endround during the third. The server's log:

map sprint:map/course (Sprint course) under sprint:gamemode/sprint "Sprint" (EventType_RaceClock 5, a time limit of 90000 ms ending failed, 3 targets) loaded in 2156 ms: …
instance 1 round 0: running on map sprint:map/course (sprint:gamemode/sprint)
scripts: sprint: sprint: round 0 on sprint:map/course
instance 1 round 0: results on map sprint:map/course (sprint:gamemode/sprint); the standings: 1. Player Finished 13700 ms 0 pts 90443 mm
scripts: sprint: sprint: a new best, 13700 ms
scripts: sprint: sprint: round 0 ended
…
scripts: sprint: sprint: round 2 on sprint:map/course
scripts: ran endround
scripts: sprint: sprint: endRound true
instance 1 round 2: ended by the scripts: sprint: ended by the operator
instance 1 round 2: results on map sprint:map/course (sprint:gamemode/sprint); the standings: 1. Player Racing 10666 ms 0 pts 76686 mm

The second round's 13 700 ms equalled the best, so no new best was printed. On the rider's HUD the round's notice shows for 5 s and each gate's split for 3 s: "Split 0 / 3.73 s" at the first gate (index counts from 0).

Ridden alone from the Maps page, the same package gives the same lines in the game's log, from the local script host, and keeps its best in ScriptData/local-server/sprint/:

scripts: the local script host runs sprint's 1 server scripts for Player
scripts: local server: [sprint] sprint: round 1 on sprint:map/course
scripts: local server: [sprint] sprint: a new best, 13700 ms
scripts: local server: [sprint] sprint: round 1 ended

gamemode.toml

Key Required Rule
title yes 1 to 64 characters
event yes race_clock, race_ghosts, race_bots, trick_score, trick_ghosts, trick_bots, survive_to_bottom, survive_to_death, freeride
time_up no what the time limit's end does: fail (the run fails) or result (the run counts as it stands). The default is result for survive_to_death and fail for every other; a race can only fail
time_limit_s no the default time limit, above 0 and at most 3599.999
any_map no true: the gamemode takes any map that has what its event needs (a finish, except free ride), without targets
[[gamemode.map]] yes, unless any_map the maps it takes, at most 256

Each [[gamemode.map]]:

Key Rule
id the map's id (<package>:map/<name>), each once
targets none, or three medal targets (gold, silver, bronze), best first, strictly in order: seconds for races, points for trick events, metres for survival. Not for survive_to_bottom and freeride
time_limit_s this map's time limit

The scripts

A gamemode's server scripts are ordinary server scripts. The round's events are the useful ones (Events and Messages): onRoundStart, onRiderCheckpoint, onRiderFinish, onRoundEnd, with getStandings, getRiderState, showNotice and endRound from the Server Script API. storage keeps records across rounds and restarts.

In single player the game runs the gamemode's server scripts in a local script host when you ride the gamemode from the Maps page, so the same package plays alone and online. There, privileged calls are always refused and the storage is ScriptData/local-server/<package>/ in your Project Avalanche folder.

Clone this wiki locally