@axpecter/lync 1.3.1 → 1.4.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 +250 -68
- package/package.json +1 -1
- package/src/Types.luau +13 -2
- package/src/api/Group.luau +77 -46
- package/src/api/Namespace.luau +13 -6
- package/src/api/Packet.luau +52 -30
- package/src/api/Query.luau +116 -90
- package/src/api/Scope.luau +73 -0
- package/src/api/Signal.luau +10 -4
- package/src/codec/Base.luau +7 -3
- package/src/codec/composite/Array.luau +18 -15
- package/src/codec/composite/Map.luau +45 -31
- package/src/codec/composite/Optional.luau +3 -2
- package/src/codec/composite/Shared.luau +7 -5
- package/src/codec/composite/Struct.luau +6 -4
- package/src/codec/composite/Tagged.luau +6 -6
- package/src/codec/composite/Tuple.luau +9 -7
- package/src/codec/datatype/Buffer.luau +3 -2
- package/src/codec/datatype/CFrame.luau +2 -1
- package/src/codec/datatype/Instance.luau +3 -2
- package/src/codec/datatype/Sequence.luau +3 -4
- package/src/codec/datatype/String.luau +3 -6
- package/src/codec/meta/Auto.luau +77 -72
- package/src/codec/meta/Bitfield.luau +5 -4
- package/src/codec/meta/Enum.luau +6 -5
- package/src/codec/meta/Quantized.luau +5 -4
- package/src/codec/meta/Unknown.luau +5 -1
- package/src/codec/primitive/Float16.luau +5 -4
- package/src/codec/primitive/Varint.luau +2 -2
- package/src/index.d.ts +65 -26
- package/src/init.luau +60 -42
- package/src/internal/Baseline.luau +6 -2
- package/src/internal/Channel.luau +11 -6
- package/src/internal/Middleware.luau +18 -8
- package/src/internal/Pool.luau +5 -5
- package/src/internal/Registry.luau +4 -4
- package/src/transport/Bridge.luau +4 -3
- package/src/transport/Client.luau +4 -3
- package/src/transport/Gate.luau +5 -5
- package/src/transport/Reader.luau +3 -3
- package/src/transport/Server.luau +14 -16
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<p align="center">Buffer networking for Roblox. Delta compression, XOR framing, built-in security.</p>
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://github.com/Axp3cter/Lync/releases/latest">Releases</a> ·
|
|
5
|
+
<a href="#example">Example</a> ·
|
|
5
6
|
<a href="#benchmarks">Benchmarks</a> ·
|
|
6
7
|
<a href="#limits--configuration">Limits</a>
|
|
7
8
|
</p>
|
|
@@ -12,17 +13,17 @@
|
|
|
12
13
|
|
|
13
14
|
```toml
|
|
14
15
|
[dependencies]
|
|
15
|
-
Lync = "axp3cter/lync@1.
|
|
16
|
+
Lync = "axp3cter/lync@1.4.1"
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
**npm (roblox-ts)**
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
|
-
npm install @
|
|
22
|
+
npm install @axpecter/lync
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
```typescript
|
|
25
|
-
import Lync from "@
|
|
26
|
+
import Lync from "@axpecter/lync";
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/latest) and drop it in `ReplicatedStorage`.
|
|
@@ -30,13 +31,139 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
|
|
|
30
31
|
> [!IMPORTANT]
|
|
31
32
|
> Define everything before calling `Lync.start()`. Packets, queries, namespaces, all of it.
|
|
32
33
|
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
**Shared**
|
|
37
|
+
|
|
38
|
+
```luau
|
|
39
|
+
local Lync = require(game.ReplicatedStorage.Lync)
|
|
40
|
+
|
|
41
|
+
local Net = {}
|
|
42
|
+
|
|
43
|
+
Net.State = Lync.definePacket("State", {
|
|
44
|
+
value = Lync.deltaStruct({
|
|
45
|
+
position = Lync.vec3,
|
|
46
|
+
health = Lync.quantizedFloat(0, 100, 0.5),
|
|
47
|
+
shield = Lync.quantizedFloat(0, 100, 0.5),
|
|
48
|
+
status = Lync.enum("idle", "moving", "attacking", "dead"),
|
|
49
|
+
alive = Lync.bool,
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
Net.Hit = Lync.definePacket("Hit", {
|
|
54
|
+
value = Lync.struct({
|
|
55
|
+
targetId = Lync.u16,
|
|
56
|
+
damage = Lync.quantizedFloat(0, 200, 0.1),
|
|
57
|
+
headshot = Lync.bool,
|
|
58
|
+
}),
|
|
59
|
+
rateLimit = { maxPerSecond = 30, burstAllowance = 5 },
|
|
60
|
+
validate = function(data, player)
|
|
61
|
+
if data.damage > 200 then return false, "damage" end
|
|
62
|
+
return true
|
|
63
|
+
end,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
Net.Chat = Lync.definePacket("Chat", {
|
|
67
|
+
value = Lync.struct({ msg = Lync.boundedString(200), channel = Lync.u8 }),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
Net.Ping = Lync.defineQuery("Ping", {
|
|
71
|
+
request = Lync.nothing,
|
|
72
|
+
response = Lync.f64,
|
|
73
|
+
timeout = 3,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return table.freeze(Net)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Server**
|
|
80
|
+
|
|
81
|
+
```luau
|
|
82
|
+
local Lync = require(game.ReplicatedStorage.Lync)
|
|
83
|
+
local Net = require(game.ReplicatedStorage.Net)
|
|
84
|
+
local Players = game:GetService("Players")
|
|
85
|
+
|
|
86
|
+
local alive = Lync.createGroup("alive")
|
|
87
|
+
|
|
88
|
+
Lync.onSend(function(data, name)
|
|
89
|
+
print("[out]", name)
|
|
90
|
+
return data
|
|
91
|
+
end)
|
|
92
|
+
|
|
93
|
+
Lync.onDrop(function(player, reason, name)
|
|
94
|
+
warn(player.Name, "dropped", name, reason)
|
|
95
|
+
end)
|
|
96
|
+
|
|
97
|
+
Lync.start()
|
|
98
|
+
|
|
99
|
+
Players.PlayerAdded:Connect(function(player)
|
|
100
|
+
alive:add(player)
|
|
101
|
+
end)
|
|
102
|
+
|
|
103
|
+
game:GetService("RunService").Heartbeat:Connect(function()
|
|
104
|
+
Net.State:send({
|
|
105
|
+
position = Vector3.new(0, 5, 0),
|
|
106
|
+
health = 100,
|
|
107
|
+
shield = 50,
|
|
108
|
+
status = "idle",
|
|
109
|
+
alive = true,
|
|
110
|
+
}, alive)
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
Net.Hit:listen(function(data, player)
|
|
114
|
+
local target = Players:GetPlayerByUserId(data.targetId)
|
|
115
|
+
if not target then return end
|
|
116
|
+
|
|
117
|
+
alive:remove(target)
|
|
118
|
+
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
|
+
end)
|
|
127
|
+
|
|
128
|
+
Net.Ping:listen(function()
|
|
129
|
+
return os.clock()
|
|
130
|
+
end)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Client**
|
|
134
|
+
|
|
135
|
+
```luau
|
|
136
|
+
local Lync = require(game.ReplicatedStorage.Lync)
|
|
137
|
+
local Net = require(game.ReplicatedStorage.Net)
|
|
138
|
+
|
|
139
|
+
Lync.start()
|
|
140
|
+
|
|
141
|
+
local scope = Lync.scope()
|
|
142
|
+
|
|
143
|
+
scope:listen(Net.State, function(state)
|
|
144
|
+
local character = game.Players.LocalPlayer.Character
|
|
145
|
+
if not character then return end
|
|
146
|
+
character:PivotTo(CFrame.new(state.position))
|
|
147
|
+
end)
|
|
148
|
+
|
|
149
|
+
scope:listen(Net.Chat, function(data)
|
|
150
|
+
print("[chat]", data.msg)
|
|
151
|
+
end)
|
|
152
|
+
|
|
153
|
+
Net.Hit:send({ targetId = 123, damage = 45.5, headshot = true })
|
|
154
|
+
|
|
155
|
+
local serverTime = Net.Ping:request(nil)
|
|
156
|
+
if serverTime then
|
|
157
|
+
print("server clock:", serverTime)
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
33
161
|
## Lifecycle
|
|
34
162
|
|
|
35
163
|
| | What it does |
|
|
36
164
|
|:---------|:------------|
|
|
37
165
|
| `Lync.start()` | Sets up transport. Server creates remotes, client connects. Call once after all definitions. |
|
|
38
|
-
| `Lync.
|
|
39
|
-
| `Lync.VERSION` | `"1.3.1"` |
|
|
166
|
+
| `Lync.VERSION` | `"1.4.1"` |
|
|
40
167
|
|
|
41
168
|
## Packets
|
|
42
169
|
|
|
@@ -50,29 +177,30 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
|
|
|
50
177
|
| `validate` | `(data, player) → (bool, string?)` | No | Server-side. Return `false, "reason"` to drop. Runs after NaN scan. |
|
|
51
178
|
| `maxPayloadBytes` | number | No | Server-side. Max bytes a single batch of this packet can consume. Fires `onDrop` with reason `"size"` if exceeded. |
|
|
52
179
|
|
|
53
|
-
**Server
|
|
180
|
+
**Server, single `send` with targets:**
|
|
54
181
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
182
|
+
```luau
|
|
183
|
+
packet:send(data, player) -- one player
|
|
184
|
+
packet:send(data, Lync.all) -- everyone
|
|
185
|
+
packet:send(data, Lync.except(player)) -- everyone except one
|
|
186
|
+
packet:send(data, Lync.except(p1, p2)) -- everyone except multiple
|
|
187
|
+
packet:send(data, { p1, p2 }) -- list of players
|
|
188
|
+
packet:send(data, group) -- group object
|
|
189
|
+
```
|
|
62
190
|
|
|
63
|
-
**Client
|
|
191
|
+
**Client:**
|
|
64
192
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
193
|
+
```luau
|
|
194
|
+
packet:send(data) -- send to server
|
|
195
|
+
```
|
|
68
196
|
|
|
69
|
-
**Shared
|
|
197
|
+
**Shared (both contexts):**
|
|
70
198
|
|
|
71
199
|
| Method | What it does |
|
|
72
200
|
|:-------|:------------|
|
|
73
|
-
| `packet:listen(fn(data, sender))` |
|
|
74
|
-
| `packet:once(fn(data, sender))` |
|
|
75
|
-
| `packet:wait()` |
|
|
201
|
+
| `packet:listen(fn(data, sender))` | Sender is `Player` on server, `nil` on client. Returns a Connection. |
|
|
202
|
+
| `packet:once(fn(data, sender))` | Auto-disconnects after one fire. |
|
|
203
|
+
| `packet:wait()` | Returns `(data, sender)`. |
|
|
76
204
|
| `packet:disconnectAll()` | Kills all listeners on this packet. |
|
|
77
205
|
|
|
78
206
|
## Queries
|
|
@@ -90,17 +218,17 @@ Or grab the `.rbxm` from [releases](https://github.com/Axp3cter/Lync/releases/la
|
|
|
90
218
|
| Method | Where | What it does |
|
|
91
219
|
|:-------|:------|:-------------|
|
|
92
220
|
| `query:listen(fn)` | Both | Register a handler. Server gets `fn(request, player) → response`. Client gets `fn(request) → response`. |
|
|
93
|
-
| `query:
|
|
94
|
-
| `query:
|
|
95
|
-
| `query:
|
|
96
|
-
| `query:
|
|
97
|
-
| `query:
|
|
221
|
+
| `query:request(data)` | Client | Send request to server, yield until response or timeout. |
|
|
222
|
+
| `query:requestFrom(player, data)` | Server | Send request to a specific client, yield until response or timeout. |
|
|
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? }`. |
|
|
98
226
|
|
|
99
227
|
## Namespaces
|
|
100
228
|
|
|
101
229
|
`Lync.defineNamespace(name, config)` returns a Namespace. Takes a `packets` table and/or a `queries` table. All names get auto-prefixed with `"YourNamespace."` so nothing collides.
|
|
102
230
|
|
|
103
|
-
Access packets and queries by their short name on the returned object: `ns.PacketName`, `ns.QueryName`.
|
|
231
|
+
Access packets and queries by their short name on the returned object: `ns.PacketName`, `ns.QueryName`. Or use the typed sub-tables: `ns.packets.PacketName`, `ns.queries.QueryName`.
|
|
104
232
|
|
|
105
233
|
| Method | What it does |
|
|
106
234
|
|:-------|:------------|
|
|
@@ -111,6 +239,8 @@ Access packets and queries by their short name on the returned object: `ns.Packe
|
|
|
111
239
|
| `ns:destroy()` | Kills listeners and removes scoped middleware. Full cleanup. |
|
|
112
240
|
| `ns:packetNames()` | Sorted list of packet short names. |
|
|
113
241
|
| `ns:queryNames()` | Sorted list of query short names. |
|
|
242
|
+
| `ns.packets` | Frozen table mapping short name → Packet object. |
|
|
243
|
+
| `ns.queries` | Frozen table mapping short name → Query object. |
|
|
114
244
|
|
|
115
245
|
## Connection
|
|
116
246
|
|
|
@@ -118,9 +248,90 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
|
|
|
118
248
|
|
|
119
249
|
| | What it does |
|
|
120
250
|
|:-------|:------------|
|
|
121
|
-
| `connection.connected` | `
|
|
251
|
+
| `connection.connected` | `boolean` |
|
|
122
252
|
| `connection:disconnect()` | Stops the listener. |
|
|
123
253
|
|
|
254
|
+
## Scope
|
|
255
|
+
|
|
256
|
+
Batches connections for lifecycle-aligned cleanup.
|
|
257
|
+
|
|
258
|
+
```luau
|
|
259
|
+
local scope = Lync.scope()
|
|
260
|
+
|
|
261
|
+
scope:listen(packetA, fnA)
|
|
262
|
+
scope:listen(packetB, fnB)
|
|
263
|
+
scope:listenAll(namespace, fnC)
|
|
264
|
+
|
|
265
|
+
scope:destroy() -- disconnects everything
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
| Method | What it does |
|
|
269
|
+
|:-------|:------------|
|
|
270
|
+
| `scope:listen(source, fn)` | Calls `source:listen(fn)` and tracks the connection. |
|
|
271
|
+
| `scope:once(source, fn)` | Calls `source:once(fn)` and tracks the connection. |
|
|
272
|
+
| `scope:listenAll(namespace, fn)` | Calls `namespace:listenAll(fn)` and tracks the connection. |
|
|
273
|
+
| `scope:add(connection)` | Also accepts RBXScriptConnection. |
|
|
274
|
+
| `scope:destroy()` | Safe to call multiple times. |
|
|
275
|
+
|
|
276
|
+
## Groups
|
|
277
|
+
|
|
278
|
+
Named player sets. Members get removed automatically on `PlayerRemoving`. `Lync.createGroup(name)` returns a Group object.
|
|
279
|
+
|
|
280
|
+
```luau
|
|
281
|
+
local vips = Lync.createGroup("vips")
|
|
282
|
+
|
|
283
|
+
vips:add(player)
|
|
284
|
+
vips:remove(player)
|
|
285
|
+
vips:has(player)
|
|
286
|
+
|
|
287
|
+
packet:send(data, vips)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
| Method | Returns | What it does |
|
|
291
|
+
|:-------|:--------|:-------------|
|
|
292
|
+
| `group:add(player)` | `boolean` | `true` if added, `false` if already in. |
|
|
293
|
+
| `group:remove(player)` | `boolean` | `true` if removed, `false` if wasnt in there. |
|
|
294
|
+
| `group:has(player)` | `boolean` | Whether the player is in the group. |
|
|
295
|
+
| `group:count()` | `number` | Number of members. |
|
|
296
|
+
| `group:getSet()` | `{ [Player]: true }` | Snapshot of the internal set. |
|
|
297
|
+
| `group:forEach(fn)` | `()` | Calls `fn(player)` for each member. |
|
|
298
|
+
| `group:destroy()` | `()` | Removes the group and all memberships. |
|
|
299
|
+
|
|
300
|
+
## Middleware
|
|
301
|
+
|
|
302
|
+
Global intercept on all packets. Handlers run in the order you registered them. Return `Lync.DROP` from a handler to drop the packet. Return the data to pass it through.
|
|
303
|
+
|
|
304
|
+
```luau
|
|
305
|
+
Lync.onSend(function(data, name, player)
|
|
306
|
+
if shouldDrop(data) then
|
|
307
|
+
return Lync.DROP
|
|
308
|
+
end
|
|
309
|
+
data.timestamp = os.clock()
|
|
310
|
+
return data
|
|
311
|
+
end)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
| Function | What it does |
|
|
315
|
+
|:---------|:------------|
|
|
316
|
+
| `Lync.onSend(fn(data, name, player) → data \| Lync.DROP)` | Runs before a packet goes out. Returns a remover function. |
|
|
317
|
+
| `Lync.onReceive(fn(data, name, player) → data \| Lync.DROP)` | Runs when a packet comes in. Returns a remover function. |
|
|
318
|
+
| `Lync.onDrop(fn(player, reason, name, data))` | Fires when a packet gets rejected. Returns a remover function. Supports multiple handlers. Reason is `"nan"`, `"rate"`, `"validate"`, `"size"`, or whatever string your validate function returned. |
|
|
319
|
+
| `Lync.DROP` | Frozen sentinel. Return from middleware to drop the packet. |
|
|
320
|
+
|
|
321
|
+
Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
|
|
322
|
+
|
|
323
|
+
## Target Descriptors
|
|
324
|
+
|
|
325
|
+
Used as the second argument to `packet:send()` on the server.
|
|
326
|
+
|
|
327
|
+
| Target | What it does |
|
|
328
|
+
|:-------|:------------|
|
|
329
|
+
| `player` | Send to one player. |
|
|
330
|
+
| `Lync.all` | Send to all connected players. |
|
|
331
|
+
| `Lync.except(player, ...)` | Send to everyone except the specified players. |
|
|
332
|
+
| `{ p1, p2, ... }` | Send to a list of players. |
|
|
333
|
+
| `group` | Send to all members of a Group object. |
|
|
334
|
+
|
|
124
335
|
## Types
|
|
125
336
|
|
|
126
337
|
### Primitives
|
|
@@ -138,7 +349,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
|
|
|
138
349
|
| `Lync.f64` | 8 | IEEE 754 double |
|
|
139
350
|
| `Lync.bool` | 1 | true/false. Gets packed into bitfields when inside structs. |
|
|
140
351
|
|
|
141
|
-
###
|
|
352
|
+
### Datatypes
|
|
142
353
|
|
|
143
354
|
| Type | Bytes | What it is |
|
|
144
355
|
|:-----|------:|:-----------|
|
|
@@ -160,6 +371,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
|
|
|
160
371
|
| `Lync.ray` | 24 | Origin Vec3 + Direction Vec3 as 6x f32. |
|
|
161
372
|
| `Lync.numberSequence` | varint + N×12 | Varint count then (time f32 + value f32 + envelope f32) per keypoint. |
|
|
162
373
|
| `Lync.colorSequence` | varint + N×7 | Varint count then (time f32 + R u8 + G u8 + B u8) per keypoint. |
|
|
374
|
+
| `Lync.boundedString(maxLength)` | varint + N | Same wire format as `Lync.string` but rejects on read if length exceeds `maxLength`. |
|
|
163
375
|
|
|
164
376
|
### Composites
|
|
165
377
|
|
|
@@ -170,7 +382,7 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
|
|
|
170
382
|
| `Lync.map(keyCodec, valueCodec, maxCount?)` | Key-value pairs with varint count. Optional `maxCount` rejects on read if exceeded. |
|
|
171
383
|
| `Lync.optional(codec)` | 1 byte flag, value only if present. |
|
|
172
384
|
| `Lync.tuple(codec, codec, ...)` | Ordered positional values, no keys. |
|
|
173
|
-
| `Lync.
|
|
385
|
+
| `Lync.tagged(tagField, { name = codec })` | Discriminated union with a u8 variant tag. Puts `tagField` into the decoded table so you know which variant it is. |
|
|
174
386
|
|
|
175
387
|
### Delta
|
|
176
388
|
|
|
@@ -182,7 +394,7 @@ Reliable only. Lync will error if you try to use these with `unreliable = true`.
|
|
|
182
394
|
| `Lync.deltaArray(codec, maxCount?)` | Same idea but for arrays. Dirty elements get sent with varint indices. Optional `maxCount` rejects on read if exceeded. |
|
|
183
395
|
| `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. |
|
|
184
396
|
|
|
185
|
-
###
|
|
397
|
+
### Meta
|
|
186
398
|
|
|
187
399
|
| Constructor | What it does |
|
|
188
400
|
|:------------|:------------|
|
|
@@ -190,41 +402,11 @@ Reliable only. Lync will error if you try to use these with `unreliable = true`.
|
|
|
190
402
|
| `Lync.quantizedFloat(min, max, precision)` | Fixed-point compression. Picks u8/u16/u32 based on your range and precision. |
|
|
191
403
|
| `Lync.quantizedVec3(min, max, precision)` | Same thing but for all 3 components. |
|
|
192
404
|
| `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 }`. |
|
|
193
|
-
| `Lync.tagged(tagField, { name = codec })` | Discriminated union with a u8 variant tag. Puts `tagField` into the decoded table so you know which variant it is. |
|
|
194
405
|
| `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. |
|
|
195
406
|
| `Lync.nothing` | Zero bytes. Reads nil. Good for fire-and-forget signals. |
|
|
196
407
|
| `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. |
|
|
197
408
|
| `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. |
|
|
198
409
|
|
|
199
|
-
## Groups
|
|
200
|
-
|
|
201
|
-
Named player sets. Members get removed automatically on `PlayerRemoving`.
|
|
202
|
-
|
|
203
|
-
| Function | Returns | What it does |
|
|
204
|
-
|:---------|:--------|:-------------|
|
|
205
|
-
| `Lync.createGroup(name)` | | Makes a new group. Errors if it already exists. |
|
|
206
|
-
| `Lync.destroyGroup(name)` | | Removes the group and all memberships. |
|
|
207
|
-
| `Lync.addToGroup(name, player)` | `boolean` | `true` if added, `false` if already in. |
|
|
208
|
-
| `Lync.removeFromGroup(name, player)` | `boolean` | `true` if removed, `false` if wasnt in there. |
|
|
209
|
-
| `Lync.hasInGroup(name, player)` | `boolean` | |
|
|
210
|
-
| `Lync.groupCount(name)` | `number` | |
|
|
211
|
-
| `Lync.getGroupSet(name)` | `{ [Player]: true }` | |
|
|
212
|
-
| `Lync.forEachInGroup(name, fn)` | | Calls `fn(player)` for each member. |
|
|
213
|
-
|
|
214
|
-
Send to a group with `packet:sendToGroup(data, groupName)`.
|
|
215
|
-
|
|
216
|
-
## Middleware
|
|
217
|
-
|
|
218
|
-
Global intercept on all packets. Handlers run in the order you registered them. Return `nil` from a handler to drop the packet.
|
|
219
|
-
|
|
220
|
-
| Function | What it does |
|
|
221
|
-
|:---------|:------------|
|
|
222
|
-
| `Lync.onSend(fn(data, name, player) → data?)` | Runs before a packet goes out. Returns a remover function. |
|
|
223
|
-
| `Lync.onReceive(fn(data, name, player) → data?)` | Runs when a packet comes in. Returns a remover function. |
|
|
224
|
-
| `Lync.onDrop(fn(player, reason, name, data))` | Fires when a packet gets rejected. Returns a remover function. Supports multiple handlers. Reason is `"nan"`, `"rate"`, `"validate"`, `"size"`, or whatever string your validate function returned. |
|
|
225
|
-
|
|
226
|
-
Packets that fail validation are dropped individually. Other packets in the same frame from the same player are unaffected.
|
|
227
|
-
|
|
228
410
|
## Benchmarks
|
|
229
411
|
|
|
230
412
|
### Lync Tests
|
|
@@ -233,10 +415,10 @@ Packets that fail validation are dropped individually. Other packets in the same
|
|
|
233
415
|
|
|
234
416
|
| Scenario | Without Lync | With Lync | FPS |
|
|
235
417
|
|:---------|------------:|---------:|----:|
|
|
236
|
-
| Static booleans (1B) | 480 Kbps | **2.
|
|
237
|
-
| Static entities (34B) | 16,320 Kbps | **2.
|
|
238
|
-
| Moving entities | 16,320 Kbps | **3.
|
|
239
|
-
| Chaotic entities | 16,320 Kbps | **4.
|
|
418
|
+
| Static booleans (1B) | 480 Kbps | **2.34 Kbps** | 60.00 |
|
|
419
|
+
| Static entities (34B) | 16,320 Kbps | **2.62 Kbps** | 60.00 |
|
|
420
|
+
| Moving entities | 16,320 Kbps | **3.14 Kbps** | 60.00 |
|
|
421
|
+
| Chaotic entities | 16,320 Kbps | **4.76 Kbps** | 59.99 |
|
|
240
422
|
|
|
241
423
|
### Cross-Library Comparison
|
|
242
424
|
|
|
@@ -255,7 +437,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
|
|
|
255
437
|
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
256
438
|
|:------------|-------:|---:|----:|----:|----:|-----:|
|
|
257
439
|
| roblox | 559,364 | 559,364 | 676,715 | 676,715 | 676,715 | 784,081 |
|
|
258
|
-
| **lync** | **3.
|
|
440
|
+
| **lync** | **3.61** | 3.53 | 3.63 | 3.64 | 3.64 | 4.64 |
|
|
259
441
|
| blink | 41.81 | 26.30 | 42.40 | 42.48 | 42.48 | 42.62 |
|
|
260
442
|
| zap | 41.71 | 25.46 | 42.19 | 42.32 | 42.32 | 42.93 |
|
|
261
443
|
| bytenet | 41.64 | 22.84 | 42.36 | 42.82 | 42.82 | 43.24 |
|
|
@@ -265,7 +447,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
|
|
|
265
447
|
| Tool (FPS) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
266
448
|
|:-----------|-------:|---:|----:|----:|----:|-----:|
|
|
267
449
|
| roblox | 21.00 | 22.00 | 20.00 | 19.00 | 19.00 | 19.00 |
|
|
268
|
-
| **lync** | **60.00** | 61.00 | 60.00 |
|
|
450
|
+
| **lync** | **60.00** | 61.00 | 60.00 | 60.00 | 60.00 | 59.00 |
|
|
269
451
|
| blink | 97.00 | 98.00 | 97.00 | 96.00 | 96.00 | 96.00 |
|
|
270
452
|
| zap | 52.00 | 53.00 | 51.00 | 51.00 | 51.00 | 49.00 |
|
|
271
453
|
| bytenet | 35.00 | 37.00 | 35.00 | 35.00 | 35.00 | 34.00 |
|
|
@@ -273,7 +455,7 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
|
|
|
273
455
|
| Tool (Kbps) | Median | P0 | P80 | P90 | P95 | P100 |
|
|
274
456
|
|:------------|-------:|---:|----:|----:|----:|-----:|
|
|
275
457
|
| roblox | 353,107 | 196,826 | 690,747 | 842,240 | 842,240 | 1,124,176 |
|
|
276
|
-
| **lync** | **4.31** | 3.
|
|
458
|
+
| **lync** | **4.31** | 3.85 | 4.36 | 4.38 | 4.38 | 4.44 |
|
|
277
459
|
| blink | 7.91 | 7.41 | 7.93 | 7.99 | 7.99 | 8.00 |
|
|
278
460
|
| zap | 8.10 | 5.75 | 8.17 | 8.22 | 8.22 | 8.27 |
|
|
279
461
|
| bytenet | 8.11 | 5.07 | 8.35 | 8.46 | 8.46 | 8.47 |
|
package/package.json
CHANGED
package/src/Types.luau
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Shared type definitions.
|
|
3
|
+
-- Shared type definitions for the Lync networking library.
|
|
4
4
|
|
|
5
5
|
export type ChannelState = {
|
|
6
6
|
buff: buffer,
|
|
@@ -10,7 +10,7 @@ export type ChannelState = {
|
|
|
10
10
|
lastId: number,
|
|
11
11
|
countPos: number,
|
|
12
12
|
itemCount: number,
|
|
13
|
-
deltas: { [number]: any },
|
|
13
|
+
deltas: { [number]: any }, -- per-codec delta caches; shape varies by codec kind
|
|
14
14
|
prevDump: buffer?,
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -19,6 +19,17 @@ export type Codec<T> = {
|
|
|
19
19
|
read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
-- Extended codec exposing internal fast-path metadata used by composite codecs.
|
|
23
|
+
export type InternalCodec<T> = {
|
|
24
|
+
write: (ch: ChannelState, value: T) -> (),
|
|
25
|
+
read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
|
|
26
|
+
_size: number?,
|
|
27
|
+
_directWrite: ((b: buffer, offset: number, value: T) -> ())?,
|
|
28
|
+
_directRead: ((b: buffer, offset: number) -> T)?,
|
|
29
|
+
_isDelta: boolean?,
|
|
30
|
+
_isBool: boolean?,
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export type RateLimitConfig = {
|
|
23
34
|
maxPerSecond: number,
|
|
24
35
|
burstAllowance: number?,
|
package/src/api/Group.luau
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
local Players = game:GetService ("Players")
|
|
6
6
|
|
|
7
|
-
-- State
|
|
7
|
+
-- State --------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
local _groups = {} :: { [string]: { [Player]: true } }
|
|
10
10
|
local _counts = {} :: { [string]: number }
|
|
11
11
|
local _playerGroups = {} :: { [Player]: { [string]: true } }
|
|
12
12
|
|
|
13
|
-
-- Private
|
|
13
|
+
-- Private ------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
local function onPlayerRemoving (player: Player): ()
|
|
16
16
|
local memberships = _playerGroups[player]
|
|
@@ -31,43 +31,16 @@ end
|
|
|
31
31
|
|
|
32
32
|
Players.PlayerRemoving:Connect (onPlayerRemoving)
|
|
33
33
|
|
|
34
|
-
local
|
|
34
|
+
local GroupImpl = {}
|
|
35
|
+
GroupImpl.__index = GroupImpl
|
|
36
|
+
|
|
37
|
+
function GroupImpl.add (self: any, player: Player): boolean
|
|
38
|
+
local name = self._name
|
|
35
39
|
local set = _groups[name]
|
|
36
40
|
if not set then
|
|
37
|
-
error (`[Lync] Group
|
|
38
|
-
end
|
|
39
|
-
return set
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
-- Public -----------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
local Group = {}
|
|
45
|
-
|
|
46
|
-
function Group.create (name: string): ()
|
|
47
|
-
if _groups[name] then
|
|
48
|
-
error (`[Lync] Group already exists: \"{name}\"`)
|
|
49
|
-
end
|
|
50
|
-
_groups[name] = {}
|
|
51
|
-
_counts[name] = 0
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
function Group.destroy (name: string): ()
|
|
55
|
-
local set = getSetOrError (name)
|
|
56
|
-
|
|
57
|
-
for player in set do
|
|
58
|
-
local memberships = _playerGroups[player]
|
|
59
|
-
if memberships then
|
|
60
|
-
memberships[name] = nil
|
|
61
|
-
end
|
|
41
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
62
42
|
end
|
|
63
43
|
|
|
64
|
-
_groups[name] = nil
|
|
65
|
-
_counts[name] = nil
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
function Group.add (name: string, player: Player): boolean
|
|
69
|
-
local set = getSetOrError (name)
|
|
70
|
-
|
|
71
44
|
if set[player] then
|
|
72
45
|
return false
|
|
73
46
|
end
|
|
@@ -85,8 +58,12 @@ function Group.add (name: string, player: Player): boolean
|
|
|
85
58
|
return true
|
|
86
59
|
end
|
|
87
60
|
|
|
88
|
-
function
|
|
89
|
-
local
|
|
61
|
+
function GroupImpl.remove (self: any, player: Player): boolean
|
|
62
|
+
local name = self._name
|
|
63
|
+
local set = _groups[name]
|
|
64
|
+
if not set then
|
|
65
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
66
|
+
end
|
|
90
67
|
|
|
91
68
|
if not set[player] then
|
|
92
69
|
return false
|
|
@@ -103,24 +80,78 @@ function Group.remove (name: string, player: Player): boolean
|
|
|
103
80
|
return true
|
|
104
81
|
end
|
|
105
82
|
|
|
106
|
-
function
|
|
107
|
-
local
|
|
83
|
+
function GroupImpl.has (self: any, player: Player): boolean
|
|
84
|
+
local name = self._name
|
|
85
|
+
local set = _groups[name]
|
|
86
|
+
if not set then
|
|
87
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
88
|
+
end
|
|
108
89
|
return set[player] == true
|
|
109
90
|
end
|
|
110
91
|
|
|
111
|
-
function
|
|
112
|
-
|
|
92
|
+
function GroupImpl.count (self: any): number
|
|
93
|
+
local name = self._name
|
|
94
|
+
if not _groups[name] then
|
|
95
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
96
|
+
end
|
|
113
97
|
return _counts[name]
|
|
114
98
|
end
|
|
115
99
|
|
|
116
|
-
function
|
|
117
|
-
|
|
100
|
+
function GroupImpl.forEach (self: any, fn: (player: Player) -> ()): ()
|
|
101
|
+
local name = self._name
|
|
102
|
+
local set = _groups[name]
|
|
103
|
+
if not set then
|
|
104
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
105
|
+
end
|
|
106
|
+
for player in set do
|
|
107
|
+
fn (player)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
function GroupImpl.getSet (self: any): { [Player]: true }
|
|
112
|
+
local name = self._name
|
|
113
|
+
local set = _groups[name]
|
|
114
|
+
if not set then
|
|
115
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
116
|
+
end
|
|
117
|
+
return set
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
function GroupImpl.destroy (self: any): ()
|
|
121
|
+
local name = self._name
|
|
122
|
+
local set = _groups[name]
|
|
123
|
+
if not set then
|
|
124
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
for player in set do
|
|
128
|
+
local memberships = _playerGroups[player]
|
|
129
|
+
if memberships then
|
|
130
|
+
memberships[name] = nil
|
|
131
|
+
end
|
|
123
132
|
end
|
|
133
|
+
|
|
134
|
+
_groups[name] = nil
|
|
135
|
+
_counts[name] = nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
table.freeze (GroupImpl)
|
|
139
|
+
|
|
140
|
+
-- Public -------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
local Group = {}
|
|
143
|
+
|
|
144
|
+
function Group.create (name: string): any
|
|
145
|
+
if _groups[name] then
|
|
146
|
+
error (`[Lync] Group already exists: "{name}"`)
|
|
147
|
+
end
|
|
148
|
+
_groups[name] = {}
|
|
149
|
+
_counts[name] = 0
|
|
150
|
+
|
|
151
|
+
return setmetatable ({
|
|
152
|
+
_tag = "group",
|
|
153
|
+
_name = name,
|
|
154
|
+
}, GroupImpl)
|
|
124
155
|
end
|
|
125
156
|
|
|
126
157
|
return table.freeze (Group)
|