@axpecter/lync 1.5.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +499 -357
- package/package.json +2 -2
- package/src/Types.luau +69 -27
- package/src/api/Group.luau +101 -106
- package/src/api/Packet.luau +191 -157
- package/src/api/Query.luau +223 -282
- package/src/api/Scope.luau +46 -57
- package/src/api/Signal.luau +104 -137
- package/src/codec/Base.luau +69 -20
- package/src/codec/composite/Array.luau +189 -201
- package/src/codec/composite/Map.luau +97 -341
- package/src/codec/composite/Optional.luau +27 -23
- package/src/codec/composite/Shared.luau +96 -65
- package/src/codec/composite/Struct.luau +201 -339
- package/src/codec/composite/Tagged.luau +64 -178
- package/src/codec/composite/Tuple.luau +81 -95
- package/src/codec/datatype/Buffer.luau +49 -21
- package/src/codec/datatype/CFrame.luau +207 -30
- package/src/codec/datatype/Color.luau +21 -11
- package/src/codec/datatype/Instance.luau +29 -19
- package/src/codec/datatype/IntVector.luau +33 -21
- package/src/codec/datatype/NumberRange.luau +19 -10
- package/src/codec/datatype/Ray.luau +26 -22
- package/src/codec/datatype/Rect.luau +20 -16
- package/src/codec/datatype/Region.luau +51 -45
- package/src/codec/datatype/Sequence.luau +64 -56
- package/src/codec/datatype/String.luau +89 -56
- package/src/codec/datatype/UDim.luau +40 -22
- package/src/codec/datatype/Vector.luau +181 -17
- package/src/codec/meta/Auto.luau +295 -253
- package/src/codec/meta/Bitfield.luau +104 -148
- package/src/codec/meta/Custom.luau +8 -4
- package/src/codec/meta/Enum.luau +29 -57
- package/src/codec/meta/Float.luau +64 -0
- package/src/codec/meta/Nothing.luau +9 -5
- package/src/codec/meta/Unknown.luau +44 -35
- package/src/codec/primitive/Bool.luau +16 -26
- package/src/codec/primitive/Float16.luau +58 -64
- package/src/codec/primitive/Int.luau +68 -0
- package/src/codec/primitive/Number.luau +21 -43
- package/src/codec/primitive/Varint.luau +67 -46
- package/src/index.d.ts +249 -306
- package/src/init.luau +332 -173
- package/src/internal/Baseline.luau +29 -24
- package/src/internal/Channel.luau +346 -161
- package/src/internal/Middleware.luau +60 -85
- package/src/internal/Pool.luau +19 -30
- package/src/internal/Registry.luau +61 -101
- package/src/transport/Bridge.luau +64 -43
- package/src/transport/Client.luau +60 -117
- package/src/transport/Gate.luau +282 -133
- package/src/transport/Reader.luau +159 -100
- package/src/transport/Server.luau +147 -292
- package/src/api/Namespace.luau +0 -251
- package/src/codec/meta/Quantized.luau +0 -174
package/README.md
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
<h1 align="center">Lync</h1>
|
|
2
|
-
<p align="center">Buffer networking for Roblox
|
|
2
|
+
<p align="center">Buffer networking for Roblox.</p>
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://github.com/Axp3cter/Lync/releases/latest">Releases</a> ·
|
|
5
|
+
<a href="#install">Install</a> ·
|
|
5
6
|
<a href="#example">Example</a> ·
|
|
6
|
-
<a href="#
|
|
7
|
-
<a href="#
|
|
7
|
+
<a href="#codecs">Codecs</a> ·
|
|
8
|
+
<a href="#wire-protocol">Wire Protocol</a> ·
|
|
9
|
+
<a href="#benchmarks">Benchmarks</a>
|
|
8
10
|
</p>
|
|
9
11
|
|
|
12
|
+
Lync serializes structured data into flat buffers, batches all sends into a single `RemoteEvent:FireClient` per player per frame, and applies XOR framing so Roblox's internal deflate compressor can eliminate redundancy across frames. On the server, every incoming payload is schema-validated and rate-limited before any listener fires.
|
|
13
|
+
|
|
14
|
+
All codecs are defined at runtime. No code generation, no build step, no external CLI. Packets, queries, groups, and middleware are configured in shared modules and resolved at `Lync.start()`.
|
|
15
|
+
|
|
10
16
|
## Install
|
|
11
17
|
|
|
12
|
-
**Wally
|
|
18
|
+
**Wally**
|
|
13
19
|
|
|
14
20
|
```toml
|
|
15
21
|
[dependencies]
|
|
16
|
-
Lync = "axp3cter/lync@1.
|
|
22
|
+
Lync = "axp3cter/lync@2.1.0"
|
|
17
23
|
```
|
|
18
24
|
|
|
19
25
|
**npm (roblox-ts)**
|
|
@@ -26,52 +32,46 @@ npm install @axpecter/lync
|
|
|
26
32
|
import Lync from "@axpecter/lync";
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
Or grab the `.rbxm` from [
|
|
35
|
+
Or grab the `.rbxm` from [Releases](https://github.com/Axp3cter/Lync/releases/latest) and drop it into `ReplicatedStorage`.
|
|
30
36
|
|
|
31
37
|
> [!IMPORTANT]
|
|
32
|
-
>
|
|
38
|
+
> All packets, queries, and groups must be defined before calling `Lync.start()`. The registry assigns sequential IDs at define time. Defining packets after `start()` will cause ID mismatches between server and client.
|
|
33
39
|
|
|
34
40
|
## Example
|
|
35
41
|
|
|
36
|
-
**Shared**
|
|
42
|
+
**Shared** (`ReplicatedStorage.Net`)
|
|
37
43
|
|
|
38
44
|
```luau
|
|
39
45
|
local Lync = require(game.ReplicatedStorage.Lync)
|
|
40
46
|
|
|
41
47
|
local Net = {}
|
|
42
48
|
|
|
43
|
-
Net.State = Lync.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
headshot = Lync.bool,
|
|
58
|
-
}),
|
|
59
|
-
rateLimit = { maxPerSecond = 30, burstAllowance = 5 },
|
|
49
|
+
Net.State = Lync.packet("State", Lync.deltaStruct({
|
|
50
|
+
position = Lync.vec3,
|
|
51
|
+
health = Lync.float(0, 100, 0.5),
|
|
52
|
+
shield = Lync.float(0, 100, 0.5),
|
|
53
|
+
status = Lync.enum("idle", "moving", "attacking", "dead"),
|
|
54
|
+
alive = Lync.bool,
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
Net.Hit = Lync.packet("Hit", Lync.struct({
|
|
58
|
+
targetId = Lync.int(0, 65535),
|
|
59
|
+
damage = Lync.float(0, 200, 0.1),
|
|
60
|
+
headshot = Lync.bool,
|
|
61
|
+
}), {
|
|
62
|
+
rateLimit = { maxPerSecond = 30, burst = 5 },
|
|
60
63
|
validate = function(data, player)
|
|
61
64
|
if data.damage > 200 then return false, "damage" end
|
|
62
65
|
return true
|
|
63
66
|
end,
|
|
64
67
|
})
|
|
65
68
|
|
|
66
|
-
Net.Chat = Lync.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
Net.Chat = Lync.packet("Chat", Lync.struct({
|
|
70
|
+
msg = Lync.string(200),
|
|
71
|
+
channel = Lync.int(0, 255),
|
|
72
|
+
}))
|
|
69
73
|
|
|
70
|
-
Net.Ping = Lync.
|
|
71
|
-
request = Lync.nothing,
|
|
72
|
-
response = Lync.f64,
|
|
73
|
-
timeout = 3,
|
|
74
|
-
})
|
|
74
|
+
Net.Ping = Lync.query("Ping", Lync.nothing, Lync.f64, { timeout = 3 })
|
|
75
75
|
|
|
76
76
|
return table.freeze(Net)
|
|
77
77
|
```
|
|
@@ -83,22 +83,15 @@ local Lync = require(game.ReplicatedStorage.Lync)
|
|
|
83
83
|
local Net = require(game.ReplicatedStorage.Net)
|
|
84
84
|
local Players = game:GetService("Players")
|
|
85
85
|
|
|
86
|
-
local alive = Lync.
|
|
86
|
+
local alive = Lync.group("alive")
|
|
87
87
|
|
|
88
|
-
Lync.
|
|
89
|
-
print("[out]", name)
|
|
90
|
-
return data
|
|
91
|
-
end)
|
|
92
|
-
|
|
93
|
-
Lync.onDrop(function(player, reason, name, data)
|
|
88
|
+
Lync.onDrop(function(player, reason, name)
|
|
94
89
|
warn(player.Name, "dropped", name, reason)
|
|
95
90
|
end)
|
|
96
91
|
|
|
97
92
|
Lync.start()
|
|
98
93
|
|
|
99
|
-
Players.PlayerAdded:Connect(function(player)
|
|
100
|
-
alive:add(player)
|
|
101
|
-
end)
|
|
94
|
+
Players.PlayerAdded:Connect(function(player) alive:add(player) end)
|
|
102
95
|
|
|
103
96
|
game:GetService("RunService").Heartbeat:Connect(function()
|
|
104
97
|
Net.State:send({
|
|
@@ -110,24 +103,14 @@ game:GetService("RunService").Heartbeat:Connect(function()
|
|
|
110
103
|
}, alive)
|
|
111
104
|
end)
|
|
112
105
|
|
|
113
|
-
Net.Hit:
|
|
106
|
+
Net.Hit:on(function(data, player)
|
|
114
107
|
local target = Players:GetPlayerByUserId(data.targetId)
|
|
115
108
|
if not target then return end
|
|
116
|
-
|
|
117
109
|
alive:remove(target)
|
|
118
110
|
Net.Chat:send({ msg = player.Name .. " eliminated " .. target.Name, channel = 0 }, Lync.all)
|
|
119
|
-
Net.State:send({
|
|
120
|
-
position = Vector3.zero,
|
|
121
|
-
health = 0,
|
|
122
|
-
shield = 0,
|
|
123
|
-
status = "dead",
|
|
124
|
-
alive = false,
|
|
125
|
-
}, Lync.except(target))
|
|
126
111
|
end)
|
|
127
112
|
|
|
128
|
-
Net.Ping:
|
|
129
|
-
return os.clock()
|
|
130
|
-
end)
|
|
113
|
+
Net.Ping:handle(function(_, player) return os.clock() end)
|
|
131
114
|
```
|
|
132
115
|
|
|
133
116
|
**Client**
|
|
@@ -140,381 +123,540 @@ Lync.start()
|
|
|
140
123
|
|
|
141
124
|
local scope = Lync.scope()
|
|
142
125
|
|
|
143
|
-
scope:
|
|
126
|
+
scope:on(Net.State, function(state)
|
|
144
127
|
local character = game.Players.LocalPlayer.Character
|
|
145
128
|
if not character then return end
|
|
146
129
|
character:PivotTo(CFrame.new(state.position))
|
|
147
130
|
end)
|
|
148
131
|
|
|
149
|
-
scope:
|
|
150
|
-
print("[chat]", data.msg)
|
|
151
|
-
end)
|
|
132
|
+
scope:on(Net.Chat, function(data) print("[chat]", data.msg) end)
|
|
152
133
|
|
|
153
134
|
Net.Hit:send({ targetId = 123, damage = 45.5, headshot = true })
|
|
154
135
|
|
|
155
136
|
local serverTime = Net.Ping:request(nil)
|
|
156
|
-
if serverTime then
|
|
157
|
-
print("server clock:", serverTime)
|
|
158
|
-
end
|
|
137
|
+
if serverTime then print("server clock:", serverTime) end
|
|
159
138
|
```
|
|
160
139
|
|
|
161
140
|
## Lifecycle
|
|
162
141
|
|
|
163
|
-
| |
|
|
164
|
-
|
|
165
|
-
| `Lync.
|
|
166
|
-
| `Lync.
|
|
142
|
+
| Function | Behavior |
|
|
143
|
+
|:---------|:---------|
|
|
144
|
+
| `Lync.configure(options)` | Sets limits and enables stats. Must be called before `start()`. See [Configuration](#configuration). |
|
|
145
|
+
| `Lync.start()` | Server creates remotes under `ReplicatedStorage.LyncRemotes`. Client waits for them. Connects the Heartbeat flush loop. Errors if called twice. |
|
|
146
|
+
| `Lync.started` | Read-only boolean. `true` after `start()` returns. |
|
|
147
|
+
| `Lync.flush()` | Forces an immediate buffer flush. Resets the accumulator to prevent double-sending on the next Heartbeat. Errors if not started. |
|
|
148
|
+
| `Lync.flushRate(hz)` | 1–60. Default 60. At 60, flushes every Heartbeat directly. Below 60, uses an elapsed-time accumulator with drift correction. Callable at runtime. |
|
|
167
149
|
|
|
168
150
|
## Packets
|
|
169
151
|
|
|
170
|
-
`Lync.
|
|
152
|
+
`Lync.packet(name, codec, options?)` returns a Packet handle. The second argument is any codec. Options go in the optional third argument.
|
|
153
|
+
|
|
154
|
+
### Packet Options
|
|
171
155
|
|
|
172
|
-
|
|
|
173
|
-
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
156
|
+
| Field | Type | Default | Behavior |
|
|
157
|
+
|:------|:-----|:--------|:---------|
|
|
158
|
+
| `unreliable` | `boolean` | `false` | Routes through `UnreliableRemoteEvent`. Incompatible with delta codecs (errors at define time). |
|
|
159
|
+
| `rateLimit` | `RateLimitConfig` | none | Server-side rate limiting on incoming fires. See [Rate Limiting](#rate-limiting). |
|
|
160
|
+
| `validate` | `(data, player) → (bool, string?)` | none | Server-side callback after schema validation. Return `false, "reason"` to drop. Fires `onDrop`. |
|
|
161
|
+
| `maxPayloadBytes` | `number` | none | Maximum bytes a single payload can consume. |
|
|
162
|
+
| `timestamp` | `"frame"`, `"offset"`, or `"full"` | none | Prepends a timestamp to each item. `"frame"` = u8 wrapping counter (1B). `"offset"` = u16 milliseconds into the current second (2B). `"full"` = f64 `os.clock()` (8B). Listeners receive it as a third argument after `sender`. |
|
|
179
163
|
|
|
180
|
-
|
|
164
|
+
### Packet Methods
|
|
165
|
+
|
|
166
|
+
**Sending (server):**
|
|
181
167
|
|
|
182
168
|
```luau
|
|
183
|
-
packet:send(data, player) --
|
|
184
|
-
packet:send(data, Lync.all) --
|
|
185
|
-
packet:send(data, Lync.except(
|
|
186
|
-
packet:send(data,
|
|
187
|
-
packet:send(data,
|
|
188
|
-
packet:send(data, group) -- group object
|
|
169
|
+
packet:send(data, player) -- single player
|
|
170
|
+
packet:send(data, Lync.all) -- all connected players
|
|
171
|
+
packet:send(data, Lync.except(p1, p2)) -- all except specified
|
|
172
|
+
packet:send(data, { p1, p2, p3 }) -- array of players
|
|
173
|
+
packet:send(data, group) -- group members
|
|
189
174
|
```
|
|
190
175
|
|
|
191
|
-
**
|
|
176
|
+
**Sending (client):**
|
|
192
177
|
|
|
193
178
|
```luau
|
|
194
|
-
packet:send(data) --
|
|
179
|
+
packet:send(data) -- to server
|
|
195
180
|
```
|
|
196
181
|
|
|
197
|
-
**
|
|
182
|
+
**Receiving (both):**
|
|
198
183
|
|
|
199
|
-
| Method |
|
|
200
|
-
|
|
201
|
-
| `packet:
|
|
202
|
-
| `packet:once(fn
|
|
203
|
-
| `packet:wait()` | Returns `(data, sender)`. |
|
|
204
|
-
| `packet:
|
|
184
|
+
| Method | Behavior |
|
|
185
|
+
|:-------|:---------|
|
|
186
|
+
| `packet:on(fn)` | Connects a listener. `fn(data, sender, timestamp?)`. Server `sender` is `Player`. Client `sender` is `nil`. Returns a [Connection](#connection). |
|
|
187
|
+
| `packet:once(fn)` | Same as `on` but auto-disconnects after one fire. |
|
|
188
|
+
| `packet:wait()` | Yields until the next fire. Returns `(data, sender, timestamp?)`. |
|
|
189
|
+
| `packet:name()` | Returns the registration name string. |
|
|
190
|
+
| `packet:stats()` | Returns `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Populated only when stats are enabled. |
|
|
205
191
|
|
|
206
192
|
## Queries
|
|
207
193
|
|
|
208
|
-
`Lync.
|
|
194
|
+
`Lync.query(name, requestCodec, responseCodec, options?)` returns a Query handle. Built on RemoteEvents with varint correlation IDs. Returns `nil` on timeout or handler error.
|
|
209
195
|
|
|
210
|
-
|
|
211
|
-
|:-------|:-----|:--------:|:-------------|
|
|
212
|
-
| `request` | Codec | Yes | How to serialize the request. |
|
|
213
|
-
| `response` | Codec | Yes | How to serialize the response. |
|
|
214
|
-
| `timeout` | number | No | Seconds before giving up. Default `5`. |
|
|
215
|
-
| `rateLimit` | `{ maxPerSecond, burstAllowance? }` | No | Server-side token bucket on incoming requests. |
|
|
216
|
-
| `validate` | `(data, player) → (bool, string?)` | No | Server-side validation on incoming requests. |
|
|
196
|
+
### Query Options
|
|
217
197
|
|
|
218
|
-
|
|
|
219
|
-
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `query:requestAll(data)` | Server | Send request to all players. Returns `{ [Player]: response? }`. |
|
|
224
|
-
| `query:requestList(players, data)` | Server | Send request to a list of players. Returns `{ [Player]: response? }`. |
|
|
225
|
-
| `query:requestGroup(group, data)` | Server | Send request to all players in a group. Returns `{ [Player]: response? }`. |
|
|
198
|
+
| Field | Type | Default | Behavior |
|
|
199
|
+
|:------|:-----|:--------|:---------|
|
|
200
|
+
| `timeout` | `number` | 5 | Seconds before the request yields `nil`. |
|
|
201
|
+
| `rateLimit` | `RateLimitConfig` | `{ maxPerSecond = 30 }` | Server-side rate limiting on incoming requests. |
|
|
202
|
+
| `validate` | `(data, player) → (bool, string?)` | none | Server-side validation on incoming requests. |
|
|
226
203
|
|
|
227
|
-
|
|
204
|
+
### Query Methods
|
|
228
205
|
|
|
229
|
-
|
|
206
|
+
| Method | Context | Behavior |
|
|
207
|
+
|:-------|:--------|:---------|
|
|
208
|
+
| `query:handle(fn)` | Both | Registers a handler. Server: `fn(request, player) → response`. Client: `fn(request) → response`. Returns a Connection that clears the handler on disconnect. |
|
|
209
|
+
| `query:request(data)` | Client | Sends request to server, yields until response or timeout. Returns the response or `nil`. |
|
|
210
|
+
| `query:request(data, player)` | Server | Sends request to one client, yields until response or timeout. |
|
|
211
|
+
| `query:request(data, target)` | Server | Sends request to multiple targets. Returns `{ [Player]: response? }`. Accepts `Lync.all`, arrays, and groups. |
|
|
212
|
+
| `query:name()` | Both | Returns the registration name. |
|
|
213
|
+
| `query:stats()` | Both | Returns combined stats for the request and response channels. |
|
|
230
214
|
|
|
231
|
-
|
|
215
|
+
Each query consumes two packet IDs internally (one for requests, one for responses).
|
|
232
216
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
rateLimit = { maxPerSecond = 30, burstAllowance = 5 },
|
|
239
|
-
validate = function(data, player)
|
|
240
|
-
if data.damage > 200 then return false, "damage" end
|
|
241
|
-
return true
|
|
242
|
-
end,
|
|
243
|
-
},
|
|
244
|
-
Death = { value = Lync.u16 },
|
|
245
|
-
},
|
|
246
|
-
queries = {
|
|
247
|
-
Stats = {
|
|
248
|
-
request = Lync.nothing,
|
|
249
|
-
response = Lync.struct({ kills = Lync.u16, deaths = Lync.u16 }),
|
|
250
|
-
timeout = 3,
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
})
|
|
217
|
+
## Groups
|
|
218
|
+
|
|
219
|
+
`Lync.group(name)` returns a Group. Members are removed automatically on `PlayerRemoving`. Names must be unique (duplicate errors). Destroyed groups free their name for reuse.
|
|
220
|
+
|
|
221
|
+
Groups implement `__iter`, so `for player in group do` works directly.
|
|
254
222
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
223
|
+
| Method | Returns | Behavior |
|
|
224
|
+
|:-------|:--------|:---------|
|
|
225
|
+
| `group:add(player)` | `boolean` | `true` if added, `false` if already a member. |
|
|
226
|
+
| `group:remove(player)` | `boolean` | `true` if removed, `false` if not a member. |
|
|
227
|
+
| `group:has(player)` | `boolean` | Membership check. |
|
|
228
|
+
| `group:count()` | `number` | Current member count. |
|
|
229
|
+
| `group:destroy()` | — | Clears all members and frees the name. Safe to call multiple times. |
|
|
230
|
+
|
|
231
|
+
## Scope
|
|
259
232
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
233
|
+
`Lync.scope()` batches connections for lifecycle-aligned cleanup.
|
|
234
|
+
|
|
235
|
+
```luau
|
|
236
|
+
local scope = Lync.scope()
|
|
237
|
+
scope:on(packetA, fnA)
|
|
238
|
+
scope:on(packetB, fnB)
|
|
239
|
+
scope:add(someRBXScriptConnection)
|
|
240
|
+
scope:destroy()
|
|
263
241
|
```
|
|
264
242
|
|
|
265
|
-
|
|
|
266
|
-
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
| Method | What it does |
|
|
273
|
-
|:-------|:------------|
|
|
274
|
-
| `ns:listenAll(fn(name, data, sender))` | Listens to every packet in the namespace. `name` is the short name without prefix. Returns a Connection. |
|
|
275
|
-
| `ns:onSend(fn(data, name, player) → data?)` | Send middleware that only runs for this namespace. Returns a remover. |
|
|
276
|
-
| `ns:onReceive(fn(data, name, player) → data?)` | Receive middleware that only runs for this namespace. Returns a remover. |
|
|
277
|
-
| `ns:disconnectAll()` | Kills all listeners made through `listenAll`. |
|
|
278
|
-
| `ns:destroy()` | Kills listeners and removes scoped middleware. Full cleanup. |
|
|
279
|
-
| `ns:packetNames()` | Sorted list of packet short names. |
|
|
280
|
-
| `ns:queryNames()` | Sorted list of query short names. |
|
|
281
|
-
| `ns.packets` | Frozen table mapping short name → Packet object. |
|
|
282
|
-
| `ns.queries` | Frozen table mapping short name → Query object. |
|
|
243
|
+
| Method | Behavior |
|
|
244
|
+
|:-------|:---------|
|
|
245
|
+
| `scope:on(source, fn)` | Calls `source:on(fn)` and tracks the returned connection. |
|
|
246
|
+
| `scope:once(source, fn)` | Calls `source:once(fn)` and tracks the returned connection. |
|
|
247
|
+
| `scope:add(connection)` | Accepts both Lync connections and `RBXScriptConnection`. |
|
|
248
|
+
| `scope:destroy()` | Disconnects all tracked connections. Safe to call multiple times. |
|
|
283
249
|
|
|
284
250
|
## Connection
|
|
285
251
|
|
|
286
|
-
Returned by `packet:
|
|
252
|
+
Returned by `packet:on()`, `packet:once()`, `query:handle()`, `scope:on()`, and middleware functions.
|
|
287
253
|
|
|
288
|
-
| |
|
|
289
|
-
|
|
290
|
-
| `connection.connected` | `boolean` |
|
|
291
|
-
| `connection:disconnect()` | Stops the listener. |
|
|
254
|
+
| Field/Method | Behavior |
|
|
255
|
+
|:-------------|:---------|
|
|
256
|
+
| `connection.connected` | `boolean`. `true` until disconnected. |
|
|
257
|
+
| `connection:disconnect()` | Stops the listener. O(1) via swap-remove. Safe to call multiple times. Safe to call during a fire (snapshot iteration prevents skipped listeners). |
|
|
292
258
|
|
|
293
|
-
##
|
|
259
|
+
## Middleware
|
|
294
260
|
|
|
295
|
-
|
|
261
|
+
Global intercept chains on all packets. Handlers run in registration order. Return a transformed value to pass it downstream. Return `nil` to pass through unchanged. Return `Lync.DROP` from `onSend` to silently drop the packet.
|
|
296
262
|
|
|
297
|
-
|
|
298
|
-
local scope = Lync.scope()
|
|
263
|
+
All three functions return a [Connection](#connection).
|
|
299
264
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
265
|
+
| Function | Behavior |
|
|
266
|
+
|:---------|:---------|
|
|
267
|
+
| `Lync.onSend(fn)` | `fn(data, name, player?) → data?`. Runs before serialization. |
|
|
268
|
+
| `Lync.onReceive(fn)` | `fn(data, name, player?) → data?`. Runs after deserialization and validation. |
|
|
269
|
+
| `Lync.onDrop(fn)` | `fn(player, reason, name, data?)`. Fires when a packet is rejected. Reason is `"rate"`, `"validation"`, or the string returned by the `validate` callback. |
|
|
270
|
+
| `Lync.DROP` | Frozen sentinel. Return from `onSend` to silently drop the packet. |
|
|
303
271
|
|
|
304
|
-
|
|
305
|
-
```
|
|
272
|
+
## Targets
|
|
306
273
|
|
|
307
|
-
|
|
308
|
-
|:-------|:------------|
|
|
309
|
-
| `scope:listen(source, fn)` | Calls `source:listen(fn)` and tracks the connection. |
|
|
310
|
-
| `scope:once(source, fn)` | Calls `source:once(fn)` and tracks the connection. |
|
|
311
|
-
| `scope:listenAll(namespace, fn)` | Calls `namespace:listenAll(fn)` and tracks the connection. |
|
|
312
|
-
| `scope:add(connection)` | Also accepts RBXScriptConnection. |
|
|
313
|
-
| `scope:destroy()` | Safe to call multiple times. |
|
|
274
|
+
Server-side second argument to `packet:send()` and `query:request()`.
|
|
314
275
|
|
|
315
|
-
|
|
276
|
+
| Target | Behavior |
|
|
277
|
+
|:-------|:---------|
|
|
278
|
+
| `player` | Single `Player` instance. |
|
|
279
|
+
| `Lync.all` | All connected players via `Players:GetPlayers()`. |
|
|
280
|
+
| `Lync.except(...)` | All players except specified. Accepts any mix of `Player` and Group arguments. |
|
|
281
|
+
| `{ p1, p2, ... }` | Lua array of players. Non-player entries are silently skipped. |
|
|
282
|
+
| `group` | All current members of a [Group](#groups). |
|
|
316
283
|
|
|
317
|
-
|
|
284
|
+
## Codecs
|
|
318
285
|
|
|
319
|
-
|
|
320
|
-
local vips = Lync.createGroup("vips")
|
|
286
|
+
### Numbers
|
|
321
287
|
|
|
322
|
-
|
|
323
|
-
vips:remove(player)
|
|
324
|
-
vips:has(player)
|
|
288
|
+
`Lync.int(min, max)` selects the smallest wire type that fits the range:
|
|
325
289
|
|
|
326
|
-
|
|
327
|
-
|
|
290
|
+
| Range | Wire | Bytes |
|
|
291
|
+
|:------|:-----|------:|
|
|
292
|
+
| `[0, 255]` | u8 | 1 |
|
|
293
|
+
| `[0, 65535]` | u16 | 2 |
|
|
294
|
+
| `[0, 4294967295]` | u32 | 4 |
|
|
295
|
+
| `[-128, 127]` | i8 | 1 |
|
|
296
|
+
| `[-32768, 32767]` | i16 | 2 |
|
|
297
|
+
| `[-2147483648, 2147483647]` | i32 | 4 |
|
|
328
298
|
|
|
329
|
-
|
|
330
|
-
|:-------|:--------|:-------------|
|
|
331
|
-
| `group:add(player)` | `boolean` | `true` if added, `false` if already in. |
|
|
332
|
-
| `group:remove(player)` | `boolean` | `true` if removed, `false` if wasnt in there. |
|
|
333
|
-
| `group:has(player)` | `boolean` | Whether the player is in the group. |
|
|
334
|
-
| `group:count()` | `number` | Number of members. |
|
|
335
|
-
| `group:getSet()` | `{ [Player]: true }` | Snapshot of the internal set. |
|
|
336
|
-
| `group:forEach(fn)` | `()` | Calls `fn(player)` for each member. |
|
|
337
|
-
| `group:destroy()` | `()` | Removes the group and all memberships. |
|
|
299
|
+
Signed integers use unsigned buffer writes (`writeu8`/`writeu16`/`writeu32`) with two's complement conversion because `writei8`/`writei16`/`writei32` are not FASTCALL-optimized in Luau.
|
|
338
300
|
|
|
339
|
-
|
|
301
|
+
| Codec | Bytes | Behavior |
|
|
302
|
+
|:------|------:|:---------|
|
|
303
|
+
| `Lync.f16` | 2 | Half-precision IEEE 754. ~3 decimal digits. ±65504 normal range. Overflow clamps to ±inf. NaN preserved. |
|
|
304
|
+
| `Lync.f32` | 4 | IEEE 754 single-precision. |
|
|
305
|
+
| `Lync.f64` | 8 | IEEE 754 double-precision. |
|
|
306
|
+
| `Lync.bool` | 1 | `true`/`false`. Inside structs, bools are separated and bitpacked (8 per byte). Inside arrays, bools are bitpacked. Standalone uses 1 byte. |
|
|
340
307
|
|
|
341
|
-
|
|
308
|
+
`Lync.float(min, max, precision)` quantizes a float range to an integer range. Wire type is selected by `ceil((max - min) / precision)`: u8 if ≤ 255, u16 if ≤ 65535, u32 otherwise. Values outside `[min, max]` are clamped.
|
|
342
309
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
310
|
+
### Strings and Buffers
|
|
311
|
+
|
|
312
|
+
| Codec | Wire format | Behavior |
|
|
313
|
+
|:------|:------------|:---------|
|
|
314
|
+
| `Lync.string` | varint length + raw bytes | Lengths 0–191 use a 1-byte prefix (dense prefix-varint). 192+ use multi-byte. Binary-safe. |
|
|
315
|
+
| `Lync.string(maxLength)` | same | Callable via `__call`. Same write path. Read rejects if decoded length exceeds `maxLength`. |
|
|
316
|
+
| `Lync.buff` | varint length + raw bytes | Same wire format as string. Read returns an isolated buffer copy. |
|
|
317
|
+
|
|
318
|
+
### Roblox Types
|
|
319
|
+
|
|
320
|
+
All fixed-size types expose `_directWrite` and `_directRead` for struct fast-path optimization, except `Lync.inst` (requires the channel's ref array).
|
|
321
|
+
|
|
322
|
+
| Codec | Bytes | Wire layout |
|
|
323
|
+
|:------|------:|:------------|
|
|
324
|
+
| `Lync.vec2` | 8 | 2× f32 |
|
|
325
|
+
| `Lync.vec3` | 12 | 3× f32 |
|
|
326
|
+
| `Lync.cframe` | 24 | 3× f32 position + 3× f32 axis-angle rotation |
|
|
327
|
+
| `Lync.color3` | 3 | 3× u8 RGB, clamped to [0, 1] then scaled to [0, 255] |
|
|
328
|
+
| `Lync.inst` | 2 | u16 index into sidecar `{ Instance }` array |
|
|
329
|
+
| `Lync.udim` | 8 | f32 Scale + i32 Offset |
|
|
330
|
+
| `Lync.udim2` | 16 | 2× UDim |
|
|
331
|
+
| `Lync.numberRange` | 8 | f32 Min + f32 Max |
|
|
332
|
+
| `Lync.rect` | 16 | 4× f32 |
|
|
333
|
+
| `Lync.ray` | 24 | 6× f32 (Origin + Direction) |
|
|
334
|
+
| `Lync.vec2int16` | 4 | 2× i16 |
|
|
335
|
+
| `Lync.vec3int16` | 6 | 3× i16 |
|
|
336
|
+
| `Lync.region3` | 24 | 6× f32 (Min + Max) |
|
|
337
|
+
| `Lync.region3int16` | 12 | 6× i16 (Min + Max) |
|
|
338
|
+
| `Lync.numberSequence` | varint + N×12 | f32 time + f32 value + f32 envelope per keypoint |
|
|
339
|
+
| `Lync.colorSequence` | varint + N×7 | f32 time + u8 R + u8 G + u8 B per keypoint |
|
|
340
|
+
|
|
341
|
+
### Quantized Variants
|
|
342
|
+
|
|
343
|
+
These codecs are callable. The bare name gives the lossless version; calling with arguments gives the quantized version.
|
|
352
344
|
|
|
353
|
-
|
|
|
354
|
-
|
|
355
|
-
| `Lync.
|
|
356
|
-
| `Lync.
|
|
357
|
-
| `Lync.
|
|
358
|
-
| `Lync.DROP` | Frozen sentinel. Return from middleware to drop the packet. |
|
|
359
|
-
|
|
360
|
-
Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
|
|
361
|
-
|
|
362
|
-
## Target Descriptors
|
|
363
|
-
|
|
364
|
-
Used as the second argument to `packet:send()` on the server.
|
|
365
|
-
|
|
366
|
-
| Target | What it does |
|
|
367
|
-
|:-------|:------------|
|
|
368
|
-
| `player` | Send to one player. |
|
|
369
|
-
| `Lync.all` | Send to all connected players. |
|
|
370
|
-
| `Lync.except(player, ...)` | Send to everyone except the specified players. |
|
|
371
|
-
| `{ p1, p2, ... }` | Send to a list of players. |
|
|
372
|
-
| `group` | Send to all members of a Group object. |
|
|
373
|
-
|
|
374
|
-
## Types
|
|
375
|
-
|
|
376
|
-
### Primitives
|
|
377
|
-
|
|
378
|
-
| Type | Bytes | Range |
|
|
379
|
-
|:-----|------:|:------|
|
|
380
|
-
| `Lync.u8` | 1 | 0 to 255 |
|
|
381
|
-
| `Lync.u16` | 2 | 0 to 65,535 |
|
|
382
|
-
| `Lync.u32` | 4 | 0 to 4,294,967,295 |
|
|
383
|
-
| `Lync.i8` | 1 | -128 to 127 |
|
|
384
|
-
| `Lync.i16` | 2 | -32,768 to 32,767 |
|
|
385
|
-
| `Lync.i32` | 4 | -2,147,483,648 to 2,147,483,647 |
|
|
386
|
-
| `Lync.f16` | 2 | ±65,504, roughly 3 digits of precision |
|
|
387
|
-
| `Lync.f32` | 4 | IEEE 754 single |
|
|
388
|
-
| `Lync.f64` | 8 | IEEE 754 double |
|
|
389
|
-
| `Lync.bool` | 1 | true/false. Gets packed into bitfields when inside structs. |
|
|
390
|
-
|
|
391
|
-
### Datatypes
|
|
392
|
-
|
|
393
|
-
| Type | Bytes | What it is |
|
|
394
|
-
|:-----|------:|:-----------|
|
|
395
|
-
| `Lync.string` | varint + N | Varint length prefix then raw bytes. |
|
|
396
|
-
| `Lync.vec2` | 8 | 2x f32. |
|
|
397
|
-
| `Lync.vec3` | 12 | 3x f32. |
|
|
398
|
-
| `Lync.cframe` | 24 | Position as 3x f32, rotation as axis-angle 3x f32. |
|
|
399
|
-
| `Lync.color3` | 3 | RGB 0-255 per channel, clamped. |
|
|
400
|
-
| `Lync.inst` | 2 | Instance ref through sidecar array. Requires refs on read, throws without them. |
|
|
401
|
-
| `Lync.buff` | varint + N | Varint length prefix then raw bytes. |
|
|
402
|
-
| `Lync.udim` | 8 | Scale f32 + Offset i32. |
|
|
403
|
-
| `Lync.udim2` | 16 | 2x UDim (X then Y). |
|
|
404
|
-
| `Lync.numberRange` | 8 | Min f32 + Max f32. |
|
|
405
|
-
| `Lync.rect` | 16 | Min.X f32 + Min.Y f32 + Max.X f32 + Max.Y f32. |
|
|
406
|
-
| `Lync.vec2int16` | 4 | 2x i16. |
|
|
407
|
-
| `Lync.vec3int16` | 6 | 3x i16. |
|
|
408
|
-
| `Lync.region3` | 24 | Min Vec3 + Max Vec3 as 6x f32. |
|
|
409
|
-
| `Lync.region3int16` | 12 | Min Vec3int16 + Max Vec3int16 as 6x i16. |
|
|
410
|
-
| `Lync.ray` | 24 | Origin Vec3 + Direction Vec3 as 6x f32. |
|
|
411
|
-
| `Lync.numberSequence` | varint + N×12 | Varint count then (time f32 + value f32 + envelope f32) per keypoint. |
|
|
412
|
-
| `Lync.colorSequence` | varint + N×7 | Varint count then (time f32 + R u8 + G u8 + B u8) per keypoint. |
|
|
413
|
-
| `Lync.boundedString(maxLength)` | varint + N | Same wire format as `Lync.string` but rejects on read if length exceeds `maxLength`. |
|
|
345
|
+
| Codec | Bytes | Behavior |
|
|
346
|
+
|:------|------:|:---------|
|
|
347
|
+
| `Lync.vec2(min, max, precision)` | 2–8 | Per-component quantization. 2B at u8, 4B at u16, 8B at u32. |
|
|
348
|
+
| `Lync.vec3(min, max, precision)` | 3–12 | Per-component quantization. 3B at u8, 6B at u16, 12B at u32. |
|
|
349
|
+
| `Lync.cframe()` | 16 | Smallest-three quaternion compression. 3× f32 position (12B) + 2-bit largest-component index + 3× 10-bit signed quaternion components (4B). Angular precision ≤ 0.16° (~0.003 radians). Saves 8 bytes vs lossless. |
|
|
414
350
|
|
|
415
351
|
### Composites
|
|
416
352
|
|
|
417
|
-
| Constructor |
|
|
418
|
-
|
|
419
|
-
| `Lync.struct({
|
|
420
|
-
| `Lync.array(
|
|
421
|
-
| `Lync.map(keyCodec, valueCodec, maxCount?)` |
|
|
422
|
-
| `Lync.optional(codec)` | 1
|
|
423
|
-
| `Lync.tuple(
|
|
424
|
-
| `Lync.tagged(tagField,
|
|
425
|
-
|
|
426
|
-
### Delta
|
|
427
|
-
|
|
428
|
-
Reliable only. Lync will error if you try to use these with `unreliable = true`.
|
|
429
|
-
|
|
430
|
-
| Constructor | What it does |
|
|
431
|
-
|:------------|:------------|
|
|
432
|
-
| `Lync.deltaStruct({ key = codec })` | First frame sends everything. After that only dirty fields get sent via bitmask. If nothing changed it costs 1 byte. |
|
|
433
|
-
| `Lync.deltaArray(codec, maxCount?)` | Same idea but for arrays. Dirty elements get sent with varint indices. Optional `maxCount` rejects on read if exceeded. |
|
|
434
|
-
| `Lync.deltaMap(keyCodec, valueCodec, maxCount?)` | Delta compression for key-value maps. Sends only upserted and removed entries after the first frame. Optional `maxCount` rejects on read if exceeded. |
|
|
435
|
-
|
|
436
|
-
### Meta
|
|
437
|
-
|
|
438
|
-
| Constructor | What it does |
|
|
439
|
-
|:------------|:------------|
|
|
440
|
-
| `Lync.enum(value, value, ...)` | u8 index, up to 256 variants. |
|
|
441
|
-
| `Lync.quantizedFloat(min, max, precision)` | Fixed-point compression. Picks u8/u16/u32 based on your range and precision. |
|
|
442
|
-
| `Lync.quantizedVec3(min, max, precision)` | Same thing but for all 3 components. |
|
|
443
|
-
| `Lync.bitfield({ key = spec })` | Sub-byte packing, 1 to 32 bits total. Spec is `{ type = "bool" }` or `{ type = "uint", width = N }` or `{ type = "int", width = N }`. |
|
|
444
|
-
| `Lync.custom(size, write, read)` | User-defined fixed-size codec. `write` is `(b, offset, value) → ()`, `read` is `(b, offset) → value`. Plugs into struct/array/delta specialization automatically. |
|
|
445
|
-
| `Lync.nothing` | Zero bytes. Reads nil. Good for fire-and-forget signals. |
|
|
446
|
-
| `Lync.unknown` | Skips serialization entirely, goes through Roblox's sidecar. Requires refs array on read (same as `Lync.inst`). Use when you dont have a codec for the value. |
|
|
447
|
-
| `Lync.auto` | Self-describing. Writes a u8 type tag then the value. Handles nil, bool, all number types, string, vec2, vec3, color3, cframe, buffer, udim, udim2, numberRange, rect, vec2int16, vec3int16, region3, region3int16, ray, numberSequence, colorSequence. |
|
|
353
|
+
| Constructor | Behavior |
|
|
354
|
+
|:------------|:---------|
|
|
355
|
+
| `Lync.struct(schema)` | `{ [string]: Codec }`. Fields serialized in sorted key order. Bools separated and bitpacked after all non-bool fields. All-fixed-size structs expose `_size`, `_directWrite`, `_directRead`. |
|
|
356
|
+
| `Lync.array(element, maxCount?)` | Varint count + elements. Fixed-size elements use a stride loop. Bool elements are bitpacked. Optional `maxCount` rejects on read. |
|
|
357
|
+
| `Lync.map(keyCodec, valueCodec, maxCount?)` | Varint count + key-value pairs. |
|
|
358
|
+
| `Lync.optional(codec)` | 1-byte flag. `0` = nil. `1` = value follows. |
|
|
359
|
+
| `Lync.tuple(...)` | Positional values without keys. All-fixed-size tuples expose `_size`. |
|
|
360
|
+
| `Lync.tagged(tagField, variants)` | Discriminated union. u8 variant tag. `variants` is `{ [string]: Codec }`, sorted alphabetically for deterministic tag assignment. Tag field is injected on read. |
|
|
448
361
|
|
|
449
|
-
|
|
362
|
+
### Delta Codecs
|
|
450
363
|
|
|
451
|
-
|
|
364
|
+
Reliable transport only. Errors at define time if combined with `unreliable = true`.
|
|
452
365
|
|
|
453
|
-
1
|
|
366
|
+
Delta codecs serialize into a scratch buffer and compare byte-for-byte against a cached baseline. Identical bytes produce a 1-byte `UNCHANGED` flag. Any difference triggers a full re-send prefixed with a `FULL` flag byte.
|
|
454
367
|
|
|
455
|
-
|
|
|
456
|
-
|
|
457
|
-
|
|
|
458
|
-
|
|
|
459
|
-
|
|
|
460
|
-
| Chaotic entities | 16,320 Kbps | **4.76 Kbps** | 59.99 |
|
|
368
|
+
| Constructor | Behavior |
|
|
369
|
+
|:------------|:---------|
|
|
370
|
+
| `Lync.deltaStruct(schema)` | Same schema as `struct`. First frame is always full. |
|
|
371
|
+
| `Lync.deltaArray(element, maxCount?)` | Delta-framed array. |
|
|
372
|
+
| `Lync.deltaMap(keyCodec, valueCodec, maxCount?)` | Delta-framed map. |
|
|
461
373
|
|
|
462
|
-
###
|
|
374
|
+
### Meta Codecs
|
|
463
375
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
|
469
|
-
|
|
470
|
-
|
|
|
471
|
-
|
|
|
472
|
-
| blink | 42.00 | 45.00 | 42.00 | 42.00 | 42.00 | 42.00 |
|
|
473
|
-
| zap | 39.00 | 40.00 | 38.00 | 38.00 | 38.00 | 38.00 |
|
|
474
|
-
| bytenet | 32.00 | 34.00 | 32.00 | 32.00 | 32.00 | 31.00 |
|
|
475
|
-
|
|
476
|
-
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
477
|
-
|:------------|-------:|---:|----:|----:|----:|-----:|
|
|
478
|
-
| roblox | 559,364 | 559,364 | 676,715 | 676,715 | 676,715 | 784,081 |
|
|
479
|
-
| **lync** | **3.61** | 3.53 | 3.63 | 3.64 | 3.64 | 4.64 |
|
|
480
|
-
| blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 |
|
|
481
|
-
| zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 |
|
|
482
|
-
| bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 |
|
|
483
|
-
|
|
484
|
-
**Booleans** (1000x bool, fired 1000 times/frame)
|
|
485
|
-
|
|
486
|
-
| Tool (FPS) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
487
|
-
|:-----------|-------:|---:|----:|----:|----:|-----:|
|
|
488
|
-
| roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 |
|
|
489
|
-
| **lync** | **60.00** | 61.00 | 60.00 | 60.00 | 60.00 | 59.00 |
|
|
490
|
-
| blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 |
|
|
491
|
-
| zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 |
|
|
492
|
-
| bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 |
|
|
493
|
-
|
|
494
|
-
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
495
|
-
|:------------|-------:|---:|----:|----:|----:|-----:|
|
|
496
|
-
| roblox | 353,107 | 196,826 | 690,747 | 842,240 | 842,240 | 1,124,176 |
|
|
497
|
-
| **lync** | **4.31** | 3.85 | 4.36 | 4.38 | 4.38 | 4.44 |
|
|
498
|
-
| blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 |
|
|
499
|
-
| zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 |
|
|
500
|
-
| bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 |
|
|
376
|
+
| Constructor | Behavior |
|
|
377
|
+
|:------------|:---------|
|
|
378
|
+
| `Lync.enum(...)` | String enum. u8 index, up to 256 variants. Errors on unknown values at write time and duplicate values at define time. |
|
|
379
|
+
| `Lync.bitfield(schema)` | Sub-byte packing, 1–32 bits. Spec: `{ type = "bool" }`, `{ type = "uint", width = N }`, or `{ type = "int", width = N }`. Wire: 1B ≤8 bits, 2B ≤16 bits, 4B ≤32 bits. Signed ints use sign extension. Fields sorted alphabetically. |
|
|
380
|
+
| `Lync.custom(size, write, read)` | User-defined fixed-size codec. `write(buffer, offset, value)`, `read(buffer, offset) → value`. |
|
|
381
|
+
| `Lync.nothing` | Zero bytes. Reads `nil`. |
|
|
382
|
+
| `Lync.unknown` | Bypasses buffer serialization. Values go through the remote's sidecar array. Warns at define time if used without `validate`. |
|
|
383
|
+
| `Lync.auto` | Self-describing. u8 type tag + value. Integers auto-sized. Floats try f32 then f64. Supports nil, bool, number, string, buffer, and 15 Roblox types. Tables error. |
|
|
501
384
|
|
|
502
|
-
|
|
503
|
-
|
|
385
|
+
## Wire Protocol
|
|
386
|
+
|
|
387
|
+
### Dense Prefix-Varint
|
|
388
|
+
|
|
389
|
+
Variable-length unsigned integer encoding. 1-byte range covers 0–191 (LEB128 only covers 0–127).
|
|
390
|
+
|
|
391
|
+
| Range | Bytes | Encoding |
|
|
392
|
+
|:------|------:|:---------|
|
|
393
|
+
| 0–191 | 1 | Direct value |
|
|
394
|
+
| 192–8,383 | 2 | `0xC0 + high5`, `low8` |
|
|
395
|
+
| 8,384–1,056,959 | 3 | `0xE0 + high4`, `low16 LE` |
|
|
396
|
+
| 1,056,960–4,294,967,295 | 5 | `0xF0`, `u32 LE` |
|
|
397
|
+
|
|
398
|
+
### MSB Batch Framing
|
|
399
|
+
|
|
400
|
+
All sends within one Heartbeat are batched into a single buffer per player per reliability channel.
|
|
401
|
+
|
|
402
|
+
**Single-item:** `[1IIIIIII] [payload]` — MSB set, 7-bit packet ID, no count byte. 1-byte header.
|
|
403
|
+
|
|
404
|
+
**Multi-item:** `[0IIIIIII] [u16 count] [payload₁] ...` — MSB clear, u16 item count follows. Used when ≥2 sends to the same packet occur in one frame.
|
|
405
|
+
|
|
406
|
+
The single-item path saves 2 bytes per packet per frame vs always writing a count. Maximum 127 packet IDs (7 bits).
|
|
407
|
+
|
|
408
|
+
### XOR Framing
|
|
409
|
+
|
|
410
|
+
Reliable channels XOR the current frame against the previous before sending. The receiver XOR's against its previous decoded frame to recover the original. Produces long zero runs that compress well under Roblox's internal deflate.
|
|
411
|
+
|
|
412
|
+
XOR operates in u32-aligned chunks with u8 remainder. Mismatched frame sizes are handled: excess bytes in a longer frame are copied directly.
|
|
413
|
+
|
|
414
|
+
Unreliable channels skip XOR (no guaranteed frame ordering).
|
|
415
|
+
|
|
416
|
+
## Security
|
|
417
|
+
|
|
418
|
+
### Schema Validation
|
|
419
|
+
|
|
420
|
+
Every incoming packet on the server passes through Gate before listeners fire:
|
|
421
|
+
|
|
422
|
+
- **`_typeCheck`**: Rejects wrong `typeof`.
|
|
423
|
+
- **`_isInteger` + `_min`/`_max`**: Rejects non-integers, NaN, inf, out-of-range.
|
|
424
|
+
- **`_schema`** (struct codecs): Recursive per-field validation.
|
|
425
|
+
- **Fallback**: NaN/inf scan up to `validationDepth` levels for codecs without metadata.
|
|
426
|
+
|
|
427
|
+
Rejected packets fire `onDrop` and are silently discarded. Other packets in the same frame from the same player are unaffected.
|
|
428
|
+
|
|
429
|
+
### Rate Limiting
|
|
430
|
+
|
|
431
|
+
Two modes (mutually exclusive):
|
|
504
432
|
|
|
505
|
-
|
|
433
|
+
**Token bucket:** `{ maxPerSecond = N, burst = M }`. Tokens refill at N/sec. Burst defaults to 1. Each fire costs one token.
|
|
506
434
|
|
|
507
|
-
|
|
435
|
+
**Cooldown:** `{ cooldown = seconds }`. Rejects fires within `cooldown` seconds of the last accepted fire.
|
|
508
436
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
437
|
+
Global rate limit: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`. Checked before per-packet limits.
|
|
438
|
+
|
|
439
|
+
### Bandwidth Throttle
|
|
440
|
+
|
|
441
|
+
`Lync.configure({ bandwidthLimit = { softLimit = bytes, maxStrikes = N } })`. Per-player. Oversized frames increment strikes. Small frames decrement (decay). Exceeding `maxStrikes` drops the entire frame.
|
|
442
|
+
|
|
443
|
+
## Stats
|
|
444
|
+
|
|
445
|
+
Disabled by default. Zero overhead when off. Enable via `Lync.configure({ stats = true })`.
|
|
446
|
+
|
|
447
|
+
| Function | Behavior |
|
|
448
|
+
|:---------|:---------|
|
|
449
|
+
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }` |
|
|
450
|
+
| `Lync.stats.player(player)` | `{ bytesSent, bytesReceived }` or `nil`. Server only. |
|
|
451
|
+
| `Lync.stats.reset()` | Zeros all counters. |
|
|
452
|
+
|
|
453
|
+
## Debug
|
|
454
|
+
|
|
455
|
+
| Function | Behavior |
|
|
456
|
+
|:---------|:---------|
|
|
457
|
+
| `Lync.debug.pending()` | In-flight query request count. |
|
|
458
|
+
| `Lync.debug.registrations()` | Frozen array of `{ name, id, kind, isUnreliable }`. |
|
|
459
|
+
|
|
460
|
+
## Configuration
|
|
461
|
+
|
|
462
|
+
`Lync.configure(options)` — call before `Lync.start()`.
|
|
463
|
+
|
|
464
|
+
| Option | Default | Range | Behavior |
|
|
465
|
+
|:-------|--------:|:------|:---------|
|
|
466
|
+
| `channelMaxSize` | 262,144 | 4,096–1,048,576 | Max bytes per channel buffer per frame. |
|
|
467
|
+
| `validationDepth` | 16 | 4–32 | Max recursion for NaN/inf scanning. |
|
|
468
|
+
| `poolSize` | 16 | 2–128 | ChannelState reuse pool size. |
|
|
469
|
+
| `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }`. Per-player. |
|
|
470
|
+
| `globalRateLimit` | none | — | `{ maxPerSecond }`. Per-player across all packets. |
|
|
471
|
+
| `stats` | `false` | — | Enables stat counters. |
|
|
472
|
+
|
|
473
|
+
## Limits
|
|
474
|
+
|
|
475
|
+
| Constraint | Value |
|
|
476
|
+
|:-----------|------:|
|
|
477
|
+
| Max packet/query registrations | 127 (7-bit wire ID, queries use 2 each) |
|
|
478
|
+
| Max buffer per channel per frame | 256 KB default, 1 MB max |
|
|
479
|
+
| Max concurrent query requests | 65,536 (varint correlation IDs) |
|
|
480
|
+
| `Lync.enum` variants | 256 |
|
|
481
|
+
| `Lync.bitfield` total bits | 32 |
|
|
482
|
+
| `Lync.tagged` variants | 256 |
|
|
483
|
+
| Bool packing density | 8 per byte |
|
|
484
|
+
| String inline varint threshold | 191 bytes (1B prefix), 192+ uses multi-byte |
|
|
485
|
+
| Delta + unreliable | Not allowed (define-time error) |
|
|
486
|
+
|
|
487
|
+
## Benchmarks
|
|
488
|
+
|
|
489
|
+
Run `rojo serve bench.project.json`, open in Studio with one local server + one client.
|
|
490
|
+
|
|
491
|
+
See `bench/Run.server.luau` for full configuration and methodology.
|
|
492
|
+
|
|
493
|
+
### Wire Sizes
|
|
494
|
+
|
|
495
|
+
Exact byte count per codec write. Raw payload only — no batch framing overhead included.
|
|
496
|
+
|
|
497
|
+
| Codec | Input | Bytes |
|
|
498
|
+
|:------|:------|------:|
|
|
499
|
+
| `bool` | `true` | 1 |
|
|
500
|
+
| `int(0, 255)` | `42` | 1 |
|
|
501
|
+
| `int(0, 65535)` | `1000` | 2 |
|
|
502
|
+
| `int(0, 1000000)` | `500000` | 4 |
|
|
503
|
+
| `int(-128, 127)` | `-50` | 1 |
|
|
504
|
+
| `f16` | `42.5` | 2 |
|
|
505
|
+
| `f32` | `3.14` | 4 |
|
|
506
|
+
| `f64` | `π` | 8 |
|
|
507
|
+
| `nothing` | `nil` | 0 |
|
|
508
|
+
| `string` | `""` (empty) | 1 |
|
|
509
|
+
| `string` | 5 chars | 6 |
|
|
510
|
+
| `string` | 191 chars (max inline prefix) | 192 |
|
|
511
|
+
| `string` | 192 chars (varint prefix) | 194 |
|
|
512
|
+
| `string` | 1000 chars | 1002 |
|
|
513
|
+
| `vec2` | lossless | 8 |
|
|
514
|
+
| `vec2(0, 100, 1)` | u8 quantized | 2 |
|
|
515
|
+
| `vec3` | lossless | 12 |
|
|
516
|
+
| `vec3(0, 100, 1)` | u8 quantized | 3 |
|
|
517
|
+
| `vec3(-1000, 1000, 0.1)` | u16 quantized | 6 |
|
|
518
|
+
| `cframe` | lossless | 24 |
|
|
519
|
+
| `cframe()` | smallest-three | 16 |
|
|
520
|
+
| `color3` | RGB | 3 |
|
|
521
|
+
| `ray` | origin + direction | 24 |
|
|
522
|
+
| entity struct | 6 fields + bool (lossless) | 34 |
|
|
523
|
+
| entity struct | quantized fields (compact) | 13 |
|
|
524
|
+
| bitfield | bool + uint packed | 2 |
|
|
525
|
+
| `array` × 100 entities | 100× struct(6× u8) | 601 |
|
|
526
|
+
| `array` × 1000 bools | bitpacked | 127 |
|
|
527
|
+
|
|
528
|
+
### Codec Throughput
|
|
529
|
+
|
|
530
|
+
Isolated CPU cost. No networking. Encode + decode measured independently. 100k iterations with warmup.
|
|
531
|
+
|
|
532
|
+
| Codec | Bytes | Encode | Decode | Round-trips/sec |
|
|
533
|
+
|:------|------:|-------:|-------:|----------------:|
|
|
534
|
+
| `bool` | 1 | 44ns | 29ns | 13,862,127 |
|
|
535
|
+
| `int(0, 255)` | 1 | 42ns | 28ns | 14,387,868 |
|
|
536
|
+
| `int(0, 65535)` | 2 | 41ns | 28ns | 14,395,324 |
|
|
537
|
+
| `f16` | 2 | 61ns | 42ns | 9,686,824 |
|
|
538
|
+
| `f32` | 4 | 41ns | 25ns | 15,003,300 |
|
|
539
|
+
| `f64` | 8 | 41ns | 26ns | 14,844,063 |
|
|
540
|
+
| `string` (empty) | 1 | 30ns | 22ns | 19,240,019 |
|
|
541
|
+
| `string` (10 chars) | 11 | 46ns | 60ns | 9,441,889 |
|
|
542
|
+
| `string` (100 chars) | 101 | 48ns | 91ns | 7,166,506 |
|
|
543
|
+
| `string` (1000 chars) | 1002 | 76ns | 238ns | 3,179,953 |
|
|
544
|
+
| `vec2` | 8 | 75ns | 43ns | 8,417,720 |
|
|
545
|
+
| `vec3` | 12 | 53ns | 27ns | 12,360,328 |
|
|
546
|
+
| `vec3` (quantized) | 3 | 130ns | 85ns | 4,636,348 |
|
|
547
|
+
| `cframe` (lossless) | 24 | 92ns | 144ns | 4,232,266 |
|
|
548
|
+
| `cframe()` (compressed) | 16 | 123ns | 170ns | 3,413,621 |
|
|
549
|
+
| `color3` | 3 | 125ns | 58ns | 5,482,756 |
|
|
550
|
+
| `udim2` | 16 | 235ns | 112ns | 2,880,482 |
|
|
551
|
+
| entity struct | 34 | 239ns | 395ns | 1,578,183 |
|
|
552
|
+
| entity compact | 13 | 377ns | 490ns | 1,153,064 |
|
|
553
|
+
| bitfield flags | 2 | 142ns | 332ns | 2,107,486 |
|
|
554
|
+
| 100× entity array | 601 | 15.2µs | 34.1µs | 20,306 |
|
|
555
|
+
| 1000× bool array | 127 | 4.3µs | 5.1µs | 106,806 |
|
|
556
|
+
|
|
557
|
+
### Delta Savings
|
|
558
|
+
|
|
559
|
+
Byte cost across three consecutive writes: initial (full), identical repeat (unchanged), and single-field mutation (changed).
|
|
560
|
+
|
|
561
|
+
| Codec | Full | Unchanged | Changed | Savings |
|
|
562
|
+
|:------|-----:|----------:|--------:|--------:|
|
|
563
|
+
| `deltaStruct` (entity) | 35B | 1B | 35B | 97% |
|
|
564
|
+
| `deltaStruct` (compact) | 14B | 1B | 14B | 93% |
|
|
565
|
+
| `deltaArray` (100× entity) | 602B | 1B | 1B | 100% |
|
|
566
|
+
| `deltaArray` (1000× bool) | 128B | 1B | 1B | 99% |
|
|
567
|
+
| `deltaMap` (string → u8) | 19B | 1B | 19B | 95% |
|
|
568
|
+
|
|
569
|
+
### Batch Framing
|
|
570
|
+
|
|
571
|
+
MSB single-item batches use a 1-byte header. Multi-item batches add a u16 count after the header.
|
|
572
|
+
|
|
573
|
+
| Scenario | Total bytes | Per-item overhead |
|
|
574
|
+
|:---------|----------:|------------------:|
|
|
575
|
+
| 1 × u8 (single-item) | 2B | 1B |
|
|
576
|
+
| 10 × u8 (multi-item) | 13B | 0.3B |
|
|
577
|
+
|
|
578
|
+
### Network Throughput
|
|
579
|
+
|
|
580
|
+
Live sends to one player. Measured over 8 seconds. FPS and Kbps at median and tail.
|
|
581
|
+
|
|
582
|
+
| Packet | Fires/frame | FPS median | FPS p1 | Kbps median | Kbps p95 | Kbps p99 |
|
|
583
|
+
|:-------|:---:|----:|----:|-----:|-----:|-----:|
|
|
584
|
+
| booleans | 1000 | 60 | 59.9 | 2.5 | 6.2 | 6.2 |
|
|
585
|
+
| entity struct | 1000 | 60 | 59.9 | 2.3 | 2.4 | 2.4 |
|
|
586
|
+
| entity compact | 1000 | 60 | 59.9 | 2.4 | 2.5 | 2.5 |
|
|
587
|
+
| 100× entities | 100 | 60 | 59.9 | 2.3 | 3.1 | 3.1 |
|
|
588
|
+
| 1000× bools | 100 | 60 | 59.9 | 2.3 | 2.3 | 2.3 |
|
|
589
|
+
| bitfield flags | 1000 | 60 | 59.9 | 2.4 | 2.5 | 2.5 |
|
|
590
|
+
| cframe lossless | 1000 | 60 | 59.9 | 2.5 | 2.5 | 2.5 |
|
|
591
|
+
| cframe compressed | 1000 | 60 | 59.8 | 2.3 | 2.3 | 2.3 |
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
### Cross-Library Comparison
|
|
596
|
+
|
|
597
|
+
The tables below use the same data shapes and methodology as [Blink's published benchmarks](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): 1,000 fires/frame, same data every frame, 10 seconds, Kbps scaled by 60/FPS.
|
|
598
|
+
|
|
599
|
+
Numbers for `blink`, `zap`, `bytenet`, and `roblox` are copied directly from [Blink v0.17.1 results](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md) (2025-04-30).
|
|
600
|
+
|
|
601
|
+
> [!NOTE]
|
|
602
|
+
> **Architectural differences that affect these numbers:**
|
|
603
|
+
> - Lync batches all sends into one buffer per Heartbeat frame. Other tools fire one RemoteEvent per `send()`, paying ~40 bytes of Roblox overhead per call.
|
|
604
|
+
> - Lync includes server-side schema validation and rate limiting. Other tools do not.
|
|
605
|
+
> - Lync bitpacks bool arrays (1,000 bools ≈ 127 bytes vs ~1,002 bytes for 1-byte-per-bool).
|
|
606
|
+
> - Lync uses runtime codecs. Blink and Zap use code generation with zero runtime schema.
|
|
607
|
+
> - Delta compression is not exercised here (same data every frame). See [Delta Savings](#delta-savings) for the real-world impact.
|
|
608
|
+
> - FPS is hardware-dependent. Kbps is FPS-scaled, making it comparable across machines.
|
|
609
|
+
|
|
610
|
+
**Tool versions:** blink v0.17.1 · zap v0.6.20 · bytenet v0.4.3 · lync v2.1.0
|
|
611
|
+
|
|
612
|
+
**Data shapes:** Entities = `100× struct { id u8, x u8, y u8, z u8, orientation u8, animation u8 }`. Booleans = `1000× bool`. [Source](https://github.com/1Axen/blink/blob/main/benchmark/src/shared/benches).
|
|
613
|
+
|
|
614
|
+
#### Entities — FPS
|
|
615
|
+
|
|
616
|
+
| Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
|
|
617
|
+
|:-----|-------:|---:|----:|----:|----:|-----:|-----:|
|
|
618
|
+
| roblox | 16.00 | 16.00 | 15.00 | 15.00 | 15.00 | 15.00 | 0% |
|
|
619
|
+
| **lync** | **60.00** | **61.00** | **60.00** | **60.00** | **60.00** | **58.00** | **0%** |
|
|
620
|
+
| blink | 42.00 | 45.00 | 42.00 | 42.00 | 42.00 | 42.00 | 0% |
|
|
621
|
+
| zap | 39.00 | 40.00 | 38.00 | 38.00 | 38.00 | 38.00 | 0% |
|
|
622
|
+
| bytenet | 32.00 | 34.00 | 32.00 | 32.00 | 32.00 | 31.00 | 0% |
|
|
623
|
+
|
|
624
|
+
#### Entities — Kbps
|
|
625
|
+
|
|
626
|
+
| Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
|
|
627
|
+
|:-----|-------:|---:|----:|----:|----:|-----:|-----:|
|
|
628
|
+
| roblox | 559,364 | 559,364 | 676,716 | 676,716 | 676,716 | 784,082 | 0% |
|
|
629
|
+
| **lync** | **3.68** | **3.61** | **3.72** | **3.75** | **3.75** | **4.18** | **0%** |
|
|
630
|
+
| blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 | 0% |
|
|
631
|
+
| zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 | 0% |
|
|
632
|
+
| bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 | 0% |
|
|
633
|
+
|
|
634
|
+
#### Booleans — FPS
|
|
635
|
+
|
|
636
|
+
| Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
|
|
637
|
+
|:-----|-------:|---:|----:|----:|----:|-----:|-----:|
|
|
638
|
+
| roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 | 0% |
|
|
639
|
+
| **lync** | **60.00** | **61.00** | **60.00** | **60.00** | **60.00** | **59.00** | **0%** |
|
|
640
|
+
| blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 | 0% |
|
|
641
|
+
| zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 | 0% |
|
|
642
|
+
| bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 | 0% |
|
|
643
|
+
|
|
644
|
+
#### Booleans — Kbps
|
|
645
|
+
|
|
646
|
+
| Tool | Median | P0 | P80 | P90 | P95 | P100 | Loss |
|
|
647
|
+
|:-----|-------:|---:|----:|----:|----:|-----:|-----:|
|
|
648
|
+
| roblox | 353,107 | 196,827 | 690,748 | 842,240 | 842,240 | 1,124,176 | 0% |
|
|
649
|
+
| **lync** | **2.49** | **2.44** | **2.50** | **2.52** | **2.52** | **2.54** | **0%** |
|
|
650
|
+
| blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 | 0% |
|
|
651
|
+
| zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 | 0% |
|
|
652
|
+
| bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 | 0% |
|
|
653
|
+
|
|
654
|
+
#### Wire Size Comparison
|
|
655
|
+
|
|
656
|
+
| Data | Lync | Other tools | Difference |
|
|
657
|
+
|:-----|-----:|------------:|-----------:|
|
|
658
|
+
| 100× entities | 601B | ~602B | -1B |
|
|
659
|
+
| 1000× bools | 127B | ~1002B | -875B (87% smaller) |
|
|
518
660
|
|
|
519
661
|
## License
|
|
520
662
|
|