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 can power things like chat, online features, and synced audio in supported mods. It automatically finds compatible mods and just works.
Note: In multiplayer, the host should run TrailBridge. If you want other players to hear synced audio too, they should run TrailBridge and join with the session code. Always activate the in-game mod first.
What It Enables
TrailBridge works with supported mods that use TrailBridge or TrailKit. Here is an example mod on the Steam Workshop:
Download TrailBridge
This is all you need. Run it while you play and supported mods can use it automatically.
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 .zip Download TrailBridge Audio Demo .zipFor Modders: How to Use
Want to use TrailBridge in your own mod? You can use TrailKit, or another supported TrailBridge bridge library. TrailBridge will automatically detect supported bridge-enabled mods.
The examples below use TrailKit.Bridge.
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. Play Audio
Use TrailKit.Bridge.playAudio(...) when you want TrailBridge to play an .mp3.
Local audio is the default. Put the audio file in your mod's data_static folder. Give it an id if you want to control it later.
Use the returned handle with pauseAudio, resumeAudio, and stopAudio.
Fade, seek, restart, and state checks are for local audio.
local myaudio = TrailKit.Bridge.playAudio("audio/battle_theme.mp3", {
id = "myaudio",
loop = true,
volume = 0.7
})
TrailKit.Bridge.fadeOutAudio(myaudio, 1500)
TrailKit.Bridge.fadeInAudio(myaudio, 1500, 0.7)
TrailKit.Bridge.seekAudio(myaudio, 45000)
TrailKit.Bridge.restartAudio(myaudio)
TrailKit.Bridge.pauseAudio(myaudio)
TrailKit.Bridge.resumeAudio(myaudio)
TrailKit.Bridge.stopAudio(myaudio)
TrailKit.Bridge.stopAllAudio()
TrailKit.Bridge.isPlaying(myaudio, function(isPlaying, err)
if not err then
TrailKit.Log.info("Is playing: " .. tostring(isPlaying))
end
end)
TrailKit.Bridge.getPosition(myaudio, function(positionMs, err)
if not err then
TrailKit.Log.info("Position: " .. tostring(positionMs))
end
end)
TrailKit.Bridge.getDuration(myaudio, function(durationMs, err)
if not err then
TrailKit.Log.info("Duration: " .. tostring(durationMs))
end
end)
TrailKit.Bridge.onEnded(myaudio, function(event)
TrailKit.Log.info("Audio ended: " .. event.id)
end)
TrailKit.Bridge.onError(myaudio, function(event)
TrailKit.Log.warn("Audio error: " .. tostring(event.error))
end)
7. Sync Audio For Players
Use the same playAudio(...) function when the host wants other players with TrailBridge open to hear the same audio.
The host opens TrailBridge and shares the session code shown in the app.
Clients join by opening TrailBridge, choosing I am joining, and entering the session code from the host.
Everyone who should hear the audio needs the same mod and audio files installed.
If you want a fallback when TrailBridge is not open, check TrailKit.Bridge.isAvailable() first.
if not TrailKit.Bridge.isAvailable() then
tm.audio.PlayAudioAtPosition("Test_Music_BoomBox", stadiumpos, 0)
else
TrailKit.Bridge.playAudio("audio/battle_theme.mp3", {
id = "myaudio",
mode = "synced",
loop = true,
volume = 0.7
})
end
local myaudio = TrailKit.Bridge.playAudio("audio/battle_theme.mp3", {
id = "myaudio",
mode = "synced",
loop = true,
volume = 0.7
})
TrailKit.Bridge.pauseAudio(myaudio)
TrailKit.Bridge.resumeAudio(myaudio)
TrailKit.Bridge.stopAudio(myaudio)
TrailKit.Bridge.stopAllAudio({ mode = "synced" })
8. Saving and Sharing Data
There are two simple choices.
Use live messages when the data only needs to be seen soon, like chat, alerts, or a live feed.
Important: live messages are deleted after 3 days.
Use saved data when the data should stay around.
Simple rule:
- setData(key, value) saves one value and replaces what was there before
- setData(key, value, { maxRecords = ... }) adds a new entry to saved history instead
- getData(key) reads one saved value
- getData(key, { limit = ... }) reads saved history
Anyone with your API_KEY can read or write that saved data, so do not store secrets.
Live message example:
TrailKit.Bridge.send("test_message", {
text = "Hello from Trailmakers"
})
Saved data example: this replaces the value stored under current_state.
local MY_MOD_API_KEY = "a-unique-string-id"
TrailKit.Bridge.setData(MY_MOD_API_KEY, "current_state", {
season = "Season 1",
winner = "Trailmakers",
score = "3-1"
}, function(response)
if response and response.status == "success" then
TrailKit.Log.info("Saved current state.")
end
end)
TrailKit.Bridge.getData(MY_MOD_API_KEY, "current_state", function(response)
if response and response.status == "success" then
TrailKit.Log.info(json.serialize(response.value))
end
end)
Saved history example: this adds entries under match_history instead of replacing the old ones.
TrailKit.Bridge.setData(MY_MOD_API_KEY, "match_history", {
winner = "Trailmakers",
score = "3-1",
map = "Galaxy Cup Stadium"
}, {
maxRecords = 1000
}, function(response)
if response and response.status == "success" then
TrailKit.Log.info("Saved match result.")
end
end)
TrailKit.Bridge.getData(MY_MOD_API_KEY, "match_history", {
limit = 25
}, function(response)
if response and response.status == "success" then
for _, record in ipairs(response.records) do
TrailKit.Log.info(json.serialize(record.value))
end
end
end)
9. Request a Server-Side Service
Use TrailKit.Bridge.requestService(...) when you want the TrailBridge server to call a service for your mod.
Current available service: discord.webhook.send. Use it to send a message to a Discord webhook.
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)
10. Showing Data on a Website
Use web_api.php when your website needs saved data.
Simple rule:
- send just key when you want one saved value
- add limit when you want saved history under that key
Add limit and offset when you want history. Results are newest first by default.
If you want oldest first, add order: "oldest".
Website example for one saved value:
const res = await fetch("https://ludixi.com/trailmakers/trailbridge/web_api.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "get",
api_key: "a-unique-string-id",
key: "current_state"
})
});
const json = await res.json();
console.log(json.value);
Website example for saved history:
const res = await fetch("https://ludixi.com/trailmakers/trailbridge/web_api.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "get",
api_key: "a-unique-string-id",
key: "match_history",
limit: 50,
offset: 0
})
});
const json = await res.json();
if (json.records) {
for (const record of json.records) {
console.log(record.value);
}
}
Use service_api.php with trailbridge.events.recent if your website needs live messages instead.
Important: website live messages follow the same 3 day limit.
Website example for live messages:
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: "test_message",
limit: 10
}
})
});
const json = await res.json();
console.log(json.events);