Modding Guide
This guide covers everything you need to create mods for Rawframe. For the complete function reference, see the API Playground.
What is a Mod?
A Rawframe mod is a self-contained package of Luau scripts that adds gameplay content to the engine. Mods can:
- Spawn and control entities (cubes, players, projectiles, etc.)
- Send and receive network messages between server and client
- Play 2D and 3D positional audio
- Register per-frame tick callbacks for game logic
- Perform physics raycasts for hit detection
- Manage teams, spawns, weapons, and game states
The engine itself ships with no built-in gameplay. All game content comes from mods — gamemodes, weapons, maps, and other content are all community-created.
Each mod runs in its own sandboxed Luau VM (per-mod isolation) with memory limits (16 MB default, configurable) and instruction limits (1M per execute). If a mod script errors, the engine logs the error and continues running. A mod crash will never crash the engine or affect other mods.
Directory Structure
Place your mod inside the mods/ directory at the engine root:
mods/
my_mod/
mod.json -- Manifest (required)
scripts/
shared/ -- Scripts loaded on BOTH server and client
config.lua
server/ -- Server-only scripts
init.lua
weapons.lua
client/ -- Client-only scripts
init.lua
hud.lua
input.lua
sounds/ -- Audio files (OGG Vorbis)
Key points:
mod.jsonis the only required file. Without it, the engine ignores the directory.scripts/shared/scripts are loaded first on both sides. Use them for configuration tables, constants, and utility functions.scripts/server/scripts run only on the dedicated server. They have access tonet.Send()andnet.Broadcast().scripts/client/scripts run only on the client. They have access tonet.SendToServer(), audio playback, mouse input, and aim direction.
mod.json Format
Every mod must have a mod.json file in its root directory:
{
"name": "my_mod",
"version": "1.0.0",
"description": "My first mod",
"author": "YourName",
"dependencies": [],
"scripts": {
"server": ["scripts/server/init.lua"],
"client": ["scripts/client/init.lua"],
"shared": ["scripts/shared/config.lua"]
}
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique mod identifier. Use snake_case. |
version | string | Yes | Semantic version (e.g. "1.0.0"). |
description | string | Yes | Human-readable description. |
author | string | Yes | Author name or team name. |
dependencies | string[] | Yes | Mod names this mod depends on. Empty [] if none. |
optional_dependencies | string[] | No | Mods loaded before this one if present, skipped if missing. |
engine_version | string | No | Engine version constraint (e.g. ">=0.18.0"). |
scripts.server | string[] | Yes | Server-side script paths. Supports glob patterns. |
scripts.client | string[] | Yes | Client-side script paths. Supports glob patterns. |
scripts.shared | string[] | Yes | Shared script paths loaded on both sides. Supports glob patterns. |
Script Glob Patterns
Script paths support glob patterns for automatic file discovery:
{
"scripts": {
"shared": ["scripts/shared/config.lua", "scripts/shared/utils/*.lua"],
"server": ["scripts/server/**/*.lua"],
"client": ["scripts/client/**/*.lua"]
}
}
scripts/*.lua— all.luafiles inscripts/(single level)scripts/**/*.lua— all.luafiles recursively- Expanded files are sorted alphabetically for deterministic load order
Script Loading Order
The engine loads mod scripts in a deterministic order:
- Dependency resolution — Mods are topologically sorted. If mod B depends on mod A, all of A’s scripts run before any of B’s scripts.
- Per-mod loading order: Shared scripts load first, then side-specific scripts (server or client).
- Per-mod VM isolation — Each mod runs in its own Luau VM. Globals set by one mod are not visible to other mods.
Per-Mod VM Isolation
Every mod gets its own Lua VM with independent memory and instruction limits:
- Crash isolation — A buggy mod cannot corrupt another mod’s state.
- Clean hot-reload — Reloading a mod destroys and recreates only its VM.
- Memory accountability — Each mod’s memory usage is tracked separately.
Cross-VM hook dispatch: When a mod calls hook.Run("MyEvent", ...), the engine dispatches the hook to all mod VMs. Arguments are serialized via LuaValue (supports nil, bool, number, string, Vec3, and nested tables).
Cross-VM function calls: Use mod.call("other_mod", "fn_name", ...) to call exported functions in another mod’s VM:
-- In mod_a: export a function
mod.export("get_config", function()
return { max_health = 100, respawn_time = 3.0 }
end)
-- In mod_b: call it
local config = mod.call("mod_a", "get_config")
local max_hp = config.max_health -- 100
Note: Functions, userdata, coroutines, and metatables cannot cross VM boundaries. Pass data as primitives, strings, Vec3, or tables of these types.
Entity Lifecycle
Entities are the fundamental building blocks of a Rawframe world. Every object (player, cube, projectile, NPC) is an entity with components.
-- Spawn a named entity
local ent = entity.create("MyCube")
-- Configure it
entity.set_position(ent, 10, 5, -3)
entity.set_rotation(ent, 0, 45, 0)
entity.set_scale(ent, 2, 2, 2)
entity.set_mesh(ent, "cube")
-- Read properties (multiple return values)
local x, y, z = entity.get_position(ent)
-- Destroy when done
entity.destroy(ent)
After destruction, any further operations on the entity ID will raise a Luau error.
Networking
Rawframe provides a built-in net library for mod-to-mod network messaging.
Architecture
- Server can
net.Send(peer_id)to one client ornet.Broadcast()to all. - Client can
net.SendToServer()to send messages to the server. - Messages are identified by string names and use a binary stream (write in order, read in same order).
Sending Messages
-- Server: send to specific client
net.Start("PlayerDamaged")
net.WriteFloat(new_health)
net.Send(target_peer_id)
-- Server: broadcast to all
net.Start("ScoreboardUpdate")
net.WriteFloat(player_count)
net.Broadcast()
-- Client: send to server
net.Start("PlayerShoot")
net.WriteVec3(aim_x, aim_y, aim_z)
net.SendToServer()
Receiving Messages
-- Server-side: receive from clients
net.Receive("PlayerShoot", function(len, sender)
local x, y, z = net.ReadVec3()
rawframe.log("Player " .. tostring(sender) .. " fired!")
end)
-- Client-side: receive from server
net.Receive("PlayerDamaged", function(len)
local health = net.ReadFloat()
rawframe.log("New health: " .. tostring(health))
end)
Write/Read Functions
| Write | Read | Type |
|---|---|---|
net.WriteString(s) | net.ReadString() | string |
net.WriteInt(n) | net.ReadInt() | 32-bit integer |
net.WriteFloat(n) | net.ReadFloat() | 32-bit float |
net.WriteVec3(x,y,z) | net.ReadVec3() | 3D vector |
net.WriteBool(b) | net.ReadBool() | boolean |
net.WriteTable(t) | net.ReadTable() | table |
Important: Always call
net.Start()before any Write function. Read data in the same order it was written.
Audio
The audio system provides cross-platform 3D positional sound.
-- 2D sound (same volume everywhere)
local handle = audio.play("sounds/click.ogg")
-- 3D positional sound (attenuates with distance)
local handle = audio.play_3d("sounds/explosion.ogg", 10, 0, -5)
-- Control playback
audio.stop(handle)
audio.set_volume(handle, 0.5)
audio.set_master_volume(0.8)
Supported formats: OGG Vorbis (recommended), WAV, MP3, FLAC.
On the server, all audio functions are no-ops — shared scripts can safely call audio functions without side-checking.
Hook System
The hook system provides priority-based event handling across all mods:
-- Register a hook
hook.Add("PlayerSpawn", "my_mod_spawn", function(peer_id)
rawframe.log("Player " .. tostring(peer_id) .. " spawned!")
end)
-- Fire a hook (dispatched to all mods)
hook.Run("MyCustomEvent", "arg1", 42)
-- Remove a hook
hook.Remove("PlayerSpawn", "my_mod_spawn")
Hooks fire in priority order. Returning false from any hook cancels propagation.
Built-in Hooks
| Hook | Arguments | Description |
|---|---|---|
Think | — | Fires every tick (replaces rawframe.on_tick) |
PlayerSpawn | peer_id | Player spawned |
PlayerDeath | victim_id, attacker_id | Player died |
PlayerChat | peer_id, message | Chat message received |
PlayerConnect | peer_id | Player connected |
PlayerDisconnect | peer_id | Player disconnected |
Game State Machine
Manage game flow with states, teams, and spawn points:
-- Set the game state
game.set_state("playing")
-- Teams
team.create(1, "Red", {255, 0, 0})
team.create(2, "Blue", {0, 0, 255})
player.set_team(peer_id, 1)
-- Spawn points
spawn.add("red_spawn", 10, 0, 5, 0)
spawn.add("blue_spawn", -10, 0, 5, 180)
Mod Management API
Query and control mods at runtime:
-- Check mod state
if mod.is_loaded("pvp_core") then
local info = mod.get_info("pvp_core")
rawframe.log("Version: " .. info.version)
end
-- Export functions for other mods
mod.export("get_score", function(peer_id)
return scores[peer_id] or 0
end)
-- Call functions in other mods
local score = mod.call("pvp_core", "get_score", peer_id)
-- Lifecycle callbacks
mod.on_start(function()
rawframe.log("Mod started!")
end)
mod.on_stop(function()
rawframe.log("Cleaning up...")
end)
Debugging
Logging
rawframe.log("Debug: player position = " .. tostring(x))
Log output appears in:
- Server terminal
- Client console
- Rawframe Studio console panel
Rawframe Studio Debugger
Rawframe Studio includes a full Luau debugger with:
- Breakpoints (F9)
- Step over (F10), step into (F11), step out (Shift+F11)
- Call stack and variable inspection
- Script profiler for performance analysis
Hot-Reload
During development, you can hot-reload mods without restarting the server:
- Files are monitored for changes
- Modified scripts trigger automatic VM reload
- In Studio, press Ctrl+Shift+R to force reload
Example: Complete PvP Mod
Here’s a minimal PvP gamemode using multiple engine systems:
-- scripts/server/init.lua
local CONFIG = { max_health = 100, respawn_time = 3.0 }
-- Setup teams
team.create(1, "Red", {255, 0, 0})
team.create(2, "Blue", {0, 0, 255})
-- Handle player spawning
hook.Add("PlayerSpawn", "pvp_spawn", function(peer_id)
local ent = player.get_entity(peer_id)
char.set_max_health(ent, CONFIG.max_health)
char.set_health(ent, CONFIG.max_health)
-- Assign to team with fewer players
local red = team.get_players(1)
local blue = team.get_players(2)
local t = #red <= #blue and 1 or 2
player.set_team(peer_id, t)
end)
-- Handle kills
hook.Add("PlayerDeath", "pvp_death", function(victim_id, attacker_id)
if attacker_id then
player.broadcast_chat(
player.get_name(attacker_id) .. " killed " .. player.get_name(victim_id)
)
end
-- Respawn after delay
timer.delay(CONFIG.respawn_time, function()
char.respawn(victim_id, 0, 5, 0)
end)
end)
Next Steps
- Browse the API Playground for all 583+ functions.
- Read the Server Guide for hosting.
- Explore Rawframe Studio for visual editing.