TrailBridge
Connect your Trailmakers mods to the web.
What is TrailBridge?
TrailBridge is a small, safe app that runs in the background. It acts as a "bridge" to let Trailmakers mods connect to the internet.
It allows for mods like cross server chat or online leaderboards to be possible. It automatically finds your mods and just works.
Note: In multiplayer, only the player who is hosting the server needs to run TrailBridge. Other players joining the server do not need to run it. Always activate the in-game mod first.
What It Enables
TrailBridge enables awesome new features. Check out the compatible mod on the Steam Workshop!
Download TrailBridge
This tiny console app is all you need. Just run it in the background while you play. No config needed.
Download for Windows Download for LinuxGet the Example Mods
Subscribe to the Cross Server Chat example on the Steam Workshop, or download the example mod files directly to see how TrailBridge features are wired in practice.
Subscribe on Workshop Download Cross Server Chat .zip Download Discord Webhook Demo .zipFor Modders: How to Use
Want to use TrailBridge in your own mod? Include trailkit.lua in your mod's data_static folder and use TrailKit.Bridge. The TrailBridge app will automatically detect embedded Bridge support inside TrailKit.
1. Load TrailKit and Use TrailKit.Bridge
Load trailkit.lua at the top of your main.lua, then use the global TrailKit API surface directly. Bridge debug/testing settings are exposed as explicit Bridge methods.
tm.os.DoFile("trailkit")
-- Optional Bridge Settings
TrailKit.Bridge.setDebugLogging(true)
TrailKit.Bridge.setReceiveOwnMessages(false) -- Good for testing
2. Register Your Update Logic
TrailKit manages the main update pipeline. Register your own loop with UpdateManager.register(...). The Bridge update loop is already registered automatically.
local function MyModUpdate()
-- Your mod logic here
end
UpdateManager.register(MyModUpdate)
3. Send Data (Broadcast)
Send any Lua table to ALL users on any "channel" name you want.
-- Send a full table (recommended)
local myData = {
playerName = "Steve",
score = 100
}
TrailKit.Bridge.send("leaderboard_update", myData)
-- or send just a single value
TrailKit.Bridge.send("simple_message", "Hello World!")
4. Receive Data (Broadcast)
Register a "callback" function to handle incoming data for a specific channel.
function OnMyData(data)
-- 'data' is the table we just received
if data.value then
-- It was a single value, access it with .value
TrailKit.Log.info("Received simple message: " .. data.value)
else
-- It was a full table, access by its keys
TrailKit.Log.info(data.sender .. " got score: " .. data.score)
end
end
TrailKit.Bridge.on("leaderboard_update", OnMyData)
TrailKit.Bridge.on("simple_message", OnMyData)
5. How to Handle Chat
The handleChatMessage function is special. It both sends the chat message AND checks for message "loops" (when your own message comes back from the server).
This loop check is only necessary if you use chat commands (like "!hello") AND you enabled TrailKit.Bridge.setReceiveOwnMessages(true) for testing.
-- In your OnChatMessage function
function OnChatMessage(sender, msg, color)
-- This function does two things:
-- 1. It saves your 'sender' name for the chat function.
-- 2. It returns 'true' if the message is a loop.
local isLoop = TrailKit.Bridge.handleChatMessage(sender, msg)
-- If TrailKit.Bridge.setReceiveOwnMessages(true), this check is required
-- to prevent commands from running twice.
if isLoop then
return
end
-- Now you can safely check for local chat commands
if msg == "!hello" then
TrailKit.Log.info("Hello command received!")
end
end
-- In your "chat" callback
function OnReceiveChat(data)
local chatColor = tm.color.Create(0.5, 1.0, 0.5, 1.0)
local taggedSender = TrailKit.Bridge.getTaggedSender(data.sender)
tm.playerUI.SendChatMessage(taggedSender, data.message, chatColor)
end
TrailKit.Bridge.on("chat", OnReceiveChat)
tm.playerUI.OnChatMessage.add(OnChatMessage)
6. How to Use the Database
The database is a simple public key-value store. Anyone with your API_KEY can read or write to any key. It can be used at any time.
RATE LIMIT: The server allows roughly 2 database requests per second (per API key). Do not spam it, and prefer batching when you need multiple reads at once. I'm also providing this service as-is and free-of-charge, I cannot guarantee data will be kept and I cannot guarantee security of data, do not use this to store critical data.
local MY_MOD_API_KEY = "a-unique-string-id"
-- === SET DATA ===
-- This function is called when the server responds
function OnSetResponse(response)
if response and response.status == "success" then
TrailKit.Log.info("Successfully saved key: " .. response.key)
else
TrailKit.Log.warn("Failed to save data.")
end
end
-- Call this to save your data
local mySave = { level = 10, name = "MySave" }
TrailKit.Bridge.setData(MY_MOD_API_KEY, "save_slot_1", mySave, OnSetResponse)
-- === GET DATA ===
-- This function is called when the server responds
function OnGetResponse(response)
if response and response.status == "success" and response.value then
-- response.value is the table we saved: { level = 10, name = "MySave" }
TrailKit.Log.info("Got data: " .. json.serialize(response.value))
elseif response and response.status == "not_found" then
TrailKit.Log.warn("Key not found.")
else
TrailKit.Log.warn("Could not get data.")
end
end
-- Call this to get your data
-- (Make sure to wait atleast a second after setting (to allow for the server to update)
TrailKit.Bridge.getData(MY_MOD_API_KEY, "save_slot_1", OnGetResponse)
7. Request a Server-Side Service
Use TrailKit.Bridge.requestService(...) when you want the TrailBridge server to run a named service or connector for you. The available service names are defined server-side. Some services are fully server-managed, while others can accept caller-supplied values such as a Discord webhook_url.
A ready-to-wire Discord connector is included as discord.webhook.send. Callers pass a webhook_url in the payload. It only accepts real Discord webhook URLs and remains non-public.
TrailKit.Bridge.requestService("discord.webhook.send", {
webhook_url = "https://discord.com/api/webhooks/...",
content = "Airfield captured by RED",
username = "TrailBridge"
}, function(response)
if response and response.status == "success" then
TrailKit.Log.info("Service request completed.")
else
TrailKit.Log.warn("Service request failed.")
end
end)
8. Build Public Websites with Service API
Browser code can call service_api.php for public read-only services. A built-in example is trailbridge.events.recent, which returns recent TrailBridge events through a safe public service endpoint.
Non-public connectors such as discord.webhook.send are not available from browser code through service_api.php.
const res = await fetch("https://ludixi.com/trailmakers/trailbridge/service_api.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service: "trailbridge.events.recent",
payload: { channel: "chat", limit: 100 }
})
});
const json = await res.json();
console.log(json.events);