@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
|
@@ -9,15 +9,36 @@ local Shared = require(script.Parent.Parent.composite.Shared)
|
|
|
9
9
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
10
10
|
local Varint = require(script.Parent.Parent.primitive.Varint)
|
|
11
11
|
|
|
12
|
+
-- Constants --------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
-- deltaArray wire flags. Distinct from Shared's struct/map flag spaces.
|
|
15
|
+
local DA_UNCHANGED = 0
|
|
16
|
+
local DA_FULL = 1
|
|
17
|
+
local DA_PATCH = 2
|
|
18
|
+
|
|
12
19
|
-- Private ----------------------------------------------------------------
|
|
13
20
|
|
|
14
21
|
local alloc = Base.alloc
|
|
22
|
+
local ensure = Base.ensure
|
|
15
23
|
local allocDeltaId = Shared.allocDeltaId
|
|
24
|
+
local acquireScratch = Shared.acquireScratch
|
|
25
|
+
local releaseScratch = Shared.releaseScratch
|
|
26
|
+
local rangeEqual = Shared.rangeEqual
|
|
27
|
+
local copyToBuf = Shared.copyToBuf
|
|
28
|
+
local containsDelta = Shared.containsDelta
|
|
29
|
+
local enforceMaxCount = Shared.enforceMaxCount
|
|
16
30
|
local boolBytes = Shared.boolBytes
|
|
17
31
|
local packBoolArray = Shared.packBoolArray
|
|
18
32
|
local unpackBoolArray = Shared.unpackBoolArray
|
|
19
33
|
local varintRead = Varint.read
|
|
20
34
|
local varintWrite = Varint.write
|
|
35
|
+
local varintLength = Varint.length
|
|
36
|
+
local writeu8 = buffer.writeu8
|
|
37
|
+
local readu8 = buffer.readu8
|
|
38
|
+
local bufCopy = buffer.copy
|
|
39
|
+
local bufLen = buffer.len
|
|
40
|
+
|
|
41
|
+
type DeltaArrayEntry = { eBuf: buffer, eLen: number }
|
|
21
42
|
|
|
22
43
|
-- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
|
|
23
44
|
local function makeBoolArray(
|
|
@@ -32,25 +53,23 @@ local function makeBoolArray(
|
|
|
32
53
|
write = function(ch: Types.ChannelState, value: { boolean }): ()
|
|
33
54
|
local len = #value
|
|
34
55
|
varintWrite(ch, len)
|
|
35
|
-
packBoolArray(ch, value, len)
|
|
56
|
+
packBoolArray(ch, value, len, if len > 0 then boolBytes(len) else 0)
|
|
36
57
|
end,
|
|
37
58
|
|
|
38
59
|
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
|
|
39
60
|
local len, lenBytes = varintRead(src, pos)
|
|
40
|
-
|
|
41
|
-
Log.error(`count {len} exceeds max {maxCount}`)
|
|
42
|
-
end
|
|
61
|
+
enforceMaxCount(len, maxCount)
|
|
43
62
|
if len == 0 then
|
|
44
63
|
return {}, lenBytes
|
|
45
64
|
end
|
|
46
65
|
|
|
47
66
|
local byteCount = boolBytes(len)
|
|
48
67
|
local dataStart = pos + lenBytes
|
|
49
|
-
if dataStart + byteCount >
|
|
68
|
+
if dataStart + byteCount > bufLen(src) then
|
|
50
69
|
Log.error(`payload {byteCount}B exceeds remaining buffer`)
|
|
51
70
|
end
|
|
52
71
|
local result = table.create(len, false)
|
|
53
|
-
unpackBoolArray(src, dataStart, len, result)
|
|
72
|
+
unpackBoolArray(src, dataStart, len, result, byteCount)
|
|
54
73
|
return result, lenBytes + byteCount
|
|
55
74
|
end,
|
|
56
75
|
}
|
|
@@ -92,16 +111,14 @@ local function makeDirectArray(
|
|
|
92
111
|
|
|
93
112
|
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
|
|
94
113
|
local len, lenBytes = varintRead(src, pos)
|
|
95
|
-
|
|
96
|
-
Log.error(`count {len} exceeds max {maxCount}`)
|
|
97
|
-
end
|
|
114
|
+
enforceMaxCount(len, maxCount)
|
|
98
115
|
if len == 0 then
|
|
99
116
|
return {}, lenBytes
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
local payloadBytes = len * elemSize
|
|
103
120
|
local dataStart = pos + lenBytes
|
|
104
|
-
if payloadBytes >
|
|
121
|
+
if payloadBytes > bufLen(src) - dataStart then
|
|
105
122
|
Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
|
|
106
123
|
end
|
|
107
124
|
|
|
@@ -136,12 +153,15 @@ local function makeGenericArray(
|
|
|
136
153
|
|
|
137
154
|
read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
|
|
138
155
|
local len, lenBytes = varintRead(src, pos)
|
|
139
|
-
|
|
140
|
-
Log.error(`count {len} exceeds max {maxCount}`)
|
|
141
|
-
end
|
|
156
|
+
enforceMaxCount(len, maxCount)
|
|
142
157
|
if len == 0 then
|
|
143
158
|
return {}, lenBytes
|
|
144
159
|
end
|
|
160
|
+
-- Every element occupies at least 1 byte; len > remaining is
|
|
161
|
+
-- unambiguously corrupt and would OOM table.create otherwise.
|
|
162
|
+
if len > bufLen(src) - (pos + lenBytes) then
|
|
163
|
+
Log.error(`array length {len} exceeds remaining buffer`)
|
|
164
|
+
end
|
|
145
165
|
|
|
146
166
|
local result = table.create(len)
|
|
147
167
|
local total = lenBytes
|
|
@@ -179,19 +199,275 @@ function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Type
|
|
|
179
199
|
if element._hasUnknown then
|
|
180
200
|
codec._hasUnknown = true
|
|
181
201
|
end
|
|
202
|
+
if containsDelta(element) then
|
|
203
|
+
codec._hasDelta = true
|
|
204
|
+
end
|
|
182
205
|
return table.freeze(codec)
|
|
183
206
|
end
|
|
184
207
|
|
|
185
208
|
--[[
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
Per-index deltaArray. Wire:
|
|
210
|
+
[0] UNCHANGED (1 byte)
|
|
211
|
+
[1] FULL <count varint> <element bytes ...>
|
|
212
|
+
[2] PATCH <newLen varint> <changeCount varint>
|
|
213
|
+
<(idx varint, element bytes) ...>
|
|
214
|
+
|
|
215
|
+
Length shrinkage is implicit in newLen — entries past newLen drop from
|
|
216
|
+
the cached map. Length growth: appended indices show up as changes.
|
|
217
|
+
Reordered lists tend to mark every index changed; the writer falls back
|
|
218
|
+
to FULL when changeCount * 2 >= newLen since PATCH index varints eat
|
|
219
|
+
any savings past that ratio.
|
|
189
220
|
]]
|
|
190
221
|
function Array.deltaArray(
|
|
191
222
|
element: Types.InternalCodec<any>,
|
|
192
223
|
maxCount: number?
|
|
193
224
|
): Types.InternalCodec<any>
|
|
194
|
-
|
|
225
|
+
-- Inner delta would chain-diff across elements within a frame, not across frames.
|
|
226
|
+
if containsDelta(element) then
|
|
227
|
+
Log.error("deltaArray element cannot contain delta state")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
local deltaId = allocDeltaId()
|
|
231
|
+
local inner = Array.array(element, maxCount)
|
|
232
|
+
|
|
233
|
+
--[[
|
|
234
|
+
Encode each element into `scratch` and capture per-index byte ranges.
|
|
235
|
+
Returns the array length plus parallel arrays of offset/length so the
|
|
236
|
+
diff path can compare and emit individual entries without re-encoding.
|
|
237
|
+
]]
|
|
238
|
+
local function encodePerIdx(
|
|
239
|
+
scratch: Types.ChannelState,
|
|
240
|
+
value: { any }
|
|
241
|
+
): (number, { number }, { number })
|
|
242
|
+
local len = #value
|
|
243
|
+
enforceMaxCount(len, maxCount)
|
|
244
|
+
local off = table.create(len)
|
|
245
|
+
local elen = table.create(len)
|
|
246
|
+
for i = 1, len do
|
|
247
|
+
local before = scratch.cursor
|
|
248
|
+
element.write(scratch, value[i])
|
|
249
|
+
off[i] = before
|
|
250
|
+
elen[i] = scratch.cursor - before
|
|
251
|
+
end
|
|
252
|
+
return len, off, elen
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
return table.freeze({
|
|
256
|
+
_isDelta = true,
|
|
257
|
+
_isArray = true,
|
|
258
|
+
_element = element,
|
|
259
|
+
_maxCount = maxCount,
|
|
260
|
+
|
|
261
|
+
write = function(ch: Types.ChannelState, value: { any }): ()
|
|
262
|
+
if typeof(value) ~= "table" then
|
|
263
|
+
Log.error(`expected table, got {typeof(value)}`)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
local cache = ch.deltas[deltaId] :: any
|
|
267
|
+
local scratch = acquireScratch()
|
|
268
|
+
local len, off, elen = encodePerIdx(scratch, value)
|
|
269
|
+
local scratchBuf = scratch.buff
|
|
270
|
+
|
|
271
|
+
-- First frame: emit FULL using the per-index scratch bytes.
|
|
272
|
+
if not cache then
|
|
273
|
+
ensure(ch, 1)
|
|
274
|
+
writeu8(ch.buff, ch.cursor, DA_FULL)
|
|
275
|
+
ch.cursor += 1
|
|
276
|
+
varintWrite(ch, len)
|
|
277
|
+
local payloadLen = scratch.cursor
|
|
278
|
+
ensure(ch, payloadLen)
|
|
279
|
+
bufCopy(ch.buff, ch.cursor, scratchBuf, 0, payloadLen)
|
|
280
|
+
ch.cursor += payloadLen
|
|
281
|
+
|
|
282
|
+
local perIdx: { [number]: DeltaArrayEntry } = {}
|
|
283
|
+
for i = 1, len do
|
|
284
|
+
local n = elen[i]
|
|
285
|
+
perIdx[i] = { eBuf = copyToBuf(nil, scratchBuf, off[i], n), eLen = n }
|
|
286
|
+
end
|
|
287
|
+
ch.deltas[deltaId] = { perIdx = perIdx, length = len } :: any
|
|
288
|
+
releaseScratch()
|
|
289
|
+
return
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
local cachedPerIdx = cache.perIdx :: { [number]: DeltaArrayEntry }
|
|
293
|
+
local cachedLen = cache.length :: number
|
|
294
|
+
|
|
295
|
+
-- Find changed indices in the overlap; appended indices count as changed.
|
|
296
|
+
local changed: { number } = {}
|
|
297
|
+
local changedCount = 0
|
|
298
|
+
local overlap = if len < cachedLen then len else cachedLen
|
|
299
|
+
for i = 1, overlap do
|
|
300
|
+
local entry = cachedPerIdx[i]
|
|
301
|
+
local n = elen[i]
|
|
302
|
+
if
|
|
303
|
+
not entry
|
|
304
|
+
or n ~= entry.eLen
|
|
305
|
+
or not rangeEqual(scratchBuf, off[i], entry.eBuf, 0, n)
|
|
306
|
+
then
|
|
307
|
+
changedCount += 1
|
|
308
|
+
changed[changedCount] = i
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
for i = overlap + 1, len do
|
|
312
|
+
changedCount += 1
|
|
313
|
+
changed[changedCount] = i
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
if changedCount == 0 and len == cachedLen then
|
|
317
|
+
ensure(ch, 1)
|
|
318
|
+
writeu8(ch.buff, ch.cursor, DA_UNCHANGED)
|
|
319
|
+
ch.cursor += 1
|
|
320
|
+
releaseScratch()
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
-- Pick FULL vs PATCH on actual byte cost. Both share `1 + varintLength(len)`
|
|
325
|
+
-- so it cancels: compare PATCH's index-header overhead + changed payload
|
|
326
|
+
-- against FULL's full payload.
|
|
327
|
+
local fullPayload = scratch.cursor
|
|
328
|
+
local patchPayload = varintLength(changedCount)
|
|
329
|
+
for i = 1, changedCount do
|
|
330
|
+
local idx = changed[i]
|
|
331
|
+
patchPayload += varintLength(idx) + elen[idx]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
if patchPayload < fullPayload then
|
|
335
|
+
ensure(ch, 1)
|
|
336
|
+
writeu8(ch.buff, ch.cursor, DA_PATCH)
|
|
337
|
+
ch.cursor += 1
|
|
338
|
+
varintWrite(ch, len)
|
|
339
|
+
varintWrite(ch, changedCount)
|
|
340
|
+
for i = 1, changedCount do
|
|
341
|
+
local idx = changed[i]
|
|
342
|
+
varintWrite(ch, idx)
|
|
343
|
+
local n = elen[idx]
|
|
344
|
+
ensure(ch, n)
|
|
345
|
+
bufCopy(ch.buff, ch.cursor, scratchBuf, off[idx], n)
|
|
346
|
+
ch.cursor += n
|
|
347
|
+
end
|
|
348
|
+
else
|
|
349
|
+
ensure(ch, 1)
|
|
350
|
+
writeu8(ch.buff, ch.cursor, DA_FULL)
|
|
351
|
+
ch.cursor += 1
|
|
352
|
+
varintWrite(ch, len)
|
|
353
|
+
ensure(ch, fullPayload)
|
|
354
|
+
bufCopy(ch.buff, ch.cursor, scratchBuf, 0, fullPayload)
|
|
355
|
+
ch.cursor += fullPayload
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
-- Update cache: refresh changed (and appended), drop truncated.
|
|
359
|
+
for i = 1, changedCount do
|
|
360
|
+
local idx = changed[i]
|
|
361
|
+
local n = elen[idx]
|
|
362
|
+
local entry = cachedPerIdx[idx]
|
|
363
|
+
if entry then
|
|
364
|
+
entry.eBuf = copyToBuf(entry.eBuf, scratchBuf, off[idx], n)
|
|
365
|
+
entry.eLen = n
|
|
366
|
+
else
|
|
367
|
+
cachedPerIdx[idx] = { eBuf = copyToBuf(nil, scratchBuf, off[idx], n), eLen = n }
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
for i = len + 1, cachedLen do
|
|
371
|
+
cachedPerIdx[i] = nil
|
|
372
|
+
end
|
|
373
|
+
cache.length = len
|
|
374
|
+
|
|
375
|
+
releaseScratch()
|
|
376
|
+
end,
|
|
377
|
+
|
|
378
|
+
read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
|
|
379
|
+
if pos >= bufLen(src) then
|
|
380
|
+
Log.error("truncated deltaArray header")
|
|
381
|
+
end
|
|
382
|
+
local flag = readu8(src, pos)
|
|
383
|
+
|
|
384
|
+
if flag == DA_UNCHANGED then
|
|
385
|
+
local cached = Baseline.getCache(deltaId)
|
|
386
|
+
if cached == nil then
|
|
387
|
+
Log.error("UNCHANGED flag before any FULL frame; baseline missing")
|
|
388
|
+
end
|
|
389
|
+
return cached, 1
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if flag == DA_FULL then
|
|
393
|
+
local value, consumed = inner.read(src, pos + 1, refs)
|
|
394
|
+
Baseline.setCache(deltaId, value)
|
|
395
|
+
return value, 1 + consumed
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
if flag == DA_PATCH then
|
|
399
|
+
local cached = Baseline.getCache(deltaId)
|
|
400
|
+
if cached == nil then
|
|
401
|
+
Log.error("deltaArray PATCH before any FULL frame; baseline missing")
|
|
402
|
+
end
|
|
403
|
+
--[[
|
|
404
|
+
Capture the cached length BEFORE clone+mutate; #result
|
|
405
|
+
becomes unreliable once we set entries at appended indices
|
|
406
|
+
past the original boundary.
|
|
407
|
+
]]
|
|
408
|
+
local cachedLen = #cached
|
|
409
|
+
local result: { any } = table.clone(cached)
|
|
410
|
+
local total = 1
|
|
411
|
+
|
|
412
|
+
local newLen, lLen = varintRead(src, pos + total)
|
|
413
|
+
if lLen == 0 then
|
|
414
|
+
Log.error("truncated deltaArray length")
|
|
415
|
+
end
|
|
416
|
+
enforceMaxCount(newLen, maxCount, "newLen")
|
|
417
|
+
total += lLen
|
|
418
|
+
|
|
419
|
+
local changeCount, cLen = varintRead(src, pos + total)
|
|
420
|
+
if cLen == 0 then
|
|
421
|
+
Log.error("truncated deltaArray change count")
|
|
422
|
+
end
|
|
423
|
+
if changeCount > newLen then
|
|
424
|
+
Log.error(`deltaArray changeCount {changeCount} exceeds newLen {newLen}`)
|
|
425
|
+
end
|
|
426
|
+
total += cLen
|
|
427
|
+
|
|
428
|
+
-- The writer marks every appended index (cachedLen+1 .. newLen)
|
|
429
|
+
-- as changed; the reader must enforce this so the wire can't
|
|
430
|
+
-- ship a holey baseline that breaks #result downstream.
|
|
431
|
+
local appendedNeeded = if newLen > cachedLen then newLen - cachedLen else 0
|
|
432
|
+
local appendedSeen = 0
|
|
433
|
+
|
|
434
|
+
for _ = 1, changeCount do
|
|
435
|
+
local idx, iLen = varintRead(src, pos + total)
|
|
436
|
+
if iLen == 0 then
|
|
437
|
+
Log.error("truncated deltaArray index")
|
|
438
|
+
end
|
|
439
|
+
if idx < 1 or idx > newLen then
|
|
440
|
+
Log.error(`deltaArray index {idx} out of range [1, {newLen}]`)
|
|
441
|
+
end
|
|
442
|
+
if idx > cachedLen then
|
|
443
|
+
appendedSeen += 1
|
|
444
|
+
end
|
|
445
|
+
total += iLen
|
|
446
|
+
local v, vc = element.read(src, pos + total, refs)
|
|
447
|
+
if vc == 0 then
|
|
448
|
+
Log.error("truncated deltaArray element")
|
|
449
|
+
end
|
|
450
|
+
total += vc
|
|
451
|
+
result[idx] = v
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
if appendedSeen ~= appendedNeeded then
|
|
455
|
+
Log.error(
|
|
456
|
+
`deltaArray PATCH grew length to {newLen} but only {appendedSeen}/{appendedNeeded} appended indices supplied`
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
for i = newLen + 1, cachedLen do
|
|
461
|
+
result[i] = nil
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
Baseline.setCache(deltaId, result)
|
|
465
|
+
return result, total
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
Log.error(`invalid deltaArray flag {flag}`)
|
|
469
|
+
end,
|
|
470
|
+
}) :: Types.InternalCodec<any>
|
|
195
471
|
end
|
|
196
472
|
|
|
197
473
|
return table.freeze(Array)
|