@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
|
@@ -18,7 +18,6 @@ local TS_FULL = 3
|
|
|
18
18
|
|
|
19
19
|
local INITIAL_BUF = 1024
|
|
20
20
|
local HEADER_BYTE = 1
|
|
21
|
-
local FRAME_MSB = Constants.FRAME_MSB
|
|
22
21
|
local U8_MAX = Constants.U8_MAX
|
|
23
22
|
local U16_MAX = Constants.U16_MAX
|
|
24
23
|
local MAX_BATCH_ITEMS = Constants.MAX_BATCH_ITEMS
|
|
@@ -48,8 +47,8 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
|
|
|
48
47
|
-- Private ----------------------------------------------------------------
|
|
49
48
|
|
|
50
49
|
local alloc = Base.alloc
|
|
50
|
+
local ensure = Base.ensure
|
|
51
51
|
local band = bit32.band
|
|
52
|
-
local bor = bit32.bor
|
|
53
52
|
local bufCopy = buffer.copy
|
|
54
53
|
local writeu8 = buffer.writeu8
|
|
55
54
|
local writeu16 = buffer.writeu16
|
|
@@ -57,21 +56,7 @@ local writef64 = buffer.writef64
|
|
|
57
56
|
local round = math.round
|
|
58
57
|
local osClock = os.clock
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
if ch.cursor + n > ch.size then
|
|
62
|
-
alloc(ch, n)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
local function writeTimestamp(b: buffer, off: number, mode: number): ()
|
|
67
|
-
if mode == TS_FRAME then
|
|
68
|
-
writeu8(b, off, _frameCounter)
|
|
69
|
-
elseif mode == TS_OFFSET then
|
|
70
|
-
writeu16(b, off, _offsetMs)
|
|
71
|
-
elseif mode == TS_FULL then
|
|
72
|
-
writef64(b, off, _clock)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
59
|
+
-- id is in [1, 127] (Registry.MAX_ID), so id + 128 == bor(FRAME_MSB, id).
|
|
75
60
|
|
|
76
61
|
--[[
|
|
77
62
|
Single -> multi upgrade. Patches the previously-emitted single-item
|
|
@@ -89,10 +74,7 @@ local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, heade
|
|
|
89
74
|
ensure(ch, 2)
|
|
90
75
|
-- Re-bind: ensure() may have realloced the buffer.
|
|
91
76
|
local b = ch.buff
|
|
92
|
-
|
|
93
|
-
if payloadLen > 0 then
|
|
94
|
-
bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
|
|
95
|
-
end
|
|
77
|
+
bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
|
|
96
78
|
|
|
97
79
|
ch.countPos = afterTs
|
|
98
80
|
writeu16(b, afterTs, 0)
|
|
@@ -100,34 +82,106 @@ local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, heade
|
|
|
100
82
|
ch.singleMode = false
|
|
101
83
|
end
|
|
102
84
|
|
|
85
|
+
-- itemCount bump is left to the caller, AFTER the body write succeeds.
|
|
86
|
+
local function openOrUpgrade(ch: Types.ChannelState, reg: Types.Registration): ()
|
|
87
|
+
if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
|
|
88
|
+
reg._openFn(ch, reg.id)
|
|
89
|
+
elseif ch.singleMode then
|
|
90
|
+
upgradeSingleToMulti(ch, reg.id, reg.headerSize)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
local function spliceEncoded(ch: Types.ChannelState, payload: buffer, payloadLen: number): ()
|
|
95
|
+
local cursor = ch.cursor
|
|
96
|
+
if cursor + payloadLen > ch.size then
|
|
97
|
+
alloc(ch, payloadLen)
|
|
98
|
+
end
|
|
99
|
+
bufCopy(ch.buff, cursor, payload, 0, payloadLen)
|
|
100
|
+
ch.cursor = cursor + payloadLen
|
|
101
|
+
end
|
|
102
|
+
|
|
103
103
|
--[[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
Hot-swapped by enableStats(). A throwing codec must rewind cursor + refs:
|
|
105
|
+
the next flush sends `[0, ch.cursor)`, so a partial write past `cursor`
|
|
106
|
+
desyncs the receiver from the truncation point onward.
|
|
107
107
|
]]
|
|
108
108
|
local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
|
|
109
109
|
local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
|
|
110
110
|
|
|
111
|
+
local function rewindOnThrow(ch: Types.ChannelState, cursor: number, refCount: number): ()
|
|
112
|
+
ch.cursor = cursor
|
|
113
|
+
if ch.refCount > refCount then
|
|
114
|
+
local refs = ch.refs
|
|
115
|
+
for i = refCount + 1, ch.refCount do
|
|
116
|
+
refs[i] = nil
|
|
117
|
+
end
|
|
118
|
+
ch.refCount = refCount
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
111
122
|
writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
openOrUpgrade(ch, reg)
|
|
124
|
+
local rewindCursor = ch.cursor
|
|
125
|
+
local rewindRefCount = ch.refCount
|
|
126
|
+
local ok, err = pcall(reg.codec.write, ch, data)
|
|
127
|
+
if not ok then
|
|
128
|
+
rewindOnThrow(ch, rewindCursor, rewindRefCount)
|
|
129
|
+
error(err, 0)
|
|
116
130
|
end
|
|
117
131
|
ch.itemCount += 1
|
|
118
|
-
reg.codec.write(ch, data)
|
|
119
132
|
end
|
|
120
133
|
|
|
121
134
|
writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
openOrUpgrade(ch, reg)
|
|
136
|
+
local rewindCursor = ch.cursor
|
|
137
|
+
local rewindRefCount = ch.refCount
|
|
138
|
+
local ok, err = pcall(reg.codec.write, ch, data)
|
|
139
|
+
if not ok then
|
|
140
|
+
rewindOnThrow(ch, rewindCursor, rewindRefCount)
|
|
141
|
+
error(err, 0)
|
|
142
|
+
end
|
|
143
|
+
ch.itemCount += 1
|
|
144
|
+
reg.bytesSent += ch.cursor - rewindCursor
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
--[[
|
|
148
|
+
Splice a pre-encoded payload into the channel. Used by Packet.broadcast
|
|
149
|
+
to encode the codec output once per fanout instead of N times.
|
|
150
|
+
]]
|
|
151
|
+
local writeBatchEncodedPlain: (Types.ChannelState, Types.Registration, buffer, number) -> ()
|
|
152
|
+
local writeBatchEncodedStats: (Types.ChannelState, Types.Registration, buffer, number) -> ()
|
|
153
|
+
|
|
154
|
+
writeBatchEncodedPlain = function(
|
|
155
|
+
ch: Types.ChannelState,
|
|
156
|
+
reg: Types.Registration,
|
|
157
|
+
payload: buffer,
|
|
158
|
+
payloadLen: number
|
|
159
|
+
): ()
|
|
160
|
+
openOrUpgrade(ch, reg)
|
|
161
|
+
local rewindCursor = ch.cursor
|
|
162
|
+
local ok, err = pcall(spliceEncoded, ch, payload, payloadLen)
|
|
163
|
+
if not ok then
|
|
164
|
+
ch.cursor = rewindCursor
|
|
165
|
+
error(err, 0)
|
|
126
166
|
end
|
|
127
167
|
ch.itemCount += 1
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
writeBatchEncodedStats = function(
|
|
171
|
+
ch: Types.ChannelState,
|
|
172
|
+
reg: Types.Registration,
|
|
173
|
+
payload: buffer,
|
|
174
|
+
payloadLen: number
|
|
175
|
+
): ()
|
|
176
|
+
openOrUpgrade(ch, reg)
|
|
177
|
+
local rewindCursor = ch.cursor
|
|
178
|
+
local ok, err = pcall(spliceEncoded, ch, payload, payloadLen)
|
|
179
|
+
if not ok then
|
|
180
|
+
ch.cursor = rewindCursor
|
|
181
|
+
error(err, 0)
|
|
182
|
+
end
|
|
183
|
+
ch.itemCount += 1
|
|
184
|
+
reg.bytesSent += payloadLen
|
|
131
185
|
end
|
|
132
186
|
|
|
133
187
|
-- Public -----------------------------------------------------------------
|
|
@@ -209,49 +263,116 @@ function Channel.fullReset(ch: Types.ChannelState): ()
|
|
|
209
263
|
Channel.reset(ch)
|
|
210
264
|
ch.prevDump = nil
|
|
211
265
|
ch.prevDumpLen = 0
|
|
212
|
-
|
|
266
|
+
if next(ch.deltas) ~= nil then
|
|
267
|
+
table.clear(ch.deltas)
|
|
268
|
+
end
|
|
213
269
|
end
|
|
214
270
|
|
|
215
271
|
--[[
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
writeBatch can upgrade single -> multi later.
|
|
272
|
+
Per-mode openFn variants. Each seals the prior batch, writes the
|
|
273
|
+
single-mode header [0x80 | id] + optional timestamp, and records the
|
|
274
|
+
cursor so writeBatch can upgrade single -> multi later.
|
|
220
275
|
]]
|
|
221
|
-
function
|
|
222
|
-
timestampMode: number,
|
|
223
|
-
name: string
|
|
224
|
-
): (ch: Types.ChannelState, id: number) -> ()
|
|
225
|
-
local headerLen = HEADER_BYTE + TS_BYTES[timestampMode]
|
|
226
|
-
|
|
276
|
+
local function makeOpenNoTs(name: string): (Types.ChannelState, number) -> ()
|
|
227
277
|
return function(ch: Types.ChannelState, id: number): ()
|
|
228
|
-
--[[
|
|
229
|
-
Inline sealCount: this is the only call site that can take the
|
|
230
|
-
multi-mode branch on a non-fresh channel.
|
|
231
|
-
]]
|
|
232
278
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
233
279
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
234
280
|
end
|
|
281
|
+
ch.currentPacket = name
|
|
282
|
+
ch.lastId = id
|
|
283
|
+
ch.itemCount = 0
|
|
284
|
+
local cursor = ch.cursor
|
|
285
|
+
if cursor + 1 > ch.size then
|
|
286
|
+
alloc(ch, 1)
|
|
287
|
+
end
|
|
288
|
+
writeu8(ch.buff, cursor, id + 128)
|
|
289
|
+
ch.singleMode = true
|
|
290
|
+
ch.singlePos = cursor
|
|
291
|
+
ch.cursor = cursor + 1
|
|
292
|
+
end
|
|
293
|
+
end
|
|
235
294
|
|
|
295
|
+
local function makeOpenFrameTs(name: string): (Types.ChannelState, number) -> ()
|
|
296
|
+
return function(ch: Types.ChannelState, id: number): ()
|
|
297
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
298
|
+
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
299
|
+
end
|
|
236
300
|
ch.currentPacket = name
|
|
237
301
|
ch.lastId = id
|
|
238
302
|
ch.itemCount = 0
|
|
303
|
+
local cursor = ch.cursor
|
|
304
|
+
if cursor + 2 > ch.size then
|
|
305
|
+
alloc(ch, 2)
|
|
306
|
+
end
|
|
307
|
+
local b = ch.buff
|
|
308
|
+
writeu8(b, cursor, id + 128)
|
|
309
|
+
writeu8(b, cursor + 1, _frameCounter)
|
|
310
|
+
ch.singleMode = true
|
|
311
|
+
ch.singlePos = cursor
|
|
312
|
+
ch.cursor = cursor + 2
|
|
313
|
+
end
|
|
314
|
+
end
|
|
239
315
|
|
|
316
|
+
local function makeOpenOffsetTs(name: string): (Types.ChannelState, number) -> ()
|
|
317
|
+
return function(ch: Types.ChannelState, id: number): ()
|
|
318
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
319
|
+
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
320
|
+
end
|
|
321
|
+
ch.currentPacket = name
|
|
322
|
+
ch.lastId = id
|
|
323
|
+
ch.itemCount = 0
|
|
240
324
|
local cursor = ch.cursor
|
|
241
|
-
if cursor +
|
|
242
|
-
alloc(ch,
|
|
325
|
+
if cursor + 3 > ch.size then
|
|
326
|
+
alloc(ch, 3)
|
|
243
327
|
end
|
|
244
328
|
local b = ch.buff
|
|
245
|
-
writeu8(b, cursor,
|
|
246
|
-
|
|
329
|
+
writeu8(b, cursor, id + 128)
|
|
330
|
+
writeu16(b, cursor + 1, _offsetMs)
|
|
331
|
+
ch.singleMode = true
|
|
332
|
+
ch.singlePos = cursor
|
|
333
|
+
ch.cursor = cursor + 3
|
|
334
|
+
end
|
|
335
|
+
end
|
|
247
336
|
|
|
337
|
+
local function makeOpenFullTs(name: string): (Types.ChannelState, number) -> ()
|
|
338
|
+
return function(ch: Types.ChannelState, id: number): ()
|
|
339
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
340
|
+
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
341
|
+
end
|
|
342
|
+
ch.currentPacket = name
|
|
343
|
+
ch.lastId = id
|
|
344
|
+
ch.itemCount = 0
|
|
345
|
+
local cursor = ch.cursor
|
|
346
|
+
if cursor + 9 > ch.size then
|
|
347
|
+
alloc(ch, 9)
|
|
348
|
+
end
|
|
349
|
+
local b = ch.buff
|
|
350
|
+
writeu8(b, cursor, id + 128)
|
|
351
|
+
writef64(b, cursor + 1, _clock)
|
|
248
352
|
ch.singleMode = true
|
|
249
353
|
ch.singlePos = cursor
|
|
250
|
-
ch.cursor = cursor +
|
|
354
|
+
ch.cursor = cursor + 9
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
function Channel.resolveOpenFn(
|
|
359
|
+
timestampMode: number,
|
|
360
|
+
name: string
|
|
361
|
+
): (ch: Types.ChannelState, id: number) -> ()
|
|
362
|
+
if timestampMode == TS_NONE then
|
|
363
|
+
return makeOpenNoTs(name)
|
|
364
|
+
end
|
|
365
|
+
if timestampMode == TS_FRAME then
|
|
366
|
+
return makeOpenFrameTs(name)
|
|
251
367
|
end
|
|
368
|
+
if timestampMode == TS_OFFSET then
|
|
369
|
+
return makeOpenOffsetTs(name)
|
|
370
|
+
end
|
|
371
|
+
return makeOpenFullTs(name)
|
|
252
372
|
end
|
|
253
373
|
|
|
254
374
|
Channel.writeBatch = writeBatchPlain
|
|
375
|
+
Channel.writeBatchEncoded = writeBatchEncodedPlain
|
|
255
376
|
|
|
256
377
|
--[[
|
|
257
378
|
Every caller (Server.flushReliable / flushUnreliable, Client.flush)
|
|
@@ -282,7 +403,8 @@ function Channel.writeQuery(
|
|
|
282
403
|
id: number,
|
|
283
404
|
correlationId: number,
|
|
284
405
|
codec: Types.InternalCodec<any>?,
|
|
285
|
-
data: any
|
|
406
|
+
data: any,
|
|
407
|
+
name: string?
|
|
286
408
|
): ()
|
|
287
409
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
288
410
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
@@ -294,9 +416,14 @@ function Channel.writeQuery(
|
|
|
294
416
|
here would never be observed before the next reset overwrites it.
|
|
295
417
|
]]
|
|
296
418
|
ch.lastId = -1
|
|
419
|
+
-- Without this, an alloc overflow inside the payload codec would log
|
|
420
|
+
-- the previous packet's name (whichever _openFn last touched currentPacket).
|
|
421
|
+
if name then
|
|
422
|
+
ch.currentPacket = name
|
|
423
|
+
end
|
|
297
424
|
|
|
298
425
|
local hasPayload = codec ~= nil and data ~= nil
|
|
299
|
-
local headerByte = if hasPayload then id else
|
|
426
|
+
local headerByte = if hasPayload then id else id + 128
|
|
300
427
|
|
|
301
428
|
local cursor = ch.cursor
|
|
302
429
|
if cursor + 1 > ch.size then
|
|
@@ -335,6 +462,7 @@ end
|
|
|
335
462
|
function Channel.enableStats(): ()
|
|
336
463
|
_statsEnabled = true
|
|
337
464
|
Channel.writeBatch = writeBatchStats
|
|
465
|
+
Channel.writeBatchEncoded = writeBatchEncodedStats
|
|
338
466
|
end
|
|
339
467
|
|
|
340
468
|
function Channel.statsEnabled(): boolean
|
|
@@ -350,6 +478,7 @@ function Channel.resetGlobals(): ()
|
|
|
350
478
|
_offsetMs = 0
|
|
351
479
|
_clock = 0.0
|
|
352
480
|
Channel.writeBatch = writeBatchPlain
|
|
481
|
+
Channel.writeBatchEncoded = writeBatchEncodedPlain
|
|
353
482
|
end
|
|
354
483
|
|
|
355
484
|
return Channel
|
package/src/internal/Pool.luau
CHANGED
|
@@ -20,23 +20,24 @@ local _maxSize = DEFAULT_SIZE
|
|
|
20
20
|
|
|
21
21
|
local Pool = {}
|
|
22
22
|
|
|
23
|
-
-- fullReset wipes the prior owner's XOR baseline and delta caches.
|
|
24
23
|
function Pool.acquire(): Types.ChannelState
|
|
25
24
|
if _depth > 0 then
|
|
26
25
|
local ch = _stack[_depth]
|
|
27
26
|
_stack[_depth] = nil
|
|
28
27
|
_depth -= 1
|
|
29
|
-
Channel.fullReset(ch)
|
|
30
28
|
return ch
|
|
31
29
|
end
|
|
32
30
|
return Channel.create()
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
--
|
|
33
|
+
-- Scrub on release so the pool never holds the prior owner's Instance refs
|
|
34
|
+
-- or XOR baseline buffer; otherwise a player who disconnects mid-frame leaves
|
|
35
|
+
-- those rooted in the pool until the slot is re-acquired (potentially never).
|
|
36
36
|
function Pool.release(ch: Types.ChannelState): ()
|
|
37
37
|
if _depth >= _maxSize then
|
|
38
38
|
return
|
|
39
39
|
end
|
|
40
|
+
Channel.fullReset(ch)
|
|
40
41
|
_depth += 1
|
|
41
42
|
_stack[_depth] = ch
|
|
42
43
|
end
|
|
@@ -207,17 +207,27 @@ function Reader.process(
|
|
|
207
207
|
local doGate = isServer and reg.needsGate and player ~= nil
|
|
208
208
|
local regSignal = reg.signal
|
|
209
209
|
local regName = reg.name
|
|
210
|
+
-- Lync.nothing reads zero bytes; bypass the pos/consumed gates that assume forward progress.
|
|
211
|
+
local isZeroSize = sizeMeta == 0
|
|
212
|
+
local accepted = 0
|
|
210
213
|
|
|
211
214
|
for _ = 1, count do
|
|
212
|
-
if pos >= incomingLen then
|
|
215
|
+
if not isZeroSize and pos >= incomingLen then
|
|
213
216
|
Log.warn(`codec for "{regName}" ran past frame end; aborting`)
|
|
214
217
|
return false
|
|
215
218
|
end
|
|
216
219
|
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
217
|
-
if consumed == 0 then
|
|
220
|
+
if not isZeroSize and consumed == 0 then
|
|
218
221
|
Log.warn(`codec for "{regName}" reported 0 bytes; aborting`)
|
|
219
222
|
return false
|
|
220
223
|
end
|
|
224
|
+
-- Fixed-size codecs MUST report consumed == _size or `pos` desyncs silently.
|
|
225
|
+
if sizeMeta and consumed ~= sizeMeta then
|
|
226
|
+
Log.warn(
|
|
227
|
+
`codec for "{regName}" consumed {consumed} bytes, expected {sizeMeta}; aborting`
|
|
228
|
+
)
|
|
229
|
+
return false
|
|
230
|
+
end
|
|
221
231
|
pos += consumed
|
|
222
232
|
if pos > incomingLen then
|
|
223
233
|
Log.warn(`codec for "{regName}" overran frame end; aborting`)
|
|
@@ -240,15 +250,14 @@ function Reader.process(
|
|
|
240
250
|
value = runReceive(value, regName, player)
|
|
241
251
|
end
|
|
242
252
|
|
|
243
|
-
|
|
244
|
-
reg.recvFires += 1
|
|
245
|
-
end
|
|
253
|
+
accepted += 1
|
|
246
254
|
|
|
247
255
|
-- tsValue is nil when mode == TS_NONE; pass through to skip a per-item branch.
|
|
248
256
|
regSignal:fire(value, player, tsValue)
|
|
249
257
|
end
|
|
250
258
|
|
|
251
259
|
if statsEnabled then
|
|
260
|
+
reg.recvFires += accepted
|
|
252
261
|
reg.bytesReceived += (pos - frameStart)
|
|
253
262
|
end
|
|
254
263
|
elseif kind == KIND_REQUEST or kind == KIND_RESPONSE then
|
|
@@ -266,13 +275,15 @@ function Reader.process(
|
|
|
266
275
|
end
|
|
267
276
|
reg.signal:fire(nil, player, corrId)
|
|
268
277
|
else
|
|
269
|
-
|
|
278
|
+
local regCodec = reg.codec
|
|
279
|
+
-- Mirror the packet path: 0-byte response codecs (Lync.nothing) bypass the gates.
|
|
280
|
+
local qIsZeroSize = regCodec._size == 0
|
|
281
|
+
if not qIsZeroSize and pos >= incomingLen then
|
|
270
282
|
Log.warn(`query "{reg.name}" body missing; aborting`)
|
|
271
283
|
return false
|
|
272
284
|
end
|
|
273
|
-
local regCodec = reg.codec
|
|
274
285
|
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
275
|
-
if consumed == 0 then
|
|
286
|
+
if not qIsZeroSize and consumed == 0 then
|
|
276
287
|
Log.warn(`query codec for "{reg.name}" reported 0 bytes; aborting`)
|
|
277
288
|
return false
|
|
278
289
|
end
|
|
@@ -32,8 +32,6 @@ local xorApply = Channel.xorApply
|
|
|
32
32
|
local sealAndDump = Channel.sealAndDump
|
|
33
33
|
local resetCh = Channel.reset
|
|
34
34
|
|
|
35
|
-
type StatsField = "bytesSent" | "bytesReceived"
|
|
36
|
-
|
|
37
35
|
local function getOrCreateChannel(
|
|
38
36
|
map: { [Player]: Types.ChannelState },
|
|
39
37
|
player: Player
|
|
@@ -46,16 +44,26 @@ local function getOrCreateChannel(
|
|
|
46
44
|
return ch
|
|
47
45
|
end
|
|
48
46
|
|
|
49
|
-
--
|
|
50
|
-
local function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
-- Hot-swapped at Server.start; stats-off path is a tail-callable noop.
|
|
48
|
+
local function noopStat(_player: Player, _byteLen: number): () end
|
|
49
|
+
|
|
50
|
+
local function realBytesSent(player: Player, byteLen: number): ()
|
|
51
|
+
local ps = _playerStats[player]
|
|
52
|
+
if ps then
|
|
53
|
+
ps.bytesSent += byteLen
|
|
56
54
|
end
|
|
57
55
|
end
|
|
58
56
|
|
|
57
|
+
local function realBytesReceived(player: Player, byteLen: number): ()
|
|
58
|
+
local ps = _playerStats[player]
|
|
59
|
+
if ps then
|
|
60
|
+
ps.bytesReceived += byteLen
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
local addBytesSent: (Player, number) -> () = noopStat
|
|
65
|
+
local addBytesReceived: (Player, number) -> () = noopStat
|
|
66
|
+
|
|
59
67
|
local function flushReliable(player: Player): ()
|
|
60
68
|
local rCh = _reliableChannels[player]
|
|
61
69
|
if not rCh or rCh.cursor == 0 then
|
|
@@ -69,7 +77,7 @@ local function flushReliable(player: Player): ()
|
|
|
69
77
|
|
|
70
78
|
Bridge.fireClient(player, xored, refs)
|
|
71
79
|
resetCh(rCh)
|
|
72
|
-
|
|
80
|
+
addBytesSent(player, byteLen)
|
|
73
81
|
end
|
|
74
82
|
|
|
75
83
|
local function flushUnreliable(player: Player): ()
|
|
@@ -81,7 +89,7 @@ local function flushUnreliable(player: Player): ()
|
|
|
81
89
|
local snapshot, refs, byteLen = sealAndDump(uCh)
|
|
82
90
|
Bridge.fireClientUnreliable(player, snapshot, refs)
|
|
83
91
|
resetCh(uCh)
|
|
84
|
-
|
|
92
|
+
addBytesSent(player, byteLen)
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
local function flushPlayer(player: Player): ()
|
|
@@ -116,7 +124,7 @@ local function handleIncomingReliable(player: Player, data: any, refs: any?): ()
|
|
|
116
124
|
_prevIncomingLen[player] = nil
|
|
117
125
|
end
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
addBytesReceived(player, incomingLen)
|
|
120
128
|
end
|
|
121
129
|
|
|
122
130
|
local function handleIncomingUnreliable(player: Player, data: any, refs: any?): ()
|
|
@@ -127,7 +135,7 @@ local function handleIncomingUnreliable(player: Player, data: any, refs: any?):
|
|
|
127
135
|
|
|
128
136
|
local incomingLen = buffer.len(incoming)
|
|
129
137
|
Reader.process(incoming, incomingLen, refs, player, true)
|
|
130
|
-
|
|
138
|
+
addBytesReceived(player, incomingLen)
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
local function clearPlayerState(player: Player): ()
|
|
@@ -161,6 +169,15 @@ function Server.getChannel(player: Player, isUnreliable: boolean): Types.Channel
|
|
|
161
169
|
)
|
|
162
170
|
end
|
|
163
171
|
|
|
172
|
+
-- Pre-resolved variants for broadcast: skip the per-audience-member isUnreliable branch.
|
|
173
|
+
function Server.getReliableChannel(player: Player): Types.ChannelState
|
|
174
|
+
return getOrCreateChannel(_reliableChannels, player)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
function Server.getUnreliableChannel(player: Player): Types.ChannelState
|
|
178
|
+
return getOrCreateChannel(_unreliableChannels, player)
|
|
179
|
+
end
|
|
180
|
+
|
|
164
181
|
function Server.flush(): ()
|
|
165
182
|
if Channel.hasTimestamps() then
|
|
166
183
|
Channel.updateTimestamp()
|
|
@@ -182,6 +199,11 @@ function Server.start(): ()
|
|
|
182
199
|
Bridge.reliable().OnServerEvent:Connect(handleIncomingReliable)
|
|
183
200
|
Bridge.unreliable().OnServerEvent:Connect(handleIncomingUnreliable)
|
|
184
201
|
|
|
202
|
+
if statsEnabled() then
|
|
203
|
+
addBytesSent = realBytesSent
|
|
204
|
+
addBytesReceived = realBytesReceived
|
|
205
|
+
end
|
|
206
|
+
|
|
185
207
|
Players.PlayerAdded:Connect(function(player: Player)
|
|
186
208
|
if statsEnabled() then
|
|
187
209
|
_playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
|
package/src/util/Buffer.luau
CHANGED
|
@@ -46,11 +46,9 @@ function Buf.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: n
|
|
|
46
46
|
return true
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
function Buf.snapshot(src: buffer, len: number): buffer
|
|
49
|
+
function Buf.snapshot(src: buffer, len: number, srcOff: number?): buffer
|
|
50
50
|
local out = bufCreate(len)
|
|
51
|
-
|
|
52
|
-
bufCopy(out, 0, src, 0, len)
|
|
53
|
-
end
|
|
51
|
+
bufCopy(out, 0, src, srcOff or 0, len)
|
|
54
52
|
return out
|
|
55
53
|
end
|
|
56
54
|
|
package/src/util/Quantize.luau
CHANGED
|
@@ -10,12 +10,34 @@ local ceil = math.ceil
|
|
|
10
10
|
local maxN = math.max
|
|
11
11
|
local minN = math.min
|
|
12
12
|
|
|
13
|
+
local writeu8 = buffer.writeu8
|
|
14
|
+
local writeu16 = buffer.writeu16
|
|
15
|
+
local readu8 = buffer.readu8
|
|
16
|
+
local readu16 = buffer.readu16
|
|
17
|
+
local band = bit32.band
|
|
18
|
+
local bor = bit32.bor
|
|
19
|
+
local rshift = bit32.rshift
|
|
20
|
+
local lshift = bit32.lshift
|
|
21
|
+
|
|
22
|
+
--[[
|
|
23
|
+
24-bit wire form. Splits into u16 + u8 since the buffer API has no native
|
|
24
|
+
u24. Saves 1 byte over u32 for ranges needing 17–24 bits of precision.
|
|
25
|
+
]]
|
|
26
|
+
local function writeu24(b: buffer, off: number, value: number): ()
|
|
27
|
+
writeu16(b, off, band(value, 0xFFFF))
|
|
28
|
+
writeu8(b, off + 2, band(rshift(value, 16), 0xFF))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local function readu24(b: buffer, off: number): number
|
|
32
|
+
return bor(readu16(b, off), lshift(readu8(b, off + 2), 16))
|
|
33
|
+
end
|
|
34
|
+
|
|
13
35
|
-- Public -----------------------------------------------------------------
|
|
14
36
|
|
|
15
37
|
local Quantize = {}
|
|
16
38
|
|
|
17
39
|
--[[
|
|
18
|
-
Pick the narrowest u8/u16/u32 wire form for [rangeMin, rangeMax] at
|
|
40
|
+
Pick the narrowest u8/u16/u24/u32 wire form for [rangeMin, rangeMax] at
|
|
19
41
|
`precision`. Returns (compBytes, wfn, rfn, scale, invScale, delta).
|
|
20
42
|
`apiName` shows in error messages so callers see their public surface.
|
|
21
43
|
]]
|
|
@@ -32,8 +54,8 @@ function Quantize.setup(
|
|
|
32
54
|
number,
|
|
33
55
|
number
|
|
34
56
|
)
|
|
35
|
-
if rangeMin
|
|
36
|
-
Log.error(`{apiName}: min ({rangeMin}) must be
|
|
57
|
+
if rangeMin >= rangeMax then
|
|
58
|
+
Log.error(`{apiName}: min ({rangeMin}) must be < max ({rangeMax})`)
|
|
37
59
|
end
|
|
38
60
|
if precision <= 0 then
|
|
39
61
|
Log.error(`{apiName}: precision must be positive, got {precision}`)
|
|
@@ -50,6 +72,8 @@ function Quantize.setup(
|
|
|
50
72
|
compBytes, wfn, rfn = 1, buffer.writeu8, buffer.readu8
|
|
51
73
|
elseif maxInt <= 0xFFFF then
|
|
52
74
|
compBytes, wfn, rfn = 2, buffer.writeu16, buffer.readu16
|
|
75
|
+
elseif maxInt <= 0xFFFFFF then
|
|
76
|
+
compBytes, wfn, rfn = 3, writeu24, readu24
|
|
53
77
|
else
|
|
54
78
|
compBytes, wfn, rfn = 4, buffer.writeu32, buffer.readu32
|
|
55
79
|
end
|