@axpecter/lync 1.5.2 → 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 +102 -9
- 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 +3 -1
- 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)**
|
|
@@ -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
|
@@ -316,13 +316,15 @@ function QueryModule.define(name: string, config: QueryConfig<any, any>): Query
|
|
|
316
316
|
local reqSignal = Signal.create()
|
|
317
317
|
local respSignal = Signal.create()
|
|
318
318
|
|
|
319
|
+
local rateLimit = config.rateLimit or { maxPerSecond = 30 }
|
|
320
|
+
|
|
319
321
|
local reqReg, respReg = Registry.registerQueryPair(
|
|
320
322
|
name,
|
|
321
323
|
config.request,
|
|
322
324
|
config.response,
|
|
323
325
|
reqSignal,
|
|
324
326
|
respSignal,
|
|
325
|
-
|
|
327
|
+
rateLimit,
|
|
326
328
|
config.validate
|
|
327
329
|
)
|
|
328
330
|
|
|
@@ -38,6 +38,7 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
|
|
|
38
38
|
|
|
39
39
|
return table.freeze({
|
|
40
40
|
_isDelta = true,
|
|
41
|
+
_hasUnknown = (element :: any)._hasUnknown or nil,
|
|
41
42
|
|
|
42
43
|
write = function(ch: ChannelState, value: { any }): ()
|
|
43
44
|
local len = #value
|
|
@@ -110,12 +111,14 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
|
|
|
110
111
|
ch.cursor += 1
|
|
111
112
|
Varint.write(ch, dirtyN)
|
|
112
113
|
|
|
114
|
+
local prevIdx = 0
|
|
113
115
|
for j = 1, dirtyN do
|
|
114
116
|
local i = dirty[j]
|
|
115
117
|
local newOff = bounds[i]
|
|
116
118
|
local newLen = bounds[i + 1] - newOff
|
|
117
119
|
|
|
118
|
-
Varint.write(ch, i - 1)
|
|
120
|
+
Varint.write(ch, (i - 1) - prevIdx)
|
|
121
|
+
prevIdx = i - 1
|
|
119
122
|
alloc(ch, newLen)
|
|
120
123
|
buffer.copy(ch.buff, ch.cursor, scratch.buff, newOff, newLen)
|
|
121
124
|
ch.cursor += newLen
|
|
@@ -167,21 +170,26 @@ function Array.deltaArray(element: Codec<any>, maxCount: number?): Codec<{ any }
|
|
|
167
170
|
end
|
|
168
171
|
|
|
169
172
|
local cache = Baseline.getCache(deltaId) :: { any }?
|
|
170
|
-
|
|
173
|
+
if not cache then
|
|
174
|
+
error("[Lync] Delta frame without baseline")
|
|
175
|
+
end
|
|
176
|
+
local result = table.clone(cache)
|
|
171
177
|
|
|
172
178
|
local dirtyN, dnBytes = Varint.read(src, absPos)
|
|
173
179
|
absPos += dnBytes
|
|
174
180
|
|
|
181
|
+
local runningIdx = 0
|
|
175
182
|
for _ = 1, dirtyN do
|
|
176
|
-
local
|
|
177
|
-
absPos +=
|
|
183
|
+
local delta, deltaBytes = Varint.read(src, absPos)
|
|
184
|
+
absPos += deltaBytes
|
|
185
|
+
runningIdx += delta
|
|
178
186
|
|
|
179
187
|
if elemRead and elemSize then
|
|
180
|
-
result[
|
|
188
|
+
result[runningIdx + 1] = elemRead(src, absPos)
|
|
181
189
|
absPos += elemSize
|
|
182
190
|
else
|
|
183
191
|
local val, n = readElement(src, absPos, refs)
|
|
184
|
-
result[
|
|
192
|
+
result[runningIdx + 1] = val
|
|
185
193
|
absPos += n
|
|
186
194
|
end
|
|
187
195
|
end
|
|
@@ -198,8 +206,78 @@ function Array.array(element: Codec<any>, maxCount: number?): Codec<{ any }>
|
|
|
198
206
|
local directWrite = internal._directWrite
|
|
199
207
|
local directRead = internal._directRead
|
|
200
208
|
|
|
209
|
+
local band = bit32.band
|
|
210
|
+
local bor = bit32.bor
|
|
211
|
+
local lshift = bit32.lshift
|
|
212
|
+
local rshift = bit32.rshift
|
|
213
|
+
local ceil = math.ceil
|
|
214
|
+
|
|
215
|
+
-- Bitpacked bool array: 8 bools per byte
|
|
216
|
+
if internal._isBool then
|
|
217
|
+
return table.freeze({
|
|
218
|
+
_hasUnknown = nil,
|
|
219
|
+
write = function(ch: ChannelState, value: { any }): ()
|
|
220
|
+
local len = #value
|
|
221
|
+
Varint.write(ch, len)
|
|
222
|
+
if len == 0 then
|
|
223
|
+
return
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
local byteCount = ceil(len / 8)
|
|
227
|
+
local c = ch.cursor
|
|
228
|
+
if c + byteCount > ch.size then
|
|
229
|
+
alloc(ch, byteCount)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
local b = ch.buff
|
|
233
|
+
local byteIdx = 0
|
|
234
|
+
local acc = 0
|
|
235
|
+
for i = 1, len do
|
|
236
|
+
local bit = (i - 1) % 8
|
|
237
|
+
if value[i] then
|
|
238
|
+
acc = bor(acc, lshift(1, bit))
|
|
239
|
+
end
|
|
240
|
+
if bit == 7 then
|
|
241
|
+
buffer.writeu8(b, c + byteIdx, acc)
|
|
242
|
+
byteIdx += 1
|
|
243
|
+
acc = 0
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
-- Flush remaining bits
|
|
247
|
+
if len % 8 ~= 0 then
|
|
248
|
+
buffer.writeu8(b, c + byteIdx, acc)
|
|
249
|
+
end
|
|
250
|
+
ch.cursor = c + byteCount
|
|
251
|
+
end,
|
|
252
|
+
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
|
|
253
|
+
local len, lenBytes = Varint.read(src, pos)
|
|
254
|
+
|
|
255
|
+
if maxCount and len > maxCount then
|
|
256
|
+
error(`[Lync] Array count {len} exceeds max {maxCount}`)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
local result = table.create(len)
|
|
260
|
+
if len == 0 then
|
|
261
|
+
return result, lenBytes
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
local byteCount = ceil(len / 8)
|
|
265
|
+
local c = pos + lenBytes
|
|
266
|
+
for i = 1, len do
|
|
267
|
+
local byteIdx = rshift(i - 1, 3)
|
|
268
|
+
local bit = (i - 1) % 8
|
|
269
|
+
local byte = buffer.readu8(src, c + byteIdx)
|
|
270
|
+
result[i] = band(byte, lshift(1, bit)) ~= 0
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
return result, lenBytes + byteCount
|
|
274
|
+
end,
|
|
275
|
+
})
|
|
276
|
+
end
|
|
277
|
+
|
|
201
278
|
if elementSize and directWrite and directRead then
|
|
202
279
|
return table.freeze({
|
|
280
|
+
_hasUnknown = (element :: any)._hasUnknown or nil,
|
|
203
281
|
write = function(ch: ChannelState, value: { any }): ()
|
|
204
282
|
local len = #value
|
|
205
283
|
Varint.write(ch, len)
|
|
@@ -244,6 +322,7 @@ function Array.array(element: Codec<any>, maxCount: number?): Codec<{ any }>
|
|
|
244
322
|
local readElement = element.read
|
|
245
323
|
|
|
246
324
|
return table.freeze({
|
|
325
|
+
_hasUnknown = (element :: any)._hasUnknown or nil,
|
|
247
326
|
write = function(ch: ChannelState, value: { any }): ()
|
|
248
327
|
local len = #value
|
|
249
328
|
Varint.write(ch, len)
|
|
@@ -44,6 +44,7 @@ function Map.map(
|
|
|
44
44
|
local entrySize = keySize + valSize
|
|
45
45
|
|
|
46
46
|
return table.freeze({
|
|
47
|
+
_hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
|
|
47
48
|
write = function(ch: ChannelState, value: { [any]: any }): ()
|
|
48
49
|
local count = 0
|
|
49
50
|
for _ in value do
|
|
@@ -100,6 +101,7 @@ function Map.map(
|
|
|
100
101
|
local readVal = valueCodec.read
|
|
101
102
|
|
|
102
103
|
return table.freeze({
|
|
104
|
+
_hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
|
|
103
105
|
write = function(ch: ChannelState, value: { [any]: any }): ()
|
|
104
106
|
local count = 0
|
|
105
107
|
for _ in value do
|
|
@@ -185,6 +187,7 @@ function Map.deltaMap(
|
|
|
185
187
|
|
|
186
188
|
return table.freeze({
|
|
187
189
|
_isDelta = true,
|
|
190
|
+
_hasUnknown = (keyCodec :: any)._hasUnknown or (valueCodec :: any)._hasUnknown or nil,
|
|
188
191
|
|
|
189
192
|
write = function(ch: ChannelState, value: { [any]: any }): ()
|
|
190
193
|
local scratch = acquireScratch()
|
|
@@ -378,7 +381,10 @@ function Map.deltaMap(
|
|
|
378
381
|
end
|
|
379
382
|
|
|
380
383
|
local cache = Baseline.getCache(deltaId) :: { [any]: any }?
|
|
381
|
-
|
|
384
|
+
if not cache then
|
|
385
|
+
error("[Lync] Delta frame without baseline")
|
|
386
|
+
end
|
|
387
|
+
local result = table.clone(cache)
|
|
382
388
|
|
|
383
389
|
local upsertN, unBytes = Varint.read(src, absPos)
|
|
384
390
|
absPos += unBytes
|
|
@@ -19,6 +19,7 @@ function Optional.optional(inner: Codec<any>): Codec<any>
|
|
|
19
19
|
local innerRead = inner.read
|
|
20
20
|
|
|
21
21
|
return table.freeze({
|
|
22
|
+
_hasUnknown = (inner :: any)._hasUnknown or nil,
|
|
22
23
|
write = function(ch: ChannelState, value: any): ()
|
|
23
24
|
local c = ch.cursor
|
|
24
25
|
if c + 1 > ch.size then
|