Cross-server chat for Garry's Mod via MQTT.
This is the Garry's Mod Lua implementation of the CrossChat protocol, compatible with the crosschat-python reference server.
GMOD Server A <---> MQTT Broker <---> GMOD Server B
| |
GMOD Clients GMOD Clients
Servers communicate through an MQTT broker. No direct server-to-server TCP. Each server publishes its state and users to MQTT topics and subscribes to messages from other servers. Local players are forwarded to/from remote servers via Garry's Mod net messages.
| Path | Purpose |
|---|---|
lua/autorun/server/crosschat.lua |
Main server module: config, MQTT integration, hooks, net messages |
lua/autorun/server/meta_mqtt.lua |
MQTT client library (mosquitto bindings) |
lua/autorun/client/crosschat_cl.lua |
Client-side net message handlers, chat UI |
lua/autorun/client/chat_teams.lua |
Team definitions for chat colours |
src/crosschat/init.lua |
Source entry point |
src/crosschat/models.lua |
Data structures: CrossChatUser, CrossChatServer, BurstFlag |
src/crosschat/protocol.lua |
Topic parsing, payload serialization helpers |
src/crosschat/state.lua |
State management (CrossChatState) |
cfg/crosschat.json |
Server configuration (not included, see below) |
- Garry's Mod server
- mosquitto Garry's Mod module
- MQTT broker (e.g. Mosquitto, EMQX)
- Copy the
lua/directory to your Garry's Mod server'sgarrysmod/addons/orgarrysmod/lua/. - Install the
mosquittomodule on your server. - Create
garrysmod/data/cfg/crosschat.json:
{
"server_id": "myserver",
"topic_prefix": "crosschat/",
"mqtt": {
"host": "10.0.0.1",
"port": 1883,
"username": "",
"password": ""
},
"meta": {
"link": "steam://connect/YOUR_IP:27015"
}
}If using Mosquitto, create /etc/mosquitto/conf.d/crosschat.conf:
listener 1883 0.0.0.0
allow_anonymous true
All topics use the configurable prefix (default crosschat/).
crosschat/
state/<server_id>/status -- {"started": <unix_ts>} (retained, LWT)
state/<server_id>/meta -- {<arbitrary json>} (retained)
state/<server_id>/<key> -- "<value>" (retained, dynamic state)
m/<from>/<to>/user -- user add/del/update + burst flag
m/<from>/<to>/say/<uid> -- {"say": "..."}
m/<from>/<to>/pm/<fuid>/<tuid> -- {"say": "..."}
m/<from>/<to>/ooc/<type> -- any JSON value
When a server comes online, each online server sends all its local users
with a burst field in the payload:
| Burst Value | Meaning |
|---|---|
"start" |
First user in burst |
true |
Middle user |
"end" |
Last user |
"startend" |
Only user |
{
"id": 1,
"cmd": "add",
"name": "PlayerName",
"first_seen": 1712345678,
"server": "myserver",
"burst": false,
"steamid64": "76561197960265728",
"team": 1,
"<extra_field>": "<value>"
}Each server subscribes to:
crosschat/state/+/#- all server statecrosschat/m/+/<own_id>/#- messages destined for this server
The server module hooks into:
PlayerInitialSpawn- broadcast new player to all serversPlayerSay- broadcast chat messagesPlayerDisconnected- broadcast player leaveOnMQTT- receive MQTT messages
Fired for every incoming OOC message. Used by crosslua.lua and crossaowl.lua.
Return true to mark the OOC type as handled (silences "no handler" warnings).
hook.Add('CrossChatOOC', 'myaddon', function(from_sid, ooc_type, payload)
if ooc_type ~= 'my_type' then return end
local data = json.decode(payload)
print('OOC from', from_sid, data)
return true
end)Fired before remote Lua execution. Can allow, deny, or pass through:
-- Allow: return true (execution continues)
-- Deny: return false, "reason" (execution rejected)
-- Pass: return nil (let other hooks decide)
hook.Add('CrossLuaPreprocess', 'myaddon', function(from_sid, data, reply_fn)
if from_sid ~= 'trusted_server' then
return false, 'untrusted source'
end
end)Fired before each aowl action executes. Return is ignored (action always proceeds):
hook.Add('CrossAowlKick', 'myaddon', function(steamid64, reason, extra)
print('Player about to be kicked:', steamid64, reason)
end)
hook.Add('CrossAowlBan', 'myaddon', function(steamid64, reason, extra) end)
hook.Add('CrossAowlSlap', 'myaddon', function(steamid64, reason, extra) end)CPlayerData(playerdata, old_data)- when player data is received/updatedCrossChatSay(ServerID, UserID, text, player)- when a crosschat message arrives
| Command | Description |
|---|---|
statusall |
Show all known players across all servers |
crosschat_show |
Client convar: show/hide crosschat messages (default 1) |
crosschat_svname |
Client convar: show/hide server name prefix (default 1) |
crosschat_postfix |
Client convar: show server name after message (default 0) |
Loaded from lua/autorun/server/crosslua.lua. Enables sending Lua code to
other servers via OOC. Extremely dangerous — full server code execution.
| Command | Description |
|---|---|
cl <code> |
Broadcast Lua code to all servers |
bl <code> |
Broadcast + execute locally |
cl<ID> <code> |
Target specific server by ID |
Convar: lua_allow_remote (default 1) — gates incoming execution.
Only aowl developers can use these commands.
Loaded from lua/autorun/server/crossaowl.lua. Enables cross-server
kick/ban/slap via OOC. Supports two targeting modes:
By SteamID64 (broadcast to all GMod servers):
crossaowl.kick('76561197986413226', 'spam', {})
crossaowl.ban('76561197986413226', 'griefing', {})
crossaowl.slap('76561197986413226', 'being annoying', {})Each server looks up the player by SteamID64 and executes player:Kick(),
player:Ban(), or player:Slap().
By server_id + user_id (targeted, for webchat users):
crossaowl.kick_user('myserver', 1, 'spam', {})
crossaowl.ban_user('myserver', 1, 'griefing', {})
crossaowl.slap_user('myserver', 1, 'being annoying', {})The message is broadcast to all servers but only the one matching server_id
acts on it (GMod servers acknowledge but ignore user_id commands).
The src/crosschat/ directory contains a reusable Lua implementation of
the protocol, mirroring the Python src/crosschat/ package structure:
- models.lua:
CrossChatUser,CrossChatServer,BurstFlagdata types - protocol.lua: Topic parsing, payload building, serialization helpers
- state.lua:
CrossChatStateclass managing server/user state