Understanding Lua Scripting and Addon Structure
An overview of GMod's Lua system, addon folder layout, the three execution realms, autorun paths, and how to send files to clients.
The Three Realms
GMod Lua runs in three distinct environments:
| Realm | Constant | Where it runs |
|---|---|---|
| Server | SERVER |
Dedicated server process only |
| Client | CLIENT |
Each connected player's game client |
| Menu | MENU |
Main menu; rarely used in server addons |
Check which realm your code is running in:
if SERVER then
-- This code runs on the server
elseif CLIENT then
-- This code runs on clients
end
Code in lua/autorun/server/ only executes server-side. Code in lua/autorun/client/ only executes client-side. Code in lua/autorun/ executes in both realms unless guarded with if SERVER / if CLIENT.
Addon Directory Structure
garrysmod/addons/<addonname>/
├── lua/
│ ├── autorun/ ← Loaded on both client and server
│ │ ├── server/ ← Server only
│ │ └── client/ ← Client only
│ └── <other folders>/ ← Loaded only when explicitly included
├── materials/
├── models/
├── sounds/
└── addon.json ← Addon metadata (optional but recommended)
Files in lua/autorun/ are loaded automatically, alphabetically (A–Z). All other Lua files must be explicitly loaded with include() or sent to clients with AddCSLuaFile().
Sending Files to Clients
The server controls which Lua files clients receive. Use AddCSLuaFile() on the server side to authorize a file for client download:
-- In a server-side autorun file
AddCSLuaFile("myfile.lua") -- Relative to the lua/ folder
AddCSLuaFile("myfolder/client.lua")
Without this call, clients do not receive the file and any client-side include() of that file fails silently. This is a common source of "why isn't my client code running?" issues.
Hooks
Hooks are the primary way to respond to game events:
hook.Add("PlayerSpawn", "MyAddon_PlayerSpawn", function(ply)
ply:SetHealth(200)
print(ply:Nick() .. " spawned with 200 HP")
end)
The second argument ("MyAddon_PlayerSpawn") is a unique identifier. If two addons register the same hook name with the same unique ID, the second registration overwrites the first. Always namespace your IDs with your addon name to avoid conflicts.
Remove a hook:
hook.Remove("PlayerSpawn", "MyAddon_PlayerSpawn")
Client-Server Communication (net library)
Direct function calls cannot cross the server/client boundary. Use the net library:
Server side:
-- Register the network string (must happen on server before use)
util.AddNetworkString("MyAddon_SendData")
-- Send data to a specific client
net.Start("MyAddon_SendData")
net.WriteString("Hello client!")
net.Send(ply)
-- Receive from client
net.Receive("MyAddon_ClientToServer", function(len, ply)
local msg = net.ReadString()
end)
Client side:
-- Send to server
net.Start("MyAddon_ClientToServer")
net.WriteString("Hello server!")
net.SendToServer()
-- Receive from server
net.Receive("MyAddon_SendData", function(len)
local msg = net.ReadString()
print(msg)
end)
util.AddNetworkString() must be called server-side before either side can use that message name. Call it in a server autorun file.
File Load Order
Files in lua/autorun/ load alphabetically. If addon A depends on addon B defining a global variable, and B's filename sorts after A's, you will get a nil error. Solutions:
- Prefix filenames to control load order (e.g.,
01_base.lua,02_config.lua) - Use
hook.Add("InitPostEntity", ...)to defer initialization until after all addons load - Check for nil before use:
if MyAddon and MyAddon.SomeFunction then
Practical Example: Server-Side Autorun
-- garrysmod/addons/myaddon/lua/autorun/server/init.lua
AddCSLuaFile("autorun/client/cl_init.lua")
AddCSLuaFile("autorun/shared.lua")
util.AddNetworkString("MyAddon_Greet")
hook.Add("PlayerInitialSpawn", "MyAddon_Greet", function(ply)
net.Start("MyAddon_Greet")
net.WriteString("Welcome to the server, " .. ply:Nick())
net.Send(ply)
end)