@axpecter/lync 2.3.1 → 2.3.3
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 +29 -19
- package/package.json +1 -1
- package/src/Types.luau +12 -6
- package/src/api/Packet.luau +93 -35
- package/src/api/Query.luau +2 -2
- package/src/api/Signal.luau +6 -1
- package/src/codec/Base.luau +8 -1
- package/src/codec/composite/Array.luau +293 -17
- package/src/codec/composite/Map.luau +345 -40
- package/src/codec/composite/Optional.luau +4 -0
- package/src/codec/composite/Shared.luau +101 -102
- package/src/codec/composite/Struct.luau +20 -10
- package/src/codec/composite/Tagged.luau +8 -0
- package/src/codec/composite/Tuple.luau +12 -1
- package/src/codec/datatype/Buffer.luau +1 -3
- package/src/codec/datatype/CFrame.luau +6 -106
- package/src/codec/meta/Bitfield.luau +14 -1
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/primitive/Varint.luau +13 -7
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +46 -0
- package/src/init.luau +7 -0
- package/src/internal/Channel.luau +187 -58
- package/src/internal/Pool.luau +4 -3
- package/src/transport/Reader.luau +19 -8
- package/src/transport/Server.luau +35 -13
- package/src/util/Buffer.luau +2 -4
- package/src/util/Quantize.luau +27 -3
- package/src/util/Quat.luau +124 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Schemas, packets, queries, groups, validation, rate limiting. Every send batches
|
|
|
16
16
|
Wally — add to your `wally.toml`:
|
|
17
17
|
|
|
18
18
|
```toml
|
|
19
|
-
Lync = "axp3cter/lync@2.3.
|
|
19
|
+
Lync = "axp3cter/lync@2.3.3"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
npm (roblox-ts):
|
|
@@ -250,8 +250,9 @@ Enable with `Lync.configure({ stats = true })`.
|
|
|
250
250
|
| Codec | Bytes | Notes |
|
|
251
251
|
|:---|---:|:---|
|
|
252
252
|
| `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
|
|
253
|
+
| `zint(min?, max?)` | 1 – 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. |
|
|
253
254
|
| `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |
|
|
254
|
-
| `float(min, max, precision)` | 1 / 2 / 4 | Quantized;
|
|
255
|
+
| `float(min, max, precision)` | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. |
|
|
255
256
|
| `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
|
|
256
257
|
|
|
257
258
|
### Strings & buffers
|
|
@@ -284,8 +285,8 @@ Call as a function for compression.
|
|
|
284
285
|
|
|
285
286
|
| Codec | Bytes | Notes |
|
|
286
287
|
|:---|---:|:---|
|
|
287
|
-
| `vec2(min, max, precision)` | 2 / 4 / 8 | Per-component
|
|
288
|
-
| `vec3(min, max, precision)` | 3 / 6 / 12 | Per-component
|
|
288
|
+
| `vec2(min, max, precision)` | 2 / 4 / 6 / 8 | Per-component, narrowest fitting width. |
|
|
289
|
+
| `vec3(min, max, precision)` | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. |
|
|
289
290
|
| `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
|
|
290
291
|
|
|
291
292
|
### Composites
|
|
@@ -301,13 +302,20 @@ Call as a function for compression.
|
|
|
301
302
|
|
|
302
303
|
### Delta — reliable transport only
|
|
303
304
|
|
|
304
|
-
|
|
305
|
+
Tracks the previous frame's value and ships only what changed. Rejected on `unreliable = true`.
|
|
305
306
|
|
|
306
|
-
| Codec |
|
|
307
|
-
|
|
308
|
-
| `deltaStruct(schema)` |
|
|
309
|
-
| `deltaArray(c, max?)` |
|
|
310
|
-
| `deltaMap(k, v, max?)` |
|
|
307
|
+
| Codec | Static | Mutation |
|
|
308
|
+
|:---|:---:|:---:|
|
|
309
|
+
| `deltaStruct(schema)` | 1 B | per-field |
|
|
310
|
+
| `deltaArray(c, max?)` | 1 B | per-changed-index |
|
|
311
|
+
| `deltaMap(k, v, max?)` | 1 B | per-changed-key |
|
|
312
|
+
| `deltaInt(min, max)` | 1 B | 1–5 B |
|
|
313
|
+
| `deltaFloat(min, max, precision)` | 1 B | 1–5 B |
|
|
314
|
+
| `deltaVec3(min, max, precision)` | 3 B | 3–15 B |
|
|
315
|
+
| `deltaCFrame(posMin, posMax, precision)` | 1 B | 4–13 B |
|
|
316
|
+
|
|
317
|
+
- `deltaArray` element / `deltaMap` key+value cannot themselves contain delta state. Use `deltaStruct` for per-field deltas inside.
|
|
318
|
+
- `deltaVec3` and `deltaCFrame` error on out-of-range components.
|
|
311
319
|
|
|
312
320
|
### Meta
|
|
313
321
|
|
|
@@ -351,21 +359,23 @@ Global per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N }
|
|
|
351
359
|
| Tool | `array<entity>[100]` | `array<bool>[1000]` |
|
|
352
360
|
|:---|:---|:---|
|
|
353
361
|
| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
|
|
354
|
-
| **lync** | **
|
|
362
|
+
| **lync** | **59 fps · 3.37 Kbps** | **61 fps · 2.45 Kbps** |
|
|
355
363
|
| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
|
|
356
364
|
| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
|
|
357
365
|
| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
|
|
358
366
|
|
|
359
367
|
### Network bandwidth — 100 fires/frame, 8 s
|
|
360
368
|
|
|
361
|
-
| Workload | Kbps |
|
|
362
|
-
|
|
363
|
-
| `array<entity>[100]`
|
|
364
|
-
| `array<entity>[100]` reused | **2.4** |
|
|
365
|
-
| `array<bool>[1000]`
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
+
| Workload | Naive Kbps | Optimized | Savings |
|
|
370
|
+
|:---|---:|:---|---:|
|
|
371
|
+
| `array<entity>[100]` random | 3,607 | `deltaArray` 3 of 100 mutated | **154** (–96%) |
|
|
372
|
+
| `array<entity>[100]` reused | 3,607 | XOR baseline (identical frames) | **2.4** (–99.9%) |
|
|
373
|
+
| `array<bool>[1000]` random | 762 | XOR baseline (1 bit flipped) | **20.4** (–97%) |
|
|
374
|
+
| `struct(state)` random | 201 | `deltaStruct` 1 field mutated | **29.0** (–86%) |
|
|
375
|
+
| `map<id, vec3>[200]` 5 keys mutated | 657 | `deltaMap` 5 keys mutated | **393** (–40%) |
|
|
376
|
+
| `array<cframe>[50]` random | 4,585 | — | — |
|
|
377
|
+
| `vec3` walking motion (continuous diff) | — | `deltaVec3` | **19.5** |
|
|
378
|
+
| `CFrame` walking pose (pos + rot) | — | `deltaCFrame` | **41.1** |
|
|
369
379
|
|
|
370
380
|
## License
|
|
371
381
|
|
package/package.json
CHANGED
package/src/Types.luau
CHANGED
|
@@ -12,17 +12,18 @@ export type ChannelState = {
|
|
|
12
12
|
itemCount: number,
|
|
13
13
|
singleMode: boolean,
|
|
14
14
|
singlePos: number,
|
|
15
|
-
|
|
15
|
+
--[[
|
|
16
|
+
Per-codec write-side delta state, keyed by deltaId. Shape is opaque
|
|
17
|
+
and codec-internal: deltaStruct stores `{raw, len, segOff, segLen}`,
|
|
18
|
+
deltaArray stores `{perIdx, length}`, deltaMap stores `{perKey}`,
|
|
19
|
+
delta scalars store quantized previous values.
|
|
20
|
+
]]
|
|
21
|
+
deltas: { [number]: any },
|
|
16
22
|
prevDump: buffer?,
|
|
17
23
|
prevDumpLen: number,
|
|
18
24
|
currentPacket: string?,
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
export type DeltaCacheEntry = {
|
|
22
|
-
raw: buffer,
|
|
23
|
-
len: number,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
27
|
export type Codec<T> = {
|
|
27
28
|
write: (ch: ChannelState, value: T) -> (),
|
|
28
29
|
read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
|
|
@@ -39,6 +40,11 @@ export type InternalCodec<T> = {
|
|
|
39
40
|
_directWrite: ((b: buffer, offset: number, value: T) -> ())?,
|
|
40
41
|
_directRead: ((b: buffer, offset: number) -> T)?,
|
|
41
42
|
_isDelta: boolean?,
|
|
43
|
+
--[[
|
|
44
|
+
True if any nested codec is delta. Lets the broadcast hot path skip
|
|
45
|
+
encode-once when delta state would diverge per-receiver.
|
|
46
|
+
]]
|
|
47
|
+
_hasDelta: boolean?,
|
|
42
48
|
_isBool: boolean?,
|
|
43
49
|
_hasUnknown: boolean?,
|
|
44
50
|
_min: number?,
|
package/src/api/Packet.luau
CHANGED
|
@@ -11,6 +11,7 @@ local Log = require(script.Parent.Parent.util.Log)
|
|
|
11
11
|
local Middleware = require(script.Parent.Parent.internal.Middleware)
|
|
12
12
|
local Player = require(script.Parent.Parent.util.Player)
|
|
13
13
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
14
|
+
local Shared = require(script.Parent.Parent.codec.composite.Shared)
|
|
14
15
|
local Transport = require(script.Parent.Parent.internal.Transport)
|
|
15
16
|
local Types = require(script.Parent.Parent.Types)
|
|
16
17
|
|
|
@@ -129,14 +130,100 @@ function Packet.define<T>(
|
|
|
129
130
|
return false, sendData
|
|
130
131
|
end
|
|
131
132
|
|
|
132
|
-
--
|
|
133
|
+
-- Non-delta codecs are stateless wire-encoders, so broadcast can encode
|
|
134
|
+
-- once and bufCopy the payload to every player. Falls through if the
|
|
135
|
+
-- encoded payload includes ref indices (the indices would need rewriting
|
|
136
|
+
-- per channel, which no codec exposes).
|
|
137
|
+
local canEncodeOnce = not codec._isDelta and not codec._hasDelta
|
|
138
|
+
|
|
133
139
|
local function broadcast(players: { Player }, data: any): ()
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
local count = #players
|
|
141
|
+
if count == 0 then
|
|
142
|
+
bumpFires()
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if canEncodeOnce then
|
|
147
|
+
local scratch = Shared.acquireScratch()
|
|
148
|
+
scratch.currentPacket = name
|
|
149
|
+
codec.write(scratch, data)
|
|
150
|
+
scratch.currentPacket = nil
|
|
151
|
+
if scratch.refCount == 0 then
|
|
152
|
+
local payload = scratch.buff
|
|
153
|
+
local payloadLen = scratch.cursor
|
|
154
|
+
local server = Transport.server()
|
|
155
|
+
local getChannel = if isUnreliable
|
|
156
|
+
then server.getUnreliableChannel
|
|
157
|
+
else server.getReliableChannel
|
|
158
|
+
-- Channel.writeBatchEncoded is hot-swapped by enableStats; re-read per call.
|
|
159
|
+
local writeBatchEncoded = Channel.writeBatchEncoded
|
|
160
|
+
for i = 1, count do
|
|
161
|
+
writeBatchEncoded(getChannel(players[i]), reg, payload, payloadLen)
|
|
162
|
+
end
|
|
163
|
+
Shared.releaseScratch()
|
|
164
|
+
bumpFires()
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
Shared.releaseScratch()
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
for i = 1, count do
|
|
171
|
+
sendToPlayer(players[i], data)
|
|
136
172
|
end
|
|
137
173
|
bumpFires()
|
|
138
174
|
end
|
|
139
175
|
|
|
176
|
+
--[[
|
|
177
|
+
Resolve a target table into a { Player } list. Returns nil for unknown
|
|
178
|
+
sentinel kinds; the caller errors with the original target type.
|
|
179
|
+
]]
|
|
180
|
+
local function resolveAudience(target: any): { Player }?
|
|
181
|
+
local kind = target._lyncKind
|
|
182
|
+
if kind == "all" then
|
|
183
|
+
return Players:GetPlayers()
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if kind == "except" then
|
|
187
|
+
local excluded = target._excluded :: { [Player]: boolean }
|
|
188
|
+
local all = Players:GetPlayers()
|
|
189
|
+
local list = table.create(#all)
|
|
190
|
+
local n = 0
|
|
191
|
+
for _, player in all do
|
|
192
|
+
if not excluded[player] then
|
|
193
|
+
n += 1
|
|
194
|
+
list[n] = player
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
return list
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if kind == "group" then
|
|
201
|
+
local members = target._members :: { [Player]: boolean }
|
|
202
|
+
local list: { Player } = {}
|
|
203
|
+
local n = 0
|
|
204
|
+
for player in members do
|
|
205
|
+
n += 1
|
|
206
|
+
list[n] = player
|
|
207
|
+
end
|
|
208
|
+
return list
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if kind == nil then
|
|
212
|
+
-- Plain { Player } array. Validate entries on the way to a clean list.
|
|
213
|
+
local list: { Player } = {}
|
|
214
|
+
local n = 0
|
|
215
|
+
for _, player in target do
|
|
216
|
+
if isPlayer(player) then
|
|
217
|
+
n += 1
|
|
218
|
+
list[n] = player :: Player
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
return list
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
return nil
|
|
225
|
+
end
|
|
226
|
+
|
|
140
227
|
local function sendToTarget(data: any, target: any): ()
|
|
141
228
|
if isPlayer(target) then
|
|
142
229
|
local dropped, sendData = applySend(data, target :: Player)
|
|
@@ -153,40 +240,11 @@ function Packet.define<T>(
|
|
|
153
240
|
if dropped then
|
|
154
241
|
return
|
|
155
242
|
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
broadcast(Players:GetPlayers(), sendData)
|
|
160
|
-
return
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
if kind == "except" then
|
|
164
|
-
local excluded = target._excluded :: { [Player]: boolean }
|
|
165
|
-
for _, p in Players:GetPlayers() do
|
|
166
|
-
if not excluded[p] then
|
|
167
|
-
sendToPlayer(p, sendData)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
bumpFires()
|
|
171
|
-
return
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
if kind == "group" then
|
|
175
|
-
for p in target._members :: { [Player]: boolean } do
|
|
176
|
-
sendToPlayer(p, sendData)
|
|
177
|
-
end
|
|
178
|
-
bumpFires()
|
|
243
|
+
local audience = resolveAudience(target)
|
|
244
|
+
if audience then
|
|
245
|
+
broadcast(audience, sendData)
|
|
179
246
|
return
|
|
180
247
|
end
|
|
181
|
-
|
|
182
|
-
-- Plain { Player } array.
|
|
183
|
-
for _, p in target do
|
|
184
|
-
if isPlayer(p) then
|
|
185
|
-
sendToPlayer(p :: Player, sendData)
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
bumpFires()
|
|
189
|
-
return
|
|
190
248
|
end
|
|
191
249
|
|
|
192
250
|
Log.error(`"{name}" expected Player, table, Group, or Lync.all, got {typeof(target)}`)
|
package/src/api/Query.luau
CHANGED
|
@@ -44,10 +44,10 @@ local function writeQueryTracked(
|
|
|
44
44
|
): ()
|
|
45
45
|
if Channel.statsEnabled() then
|
|
46
46
|
local before = ch.cursor
|
|
47
|
-
Channel.writeQuery(ch, reg.id, corrId, codec, data)
|
|
47
|
+
Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
|
|
48
48
|
reg.bytesSent += ch.cursor - before
|
|
49
49
|
else
|
|
50
|
-
Channel.writeQuery(ch, reg.id, corrId, codec, data)
|
|
50
|
+
Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
package/src/api/Signal.luau
CHANGED
|
@@ -17,11 +17,16 @@ local _freeThread: thread? = nil
|
|
|
17
17
|
|
|
18
18
|
-- Private ----------------------------------------------------------------
|
|
19
19
|
|
|
20
|
+
-- pcall wrapper: a handler error must not consume the recycled thread, otherwise
|
|
21
|
+
-- every subsequent fire pays a coroutine.create until process restart.
|
|
20
22
|
local function passer(fn: (...any) -> (), ...): ()
|
|
21
23
|
local thread = _freeThread
|
|
22
24
|
_freeThread = nil
|
|
23
|
-
fn
|
|
25
|
+
local ok, err = pcall(fn, ...)
|
|
24
26
|
_freeThread = thread
|
|
27
|
+
if not ok then
|
|
28
|
+
task.spawn(error, err)
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
local function yielder(): ()
|
package/src/codec/Base.luau
CHANGED
|
@@ -7,7 +7,7 @@ local Types = require(script.Parent.Parent.Types)
|
|
|
7
7
|
|
|
8
8
|
-- Constants --------------------------------------------------------------
|
|
9
9
|
|
|
10
|
-
local DEFAULT_MAX =
|
|
10
|
+
local DEFAULT_MAX = 1048576
|
|
11
11
|
local INITIAL_BUF = 1024
|
|
12
12
|
|
|
13
13
|
-- State ------------------------------------------------------------------
|
|
@@ -56,6 +56,13 @@ local Base = {}
|
|
|
56
56
|
Base.INITIAL_BUF = INITIAL_BUF
|
|
57
57
|
Base.alloc = alloc
|
|
58
58
|
|
|
59
|
+
-- Cheap inline guard hoisted into every codec that grows a channel directly.
|
|
60
|
+
function Base.ensure(ch: Types.ChannelState, n: number): ()
|
|
61
|
+
if ch.cursor + n > ch.size then
|
|
62
|
+
alloc(ch, n)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
-- Floor is INITIAL_BUF: anything smaller errors on the very first write.
|
|
60
67
|
function Base.setMaxSize(bytes: number): ()
|
|
61
68
|
if bytes < INITIAL_BUF then
|