Bundle Network API Developer Guide
Bundle Network API Developer Guide
The selene.network module lets bundle scripts send custom payloads between client Lua and server Lua. Use it for gameplay messages that are not already covered by built-in systems, such as UI actions, chat commands, inventory requests, HUD updates, notifications, and small state snapshots.
The module exists on both platforms with different send functions:
- Client scripts can send payloads to the server with
sendToServer. - Server scripts can send payloads to players, groups of players, or the players controlling entities.
- Both sides can register incoming payload handlers with
handlePayload.
Network payloads are serialized as JSON objects. Treat every client-to-server payload as untrusted input and keep the server authoritative.
When to use network payloads
Use selene.network when a bundle needs an explicit client/server message:
- A client UI button asks the server to use an item, cast a spell, or submit chat text.
- The server sends HUD values, inventory slots, quest state, music changes, or notifications to a specific player.
- The server sends entity-related UI updates to whoever controls that entity.
Do not use custom network payloads for data that the engine already synchronizes, such as normal map chunks, entity movement, camera packets, sound playback APIs, or resource bundle updates.
Client to server flow
On the client, require the module from client Lua and call sendToServer(payloadId, payload).
local Network = require("selene.network")
local function requestUseSlot(slotIndex)
Network.sendToServer("myworld:inventory/use_slot", {
slot = slotIndex
})
end
On the server, register a matching handler. Server handlers receive the Player that sent the payload as their first argument and the decoded payload table as their second argument.
local Network = require("selene.network")
Network.handlePayload("myworld:inventory/use_slot", function(player, payload)
local slot = payload.slot
if type(slot) ~= "number" then
return
end
-- Look up the player's server-side inventory here and apply the action.
-- Never trust the client to decide whether the action is allowed.
end)
The player argument is the authenticated connection's player object. Use it to scope the action to the sender instead of accepting a player id from the payload.
Server to client flow
On the client, register a handler for the server payload.
local Network = require("selene.network")
local currentHealth = 0
local maxHealth = 0
Network.handlePayload("myworld:hud/health", function(payload)
if type(payload.current) ~= "number" or type(payload.max) ~= "number" then
return
end
currentHealth = payload.current
maxHealth = payload.max
end)
On the server, send the payload to the target player.
local Network = require("selene.network")
local function sendHealth(player, current, max)
Network.sendToPlayer(player, "myworld:hud/health", {
current = current,
max = max
})
end
The server can also send to multiple players or to the players currently controlling entities.
Network.sendToPlayers(players, "myworld:notice", {
message = "The gate opens."
})
Network.sendToEntity(entity, "myworld:hud/target", {
name = "Training Dummy"
})
Network.sendToEntities(entities, "myworld:effects/pulse", {
intensity = 0.75
})
sendToEntity and sendToEntities do not broadcast to everyone near an entity. They send to the controlling players of those entities.
Payload ids
Payload ids are plain strings. Use the same namespaced style as registry identifiers:
myworld:inventory/use_slot
myworld:hud/health
myworld:chat/send
Use one payload id per message shape. A clear id makes handlers easy to find and reduces accidental conflicts between bundles.
Avoid generic ids such as update, message, or data. They are global within the payload handler registry on that side. Registering another handler for the same payload id replaces the previous one.
Payload shape
Payloads must be Lua tables that can be serialized to JSON. The top-level table must use string keys:
Network.sendToServer("myworld:chat/send", {
text = "Hello",
channel = "local",
tags = { "nearby", "roleplay" },
metadata = {
color = "white"
}
})
Supported values are:
- strings
- numbers
- booleans
- nested tables with string keys
- array-like tables
Do not send Lua functions, userdata, coroutine objects, or tables with mixed key types. They cannot be represented as a JSON payload. Omit absent fields instead of trying to send Lua nil values.
Keep payloads small and specific. Send identifiers, choices, and compact state rather than full registry objects or large map data.
Optional empty payloads
All send functions accept an omitted payload. The engine sends an empty table in that case.
Network.sendToServer("myworld:ready")
Network.sendToPlayer(player, "myworld:reload_hud")
Handlers still receive a payload table, but it will be empty.
Handler lifetime
Register handlers from an entrypoint or module initialization path that runs when your bundle loads.
local Network = require("selene.network")
Network.handlePayload("myworld:ready", function(player, payload)
Network.sendToPlayer(player, "myworld:ready_ack")
end)
If a payload arrives with no registered handler, Selene ignores it. If a Lua handler errors, Selene logs the error with the payload id and the registration trace.
Because only one handler is stored per payload id on each side, centralize registration for a message instead of registering the same id from multiple files.
Request and response pattern
For client UI actions, prefer a request/response shape:
Client:
local Network = require("selene.network")
Network.sendToServer("myworld:settings/set_title", {
title = "Blacksmith"
})
Network.handlePayload("myworld:settings/title_result", function(payload)
if payload.ok then
-- Update local UI state.
else
-- Show payload.reason in your UI if appropriate.
end
end)
Server:
local Network = require("selene.network")
Network.handlePayload("myworld:settings/set_title", function(player, payload)
local title = payload.title
if type(title) ~= "string" or #title > 32 then
Network.sendToPlayer(player, "myworld:settings/title_result", {
ok = false,
reason = "invalid_title"
})
return
end
-- Persist the accepted title server-side.
Network.sendToPlayer(player, "myworld:settings/title_result", {
ok = true,
title = title
})
end)
This keeps the client responsive without giving it authority over server state.
Targeting choices on the server
Choose the narrowest target that matches the gameplay event:
sendToPlayer(player, payloadId, payload)sends to one player.sendToPlayers(players, payloadId, payload)sends the same payload to several players.sendToEntity(entity, payloadId, payload)sends to all players controlling that entity.sendToEntities(entities, payloadId, payload)sends to all players controlling any of those entities.
Use sendToPlayer for private UI state such as inventory, health, quests, and permissions. Use entity targeting when the message belongs to controlled-entity state and the controlling player set is the intended audience.
Validation and security
Client-to-server payloads are requests, not facts. Validate:
- field types
- string length
- numeric range
- whether the sender is allowed to perform the action
- whether referenced entities, items, coordinates, or registry ids are visible and usable by that player
Avoid payloads where the client tells the server who the acting player is. The server handler already receives the authenticated player.
For server-to-client payloads, do not send secrets or server-only state. Clients can inspect their local scripts and network data.
Reference
The Lua reference pages list the exact function signatures: