@axpecter/lync 2.2.0 → 2.3.1
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 +95 -143
- package/package.json +1 -1
- package/src/Types.luau +22 -16
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +95 -72
- package/src/api/Query.luau +128 -79
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +21 -14
- package/src/codec/composite/Array.luau +56 -134
- package/src/codec/composite/Map.luau +103 -72
- package/src/codec/composite/Optional.luau +6 -2
- package/src/codec/composite/Shared.luau +160 -69
- package/src/codec/composite/Struct.luau +283 -42
- package/src/codec/composite/Tagged.luau +13 -16
- package/src/codec/composite/Tuple.luau +21 -15
- package/src/codec/datatype/Buffer.luau +6 -4
- package/src/codec/datatype/CFrame.luau +56 -44
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +69 -38
- package/src/index.d.ts +161 -53
- package/src/init.luau +123 -102
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +153 -154
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +162 -105
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +92 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +60 -0
- package/src/internal/Util.luau +0 -26
package/README.md
CHANGED
|
@@ -9,18 +9,17 @@
|
|
|
9
9
|
<a href="#benchmarks">Benchmarks</a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Schemas, packets, queries, groups, validation, rate limiting. Every send batches into one buffer per player per frame; identical frames XOR to ones already in flight; delta codecs collapse unchanged state to a single byte. No code generation.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Wally — add to your `wally.toml`:
|
|
17
17
|
|
|
18
18
|
```toml
|
|
19
|
-
|
|
20
|
-
Lync = "axp3cter/lync@2.2.0"
|
|
19
|
+
Lync = "axp3cter/lync@2.3.1"
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
npm (roblox-ts):
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
25
|
npm install @axpecter/lync
|
|
@@ -30,10 +29,7 @@ npm install @axpecter/lync
|
|
|
30
29
|
import Lync from "@axpecter/lync";
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
> [!IMPORTANT]
|
|
36
|
-
> Define all packets, queries, and groups before calling `Lync.start()`.
|
|
32
|
+
**Important.** Define every packet, query, and group before `Lync.start()`. Definitions assign sequential 7-bit IDs that both peers must agree on; defining late on one side desyncs the wire.
|
|
37
33
|
|
|
38
34
|
## Example
|
|
39
35
|
|
|
@@ -72,7 +68,7 @@ local Players = game:GetService("Players")
|
|
|
72
68
|
local alive = Lync.group("alive")
|
|
73
69
|
Players.PlayerAdded:Connect(function(p) alive:add(p) end)
|
|
74
70
|
|
|
75
|
-
Net.Hit:on(function(data, sender)
|
|
71
|
+
Net.Hit:on(function(data, sender) end)
|
|
76
72
|
Net.Ping:handle(function() return os.clock() end)
|
|
77
73
|
|
|
78
74
|
Lync.start()
|
|
@@ -91,7 +87,7 @@ local Net = require(game.ReplicatedStorage.Net)
|
|
|
91
87
|
Lync.start()
|
|
92
88
|
|
|
93
89
|
local scope = Lync.scope()
|
|
94
|
-
scope:on(Net.State, function(state)
|
|
90
|
+
scope:on(Net.State, function(state) end)
|
|
95
91
|
|
|
96
92
|
Net.Hit:send({ targetId = 123, damage = 45 })
|
|
97
93
|
local serverTime = Net.Ping:request(nil)
|
|
@@ -103,18 +99,19 @@ local serverTime = Net.Ping:request(nil)
|
|
|
103
99
|
|
|
104
100
|
| Function | Description |
|
|
105
101
|
|:---|:---|
|
|
106
|
-
| `Lync.configure(opts)` |
|
|
102
|
+
| `Lync.configure(opts)` | Apply options. Must precede `start()`. |
|
|
107
103
|
| `Lync.start()` | Initialize transport. Call once. |
|
|
108
104
|
| `Lync.isStarted()` | `true` after `start()`. |
|
|
109
105
|
| `Lync.flush()` | Force an immediate send. |
|
|
110
|
-
| `Lync.flushRate(hz)` | 1–60. Default 60. |
|
|
106
|
+
| `Lync.flushRate(hz)` | 1–60 Hz. Default 60. |
|
|
107
|
+
| `Lync.reset()` | Restore module state to post-require defaults. For tests / hot reload. |
|
|
111
108
|
|
|
112
109
|
### Configure options
|
|
113
110
|
|
|
114
111
|
| Option | Default | Range | Description |
|
|
115
112
|
|:---|---:|:---|:---|
|
|
116
|
-
| `channelMaxSize` | 262144 | 4 KB – 1 MB |
|
|
117
|
-
| `validationDepth` | 16 | 4–32 |
|
|
113
|
+
| `channelMaxSize` | 262144 | 4 KB – 1 MB | Per-frame buffer cap. |
|
|
114
|
+
| `validationDepth` | 16 | 4–32 | Schema-walk recursion limit. |
|
|
118
115
|
| `poolSize` | 16 | 2–128 | Reusable channel-state pool. |
|
|
119
116
|
| `bandwidthLimit` | none | — | `{ softLimit, maxStrikes }` per-player throttle. |
|
|
120
117
|
| `globalRateLimit` | none | — | `{ maxPerSecond }` across all packets per player. |
|
|
@@ -128,44 +125,44 @@ local serverTime = Net.Ping:request(nil)
|
|
|
128
125
|
-- Server
|
|
129
126
|
packet:send(data, player)
|
|
130
127
|
packet:send(data, Lync.all)
|
|
131
|
-
packet:send(data, Lync.except(p1,
|
|
128
|
+
packet:send(data, Lync.except(p1, group1))
|
|
132
129
|
packet:send(data, { p1, p2, p3 })
|
|
133
130
|
packet:send(data, group)
|
|
134
131
|
|
|
135
132
|
-- Client
|
|
136
133
|
packet:send(data)
|
|
137
134
|
|
|
138
|
-
-- Both
|
|
139
|
-
packet:on(function(data, sender, timestamp
|
|
135
|
+
-- Both sides
|
|
136
|
+
local conn = packet:on(function(data, sender, timestamp) end)
|
|
140
137
|
packet:once(fn)
|
|
141
|
-
|
|
138
|
+
local data, sender, timestamp = packet:wait()
|
|
142
139
|
packet:name()
|
|
143
|
-
packet:stats()
|
|
140
|
+
packet:stats() -- requires stats=true
|
|
144
141
|
```
|
|
145
142
|
|
|
146
143
|
| Option | Type | Description |
|
|
147
144
|
|:---|:---|:---|
|
|
148
|
-
| `unreliable` | boolean | Use `UnreliableRemoteEvent`.
|
|
149
|
-
| `rateLimit` | `RateLimitConfig` | Server-side per-player
|
|
150
|
-
| `validate` | `(data, player) → (bool, string?)` | Drop on `false`. |
|
|
151
|
-
| `maxPayloadBytes` | number | Reject oversize incoming payloads. |
|
|
152
|
-
| `timestamp` | `"frame"
|
|
145
|
+
| `unreliable` | boolean | Use `UnreliableRemoteEvent`. Rejected with delta codecs (a dropped frame would desync the baseline). |
|
|
146
|
+
| `rateLimit` | `RateLimitConfig` | Server-side per-player. |
|
|
147
|
+
| `validate` | `(data, player) → (bool, string?)` | Drop on `false`. Reason is forwarded to `onDrop`. |
|
|
148
|
+
| `maxPayloadBytes` | number | Reject oversize incoming payloads early. |
|
|
149
|
+
| `timestamp` | `"frame"`, `"offset"`, `"full"` | Append 1B / 2B / 8B timestamp. Read as the third arg. |
|
|
153
150
|
|
|
154
151
|
### Queries
|
|
155
152
|
|
|
156
153
|
`Lync.query(name, requestCodec, responseCodec, options?)`
|
|
157
154
|
|
|
158
|
-
Request-response on top of two
|
|
155
|
+
Request-response on top of two paired registrations. Single-target requests yield until reply or timeout; multi-target requests gather a partial map.
|
|
159
156
|
|
|
160
157
|
```luau
|
|
161
158
|
-- Server
|
|
162
159
|
query:handle(function(data, player) return response end)
|
|
163
|
-
query:request(data, player)
|
|
164
|
-
query:request(data,
|
|
160
|
+
local resp = query:request(data, player) -- response?
|
|
161
|
+
local map = query:request(data, group) -- { [Player]: response? }
|
|
165
162
|
|
|
166
163
|
-- Client
|
|
167
164
|
query:handle(function(data) return response end)
|
|
168
|
-
query:request(data)
|
|
165
|
+
local resp = query:request(data) -- yields; nil on timeout
|
|
169
166
|
```
|
|
170
167
|
|
|
171
168
|
| Option | Default | Description |
|
|
@@ -180,14 +177,14 @@ query:request(data) -- yields, → response? (nil on timeout
|
|
|
180
177
|
|
|
181
178
|
| Method | Returns | Description |
|
|
182
179
|
|:---|:---|:---|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
180
|
+
| `:add(p)` / `:remove(p)` | `boolean` | `true` if membership changed. |
|
|
181
|
+
| `:has(p)` | `boolean` | |
|
|
182
|
+
| `:count()` | `number` | |
|
|
183
|
+
| `:destroy()` | — | Clear members and free the name. |
|
|
187
184
|
|
|
188
185
|
### Scope
|
|
189
186
|
|
|
190
|
-
`Lync.scope()` — batches connections for
|
|
187
|
+
`Lync.scope()` — batches connections for a single `:destroy()`.
|
|
191
188
|
|
|
192
189
|
```luau
|
|
193
190
|
local scope = Lync.scope()
|
|
@@ -205,43 +202,46 @@ Server-side `:send` second arg.
|
|
|
205
202
|
|:---|:---|
|
|
206
203
|
| `Player` | One player. |
|
|
207
204
|
| `Lync.all` | All connected. |
|
|
208
|
-
| `Lync.except(...)` | Everyone except given
|
|
205
|
+
| `Lync.except(...)` | Everyone except given Players or Groups. |
|
|
209
206
|
| `{ p1, p2 }` | Array of players. |
|
|
210
207
|
| `group` | All members. |
|
|
211
208
|
|
|
212
209
|
### Middleware
|
|
213
210
|
|
|
214
211
|
```luau
|
|
215
|
-
|
|
212
|
+
-- Return Lync.DROP from onSend to discard a packet.
|
|
213
|
+
Lync.onSend(function(data, name, player) return data end)
|
|
216
214
|
Lync.onReceive(function(data, name, player) return data end)
|
|
217
215
|
Lync.onDrop(function(player, reason, name, data) end)
|
|
218
216
|
```
|
|
219
217
|
|
|
220
|
-
All return a `Connection`.
|
|
218
|
+
All return a `Connection`. A throwing hook surfaces to the caller and aborts the chain at that point.
|
|
221
219
|
|
|
222
220
|
### Connection
|
|
223
221
|
|
|
224
222
|
| | |
|
|
225
223
|
|:---|:---|
|
|
226
|
-
| `c.connected` | boolean |
|
|
224
|
+
| `c.connected` | `boolean` |
|
|
227
225
|
| `c:disconnect()` | Idempotent. |
|
|
228
226
|
|
|
229
227
|
### Stats
|
|
230
228
|
|
|
231
|
-
`Lync.configure({ stats = true })`.
|
|
229
|
+
Enable with `Lync.configure({ stats = true })`.
|
|
232
230
|
|
|
233
231
|
| Function | Description |
|
|
234
232
|
|:---|:---|
|
|
235
233
|
| `Lync.stats.player(p)` | `{ bytesSent, bytesReceived }`. Server only. |
|
|
236
234
|
| `Lync.stats.reset()` | Zero all counters. |
|
|
237
|
-
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }
|
|
235
|
+
| `packet:stats()` | `{ bytesSent, bytesReceived, fires, recvFires, drops }`. Aggregated across the request + response registrations on queries. |
|
|
238
236
|
|
|
239
237
|
### Debug
|
|
240
238
|
|
|
241
239
|
| Function | Description |
|
|
242
240
|
|:---|:---|
|
|
243
|
-
| `Lync.debug.pending()` | In-flight query
|
|
244
|
-
| `Lync.debug.registrations()` | Frozen
|
|
241
|
+
| `Lync.debug.pending()` | In-flight query correlation IDs. |
|
|
242
|
+
| `Lync.debug.registrations()` | Frozen `{ name, id, kind, isUnreliable }` per registration. |
|
|
243
|
+
|
|
244
|
+
`capture` / `stop` / `dump` are reserved no-ops for capture/replay tooling.
|
|
245
245
|
|
|
246
246
|
## Codecs
|
|
247
247
|
|
|
@@ -249,9 +249,9 @@ All return a `Connection`.
|
|
|
249
249
|
|
|
250
250
|
| Codec | Bytes | Notes |
|
|
251
251
|
|:---|---:|:---|
|
|
252
|
-
| `int(min, max)` | 1 / 2 / 4 | Picks
|
|
253
|
-
| `f16` / `f32` / `f64` | 2 / 4 / 8 | f16
|
|
254
|
-
| `float(min, max, precision)` | 1
|
|
252
|
+
| `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
|
|
253
|
+
| `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |
|
|
254
|
+
| `float(min, max, precision)` | 1 / 2 / 4 | Quantized; clamped to range. |
|
|
255
255
|
| `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
|
|
256
256
|
|
|
257
257
|
### Strings & buffers
|
|
@@ -260,7 +260,7 @@ All return a `Connection`.
|
|
|
260
260
|
|:---|:---|
|
|
261
261
|
| `string` | Variable length. Binary-safe. |
|
|
262
262
|
| `string(maxLength)` | Bounded. Rejects on read if exceeded. |
|
|
263
|
-
| `buff` | Variable-length buffer
|
|
263
|
+
| `buff` | Variable-length raw `buffer`. |
|
|
264
264
|
|
|
265
265
|
### Roblox types
|
|
266
266
|
|
|
@@ -269,7 +269,7 @@ All return a `Connection`.
|
|
|
269
269
|
| `vec2` / `vec3` | 8 / 12 |
|
|
270
270
|
| `cframe` | 24 |
|
|
271
271
|
| `color3` | 3 |
|
|
272
|
-
| `inst` | 2 |
|
|
272
|
+
| `inst` | 2 (sidecar ref index) |
|
|
273
273
|
| `udim` / `udim2` | 8 / 16 |
|
|
274
274
|
| `numberRange` | 8 |
|
|
275
275
|
| `rect` | 16 |
|
|
@@ -284,136 +284,88 @@ Call as a function for compression.
|
|
|
284
284
|
|
|
285
285
|
| Codec | Bytes | Notes |
|
|
286
286
|
|:---|---:|:---|
|
|
287
|
-
| `vec2(min, max, precision)` | 2
|
|
288
|
-
| `vec3(min, max, precision)` | 3
|
|
289
|
-
| `cframe()` | 16 | Smallest-three quaternion. ≤0.16° rotation error. |
|
|
287
|
+
| `vec2(min, max, precision)` | 2 / 4 / 8 | Per-component quantization. |
|
|
288
|
+
| `vec3(min, max, precision)` | 3 / 6 / 12 | Per-component quantization. |
|
|
289
|
+
| `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
|
|
290
290
|
|
|
291
291
|
### Composites
|
|
292
292
|
|
|
293
293
|
| Codec | Notes |
|
|
294
294
|
|:---|:---|
|
|
295
|
-
| `struct({k = c})` | Named fields. Bools auto-bitpacked. |
|
|
296
|
-
| `array(c, max?)` | List. Bool arrays bitpacked. |
|
|
297
|
-
| `map(k, v, max?)` | Key-value pairs. |
|
|
298
|
-
| `optional(c)` | 1B
|
|
299
|
-
| `tuple(...)` | Positional. |
|
|
300
|
-
| `tagged(field, {name = c})` | Discriminated union. 1B tag.
|
|
295
|
+
| `struct({k = c})` | Named fields. Bools auto-bitpacked into a tail block. |
|
|
296
|
+
| `array(c, max?)` | List. Bool arrays bitpacked. Direct path for fixed-size elements. |
|
|
297
|
+
| `map(k, v, max?)` | Key-value pairs; keys sorted at encode for stable wire bytes. |
|
|
298
|
+
| `optional(c)` | 1B presence flag + value. |
|
|
299
|
+
| `tuple(...)` | Positional. All-direct fast path when every element is fixed-size. |
|
|
300
|
+
| `tagged(field, {name = c})` | Discriminated union. 1B tag. Up to 256 variants. |
|
|
301
301
|
|
|
302
|
-
### Delta — reliable only
|
|
302
|
+
### Delta — reliable transport only
|
|
303
303
|
|
|
304
|
-
Sends 1 byte when
|
|
304
|
+
Sends 1 byte when the value is byte-equal to the cached previous frame.
|
|
305
305
|
|
|
306
|
-
| Codec |
|
|
307
|
-
|
|
308
|
-
| `deltaStruct(schema)` |
|
|
309
|
-
| `deltaArray(c, max?)` |
|
|
310
|
-
| `deltaMap(k, v, max?)` |
|
|
306
|
+
| Codec | Notes |
|
|
307
|
+
|:---|:---|
|
|
308
|
+
| `deltaStruct(schema)` | Per-segment dirty bitmap; single-segment fast path. |
|
|
309
|
+
| `deltaArray(c, max?)` | Whole-array byte-equality. |
|
|
310
|
+
| `deltaMap(k, v, max?)` | Whole-map byte-equality. |
|
|
311
311
|
|
|
312
312
|
### Meta
|
|
313
313
|
|
|
314
314
|
| Codec | Notes |
|
|
315
315
|
|:---|:---|
|
|
316
|
-
| `enum(...)` | String enum. ≤256 variants. 1B. |
|
|
317
|
-
| `bitfield(schema)` | 1–32 bits.
|
|
318
|
-
| `custom(size, write, read, typeCheck?)` | User-defined fixed-size. |
|
|
319
|
-
| `nothing` | 0 bytes
|
|
320
|
-
| `unknown` |
|
|
321
|
-
| `auto` | Self-describing
|
|
316
|
+
| `enum(...)` | String enum. ≤ 256 variants. 1B u8 index. |
|
|
317
|
+
| `bitfield(schema)` | 1–32 bits total. `{ type = "bool" }`, `{ type = "uint", width }`, `{ type = "int", width }`. |
|
|
318
|
+
| `custom(size, write, read, typeCheck?)` | User-defined fixed-size codec. |
|
|
319
|
+
| `nothing` | 0 bytes; reads `nil`. For fire-and-forget signals. |
|
|
320
|
+
| `unknown` | Bypasses serialization through the channel sidecar. Must be paired with `validate`. |
|
|
321
|
+
| `auto` | Self-describing: nil / bool / numbers / strings / buffers / Roblox datatypes. 1B type tag + payload. |
|
|
322
322
|
|
|
323
323
|
## Rate limiting
|
|
324
324
|
|
|
325
325
|
Per-packet, pick one mode:
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{ cooldown = seconds } -- cooldown
|
|
330
|
-
```
|
|
327
|
+
- Token bucket: `{ maxPerSecond = N, burst = M }`
|
|
328
|
+
- Cooldown: `{ cooldown = seconds }`
|
|
331
329
|
|
|
332
|
-
Global per-player: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
|
|
330
|
+
Global per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N } })`.
|
|
333
331
|
|
|
334
332
|
## Limits
|
|
335
333
|
|
|
336
334
|
| | |
|
|
337
335
|
|:---|---:|
|
|
338
|
-
| Packet + query IDs | 127 |
|
|
336
|
+
| Packet + query IDs (combined) | 127 |
|
|
339
337
|
| Buffer per frame | 1 MB max |
|
|
340
|
-
| In-flight queries | 65,
|
|
338
|
+
| In-flight queries | 65,535 |
|
|
341
339
|
| Enum / tagged variants | 256 |
|
|
342
340
|
| Bitfield total bits | 32 |
|
|
341
|
+
| Sidecar refs per frame | 65,535 |
|
|
343
342
|
|
|
344
343
|
## Benchmarks
|
|
345
344
|
|
|
346
345
|
`rojo serve bench.project.json` with one server + one client.
|
|
347
346
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
### Codec throughput
|
|
351
|
-
|
|
352
|
-
| Codec | Encode | Decode | RT/s |
|
|
353
|
-
|:---|---:|---:|---:|
|
|
354
|
-
| `bool` | 43 ns | 28 ns | 14.1 M |
|
|
355
|
-
| `int(0, 255)` | 41 ns | 25 ns | 15.2 M |
|
|
356
|
-
| `int(0, 65535)` | 40 ns | 25 ns | 15.3 M |
|
|
357
|
-
| `f16` | 60 ns | 42 ns | 9.7 M |
|
|
358
|
-
| `f32` | 41 ns | 26 ns | 14.9 M |
|
|
359
|
-
| `f64` | 41 ns | 25 ns | 15.3 M |
|
|
360
|
-
| `string` (10 chars) | 45 ns | 73 ns | 8.4 M |
|
|
361
|
-
| `string` (1000 chars) | 74 ns | 250 ns | 3.1 M |
|
|
362
|
-
| `vec3` | 56 ns | 27 ns | 12.1 M |
|
|
363
|
-
| `vec3` quantized | 121 ns | 85 ns | 4.9 M |
|
|
364
|
-
| `cframe` | 88 ns | 186 ns | 3.6 M |
|
|
365
|
-
| `cframe()` | 118 ns | 214 ns | 3.0 M |
|
|
366
|
-
| entity struct (6 fields) | 234 ns | 476 ns | 1.4 M |
|
|
367
|
-
| 100× entities | 15.3 µs | 34.6 µs | 20 K |
|
|
368
|
-
| 1000× bools (bitpacked) | 4.3 µs | 5.3 µs | 104 K |
|
|
369
|
-
|
|
370
|
-
### Wire sizes
|
|
347
|
+
### Cross-library — 1000 fires/frame, 10 s
|
|
371
348
|
|
|
372
|
-
|
|
349
|
+
[Blink's methodology](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): same payload reused every frame, identical entity / bool shapes. Other tools from Blink v0.17.1.
|
|
350
|
+
|
|
351
|
+
| Tool | `array<entity>[100]` | `array<bool>[1000]` |
|
|
352
|
+
|:---|:---|:---|
|
|
353
|
+
| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
|
|
354
|
+
| **lync** | **60 fps · 3.44 Kbps** | **60 fps · 2.46 Kbps** |
|
|
355
|
+
| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
|
|
356
|
+
| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
|
|
357
|
+
| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
|
|
358
|
+
|
|
359
|
+
### Network bandwidth — 100 fires/frame, 8 s
|
|
360
|
+
|
|
361
|
+
| Workload | Kbps |
|
|
373
362
|
|:---|---:|
|
|
374
|
-
| entity
|
|
375
|
-
| entity
|
|
376
|
-
|
|
|
377
|
-
| 1000
|
|
378
|
-
|
|
|
379
|
-
| `
|
|
380
|
-
|
|
381
|
-
### Delta savings
|
|
382
|
-
|
|
383
|
-
| Codec | Full | Unchanged |
|
|
384
|
-
|:---|---:|---:|
|
|
385
|
-
| `deltaStruct` (entity) | 35 B | 1 B |
|
|
386
|
-
| `deltaStruct` (compact) | 14 B | 1 B |
|
|
387
|
-
| `deltaArray` (100× entity) | 602 B | 1 B |
|
|
388
|
-
| `deltaArray` (1000× bool) | 128 B | 1 B |
|
|
389
|
-
| `deltaMap` (string → u8) | 19 B | 1 B |
|
|
390
|
-
|
|
391
|
-
### Cross-library comparison
|
|
392
|
-
|
|
393
|
-
Same methodology as [Blink](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md): 1000 fires/frame, identical data, 10 s. Other-tool numbers from Blink v0.17.1.
|
|
394
|
-
|
|
395
|
-
> [!NOTE]
|
|
396
|
-
> Lync batches all sends into one buffer per frame and bitpacks bools (1000 = 127 B vs ~1002 B). Delta compression isn't exercised here.
|
|
397
|
-
|
|
398
|
-
**100× struct(6× u8) entities**
|
|
399
|
-
|
|
400
|
-
| Tool | FPS | Kbps |
|
|
401
|
-
|:---|---:|---:|
|
|
402
|
-
| roblox | 16 | 559,364 |
|
|
403
|
-
| **lync** | **60** | **3.47** |
|
|
404
|
-
| blink | 42 | 41.81 |
|
|
405
|
-
| zap | 39 | 41.71 |
|
|
406
|
-
| bytenet | 32 | 41.64 |
|
|
407
|
-
|
|
408
|
-
**1000× bool**
|
|
409
|
-
|
|
410
|
-
| Tool | FPS | Kbps |
|
|
411
|
-
|:---|---:|---:|
|
|
412
|
-
| roblox | 21 | 353,107 |
|
|
413
|
-
| **lync** | **60** | **2.33** |
|
|
414
|
-
| blink | 97 | 7.91 |
|
|
415
|
-
| zap | 52 | 8.10 |
|
|
416
|
-
| bytenet | 35 | 8.11 |
|
|
363
|
+
| `array<entity>[100]` randomised | 3,608 |
|
|
364
|
+
| `array<entity>[100]` reused | **2.4** |
|
|
365
|
+
| `array<bool>[1000]` randomised | 763 |
|
|
366
|
+
| `array<bool>[1000]` 1 bit flipped | **20.5** |
|
|
367
|
+
| `struct(state)` randomised | 201 |
|
|
368
|
+
| `deltaStruct(state)` 1 field mutated | **29.2** |
|
|
417
369
|
|
|
418
370
|
## License
|
|
419
371
|
|
package/package.json
CHANGED
package/src/Types.luau
CHANGED
|
@@ -29,9 +29,8 @@ export type Codec<T> = {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
--[[
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
(direct write/read fast path).
|
|
32
|
+
Optional metadata used by Gate (validation), Reader (max-item bound),
|
|
33
|
+
Auto (type dispatch), and composite codecs (direct write/read fast path).
|
|
35
34
|
]]
|
|
36
35
|
export type InternalCodec<T> = {
|
|
37
36
|
write: (ch: ChannelState, value: T) -> (),
|
|
@@ -45,27 +44,28 @@ export type InternalCodec<T> = {
|
|
|
45
44
|
_min: number?,
|
|
46
45
|
_max: number?,
|
|
47
46
|
_isInteger: boolean?,
|
|
48
|
-
_schema: { [string]: InternalCodec<
|
|
47
|
+
_schema: { [string]: InternalCodec<T> }?,
|
|
49
48
|
_typeCheck: string?,
|
|
50
49
|
--[[
|
|
51
|
-
Composite
|
|
52
|
-
|
|
50
|
+
Composite metadata: lets Gate recurse element/variant validation
|
|
51
|
+
instead of falling through to the generic NaN/inf scan.
|
|
53
52
|
]]
|
|
54
53
|
_isOptional: boolean?,
|
|
55
|
-
_inner: InternalCodec<
|
|
54
|
+
_inner: InternalCodec<T>?,
|
|
56
55
|
_isTagged: boolean?,
|
|
57
|
-
_variants: { [string]: InternalCodec<
|
|
56
|
+
_variants: { [string]: InternalCodec<T> }?,
|
|
58
57
|
_tagField: string?,
|
|
59
58
|
_isArray: boolean?,
|
|
60
|
-
_element: InternalCodec<
|
|
59
|
+
_element: InternalCodec<T>?,
|
|
61
60
|
_maxCount: number?,
|
|
62
61
|
_isMap: boolean?,
|
|
63
|
-
_keyCodec: InternalCodec<
|
|
64
|
-
_valueCodec: InternalCodec<
|
|
62
|
+
_keyCodec: InternalCodec<T>?,
|
|
63
|
+
_valueCodec: InternalCodec<T>?,
|
|
65
64
|
_isTuple: boolean?,
|
|
66
|
-
_elements: { InternalCodec<
|
|
65
|
+
_elements: { InternalCodec<T> }?,
|
|
67
66
|
_isEnum: boolean?,
|
|
68
67
|
_enumValues: { [string]: number }?,
|
|
68
|
+
_maxStringLength: number?,
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export type RateLimitState = {
|
|
@@ -76,7 +76,7 @@ export type RateLimitState = {
|
|
|
76
76
|
|
|
77
77
|
--[[
|
|
78
78
|
Token-bucket (maxPerSecond + optional burst) or cooldown (cooldown
|
|
79
|
-
seconds between accepts). Mixing both forms is undefined.
|
|
79
|
+
seconds between accepts). Mixing both forms is undefined behavior.
|
|
80
80
|
]]
|
|
81
81
|
export type RateLimitConfig = {
|
|
82
82
|
maxPerSecond: number?,
|
|
@@ -84,6 +84,10 @@ export type RateLimitConfig = {
|
|
|
84
84
|
cooldown: number?,
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
--[[
|
|
88
|
+
User-supplied per-packet validator. Returns (true, nil) on accept;
|
|
89
|
+
(false, reason) on reject — the reason is forwarded to onDrop hooks.
|
|
90
|
+
]]
|
|
87
91
|
export type ValidateFn = (data: any, player: Player) -> (boolean, string?)
|
|
88
92
|
|
|
89
93
|
export type PacketOptions = {
|
|
@@ -106,6 +110,7 @@ export type Registration = {
|
|
|
106
110
|
kind: number,
|
|
107
111
|
codec: InternalCodec<any>,
|
|
108
112
|
isUnreliable: boolean,
|
|
113
|
+
-- Response registration id; set on REQUEST regs only.
|
|
109
114
|
partner: number?,
|
|
110
115
|
signal: SignalLike,
|
|
111
116
|
rateLimit: RateLimitConfig?,
|
|
@@ -160,9 +165,10 @@ export type PlayerStats = {
|
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
--[[
|
|
163
|
-
Server keys baselines per Player; client uses `false` since booleans
|
|
164
|
-
|
|
168
|
+
Server keys baselines per Player; client uses `false` since booleans are
|
|
169
|
+
valid table keys but Players never collide with that.
|
|
165
170
|
]]
|
|
166
171
|
export type BaselineKey = Player | false
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
-- Type-only module: runtime table is empty so re-requires are cheap.
|
|
174
|
+
return table.freeze({})
|
package/src/api/Group.luau
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Named player sets with __iter
|
|
3
|
+
-- Named player sets with __iter and auto-cleanup on PlayerRemoving.
|
|
4
4
|
|
|
5
5
|
local Players = game:GetService("Players")
|
|
6
6
|
|
|
7
|
-
local
|
|
7
|
+
local Array = require(script.Parent.Parent.util.Array)
|
|
8
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
8
9
|
|
|
9
10
|
-- Public types -----------------------------------------------------------
|
|
10
11
|
|
|
@@ -14,22 +15,24 @@ export type GroupHandle = {
|
|
|
14
15
|
has: (self: GroupHandle, player: Player) -> boolean,
|
|
15
16
|
count: (self: GroupHandle) -> number,
|
|
16
17
|
destroy: (self: GroupHandle) -> (),
|
|
17
|
-
|
|
18
|
+
_lyncKind: "group",
|
|
18
19
|
_members: { [Player]: boolean },
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
type
|
|
22
|
+
type GroupInternal = GroupHandle & { _count: number, _name: string }
|
|
22
23
|
|
|
23
24
|
-- State ------------------------------------------------------------------
|
|
24
25
|
|
|
25
|
-
local _groups: { [string]:
|
|
26
|
+
local _groups: { [string]: GroupInternal } = {}
|
|
27
|
+
-- Reverse index used by PlayerRemoving so we don't scan every group on disconnect.
|
|
26
28
|
local _playerCleanup: { [Player]: { Group } } = {}
|
|
27
29
|
local _cleanupConnected = false
|
|
28
30
|
|
|
29
31
|
-- Private ----------------------------------------------------------------
|
|
30
32
|
|
|
31
|
-
local swapRemove =
|
|
33
|
+
local swapRemove = Array.swapRemove
|
|
32
34
|
|
|
35
|
+
-- Connect once on the first Group.create; subsequent creates reuse the same connection.
|
|
33
36
|
local function ensureCleanup(): ()
|
|
34
37
|
if _cleanupConnected then
|
|
35
38
|
return
|
|
@@ -49,6 +52,17 @@ local function ensureCleanup(): ()
|
|
|
49
52
|
end)
|
|
50
53
|
end
|
|
51
54
|
|
|
55
|
+
local function untrackCleanup(player: Player, group: GroupInternal): ()
|
|
56
|
+
local cleanup = _playerCleanup[player]
|
|
57
|
+
if not cleanup then
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
local idx = table.find(cleanup, group)
|
|
61
|
+
if idx then
|
|
62
|
+
swapRemove(cleanup, idx)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
52
66
|
-- Public -----------------------------------------------------------------
|
|
53
67
|
|
|
54
68
|
local Group = {}
|
|
@@ -56,15 +70,12 @@ local Group = {}
|
|
|
56
70
|
local GroupMeta = {}
|
|
57
71
|
GroupMeta.__index = GroupMeta
|
|
58
72
|
|
|
59
|
-
--
|
|
60
|
-
|
|
61
|
-
uses Luau's builtin path (luaH_next) with zero closure allocation.
|
|
62
|
-
]]
|
|
63
|
-
function GroupMeta.__iter(self: Group): (any, any, any)
|
|
73
|
+
-- __iter lets `for player in group do` skip the explicit :members() call.
|
|
74
|
+
function GroupMeta.__iter(self: GroupInternal): (any, any, any)
|
|
64
75
|
return next, self._members, nil
|
|
65
76
|
end
|
|
66
77
|
|
|
67
|
-
function GroupMeta.add(self:
|
|
78
|
+
function GroupMeta.add(self: GroupInternal, player: Player): boolean
|
|
68
79
|
if self._members[player] then
|
|
69
80
|
return false
|
|
70
81
|
end
|
|
@@ -81,45 +92,32 @@ function GroupMeta.add(self: Group, player: Player): boolean
|
|
|
81
92
|
return true
|
|
82
93
|
end
|
|
83
94
|
|
|
84
|
-
function GroupMeta.remove(self:
|
|
95
|
+
function GroupMeta.remove(self: GroupInternal, player: Player): boolean
|
|
85
96
|
if not self._members[player] then
|
|
86
97
|
return false
|
|
87
98
|
end
|
|
88
99
|
|
|
89
100
|
self._members[player] = nil
|
|
90
101
|
self._count -= 1
|
|
91
|
-
|
|
92
|
-
local cleanup = _playerCleanup[player]
|
|
93
|
-
if cleanup then
|
|
94
|
-
local idx = table.find(cleanup, self)
|
|
95
|
-
if idx then
|
|
96
|
-
swapRemove(cleanup, idx)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
102
|
+
untrackCleanup(player, self)
|
|
99
103
|
return true
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
function GroupMeta.has(self:
|
|
106
|
+
function GroupMeta.has(self: GroupInternal, player: Player): boolean
|
|
103
107
|
return self._members[player] == true
|
|
104
108
|
end
|
|
105
109
|
|
|
106
|
-
function GroupMeta.count(self:
|
|
110
|
+
function GroupMeta.count(self: GroupInternal): number
|
|
107
111
|
return self._count
|
|
108
112
|
end
|
|
109
113
|
|
|
110
|
-
function GroupMeta.destroy(self:
|
|
114
|
+
function GroupMeta.destroy(self: GroupInternal): ()
|
|
111
115
|
if _groups[self._name] == self then
|
|
112
116
|
_groups[self._name] = nil
|
|
113
117
|
end
|
|
114
118
|
|
|
115
119
|
for player in self._members do
|
|
116
|
-
|
|
117
|
-
if cleanup then
|
|
118
|
-
local idx = table.find(cleanup, self)
|
|
119
|
-
if idx then
|
|
120
|
-
swapRemove(cleanup, idx)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
120
|
+
untrackCleanup(player, self)
|
|
123
121
|
end
|
|
124
122
|
|
|
125
123
|
table.clear(self._members)
|
|
@@ -130,13 +128,13 @@ table.freeze(GroupMeta)
|
|
|
130
128
|
|
|
131
129
|
function Group.create(name: string): GroupHandle
|
|
132
130
|
if _groups[name] then
|
|
133
|
-
error(`
|
|
131
|
+
Log.error(`"{name}" already exists`)
|
|
134
132
|
end
|
|
135
133
|
|
|
136
134
|
ensureCleanup()
|
|
137
135
|
|
|
138
136
|
local group = setmetatable({
|
|
139
|
-
|
|
137
|
+
_lyncKind = "group" :: "group",
|
|
140
138
|
_members = {} :: { [Player]: boolean },
|
|
141
139
|
_count = 0,
|
|
142
140
|
_name = name,
|
|
@@ -146,4 +144,13 @@ function Group.create(name: string): GroupHandle
|
|
|
146
144
|
return group
|
|
147
145
|
end
|
|
148
146
|
|
|
147
|
+
function Group.reset(): ()
|
|
148
|
+
for _, group in _groups do
|
|
149
|
+
table.clear(group._members)
|
|
150
|
+
group._count = 0
|
|
151
|
+
end
|
|
152
|
+
table.clear(_groups)
|
|
153
|
+
table.clear(_playerCleanup)
|
|
154
|
+
end
|
|
155
|
+
|
|
149
156
|
return table.freeze(Group)
|