@axpecter/lync 1.3.1 → 1.4.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 +239 -57
- package/package.json +1 -1
- package/src/api/Group.luau +77 -44
- package/src/api/Namespace.luau +7 -0
- package/src/api/Packet.luau +43 -22
- package/src/api/Query.luau +107 -83
- package/src/api/Scope.luau +73 -0
- package/src/index.d.ts +65 -26
- package/src/init.luau +25 -9
- package/src/internal/Middleware.luau +10 -1
- package/src/transport/Server.luau +7 -8
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.0"
|
|
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** (ReplicatedStorage, required by both)
|
|
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.0"` |
|
|
40
167
|
|
|
41
168
|
## Packets
|
|
42
169
|
|
|
@@ -50,23 +177,24 @@ 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
|
|:-------|:------------|
|
|
@@ -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
|
|
|
@@ -121,6 +251,87 @@ Returned by `packet:listen()`, `packet:once()`, `query:listen()`, and `ns:listen
|
|
|
121
251
|
| `connection.connected` | `true` if still connected, `false` after disconnect. |
|
|
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)` | Track a raw Connection or RBXScriptConnection. |
|
|
274
|
+
| `scope:destroy()` | Disconnects all tracked connections. 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` | |
|
|
295
|
+
| `group:count()` | `number` | |
|
|
296
|
+
| `group:getSet()` | `{ [Player]: true }` | |
|
|
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
|
package/package.json
CHANGED
package/src/api/Group.luau
CHANGED
|
@@ -31,43 +31,18 @@ end
|
|
|
31
31
|
|
|
32
32
|
Players.PlayerRemoving:Connect (onPlayerRemoving)
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
local set = _groups[name]
|
|
36
|
-
if not set then
|
|
37
|
-
error (`[Lync] Group does not exist: \"{name}\"`)
|
|
38
|
-
end
|
|
39
|
-
return set
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
-- Public -----------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
local Group = {}
|
|
34
|
+
-- GroupImpl ---------------------------------------------------------------
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
error (`[Lync] Group already exists: \"{name}\"`)
|
|
49
|
-
end
|
|
50
|
-
_groups[name] = {}
|
|
51
|
-
_counts[name] = 0
|
|
52
|
-
end
|
|
36
|
+
local GroupImpl = {}
|
|
37
|
+
GroupImpl.__index = GroupImpl
|
|
53
38
|
|
|
54
|
-
function
|
|
55
|
-
local
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if memberships then
|
|
60
|
-
memberships[name] = nil
|
|
61
|
-
end
|
|
39
|
+
function GroupImpl.add (self: any, player: Player): boolean
|
|
40
|
+
local name = self._name
|
|
41
|
+
local set = _groups[name]
|
|
42
|
+
if not set then
|
|
43
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
62
44
|
end
|
|
63
45
|
|
|
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
46
|
if set[player] then
|
|
72
47
|
return false
|
|
73
48
|
end
|
|
@@ -85,8 +60,12 @@ function Group.add (name: string, player: Player): boolean
|
|
|
85
60
|
return true
|
|
86
61
|
end
|
|
87
62
|
|
|
88
|
-
function
|
|
89
|
-
local
|
|
63
|
+
function GroupImpl.remove (self: any, player: Player): boolean
|
|
64
|
+
local name = self._name
|
|
65
|
+
local set = _groups[name]
|
|
66
|
+
if not set then
|
|
67
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
68
|
+
end
|
|
90
69
|
|
|
91
70
|
if not set[player] then
|
|
92
71
|
return false
|
|
@@ -103,24 +82,78 @@ function Group.remove (name: string, player: Player): boolean
|
|
|
103
82
|
return true
|
|
104
83
|
end
|
|
105
84
|
|
|
106
|
-
function
|
|
107
|
-
local
|
|
85
|
+
function GroupImpl.has (self: any, player: Player): boolean
|
|
86
|
+
local name = self._name
|
|
87
|
+
local set = _groups[name]
|
|
88
|
+
if not set then
|
|
89
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
90
|
+
end
|
|
108
91
|
return set[player] == true
|
|
109
92
|
end
|
|
110
93
|
|
|
111
|
-
function
|
|
112
|
-
|
|
94
|
+
function GroupImpl.count (self: any): number
|
|
95
|
+
local name = self._name
|
|
96
|
+
if not _groups[name] then
|
|
97
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
98
|
+
end
|
|
113
99
|
return _counts[name]
|
|
114
100
|
end
|
|
115
101
|
|
|
116
|
-
function
|
|
117
|
-
|
|
102
|
+
function GroupImpl.forEach (self: any, fn: (player: Player) -> ()): ()
|
|
103
|
+
local name = self._name
|
|
104
|
+
local set = _groups[name]
|
|
105
|
+
if not set then
|
|
106
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
107
|
+
end
|
|
108
|
+
for player in set do
|
|
109
|
+
fn (player)
|
|
110
|
+
end
|
|
118
111
|
end
|
|
119
112
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
function GroupImpl.getSet (self: any): { [Player]: true }
|
|
114
|
+
local name = self._name
|
|
115
|
+
local set = _groups[name]
|
|
116
|
+
if not set then
|
|
117
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
118
|
+
end
|
|
119
|
+
return set
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
function GroupImpl.destroy (self: any): ()
|
|
123
|
+
local name = self._name
|
|
124
|
+
local set = _groups[name]
|
|
125
|
+
if not set then
|
|
126
|
+
error (`[Lync] Group has been destroyed: "{name}"`)
|
|
123
127
|
end
|
|
128
|
+
|
|
129
|
+
for player in set do
|
|
130
|
+
local memberships = _playerGroups[player]
|
|
131
|
+
if memberships then
|
|
132
|
+
memberships[name] = nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
_groups[name] = nil
|
|
137
|
+
_counts[name] = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
table.freeze (GroupImpl)
|
|
141
|
+
|
|
142
|
+
-- Public -----------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
local Group = {}
|
|
145
|
+
|
|
146
|
+
function Group.create (name: string): any
|
|
147
|
+
if _groups[name] then
|
|
148
|
+
error (`[Lync] Group already exists: "{name}"`)
|
|
149
|
+
end
|
|
150
|
+
_groups[name] = {}
|
|
151
|
+
_counts[name] = 0
|
|
152
|
+
|
|
153
|
+
return setmetatable ({
|
|
154
|
+
_tag = "group",
|
|
155
|
+
_name = name,
|
|
156
|
+
}, GroupImpl)
|
|
124
157
|
end
|
|
125
158
|
|
|
126
159
|
return table.freeze (Group)
|
package/src/api/Namespace.luau
CHANGED
|
@@ -194,6 +194,8 @@ function Namespace.define (name: string, config: NamespaceConfig): any
|
|
|
194
194
|
|
|
195
195
|
local packets = {} :: { [string]: any }
|
|
196
196
|
local queries = {} :: { [string]: any }
|
|
197
|
+
local packetsByShort = {} :: { [string]: any }
|
|
198
|
+
local queriesByShort = {} :: { [string]: any }
|
|
197
199
|
local fields = {} :: { [string]: any }
|
|
198
200
|
|
|
199
201
|
fields._name = name
|
|
@@ -207,6 +209,7 @@ function Namespace.define (name: string, config: NamespaceConfig): any
|
|
|
207
209
|
local fullName = prefixName (name, shortName)
|
|
208
210
|
local packet = Packet.define (fullName, packetConfig)
|
|
209
211
|
packets[fullName] = packet
|
|
212
|
+
packetsByShort[shortName] = packet
|
|
210
213
|
fields[shortName] = packet
|
|
211
214
|
end
|
|
212
215
|
end
|
|
@@ -216,10 +219,14 @@ function Namespace.define (name: string, config: NamespaceConfig): any
|
|
|
216
219
|
local fullName = prefixName (name, shortName)
|
|
217
220
|
local query = Query.define (fullName, queryConfig)
|
|
218
221
|
queries[fullName] = query
|
|
222
|
+
queriesByShort[shortName] = query
|
|
219
223
|
fields[shortName] = query
|
|
220
224
|
end
|
|
221
225
|
end
|
|
222
226
|
|
|
227
|
+
fields.packets = table.freeze (packetsByShort)
|
|
228
|
+
fields.queries = table.freeze (queriesByShort)
|
|
229
|
+
|
|
223
230
|
return setmetatable (fields, NamespaceImpl)
|
|
224
231
|
end
|
|
225
232
|
|
package/src/api/Packet.luau
CHANGED
|
@@ -21,7 +21,7 @@ local serverWriteTo = Server.writeTo
|
|
|
21
21
|
local serverWriteToAll = Server.writeToAll
|
|
22
22
|
local serverWriteToList = Server.writeToList
|
|
23
23
|
local serverWriteToAllExcept = Server.writeToAllExcept
|
|
24
|
-
local
|
|
24
|
+
local serverWriteToSet = Server.writeToSet
|
|
25
25
|
local clientWrite = Client.write
|
|
26
26
|
|
|
27
27
|
-- Private ----------------------------------------------------------------
|
|
@@ -45,7 +45,7 @@ function SharedImpl.disconnectAll (self: any): ()
|
|
|
45
45
|
self._signal:disconnectAll ()
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
-- Server-only metatable
|
|
48
|
+
-- Server-only metatable. Single send() dispatches on target type.
|
|
49
49
|
local ServerImpl = setmetatable ({}, { __index = SharedImpl })
|
|
50
50
|
ServerImpl.__index = ServerImpl
|
|
51
51
|
|
|
@@ -64,34 +64,55 @@ local function checkDeltaMode (self: any, mode: number): ()
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
function ServerImpl.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
end
|
|
67
|
+
function ServerImpl.send (self: any, data: any, target: any?): ()
|
|
68
|
+
if target == nil then
|
|
69
|
+
error ("[Lync] Server packet:send requires a target")
|
|
70
|
+
end
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
local id = self._id
|
|
73
|
+
local name = self._name
|
|
74
|
+
local codec = self._codec
|
|
75
|
+
local isUnreliable = self._isUnreliable
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
-- Single player
|
|
78
|
+
if typeof (target) == "Instance" then
|
|
79
|
+
checkDeltaMode (self, DELTA_TARGETED)
|
|
80
|
+
serverWriteTo (target :: Player, id, name, codec, data, isUnreliable)
|
|
81
|
+
return
|
|
82
|
+
end
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
serverWriteToAllExcept (except, self._id, self._name, self._codec, data, self._isUnreliable)
|
|
85
|
-
end
|
|
84
|
+
-- Must be a table from here
|
|
85
|
+
local tag = target._tag
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
-- Lync.all sentinel
|
|
88
|
+
if tag == "all" then
|
|
89
|
+
checkDeltaMode (self, DELTA_BROADCAST)
|
|
90
|
+
serverWriteToAll (id, name, codec, data, isUnreliable)
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
-- Lync.except(player, ...) descriptor
|
|
95
|
+
if tag == "except" then
|
|
96
|
+
checkDeltaMode (self, DELTA_BROADCAST)
|
|
97
|
+
serverWriteToAllExcept (target._set, id, name, codec, data, isUnreliable)
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
-- Group object
|
|
102
|
+
if tag == "group" then
|
|
103
|
+
checkDeltaMode (self, DELTA_BROADCAST)
|
|
104
|
+
serverWriteToSet (target:getSet (), id, name, codec, data, isUnreliable)
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
-- Player list (array)
|
|
109
|
+
checkDeltaMode (self, DELTA_TARGETED)
|
|
110
|
+
serverWriteToList (target :: { Player }, id, name, codec, data, isUnreliable)
|
|
90
111
|
end
|
|
91
112
|
|
|
92
113
|
table.freeze (ServerImpl)
|
|
93
114
|
|
|
94
|
-
-- Client-only metatable
|
|
115
|
+
-- Client-only metatable. Has send(), no target needed.
|
|
95
116
|
local ClientImpl = setmetatable ({}, { __index = SharedImpl })
|
|
96
117
|
ClientImpl.__index = ClientImpl
|
|
97
118
|
|
package/src/api/Query.luau
CHANGED
|
@@ -6,7 +6,6 @@ local Players = game:GetService ("Players")
|
|
|
6
6
|
local RunService = game:GetService ("RunService")
|
|
7
7
|
|
|
8
8
|
local Client = require (script.Parent.Parent.transport.Client)
|
|
9
|
-
local Group = require (script.Parent.Group)
|
|
10
9
|
local Registry = require (script.Parent.Parent.internal.Registry)
|
|
11
10
|
local Server = require (script.Parent.Parent.transport.Server)
|
|
12
11
|
local Signal = require (script.Parent.Signal)
|
|
@@ -82,90 +81,35 @@ local function onTimeout (correlation: number): ()
|
|
|
82
81
|
completeQuery (correlation, nil)
|
|
83
82
|
end
|
|
84
83
|
|
|
85
|
-
--
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
function QueryImpl.listen (self: any, callback: (...any) -> any): Connection
|
|
91
|
-
local respId = self._respReg.id
|
|
92
|
-
local respName = self._name
|
|
93
|
-
local respCodec = self._respReg.codec
|
|
94
|
-
|
|
95
|
-
if IS_SERVER then
|
|
96
|
-
return self._reqReg.signal:connect (
|
|
97
|
-
function (request: any, player: Player, correlation: number): ()
|
|
98
|
-
local ok, response = pcall (callback, request, player)
|
|
99
|
-
if not ok then
|
|
100
|
-
warn (`[Lync] Query handler error on "{respName}": {response}`)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
if ok and response ~= nil then
|
|
104
|
-
Server.writeQuery (player, respId, respName, correlation, respCodec, response)
|
|
105
|
-
else
|
|
106
|
-
Server.writeQueryNil (player, respId, respName, correlation)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
)
|
|
110
|
-
else
|
|
111
|
-
return self._reqReg.signal:connect (
|
|
112
|
-
function (request: any, _player: Player?, correlation: number): ()
|
|
113
|
-
local ok, response = pcall (callback, request)
|
|
114
|
-
if not ok then
|
|
115
|
-
warn (`[Lync] Query handler error on "{respName}": {response}`)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
if ok and response ~= nil then
|
|
119
|
-
Client.writeQuery (respId, respName, correlation, respCodec, response)
|
|
120
|
-
else
|
|
121
|
-
Client.writeQueryNil (respId, respName, correlation)
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
)
|
|
84
|
+
-- Send a query to multiple players and yield. Returns { [Player]: response? }.
|
|
85
|
+
local function requestMulti (self: any, players: { Player }, data: any): { [Player]: any? }
|
|
86
|
+
if not IS_SERVER then
|
|
87
|
+
error ("[Lync] Multi-request is server-only")
|
|
125
88
|
end
|
|
126
|
-
end
|
|
127
89
|
|
|
128
|
-
|
|
129
|
-
if
|
|
130
|
-
|
|
90
|
+
local count = #players
|
|
91
|
+
if count == 0 then
|
|
92
|
+
return {}
|
|
131
93
|
end
|
|
132
94
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
95
|
+
if count == 1 then
|
|
96
|
+
-- Delegate to single-player path
|
|
97
|
+
local correlation = allocCorrelation ()
|
|
98
|
+
local thread = coroutine.running ()
|
|
99
|
+
local entry: PendingQuery = { thread = thread, timeout = nil }
|
|
100
|
+
_pending[correlation] = entry
|
|
138
101
|
|
|
139
|
-
if IS_SERVER then
|
|
140
102
|
Server.writeQuery (
|
|
141
|
-
|
|
103
|
+
players[1],
|
|
142
104
|
self._reqReg.id,
|
|
143
105
|
self._name,
|
|
144
106
|
correlation,
|
|
145
107
|
self._reqReg.codec,
|
|
146
|
-
|
|
108
|
+
data
|
|
147
109
|
)
|
|
148
|
-
else
|
|
149
|
-
Client.writeQuery (self._reqReg.id, self._name, correlation, self._reqReg.codec, request)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
entry.timeout = task.delay (self._timeout, onTimeout, correlation)
|
|
153
|
-
return coroutine.yield ()
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
-- Send a query to multiple players and yield. Returns { [Player]: response? }.
|
|
157
|
-
local function invokeMulti (self: any, players: { Player }, request: any): { [Player]: any? }
|
|
158
|
-
if not IS_SERVER then
|
|
159
|
-
error ("[Lync] Multi-invoke is server-only")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
local count = #players
|
|
163
|
-
if count == 0 then
|
|
164
|
-
return {}
|
|
165
|
-
end
|
|
166
110
|
|
|
167
|
-
|
|
168
|
-
local result =
|
|
111
|
+
entry.timeout = task.delay (self._timeout, onTimeout, correlation)
|
|
112
|
+
local result = coroutine.yield ()
|
|
169
113
|
return { [players[1]] = result }
|
|
170
114
|
end
|
|
171
115
|
|
|
@@ -187,8 +131,8 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
|
|
|
187
131
|
|
|
188
132
|
local entry: PendingQuery = {
|
|
189
133
|
thread = nil,
|
|
190
|
-
callback = function (
|
|
191
|
-
results[player] =
|
|
134
|
+
callback = function (resp: any): ()
|
|
135
|
+
results[player] = resp
|
|
192
136
|
remaining -= 1
|
|
193
137
|
if remaining == 0 and not finished then
|
|
194
138
|
finished = true
|
|
@@ -200,7 +144,7 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
|
|
|
200
144
|
|
|
201
145
|
_pending[correlation] = entry
|
|
202
146
|
|
|
203
|
-
Server.writeQuery (player, reqId, reqName, correlation, reqCodec,
|
|
147
|
+
Server.writeQuery (player, reqId, reqName, correlation, reqCodec, data)
|
|
204
148
|
end
|
|
205
149
|
|
|
206
150
|
local timeoutThread = task.delay (timeout, function (): ()
|
|
@@ -232,22 +176,102 @@ local function invokeMulti (self: any, players: { Player }, request: any): { [Pl
|
|
|
232
176
|
return coroutine.yield ()
|
|
233
177
|
end
|
|
234
178
|
|
|
235
|
-
|
|
179
|
+
-- Query ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
local QueryImpl = {}
|
|
182
|
+
QueryImpl.__index = QueryImpl
|
|
183
|
+
|
|
184
|
+
function QueryImpl.listen (self: any, callback: (...any) -> any): Connection
|
|
185
|
+
local respId = self._respReg.id
|
|
186
|
+
local respName = self._name
|
|
187
|
+
local respCodec = self._respReg.codec
|
|
188
|
+
|
|
189
|
+
if IS_SERVER then
|
|
190
|
+
return self._reqReg.signal:connect (
|
|
191
|
+
function (request: any, player: Player, correlation: number): ()
|
|
192
|
+
local ok, response = pcall (callback, request, player)
|
|
193
|
+
if not ok then
|
|
194
|
+
warn (`[Lync] Query handler error on "{respName}": {response}`)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if ok and response ~= nil then
|
|
198
|
+
Server.writeQuery (player, respId, respName, correlation, respCodec, response)
|
|
199
|
+
else
|
|
200
|
+
Server.writeQueryNil (player, respId, respName, correlation)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
)
|
|
204
|
+
else
|
|
205
|
+
return self._reqReg.signal:connect (
|
|
206
|
+
function (request: any, _player: Player?, correlation: number): ()
|
|
207
|
+
local ok, response = pcall (callback, request)
|
|
208
|
+
if not ok then
|
|
209
|
+
warn (`[Lync] Query handler error on "{respName}": {response}`)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if ok and response ~= nil then
|
|
213
|
+
Client.writeQuery (respId, respName, correlation, respCodec, response)
|
|
214
|
+
else
|
|
215
|
+
Client.writeQueryNil (respId, respName, correlation)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
-- Client: send request to server, yield until response.
|
|
223
|
+
function QueryImpl.request (self: any, data: any): any
|
|
224
|
+
if IS_SERVER then
|
|
225
|
+
error ("[Lync] query:request is client-only. Use query:requestFrom on server")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
local correlation = allocCorrelation ()
|
|
229
|
+
local thread = coroutine.running ()
|
|
230
|
+
local entry: PendingQuery = { thread = thread, timeout = nil }
|
|
231
|
+
_pending[correlation] = entry
|
|
232
|
+
|
|
233
|
+
Client.writeQuery (self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
|
|
234
|
+
|
|
235
|
+
entry.timeout = task.delay (self._timeout, onTimeout, correlation)
|
|
236
|
+
return coroutine.yield ()
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
-- Server: send request to one player, yield until response.
|
|
240
|
+
function QueryImpl.requestFrom (self: any, player: Player, data: any): any
|
|
241
|
+
if not IS_SERVER then
|
|
242
|
+
error ("[Lync] query:requestFrom is server-only. Use query:request on client")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
local correlation = allocCorrelation ()
|
|
246
|
+
local thread = coroutine.running ()
|
|
247
|
+
local entry: PendingQuery = { thread = thread, timeout = nil }
|
|
248
|
+
_pending[correlation] = entry
|
|
249
|
+
|
|
250
|
+
Server.writeQuery (player, self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
|
|
251
|
+
|
|
252
|
+
entry.timeout = task.delay (self._timeout, onTimeout, correlation)
|
|
253
|
+
return coroutine.yield ()
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
-- Server: send request to all players, yield until all respond or timeout.
|
|
257
|
+
function QueryImpl.requestAll (self: any, data: any): { [Player]: any? }
|
|
236
258
|
local players = Players:GetPlayers ()
|
|
237
|
-
return
|
|
259
|
+
return requestMulti (self, players, data)
|
|
238
260
|
end
|
|
239
261
|
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
-- Server: send request to a list of players.
|
|
263
|
+
function QueryImpl.requestList (self: any, players: { Player }, data: any): { [Player]: any? }
|
|
264
|
+
return requestMulti (self, players, data)
|
|
242
265
|
end
|
|
243
266
|
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
-- Server: send request to all players in a group.
|
|
268
|
+
function QueryImpl.requestGroup (self: any, group: any, data: any): { [Player]: any? }
|
|
269
|
+
local set = group:getSet ()
|
|
246
270
|
local players = {} :: { Player }
|
|
247
271
|
for player in set do
|
|
248
272
|
table.insert (players, player)
|
|
249
273
|
end
|
|
250
|
-
return
|
|
274
|
+
return requestMulti (self, players, data)
|
|
251
275
|
end
|
|
252
276
|
|
|
253
277
|
-- Public -----------------------------------------------------------------
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!optimize 2
|
|
3
|
+
-- Batched connection lifecycle management.
|
|
4
|
+
|
|
5
|
+
-- Private ----------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
local ScopeImpl = {}
|
|
8
|
+
ScopeImpl.__index = ScopeImpl
|
|
9
|
+
|
|
10
|
+
function ScopeImpl.add (self: any, conn: any): ()
|
|
11
|
+
if not conn then
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
local entries = self._entries
|
|
16
|
+
local count = self._count + 1
|
|
17
|
+
self._count = count
|
|
18
|
+
entries[count] = conn
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
function ScopeImpl.listen (self: any, source: any, callback: (...any) -> ()): ()
|
|
22
|
+
local conn = source:listen (callback)
|
|
23
|
+
local entries = self._entries
|
|
24
|
+
local count = self._count + 1
|
|
25
|
+
self._count = count
|
|
26
|
+
entries[count] = conn
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
function ScopeImpl.once (self: any, source: any, callback: (...any) -> ()): ()
|
|
30
|
+
local conn = source:once (callback)
|
|
31
|
+
local entries = self._entries
|
|
32
|
+
local count = self._count + 1
|
|
33
|
+
self._count = count
|
|
34
|
+
entries[count] = conn
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
function ScopeImpl.listenAll (self: any, namespace: any, callback: (...any) -> ()): ()
|
|
38
|
+
local conn = namespace:listenAll (callback)
|
|
39
|
+
local entries = self._entries
|
|
40
|
+
local count = self._count + 1
|
|
41
|
+
self._count = count
|
|
42
|
+
entries[count] = conn
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
function ScopeImpl.destroy (self: any): ()
|
|
46
|
+
local entries = self._entries
|
|
47
|
+
local count = self._count
|
|
48
|
+
|
|
49
|
+
for i = 1, count do
|
|
50
|
+
local conn = entries[i]
|
|
51
|
+
entries[i] = nil
|
|
52
|
+
|
|
53
|
+
if typeof (conn) == "RBXScriptConnection" then
|
|
54
|
+
conn:Disconnect ()
|
|
55
|
+
elseif conn.disconnect then
|
|
56
|
+
conn:disconnect ()
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
self._count = 0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
table.freeze (ScopeImpl)
|
|
64
|
+
|
|
65
|
+
-- Public -----------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
local Scope = {}
|
|
68
|
+
|
|
69
|
+
function Scope.create (): any
|
|
70
|
+
return setmetatable ({ _entries = {}, _count = 0 }, ScopeImpl)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
return table.freeze (Scope)
|
package/src/index.d.ts
CHANGED
|
@@ -72,6 +72,46 @@ type InferBitfield<S extends Record<string, FieldSpec>> = Prettify<{
|
|
|
72
72
|
[K in keyof S]: InferFieldSpec<S[K]>;
|
|
73
73
|
}>;
|
|
74
74
|
|
|
75
|
+
// -- Target descriptors ------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export interface AllTarget {
|
|
78
|
+
readonly _tag: "all";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ExceptTarget {
|
|
82
|
+
readonly _tag: "except";
|
|
83
|
+
readonly _set: ReadonlyMap<Player, true>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface GroupObject {
|
|
87
|
+
readonly _tag: "group";
|
|
88
|
+
add(this: GroupObject, player: Player): boolean;
|
|
89
|
+
remove(this: GroupObject, player: Player): boolean;
|
|
90
|
+
has(this: GroupObject, player: Player): boolean;
|
|
91
|
+
count(this: GroupObject): number;
|
|
92
|
+
forEach(this: GroupObject, fn: (player: Player) => void): void;
|
|
93
|
+
getSet(this: GroupObject): ReadonlyMap<Player, true>;
|
|
94
|
+
destroy(this: GroupObject): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type Target = Player | AllTarget | ExceptTarget | GroupObject | Player[];
|
|
98
|
+
|
|
99
|
+
// -- Scope -------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export interface Scope {
|
|
102
|
+
add(this: Scope, conn: Connection | RBXScriptConnection): void;
|
|
103
|
+
listen<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
|
|
104
|
+
once<T>(this: Scope, source: Packet<T>, callback: (data: T, sender: Player | undefined) => void): void;
|
|
105
|
+
listenAll(this: Scope, namespace: Namespace, callback: (name: string, data: unknown, sender: Player | undefined) => void): void;
|
|
106
|
+
destroy(this: Scope): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -- DROP sentinel -----------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export interface DropSentinel {
|
|
112
|
+
readonly _tag: "drop";
|
|
113
|
+
}
|
|
114
|
+
|
|
75
115
|
// -- Packet ------------------------------------------------------------
|
|
76
116
|
|
|
77
117
|
export interface PacketConfig<T> {
|
|
@@ -82,14 +122,8 @@ export interface PacketConfig<T> {
|
|
|
82
122
|
maxPayloadBytes?: number;
|
|
83
123
|
}
|
|
84
124
|
|
|
85
|
-
// Server methods throw on client and vice versa.
|
|
86
125
|
export interface Packet<T> {
|
|
87
|
-
|
|
88
|
-
sendToAll(this: Packet<T>, data: T): void;
|
|
89
|
-
sendToAllExcept(this: Packet<T>, data: T, except: Player): void;
|
|
90
|
-
sendToList(this: Packet<T>, data: T, players: Player[]): void;
|
|
91
|
-
sendToGroup(this: Packet<T>, data: T, groupName: string): void;
|
|
92
|
-
send(this: Packet<T>, data: T): void;
|
|
126
|
+
send(this: Packet<T>, data: T, target?: Target): void;
|
|
93
127
|
listen(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
|
|
94
128
|
once(this: Packet<T>, callback: (data: T, sender: Player | undefined) => void): Connection;
|
|
95
129
|
wait(this: Packet<T>): LuaTuple<[T, Player | undefined]>;
|
|
@@ -111,17 +145,18 @@ export interface Query<Req, Resp> {
|
|
|
111
145
|
this: Query<Req, Resp>,
|
|
112
146
|
callback: (request: Req, player: Player) => Resp | undefined,
|
|
113
147
|
): Connection;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
request(this: Query<Req, Resp>, data: Req): Resp | undefined;
|
|
149
|
+
requestFrom(this: Query<Req, Resp>, player: Player, data: Req): Resp | undefined;
|
|
150
|
+
requestAll(this: Query<Req, Resp>, data: Req): Map<Player, Resp | undefined>;
|
|
151
|
+
requestList(
|
|
117
152
|
this: Query<Req, Resp>,
|
|
118
|
-
request: Req,
|
|
119
153
|
players: Player[],
|
|
154
|
+
data: Req,
|
|
120
155
|
): Map<Player, Resp | undefined>;
|
|
121
|
-
|
|
156
|
+
requestGroup(
|
|
122
157
|
this: Query<Req, Resp>,
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
group: GroupObject,
|
|
159
|
+
data: Req,
|
|
125
160
|
): Map<Player, Resp | undefined>;
|
|
126
161
|
}
|
|
127
162
|
|
|
@@ -143,6 +178,8 @@ type InferQueries<Q extends Record<string, QueryConfig<unknown, unknown>>> = {
|
|
|
143
178
|
};
|
|
144
179
|
|
|
145
180
|
export interface Namespace {
|
|
181
|
+
readonly packets: Record<string, Packet<unknown>>;
|
|
182
|
+
readonly queries: Record<string, Query<unknown, unknown>>;
|
|
146
183
|
listenAll(
|
|
147
184
|
this: Namespace,
|
|
148
185
|
callback: (name: string, data: unknown, sender: Player | undefined) => void,
|
|
@@ -166,7 +203,6 @@ export interface Namespace {
|
|
|
166
203
|
declare namespace Lync {
|
|
167
204
|
// Lifecycle
|
|
168
205
|
export const VERSION: string;
|
|
169
|
-
export const version: string;
|
|
170
206
|
export function start(): void;
|
|
171
207
|
|
|
172
208
|
// Definition
|
|
@@ -251,21 +287,24 @@ declare namespace Lync {
|
|
|
251
287
|
callback: (player: Player, reason: string, packetName: string, data: unknown) => void,
|
|
252
288
|
): () => void;
|
|
253
289
|
export function onSend(
|
|
254
|
-
handler: (data: unknown, name: string, player: Player | undefined) => unknown | undefined,
|
|
290
|
+
handler: (data: unknown, name: string, player: Player | undefined) => unknown | DropSentinel | undefined,
|
|
255
291
|
): () => void;
|
|
256
292
|
export function onReceive(
|
|
257
|
-
handler: (data: unknown, name: string, player: Player | undefined) => unknown | undefined,
|
|
293
|
+
handler: (data: unknown, name: string, player: Player | undefined) => unknown | DropSentinel | undefined,
|
|
258
294
|
): () => void;
|
|
259
295
|
|
|
296
|
+
// Target descriptors
|
|
297
|
+
export const all: AllTarget;
|
|
298
|
+
export function except(...players: Player[]): ExceptTarget;
|
|
299
|
+
|
|
300
|
+
// Middleware sentinel
|
|
301
|
+
export const DROP: DropSentinel;
|
|
302
|
+
|
|
260
303
|
// Groups
|
|
261
|
-
export function createGroup(name: string):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
export function
|
|
265
|
-
export function hasInGroup(name: string, player: Player): boolean;
|
|
266
|
-
export function getGroupSet(name: string): ReadonlyMap<Player, true>;
|
|
267
|
-
export function groupCount(name: string): number;
|
|
268
|
-
export function forEachInGroup(name: string, fn: (player: Player) => void): void;
|
|
304
|
+
export function createGroup(name: string): GroupObject;
|
|
305
|
+
|
|
306
|
+
// Scope
|
|
307
|
+
export function scope(): Scope;
|
|
269
308
|
|
|
270
309
|
// Configuration
|
|
271
310
|
export function setChannelMaxSize(bytes: number): void;
|
|
@@ -276,4 +315,4 @@ declare namespace Lync {
|
|
|
276
315
|
export function queryPendingCount(): number;
|
|
277
316
|
}
|
|
278
317
|
|
|
279
|
-
export default Lync;
|
|
318
|
+
export default Lync;
|
package/src/init.luau
CHANGED
|
@@ -52,11 +52,25 @@ local Group = require (script.api.Group)
|
|
|
52
52
|
local Namespace = require (script.api.Namespace)
|
|
53
53
|
local Packet = require (script.api.Packet)
|
|
54
54
|
local Query = require (script.api.Query)
|
|
55
|
+
local Scope = require (script.api.Scope)
|
|
55
56
|
|
|
56
57
|
-- Transport
|
|
57
58
|
local Client = require (script.transport.Client)
|
|
58
59
|
local Server = require (script.transport.Server)
|
|
59
60
|
|
|
61
|
+
-- Sentinels --------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
local ALL = table.freeze ({ _tag = "all" })
|
|
64
|
+
|
|
65
|
+
local function except (...: any): any
|
|
66
|
+
local count = select ("#", ...)
|
|
67
|
+
local set = {} :: { [Player]: true }
|
|
68
|
+
for i = 1, count do
|
|
69
|
+
set[select (i, ...)] = true
|
|
70
|
+
end
|
|
71
|
+
return { _tag = "except", _set = set }
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
-- Public -----------------------------------------------------------------
|
|
61
75
|
|
|
62
76
|
local function start (): ()
|
|
@@ -68,8 +82,7 @@ local function start (): ()
|
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
local Lync = {
|
|
71
|
-
VERSION = "1.
|
|
72
|
-
version = "1.3.1",
|
|
85
|
+
VERSION = "1.4.0",
|
|
73
86
|
|
|
74
87
|
-- Lifecycle
|
|
75
88
|
start = start,
|
|
@@ -139,15 +152,18 @@ local Lync = {
|
|
|
139
152
|
onSend = Middleware.addSend,
|
|
140
153
|
onReceive = Middleware.addReceive,
|
|
141
154
|
|
|
155
|
+
-- Target descriptors
|
|
156
|
+
all = ALL,
|
|
157
|
+
except = except,
|
|
158
|
+
|
|
159
|
+
-- Middleware sentinel
|
|
160
|
+
DROP = Middleware.DROP,
|
|
161
|
+
|
|
142
162
|
-- Groups
|
|
143
163
|
createGroup = Group.create,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
hasInGroup = Group.has,
|
|
148
|
-
getGroupSet = Group.getSet,
|
|
149
|
-
groupCount = Group.count,
|
|
150
|
-
forEachInGroup = Group.forEach,
|
|
164
|
+
|
|
165
|
+
-- Scope
|
|
166
|
+
scope = Scope.create,
|
|
151
167
|
|
|
152
168
|
-- Configuration (call before start)
|
|
153
169
|
setChannelMaxSize = Channel.setMaxSize,
|
|
@@ -14,13 +14,17 @@ type Chain = {
|
|
|
14
14
|
local _send: Chain = { handlers = {}, snapshot = nil }
|
|
15
15
|
local _receive: Chain = { handlers = {}, snapshot = nil }
|
|
16
16
|
|
|
17
|
+
-- Constants --------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
local DROP = table.freeze ({ _tag = "drop" })
|
|
20
|
+
|
|
17
21
|
-- Private ----------------------------------------------------------------
|
|
18
22
|
|
|
19
23
|
local function runChain (chain: { Handler }, data: any, name: string, player: Player?): any?
|
|
20
24
|
local current = data
|
|
21
25
|
for i = 1, #chain do
|
|
22
26
|
local result = chain[i] (current, name, player)
|
|
23
|
-
if result == nil then
|
|
27
|
+
if result == nil or result == DROP then
|
|
24
28
|
return nil
|
|
25
29
|
end
|
|
26
30
|
current = result
|
|
@@ -55,6 +59,10 @@ local function run (chain: Chain, data: any, name: string, player: Player?): any
|
|
|
55
59
|
return nil
|
|
56
60
|
end
|
|
57
61
|
|
|
62
|
+
if result == DROP then
|
|
63
|
+
return nil
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
return result
|
|
59
67
|
end
|
|
60
68
|
|
|
@@ -105,5 +113,6 @@ end
|
|
|
105
113
|
|
|
106
114
|
Middleware.hasSend = false
|
|
107
115
|
Middleware.hasReceive = false
|
|
116
|
+
Middleware.DROP = DROP
|
|
108
117
|
|
|
109
118
|
return Middleware
|
|
@@ -14,7 +14,6 @@ local channelWriteBatchRaw = Channel.writeBatchRaw
|
|
|
14
14
|
local channelSetPacket = Channel.setCurrentPacket
|
|
15
15
|
|
|
16
16
|
local Gate = require (script.Parent.Gate)
|
|
17
|
-
local Group = require (script.Parent.Parent.api.Group)
|
|
18
17
|
local Middleware = require (script.Parent.Parent.internal.Middleware)
|
|
19
18
|
local Pool = require (script.Parent.Parent.internal.Pool)
|
|
20
19
|
local Reader = require (script.Parent.Reader)
|
|
@@ -161,7 +160,7 @@ local function broadcastToAll (
|
|
|
161
160
|
codec: Codec<any>,
|
|
162
161
|
data: any,
|
|
163
162
|
isUnreliable: boolean,
|
|
164
|
-
|
|
163
|
+
exceptSet: { [Player]: true }?
|
|
165
164
|
): ()
|
|
166
165
|
local final, snapLen, snapRefs = prepareBroadcast (name, codec, data)
|
|
167
166
|
if final == nil and data ~= nil then
|
|
@@ -170,7 +169,7 @@ local function broadcastToAll (
|
|
|
170
169
|
|
|
171
170
|
local scratchBuff = _broadScratch.buff
|
|
172
171
|
for player, state in _players do
|
|
173
|
-
if
|
|
172
|
+
if exceptSet and exceptSet[player] then
|
|
174
173
|
continue
|
|
175
174
|
end
|
|
176
175
|
local ch = if isUnreliable then state.unreliable else state.reliable
|
|
@@ -304,25 +303,25 @@ function Server.writeToList (
|
|
|
304
303
|
end
|
|
305
304
|
|
|
306
305
|
function Server.writeToAllExcept (
|
|
307
|
-
|
|
306
|
+
exceptSet: { [Player]: true },
|
|
308
307
|
id: number,
|
|
309
308
|
name: string,
|
|
310
309
|
codec: Codec<any>,
|
|
311
310
|
data: any,
|
|
312
311
|
isUnreliable: boolean
|
|
313
312
|
): ()
|
|
314
|
-
broadcastToAll (id, name, codec, data, isUnreliable,
|
|
313
|
+
broadcastToAll (id, name, codec, data, isUnreliable, exceptSet)
|
|
315
314
|
end
|
|
316
315
|
|
|
317
|
-
function Server.
|
|
318
|
-
|
|
316
|
+
function Server.writeToSet (
|
|
317
|
+
set: { [Player]: true },
|
|
319
318
|
id: number,
|
|
320
319
|
name: string,
|
|
321
320
|
codec: Codec<any>,
|
|
322
321
|
data: any,
|
|
323
322
|
isUnreliable: boolean
|
|
324
323
|
): ()
|
|
325
|
-
broadcastToSet (
|
|
324
|
+
broadcastToSet (set, id, name, codec, data, isUnreliable)
|
|
326
325
|
end
|
|
327
326
|
|
|
328
327
|
-- Shared for both query requests and responses (identical wire format).
|