Skip to content

Mods and modules

A mod is a folder of Luau files and the models, sounds and images they name. The server runs it. Every player’s game runs its client scripts, which it downloads from the server as it joins.

The game’s own modes are a mod too: the stock mod. Every mod sits on top of it, so yours holds only what differs.

content/mods/mymod/
main.server.luau runs first
modules/
killstreak/
killstreak.server.luau the server's half of a feature
killstreak.client.luau what every player's game runs of it
gametypes/
koth/
koth.server.luau a game mode
maps/
yard/
yard.server.luau code for one map, in any mode
yard.client.luau what every player's game runs on that map
weapons/
ak47.weapon.luau a weapon's numbers

A server runs it with server --mod content/mods/mymod, as running a server says.

--mod also takes a single file, mymod.server.luau. A mod of one file has the stock mod’s models, sounds and images, but none of its code. Its weapons go in a weapons/ folder beside it.

A file’s name says where it runs:

Name ends in Runs on
.server.luau the server
.client.luau every player’s game
.shared.luau both: code the two sides share, such as a jump pad’s push
.weapon.luau both: a weapon’s numbers, as a table

Any other .luau file fails the load, naming it. So does a misspelt side, such as ranks.sever.luau.

The server never runs a client file. A player’s game never sees a server file. So keep secrets, prices and rules on the server: anything a client sends, the server checks.

A module is one feature in a folder of modules/: its server file, its client file, and the files and images it uses. Adding a module touches nothing else. Only the mod’s root has modules/.

main.server.luau runs first: your mod’s, or else the stock mod’s. It declares and listens as a module does, and its handlers run before the modules’. Modules load in the order of their names, after it. Then the map’s code loads, then the game mode’s last.

Each module has its own globals, and reads through to those of main.server.luau. Modules share only what they declare: their state and their events. A helper file one module requires, such as modules/teams/teams_lib.server.luau, is shared by every module that requires it. It runs once a load, with the globals of main.server.luau.

A game mode is a folder of gametypes/ with its <name>.server.luau, and runs only while it is played. A mod with no gametypes/, its own or the stock mod’s, plays any game mode its map lists. A module, a game mode and a map with code never share a name: the load fails.

A mod has three layers: its root, the game mode being played (gametypes/<name>/) and the map being played (maps/<name>/). A file in a closer layer wins: the map’s copy, then the game mode’s, then the root’s. In each layer your mod’s copy wins over stock’s.

So weapons/ak47.weapon.luau in your mod changes stock’s AK-47 everywhere, and the same file in maps/yard/ changes it on yard alone. The server prints each file a mod replaces as a match starts. A model’s or an effect’s textures are found through the layers too, so a map may replace the texture of a stock model.

A mod names each file by its path, without the extension. The call says the kind: p:give("weapons/awp") names a weapon, play_sound("sounds/horn") a sound.

  • A path is lowercase letters, digits and _, between slashes.
  • Each part is 64 bytes at most, the whole path 255. No part is a name Windows keeps for a device, such as con or com1.
  • A path is relative to its layer, never to the file that names it.
  • Stock’s own sounds, models and images have no folder: ak47_fire, zombie_punch, kill_skull.

Every file a mod names is checked when it is first named. A name of no file fails at that call. A mod names 4096 files of each kind at most. Name the files a module uses at its top with preload, so players load them as they join rather than during play.

A material, which a shape is drawn with, is a file <name>.material.json with its images beside it, as stock’s wood.material.json:

{ "color": "wood_color.jpg", "normal": "wood_normal.jpg", "roughness": 0.8 }

color and normal are its colour image and normal map, each optional. tint multiplies the colour, as linear [r, g, b, a], white if left out. roughness is 1 and metallic 0 if left out. Stock’s wood is there for any mod’s shapes.

The server runs the mod 20 times a second. Every handler, timer and command runs between two of those ticks. A call that takes over 50 ms stops the mod until the next match.

A mod runs in a sandbox:

  • It reads no files but its own, and starts no programs.
  • It reaches the network only through its calls: HTTP, SQL and WebSockets.
  • Luau’s libraries are read-only. math.random is seeded from the clock.
  • It has 64 MB of memory.
  • A call gives 4096 orders at most.
  • 1024 NPCs, models, looping sounds and looping effects are alive at once at most.

Each error is printed once, cut to 512 bytes, not every tick.

A player’s game gives its client scripts 32 MB in all, and 4 ms of each frame, 2 ms each. A script that runs over its time is stopped, and called no more until its file changes. Any other error ends that call alone.

Start the server with --reload. It checks the mod every second. An edit takes effect at once, and the match goes on. If you changed what a module declares, such as a new player state, the match starts again instead, and the server says why. So does a client script added or removed.

  • A reload in place keeps the mod’s globals, so keep a table as hits = hits or {}. A function you deleted stays defined until the next match.
  • An edit that fails to load changes nothing: the mod as it was runs on.
  • A mod stopped for running over 50 ms runs again at your next edit.
  • Weapon files, client scripts, models and sounds reach the players as they change, without reloading the mod.

A mod of one file, or main.server.luau, may also define two functions:

  • on_tick(world) runs every tick. world.time is the mod’s clock. world.players lists every player as a table, then every NPC.
  • on_event(kind, ...) hears the engine’s events by name: "joined", "leaving", "fired", "hit", "died", "chat", "fell", and "command" for a command declared without its handler.

on_event runs before the modules hear an event. Returning false keeps it from them; returning a table puts its fields over the event’s. Modules hear the same events with on, which is what the rest of this site uses.