Skip to content

Shared state and peers

aqua.sync shares small state values with other Aqua users on the current game server. Enable Sync in the menu first. FFI is not required. All calls use copied state without waiting for network I/O on the callback thread.

API Result
aqua.sync.status() {enabled, connected, inMatch, available, steamId}
aqua.sync.peers() Array of known Aqua peers in the current player roster, including the local user
aqua.sync.publish(channel, value) true when accepted locally, or false, reason
aqua.sync.publish(channel, nil) Withdraw this script’s channel
aqua.sync.read(steamId, channel) A fresh copied value plus age in seconds, or nil

available means Sync is enabled, a match is active, and the server endpoint and local identity are known. It does not require connected, so publishing can queue the latest state during a temporary relay outage. connected describes the last relay cycle, not an acknowledgement of an individual value. steamId is always an exact decimal string ("0" when unknown); read also requires this string, never a rounded Lua number.

Each peer has:

Field Meaning
steamId Exact decimal SteamID64 string
index, name, team Current copied player metadata; team 2 T / 3 CT / 0 unavailable
isLocalPlayer This client
hasData Fresh script channels are available; does not imply your particular channel exists

Remote discovery uses the same cached loadouts as cosmetic sync. Older Aqua clients can appear with hasData=false. A peer entry is not proof that Aqua is currently running or that the identity is authenticated; absent remote peers may also be waiting for their first relay poll.

local channel = "my_script.ready.v1" -- same name on every cooperating client
function on_draw()
local status = aqua.sync.status()
if not status.available then return end
-- Identical values are free; changed values are rate-limited.
local ok, reason = aqua.sync.publish(channel, {ready = true})
if not ok then return end
for _, peer in ipairs(aqua.sync.peers()) do
if not peer.isLocalPlayer then
local data, age = aqua.sync.read(peer.steamId, channel)
if type(data) == "table" and data.ready == true then
-- Draw your own indicator using peer.index/name and age.
end
end
end
end

Use namespaced channel names to avoid collisions between scripts. Channels accept 1-48 ASCII letters, digits, ., _, -, :. A local channel belongs to the script that first publishes it; another local script cannot overwrite or withdraw it. Other clients can publish the same channel under their own SteamID.

Values may be booleans, finite numbers, UTF-8 strings or tables. Tables must be objects with string keys or dense arrays with integer keys 1..N; empty tables encode as objects. Nested nil/null, mixed/sparse keys, cycles, functions, threads and userdata are rejected. Numbers have Lua’s usual floating-point precision; send identifiers as strings. Reading creates a copy; editing it never changes published state.

Limits: 1024 encoded JSON bytes per value, nesting depth 8, 128 value nodes, 8 channels per script, 16 channels per client. Each script may make 4 changed publishes per second, with a burst of 4. Repeating the same value consumes no rate allowance. Withdrawal remains possible when rate-limited or unavailable; invalid input returns false, reason. An unknown channel, invalid SteamID string, absent peer or stale value reads as nil.

This is latest state, with the existing roughly 3-second relay cycle and additional delay from polling/network failures. Rapid updates can be coalesced; it is not a reliable message/event stream. Active state refreshes each cycle and expires after 30 seconds without a fresh publication. The returned age uses the publisher’s whole-second timestamp (local reads return 0); clocks must be within 10 seconds for future timestamps to be accepted.

Disabling Sync, leaving the match, changing server/local identity, or stopping, reloading or erroring the publishing script clears its local state. Republish when available again. Withdrawals reach peers on a later poll; during an outage they can retain the old value until expiry. Departed players are removed from the local roster. State is scoped to the full server endpoint, including port, rather than a globally unique match ID; a quick rematch at the same endpoint may briefly expose the prior state until its deadline.

The current relay does not authenticate SteamIDs or encrypt these payloads. Treat identities and received values as untrusted. Do not send secrets, make authorization decisions from this data, or execute received code/commands.