@axpecter/lync 1.5.1 → 2.0.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 +105 -12
- package/package.json +2 -2
- package/src/Types.luau +10 -0
- package/src/api/Namespace.luau +8 -3
- package/src/api/Packet.luau +76 -43
- package/src/api/Query.luau +35 -14
- package/src/api/Signal.luau +4 -0
- package/src/codec/composite/Array.luau +85 -6
- package/src/codec/composite/Map.luau +7 -1
- package/src/codec/composite/Optional.luau +1 -0
- package/src/codec/composite/Struct.luau +23 -1
- package/src/codec/composite/Tagged.luau +11 -0
- package/src/codec/composite/Tuple.luau +10 -0
- package/src/codec/datatype/Instance.luau +3 -0
- package/src/codec/meta/Unknown.luau +1 -0
- package/src/index.d.ts +36 -7
- package/src/init.luau +49 -2
- package/src/internal/Channel.luau +172 -42
- package/src/internal/Registry.luau +20 -3
- package/src/transport/Client.luau +65 -11
- package/src/transport/Gate.luau +9 -0
- package/src/transport/Reader.luau +75 -24
- package/src/transport/Server.luau +273 -68
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
```toml
|
|
15
15
|
[dependencies]
|
|
16
|
-
Lync = "axp3cter/lync@
|
|
16
|
+
Lync = "axp3cter/lync@2.0.0"
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
**npm (roblox-ts)**
|
|
@@ -85,12 +85,12 @@ local Players = game:GetService("Players")
|
|
|
85
85
|
|
|
86
86
|
local alive = Lync.createGroup("alive")
|
|
87
87
|
|
|
88
|
-
Lync.onSend(function(data, name)
|
|
88
|
+
Lync.onSend(function(data, name, player)
|
|
89
89
|
print("[out]", name)
|
|
90
90
|
return data
|
|
91
91
|
end)
|
|
92
92
|
|
|
93
|
-
Lync.onDrop(function(player, reason, name)
|
|
93
|
+
Lync.onDrop(function(player, reason, name, data)
|
|
94
94
|
warn(player.Name, "dropped", name, reason)
|
|
95
95
|
end)
|
|
96
96
|
|
|
@@ -125,7 +125,7 @@ Net.Hit:listen(function(data, player)
|
|
|
125
125
|
}, Lync.except(target))
|
|
126
126
|
end)
|
|
127
127
|
|
|
128
|
-
Net.Ping:listen(function()
|
|
128
|
+
Net.Ping:listen(function(request, player)
|
|
129
129
|
return os.clock()
|
|
130
130
|
end)
|
|
131
131
|
```
|
|
@@ -163,7 +163,7 @@ end
|
|
|
163
163
|
| | What it does |
|
|
164
164
|
|:---------|:------------|
|
|
165
165
|
| `Lync.start()` | Sets up transport. Server creates remotes, client connects. Call once after all definitions. |
|
|
166
|
-
| `Lync.VERSION` | `"
|
|
166
|
+
| `Lync.VERSION` | `"2.0.0"` |
|
|
167
167
|
|
|
168
168
|
## Packets
|
|
169
169
|
|
|
@@ -176,6 +176,7 @@ end
|
|
|
176
176
|
| `rateLimit` | `{ maxPerSecond, burstAllowance? }` | No | Server-side token bucket. Burst defaults to maxPerSecond if you dont set it. |
|
|
177
177
|
| `validate` | `(data, player) → (bool, string?)` | No | Server-side. Return `false, "reason"` to drop. Runs after NaN scan. |
|
|
178
178
|
| `maxPayloadBytes` | number | No | Server-side. Max bytes a single batch of this packet can consume. Fires `onDrop` with reason `"size"` if exceeded. |
|
|
179
|
+
| `timestamp` | `"frame" \| "offset" \| "full"` | No | Prepends a timestamp to each item. `"frame"` = u8 wrapping counter (1B), `"offset"` = u16 ms (2B), `"full"` = f64 clock (8B). Listeners receive it as a third argument. |
|
|
179
180
|
|
|
180
181
|
**Server, single `send` with targets:**
|
|
181
182
|
|
|
@@ -198,8 +199,8 @@ packet:send(data) -- send to server
|
|
|
198
199
|
|
|
199
200
|
| Method | What it does |
|
|
200
201
|
|:-------|:------------|
|
|
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. |
|
|
202
|
+
| `packet:listen(fn(data, sender, timestamp?))` | Sender is `Player` on server, `nil` on client. `timestamp` is present when the packet has a `timestamp` config. Returns a Connection. |
|
|
203
|
+
| `packet:once(fn(data, sender, timestamp?))` | Auto-disconnects after one fire. |
|
|
203
204
|
| `packet:wait()` | Returns `(data, sender)`. |
|
|
204
205
|
| `packet:disconnectAll()` | Kills all listeners on this packet. |
|
|
205
206
|
|
|
@@ -271,7 +272,7 @@ Access packets and queries by their short name on the returned object: `ns.Packe
|
|
|
271
272
|
|
|
272
273
|
| Method | What it does |
|
|
273
274
|
|:-------|:------------|
|
|
274
|
-
| `ns:listenAll(fn(name, data, sender))` | Listens to every packet in the namespace. `name` is the short name without prefix. Returns a Connection. |
|
|
275
|
+
| `ns:listenAll(fn(name, data, sender, timestamp?))` | Listens to every packet in the namespace. `name` is the short name without prefix. Returns a Connection. |
|
|
275
276
|
| `ns:onSend(fn(data, name, player) → data?)` | Send middleware that only runs for this namespace. Returns a remover. |
|
|
276
277
|
| `ns:onReceive(fn(data, name, player) → data?)` | Receive middleware that only runs for this namespace. Returns a remover. |
|
|
277
278
|
| `ns:disconnectAll()` | Kills all listeners made through `listenAll`. |
|
|
@@ -386,7 +387,7 @@ Used as the second argument to `packet:send()` on the server.
|
|
|
386
387
|
| `Lync.f16` | 2 | ±65,504, roughly 3 digits of precision |
|
|
387
388
|
| `Lync.f32` | 4 | IEEE 754 single |
|
|
388
389
|
| `Lync.f64` | 8 | IEEE 754 double |
|
|
389
|
-
| `Lync.bool` | 1 | true/false. Gets packed into bitfields when inside structs. |
|
|
390
|
+
| `Lync.bool` | 1 | true/false. Gets packed into bitfields when inside structs, and 8-per-byte when inside arrays. |
|
|
390
391
|
|
|
391
392
|
### Datatypes
|
|
392
393
|
|
|
@@ -417,7 +418,7 @@ Used as the second argument to `packet:send()` on the server.
|
|
|
417
418
|
| Constructor | What it does |
|
|
418
419
|
|:------------|:------------|
|
|
419
420
|
| `Lync.struct({ key = codec })` | Named fields. Bools get packed into bitfields automatically. |
|
|
420
|
-
| `Lync.array(codec, maxCount?)` | Variable length list with varint count. Optional `maxCount` rejects on read if exceeded. |
|
|
421
|
+
| `Lync.array(codec, maxCount?)` | Variable length list with varint count. Optional `maxCount` rejects on read if exceeded. Bool arrays are bitpacked (8 per byte). |
|
|
421
422
|
| `Lync.map(keyCodec, valueCodec, maxCount?)` | Key-value pairs with varint count. Optional `maxCount` rejects on read if exceeded. |
|
|
422
423
|
| `Lync.optional(codec)` | 1 byte flag, value only if present. |
|
|
423
424
|
| `Lync.tuple(codec, codec, ...)` | Ordered positional values, no keys. |
|
|
@@ -502,17 +503,109 @@ Same data shapes and methodology as [Blink's benchmark suite](https://github.com
|
|
|
502
503
|
> [!NOTE]
|
|
503
504
|
> Lync benchmarks run on Ryzen 7 7800X3D, 32GB DDR5-4800. Other tool numbers are from [Blink's published benchmarks](https://github.com/1Axen/blink/blob/main/benchmark/Benchmarks.md) (v0.17.1, Ryzen 9 7900X, 34GB DDR5-4800). Different CPUs so FPS numbers arent directly comparable but bandwidth numbers are since Kbps is scaled by 60/FPS. Lync hits the 60 FPS frame cap in both tests.
|
|
504
505
|
|
|
506
|
+
## Stats
|
|
507
|
+
|
|
508
|
+
Off by default. Call `Lync.enableStats()` before `Lync.start()` to activate. When disabled, zero overhead on send and receive paths.
|
|
509
|
+
|
|
510
|
+
Per-packet counters are available directly on the Packet object. Per-player counters are available via `Lync.getPlayerStats()`.
|
|
511
|
+
|
|
512
|
+
```luau
|
|
513
|
+
Lync.enableStats()
|
|
514
|
+
Lync.start()
|
|
515
|
+
|
|
516
|
+
-- Per-packet (both sides)
|
|
517
|
+
print(Net.State:getBytesSent(), Net.State:getFires(), Net.State:getDrops())
|
|
518
|
+
|
|
519
|
+
-- Per-player (server only)
|
|
520
|
+
local stats = Lync.getPlayerStats(player)
|
|
521
|
+
if stats then
|
|
522
|
+
print(stats.bytesSent, stats.bytesReceived)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
Lync.resetStats() -- zeros everything in-place
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
| Function / Method | What it does |
|
|
529
|
+
|:-------|:----------------|
|
|
530
|
+
| `Lync.enableStats()` | Activates stat counters. Call before `start()`. |
|
|
531
|
+
| `packet:getBytesSent()` | Wire bytes produced (includes batch header overhead). |
|
|
532
|
+
| `packet:getBytesReceived()` | Wire bytes consumed on receive. |
|
|
533
|
+
| `packet:getFires()` | Send fire count. |
|
|
534
|
+
| `packet:getRecvFires()` | Receive fire count. |
|
|
535
|
+
| `packet:getDrops()` | Gate rejections (rate limit, NaN, validate). |
|
|
536
|
+
| `Lync.getPlayerStats(player)` | Returns `{ bytesSent, bytesReceived }` or nil. Server only. |
|
|
537
|
+
| `Lync.resetStats()` | Zeros all counters in-place. |
|
|
538
|
+
|
|
539
|
+
## Flush Control
|
|
540
|
+
|
|
541
|
+
By default Lync flushes at 60hz (every Heartbeat). You can change this at runtime.
|
|
542
|
+
|
|
543
|
+
```luau
|
|
544
|
+
Lync.setFlushRate(30) -- flush every ~33ms instead of ~16ms
|
|
545
|
+
Lync.flush() -- force an immediate flush, resets the accumulator
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
| Function | What it does |
|
|
549
|
+
|:---------|:------------|
|
|
550
|
+
| `Lync.setFlushRate(hz)` | 1 to 60. Default 60. Callable at runtime. At 60hz, flushes every Heartbeat directly (no accumulator). Below 60, uses an elapsed-time accumulator with drift correction. |
|
|
551
|
+
| `Lync.flush()` | Immediate flush. Skips the next scheduled Heartbeat flush to prevent double-sending and XOR chain desync. |
|
|
552
|
+
|
|
553
|
+
## Security
|
|
554
|
+
|
|
555
|
+
### Bandwidth Throttle
|
|
556
|
+
|
|
557
|
+
Server-side per-player bandwidth strike counter. Counts consecutive oversized frames with decay. Protects against clients flooding the server.
|
|
558
|
+
|
|
559
|
+
```luau
|
|
560
|
+
Lync.setBandwidthLimit(16384, 10) -- 16KB soft limit, 10 strikes before drop
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Fires `onDrop` with reason `"bandwidth"` when a player exceeds the threshold. Read failures (corrupted buffers, XOR desync) also count as strikes.
|
|
564
|
+
|
|
565
|
+
### Unknown Codec Warning
|
|
566
|
+
|
|
567
|
+
If a packet uses `Lync.unknown` anywhere in its codec tree without a `validate` callback, Lync prints a warning at define time. The `unknown` codec bypasses schema validation entirely; client data goes through Roblox's sidecar without type checking. Adding `validate` suppresses the warning.
|
|
568
|
+
|
|
569
|
+
## Packet Capture
|
|
570
|
+
|
|
571
|
+
Server-only debug tool. Records raw and XOR'd buffer hex for analysis.
|
|
572
|
+
|
|
573
|
+
```luau
|
|
574
|
+
Lync.startCapture("My test")
|
|
575
|
+
-- fire packets...
|
|
576
|
+
Lync.flush()
|
|
577
|
+
Lync.stopCapture()
|
|
578
|
+
|
|
579
|
+
Lync.startCapture("Another test")
|
|
580
|
+
-- fire packets...
|
|
581
|
+
Lync.flush()
|
|
582
|
+
Lync.stopCapture()
|
|
583
|
+
|
|
584
|
+
Lync.dumpCaptures() -- writes JSON to ServerStorage.LyncCapture
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Each entry contains the label, frame number, raw hex (pre-XOR), XOR'd hex (post-XOR, nil for unreliable), byte count, and refs count. Hex is capped at 512 bytes per buffer to keep the output manageable.
|
|
588
|
+
|
|
589
|
+
| Function | What it does |
|
|
590
|
+
|:---------|:------------|
|
|
591
|
+
| `Lync.startCapture(label?)` | Start recording. Tags entries with the label. |
|
|
592
|
+
| `Lync.stopCapture()` | Stop recording. |
|
|
593
|
+
| `Lync.dumpCaptures()` | Writes all entries as JSON to a StringValue in ServerStorage, then clears. |
|
|
594
|
+
|
|
505
595
|
## Limits & Configuration
|
|
506
596
|
|
|
507
|
-
Call these before `Lync.start()
|
|
597
|
+
Call these before `Lync.start()` unless noted otherwise.
|
|
508
598
|
|
|
509
599
|
| What | Default | How to change | Notes |
|
|
510
600
|
|:-----|--------:|:--------------|:------|
|
|
511
601
|
| Packet types | 255 | Cant change | u8 on the wire. Each query eats 2 IDs. |
|
|
512
602
|
| Buffer per channel per frame | 256 KB | `Lync.setChannelMaxSize(n)` | 4 KB to 1 MB. |
|
|
513
|
-
| Concurrent queries | 65,536 | Cant change |
|
|
603
|
+
| Concurrent queries | 65,536 | Cant change | Varint correlation IDs. Freed on response or timeout. `Lync.queryPendingCount()` returns in-flight count. Queries default to 30/s rate limit. |
|
|
604
|
+
| Stats | Off | `Lync.enableStats()` | Zero overhead when off. Counters on packets and players. |
|
|
514
605
|
| NaN/inf scan depth | 16 | `Lync.setValidationDepth(n)` | 4 to 32. |
|
|
515
606
|
| Channel pool | 16 | `Lync.setPoolSize(n)` | 2 to 128. Extra gets GCd. |
|
|
607
|
+
| Flush rate | 60 hz | `Lync.setFlushRate(n)` | 1 to 60. Runtime-safe. |
|
|
608
|
+
| Bandwidth limit | 16 KB / 10 strikes | `Lync.setBandwidthLimit(n, m)` | Server-only. Per-player. |
|
|
516
609
|
| Namespaces | 64 | Cant change | |
|
|
517
610
|
| Delta + unreliable | Nope | Cant change | Errors at define time. |
|
|
518
611
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axpecter/lync",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Buffer networking for Roblox. Delta compression, XOR framing, built-in security
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Buffer networking for Roblox. Delta compression, XOR framing, built-in security",
|
|
5
5
|
"main": "src/init.luau",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
7
|
"module": "commonjs",
|
package/src/Types.luau
CHANGED
|
@@ -41,6 +41,7 @@ export type PacketConfig<T> = {
|
|
|
41
41
|
rateLimit: RateLimitConfig?,
|
|
42
42
|
validate: ((data: T, player: Player) -> (boolean, string?))?,
|
|
43
43
|
maxPayloadBytes: number?,
|
|
44
|
+
timestamp: ("frame" | "offset" | "full")?,
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export type QueryConfig<Req, Resp> = {
|
|
@@ -76,6 +77,15 @@ export type Registration = {
|
|
|
76
77
|
validate: ((data: any, player: Player) -> (boolean, string?))?,
|
|
77
78
|
needsGate: boolean,
|
|
78
79
|
maxPayloadBytes: number?,
|
|
80
|
+
timestampMode: number,
|
|
81
|
+
_openFn: (ch: ChannelState, id: number, name: string) -> (), -- resolved at define time (#2)
|
|
82
|
+
|
|
83
|
+
-- Stats counters (mutable, incremented at runtime)
|
|
84
|
+
bytesSent: number,
|
|
85
|
+
bytesReceived: number,
|
|
86
|
+
fires: number,
|
|
87
|
+
recvFires: number,
|
|
88
|
+
drops: number,
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
export type Connection = {
|
package/src/api/Namespace.luau
CHANGED
|
@@ -62,7 +62,12 @@ export type Namespace = typeof(setmetatable({} :: NamespaceFields, NamespaceImpl
|
|
|
62
62
|
-- Listens on every packet in this namespace. Returns a Connection.
|
|
63
63
|
function NamespaceImpl.listenAll(
|
|
64
64
|
self: Namespace,
|
|
65
|
-
callback: (
|
|
65
|
+
callback: (
|
|
66
|
+
name: string,
|
|
67
|
+
data: any,
|
|
68
|
+
sender: Player?,
|
|
69
|
+
timestamp: number?
|
|
70
|
+
) -> ()
|
|
66
71
|
): Connection
|
|
67
72
|
local connections = {} :: { Connection }
|
|
68
73
|
local prefix = self._name .. "."
|
|
@@ -70,8 +75,8 @@ function NamespaceImpl.listenAll(
|
|
|
70
75
|
|
|
71
76
|
for fullName, packet in self._packets do
|
|
72
77
|
local shortName = string.sub(fullName, prefixLen + 1)
|
|
73
|
-
local conn = packet:listen(function(data: any, sender: Player?): ()
|
|
74
|
-
callback(shortName, data, sender)
|
|
78
|
+
local conn = packet:listen(function(data: any, sender: Player?, timestamp: number?): ()
|
|
79
|
+
callback(shortName, data, sender, timestamp)
|
|
75
80
|
end)
|
|
76
81
|
table.insert(connections, conn)
|
|
77
82
|
table.insert(self._tracked, conn)
|
package/src/api/Packet.luau
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
local RunService = game:GetService("RunService")
|
|
6
6
|
|
|
7
|
+
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
7
8
|
local Client = require(script.Parent.Parent.transport.Client)
|
|
8
9
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
9
10
|
local Server = require(script.Parent.Parent.transport.Server)
|
|
@@ -14,34 +15,44 @@ type Connection = Types.Connection
|
|
|
14
15
|
type Codec<T> = Types.Codec<T>
|
|
15
16
|
type InternalCodec<T> = Types.InternalCodec<T>
|
|
16
17
|
type PacketConfig<T> = Types.PacketConfig<T>
|
|
18
|
+
type Registration = Types.Registration
|
|
17
19
|
|
|
18
20
|
-- Constants -----------------------------------------------------------
|
|
19
21
|
|
|
20
22
|
local IS_SERVER = RunService:IsServer()
|
|
21
23
|
|
|
22
|
-
local serverWriteTo = Server.writeTo
|
|
23
|
-
local serverWriteToAll = Server.writeToAll
|
|
24
|
-
local serverWriteToList = Server.writeToList
|
|
25
|
-
local serverWriteToAllExcept = Server.writeToAllExcept
|
|
26
|
-
local serverWriteToSet = Server.writeToSet
|
|
27
|
-
local clientWrite = Client.write
|
|
28
|
-
|
|
29
24
|
local DELTA_TARGETED = 1
|
|
30
25
|
local DELTA_BROADCAST = 2
|
|
31
26
|
|
|
27
|
+
local TS_LOOKUP = {
|
|
28
|
+
frame = Channel.TS_FRAME,
|
|
29
|
+
offset = Channel.TS_OFFSET,
|
|
30
|
+
full = Channel.TS_FULL,
|
|
31
|
+
}
|
|
32
|
+
|
|
32
33
|
-- Private -------------------------------------------------------------
|
|
33
34
|
|
|
34
35
|
type PacketFields = {
|
|
35
|
-
|
|
36
|
-
_name: string,
|
|
37
|
-
_codec: Codec<any>,
|
|
36
|
+
_reg: Registration,
|
|
38
37
|
_isUnreliable: boolean,
|
|
39
38
|
_isDelta: boolean,
|
|
40
39
|
_deltaMode: number,
|
|
41
40
|
_signal: Signal.Signal,
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
local function checkDeltaMode(self: any, mode: number): ()
|
|
44
|
+
if not self._isDelta then
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
local current = self._deltaMode
|
|
48
|
+
if current == 0 then
|
|
49
|
+
self._deltaMode = mode
|
|
50
|
+
elseif current ~= mode then
|
|
51
|
+
error(`[Lync] Delta packet cannot mix targeted and broadcast: "{self._reg.name}"`)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
-- Server-only metatable
|
|
45
56
|
local ServerImpl = {}
|
|
46
57
|
ServerImpl.__index = ServerImpl
|
|
47
58
|
|
|
@@ -63,67 +74,68 @@ function ServerImpl.disconnectAll(self: Packet): ()
|
|
|
63
74
|
self._signal:disconnectAll()
|
|
64
75
|
end
|
|
65
76
|
|
|
66
|
-
local function checkDeltaMode(self: Packet, mode: number): ()
|
|
67
|
-
if not self._isDelta then
|
|
68
|
-
return
|
|
69
|
-
end
|
|
70
|
-
local current = self._deltaMode
|
|
71
|
-
if current == 0 then
|
|
72
|
-
self._deltaMode = mode
|
|
73
|
-
elseif current ~= mode then
|
|
74
|
-
error(`[Lync] Delta packet cannot mix targeted and broadcast: "{self._name}"`)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
77
|
function ServerImpl.send(self: Packet, data: any, target: any?): ()
|
|
79
78
|
if target == nil then
|
|
80
79
|
error("[Lync] Server packet:send requires a target")
|
|
81
80
|
end
|
|
82
81
|
|
|
83
|
-
local
|
|
84
|
-
local name = self._name
|
|
85
|
-
local codec = self._codec
|
|
82
|
+
local reg = self._reg
|
|
86
83
|
local isUnreliable = self._isUnreliable
|
|
87
84
|
|
|
88
|
-
-- Single player
|
|
89
85
|
if typeof(target) == "Instance" then
|
|
90
86
|
checkDeltaMode(self, DELTA_TARGETED)
|
|
91
|
-
|
|
87
|
+
Server.writeTo(target :: Player, reg, data, isUnreliable)
|
|
92
88
|
return
|
|
93
89
|
end
|
|
94
90
|
|
|
95
|
-
-- Must be a table from here
|
|
96
91
|
local tag = target._tag
|
|
97
92
|
|
|
98
|
-
-- Lync.all sentinel
|
|
99
93
|
if tag == "all" then
|
|
100
94
|
checkDeltaMode(self, DELTA_BROADCAST)
|
|
101
|
-
|
|
95
|
+
Server.writeToAll(reg, data, isUnreliable)
|
|
102
96
|
return
|
|
103
97
|
end
|
|
104
98
|
|
|
105
|
-
-- Lync.except(player, ...) descriptor
|
|
106
99
|
if tag == "except" then
|
|
107
100
|
checkDeltaMode(self, DELTA_BROADCAST)
|
|
108
|
-
|
|
101
|
+
Server.writeToAllExcept(target._set, reg, data, isUnreliable)
|
|
109
102
|
return
|
|
110
103
|
end
|
|
111
104
|
|
|
112
|
-
-- Group object
|
|
113
105
|
if tag == "group" then
|
|
114
106
|
checkDeltaMode(self, DELTA_BROADCAST)
|
|
115
|
-
|
|
107
|
+
Server.writeToSet(target:getSet(), reg, data, isUnreliable)
|
|
116
108
|
return
|
|
117
109
|
end
|
|
118
110
|
|
|
119
|
-
-- Player list(array)
|
|
120
111
|
checkDeltaMode(self, DELTA_TARGETED)
|
|
121
|
-
|
|
112
|
+
Server.writeToList(target :: { Player }, reg, data, isUnreliable)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
-- Stats: read directly from registration
|
|
116
|
+
function ServerImpl.getBytesSent(self: Packet): number
|
|
117
|
+
return self._reg.bytesSent
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
function ServerImpl.getBytesReceived(self: Packet): number
|
|
121
|
+
return self._reg.bytesReceived
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
function ServerImpl.getFires(self: Packet): number
|
|
125
|
+
return self._reg.fires
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
function ServerImpl.getRecvFires(self: Packet): number
|
|
129
|
+
return self._reg.recvFires
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
function ServerImpl.getDrops(self: Packet): number
|
|
133
|
+
return self._reg.drops
|
|
122
134
|
end
|
|
123
135
|
|
|
124
136
|
table.freeze(ServerImpl)
|
|
125
137
|
|
|
126
|
-
-- Client-only metatable
|
|
138
|
+
-- Client-only metatable
|
|
127
139
|
local ClientImpl = {}
|
|
128
140
|
ClientImpl.__index = ClientImpl
|
|
129
141
|
|
|
@@ -144,9 +156,15 @@ function ClientImpl.disconnectAll(self: Packet): ()
|
|
|
144
156
|
end
|
|
145
157
|
|
|
146
158
|
function ClientImpl.send(self: Packet, data: any): ()
|
|
147
|
-
|
|
159
|
+
Client.write(self._reg, data, self._isUnreliable)
|
|
148
160
|
end
|
|
149
161
|
|
|
162
|
+
ClientImpl.getBytesSent = ServerImpl.getBytesSent
|
|
163
|
+
ClientImpl.getBytesReceived = ServerImpl.getBytesReceived
|
|
164
|
+
ClientImpl.getFires = ServerImpl.getFires
|
|
165
|
+
ClientImpl.getRecvFires = ServerImpl.getRecvFires
|
|
166
|
+
ClientImpl.getDrops = ServerImpl.getDrops
|
|
167
|
+
|
|
150
168
|
table.freeze(ClientImpl)
|
|
151
169
|
|
|
152
170
|
-- Public --------------------------------------------------------------
|
|
@@ -167,6 +185,22 @@ function PacketModule.define(name: string, config: PacketConfig<any>): Packet
|
|
|
167
185
|
error(`[Lync] Delta codec requires reliable delivery: "{name}"`)
|
|
168
186
|
end
|
|
169
187
|
|
|
188
|
+
local timestampMode = if config.timestamp then TS_LOOKUP[config.timestamp] else nil
|
|
189
|
+
if config.timestamp and not timestampMode then
|
|
190
|
+
error(`[Lync] Invalid timestamp mode: "{config.timestamp}" on packet "{name}"`)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if timestampMode then
|
|
194
|
+
Channel.enableTimestamps()
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
local hasUnknown = (config.value :: any)._hasUnknown
|
|
198
|
+
if hasUnknown and not config.validate then
|
|
199
|
+
warn(
|
|
200
|
+
`[Lync] Packet "{name}" uses Lync.unknown: client data bypasses schema validation. Add a validate callback.`
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
170
204
|
local signal = Signal.create()
|
|
171
205
|
|
|
172
206
|
local reg = Registry.register(
|
|
@@ -176,15 +210,14 @@ function PacketModule.define(name: string, config: PacketConfig<any>): Packet
|
|
|
176
210
|
signal,
|
|
177
211
|
config.rateLimit,
|
|
178
212
|
config.validate,
|
|
179
|
-
config.maxPayloadBytes
|
|
213
|
+
config.maxPayloadBytes,
|
|
214
|
+
timestampMode
|
|
180
215
|
)
|
|
181
216
|
|
|
182
217
|
local isDelta = (config.value :: InternalCodec<any>)._isDelta == true
|
|
183
218
|
|
|
184
219
|
local fields: PacketFields = {
|
|
185
|
-
|
|
186
|
-
_name = reg.name,
|
|
187
|
-
_codec = reg.codec,
|
|
220
|
+
_reg = reg,
|
|
188
221
|
_isUnreliable = isUnreliable,
|
|
189
222
|
_isDelta = isDelta,
|
|
190
223
|
_deltaMode = 0,
|
package/src/api/Query.luau
CHANGED
|
@@ -28,6 +28,7 @@ type PendingQuery = {
|
|
|
28
28
|
thread: thread?,
|
|
29
29
|
callback: ((data: any) -> ())?,
|
|
30
30
|
timeout: thread?,
|
|
31
|
+
source: Player?,
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
local _pending = {} :: { [number]: PendingQuery }
|
|
@@ -60,12 +61,17 @@ local function releaseCorrelation(): ()
|
|
|
60
61
|
_activeCount -= 1
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
local function completeQuery(id: number, data: any): ()
|
|
64
|
+
local function completeQuery(id: number, data: any, source: Player?): ()
|
|
64
65
|
local entry = _pending[id]
|
|
65
66
|
if not entry then
|
|
66
67
|
return
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
-- Reject responses from wrong player (prevents correlation spoofing)
|
|
71
|
+
if source ~= nil and entry.source ~= nil and source ~= entry.source then
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
69
75
|
_pending[id] = nil
|
|
70
76
|
releaseCorrelation()
|
|
71
77
|
|
|
@@ -110,7 +116,7 @@ local function requestMulti(self: Query, players: { Player }, data: any): { [Pla
|
|
|
110
116
|
if count == 1 then
|
|
111
117
|
local correlation = allocCorrelation()
|
|
112
118
|
local thread = coroutine.running()
|
|
113
|
-
local entry: PendingQuery = { thread = thread, timeout = nil }
|
|
119
|
+
local entry: PendingQuery = { thread = thread, timeout = nil, source = players[1] }
|
|
114
120
|
_pending[correlation] = entry
|
|
115
121
|
|
|
116
122
|
Server.writeQuery(
|
|
@@ -154,6 +160,7 @@ local function requestMulti(self: Query, players: { Player }, data: any): { [Pla
|
|
|
154
160
|
end
|
|
155
161
|
end,
|
|
156
162
|
timeout = nil,
|
|
163
|
+
source = player,
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
_pending[correlation] = entry
|
|
@@ -196,12 +203,18 @@ function QueryImpl.listen(self: Query, callback: (...any) -> any): Connection
|
|
|
196
203
|
if IS_SERVER then
|
|
197
204
|
return self._reqReg.signal:connect(
|
|
198
205
|
function(request: any, player: Player, correlation: number): ()
|
|
199
|
-
local
|
|
200
|
-
if not
|
|
201
|
-
warn(`[Lync] Query handler error on "{respName}": {
|
|
206
|
+
local results = table.pack(pcall(callback, request, player))
|
|
207
|
+
if not results[1] then
|
|
208
|
+
warn(`[Lync] Query handler error on "{respName}": {results[2]}`)
|
|
209
|
+
Server.writeQueryNil(player, respId, respName, correlation)
|
|
210
|
+
return
|
|
202
211
|
end
|
|
203
212
|
|
|
204
|
-
|
|
213
|
+
local response = if results.n > 2
|
|
214
|
+
then table.move(results, 2, results.n, 1, table.create(results.n - 1))
|
|
215
|
+
else results[2]
|
|
216
|
+
|
|
217
|
+
if response ~= nil then
|
|
205
218
|
Server.writeQuery(player, respId, respName, correlation, respCodec, response)
|
|
206
219
|
else
|
|
207
220
|
Server.writeQueryNil(player, respId, respName, correlation)
|
|
@@ -211,12 +224,18 @@ function QueryImpl.listen(self: Query, callback: (...any) -> any): Connection
|
|
|
211
224
|
else
|
|
212
225
|
return self._reqReg.signal:connect(
|
|
213
226
|
function(request: any, _player: Player?, correlation: number): ()
|
|
214
|
-
local
|
|
215
|
-
if not
|
|
216
|
-
warn(`[Lync] Query handler error on "{respName}": {
|
|
227
|
+
local results = table.pack(pcall(callback, request))
|
|
228
|
+
if not results[1] then
|
|
229
|
+
warn(`[Lync] Query handler error on "{respName}": {results[2]}`)
|
|
230
|
+
Client.writeQueryNil(respId, respName, correlation)
|
|
231
|
+
return
|
|
217
232
|
end
|
|
218
233
|
|
|
219
|
-
|
|
234
|
+
local response = if results.n > 2
|
|
235
|
+
then table.move(results, 2, results.n, 1, table.create(results.n - 1))
|
|
236
|
+
else results[2]
|
|
237
|
+
|
|
238
|
+
if response ~= nil then
|
|
220
239
|
Client.writeQuery(respId, respName, correlation, respCodec, response)
|
|
221
240
|
else
|
|
222
241
|
Client.writeQueryNil(respId, respName, correlation)
|
|
@@ -251,7 +270,7 @@ function QueryImpl.requestFrom(self: Query, player: Player, data: any): any
|
|
|
251
270
|
|
|
252
271
|
local correlation = allocCorrelation()
|
|
253
272
|
local thread = coroutine.running()
|
|
254
|
-
local entry: PendingQuery = { thread = thread, timeout = nil }
|
|
273
|
+
local entry: PendingQuery = { thread = thread, timeout = nil, source = player }
|
|
255
274
|
_pending[correlation] = entry
|
|
256
275
|
|
|
257
276
|
Server.writeQuery(player, self._reqReg.id, self._name, correlation, self._reqReg.codec, data)
|
|
@@ -297,18 +316,20 @@ function QueryModule.define(name: string, config: QueryConfig<any, any>): Query
|
|
|
297
316
|
local reqSignal = Signal.create()
|
|
298
317
|
local respSignal = Signal.create()
|
|
299
318
|
|
|
319
|
+
local rateLimit = config.rateLimit or { maxPerSecond = 30 }
|
|
320
|
+
|
|
300
321
|
local reqReg, respReg = Registry.registerQueryPair(
|
|
301
322
|
name,
|
|
302
323
|
config.request,
|
|
303
324
|
config.response,
|
|
304
325
|
reqSignal,
|
|
305
326
|
respSignal,
|
|
306
|
-
|
|
327
|
+
rateLimit,
|
|
307
328
|
config.validate
|
|
308
329
|
)
|
|
309
330
|
|
|
310
|
-
respSignal:connect(function(resp: any, correlation: number): ()
|
|
311
|
-
completeQuery(correlation, resp)
|
|
331
|
+
respSignal:connect(function(resp: any, source: Player?, correlation: number): ()
|
|
332
|
+
completeQuery(correlation, resp, source)
|
|
312
333
|
end)
|
|
313
334
|
|
|
314
335
|
return setmetatable({
|
package/src/api/Signal.luau
CHANGED
|
@@ -91,6 +91,10 @@ SignalImpl.__index = SignalImpl
|
|
|
91
91
|
export type Signal = typeof(setmetatable({} :: SignalFields, SignalImpl))
|
|
92
92
|
|
|
93
93
|
local function addEntry(self: Signal, callback: (...any) -> (), isOnce: boolean): Connection
|
|
94
|
+
if type(callback) ~= "function" then
|
|
95
|
+
error("[Lync] Expected function, got " .. type(callback))
|
|
96
|
+
end
|
|
97
|
+
|
|
94
98
|
local count = self._count + 1
|
|
95
99
|
self._count = count
|
|
96
100
|
|