@axpecter/lync 2.3.2 → 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 +1 -1
- package/package.json +1 -1
- package/src/api/Packet.luau +4 -6
- package/src/api/Query.luau +2 -2
- package/src/api/Signal.luau +6 -1
- package/src/codec/Base.luau +1 -1
- package/src/codec/composite/Array.luau +22 -5
- package/src/codec/composite/Map.luau +29 -13
- package/src/codec/composite/Shared.luau +41 -21
- package/src/codec/composite/Struct.luau +7 -7
- package/src/codec/meta/Bitfield.luau +14 -1
- package/src/internal/Channel.luau +136 -42
- package/src/internal/Pool.luau +4 -3
- package/src/transport/Reader.luau +3 -3
- package/src/transport/Server.luau +35 -13
package/README.md
CHANGED
package/package.json
CHANGED
package/src/api/Packet.luau
CHANGED
|
@@ -152,15 +152,13 @@ function Packet.define<T>(
|
|
|
152
152
|
local payload = scratch.buff
|
|
153
153
|
local payloadLen = scratch.cursor
|
|
154
154
|
local server = Transport.server()
|
|
155
|
+
local getChannel = if isUnreliable
|
|
156
|
+
then server.getUnreliableChannel
|
|
157
|
+
else server.getReliableChannel
|
|
155
158
|
-- Channel.writeBatchEncoded is hot-swapped by enableStats; re-read per call.
|
|
156
159
|
local writeBatchEncoded = Channel.writeBatchEncoded
|
|
157
160
|
for i = 1, count do
|
|
158
|
-
writeBatchEncoded(
|
|
159
|
-
server.getChannel(players[i], isUnreliable),
|
|
160
|
-
reg,
|
|
161
|
-
payload,
|
|
162
|
-
payloadLen
|
|
163
|
-
)
|
|
161
|
+
writeBatchEncoded(getChannel(players[i]), reg, payload, payloadLen)
|
|
164
162
|
end
|
|
165
163
|
Shared.releaseScratch()
|
|
166
164
|
bumpFires()
|
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 ------------------------------------------------------------------
|
|
@@ -53,7 +53,7 @@ local function makeBoolArray(
|
|
|
53
53
|
write = function(ch: Types.ChannelState, value: { boolean }): ()
|
|
54
54
|
local len = #value
|
|
55
55
|
varintWrite(ch, len)
|
|
56
|
-
packBoolArray(ch, value, len)
|
|
56
|
+
packBoolArray(ch, value, len, if len > 0 then boolBytes(len) else 0)
|
|
57
57
|
end,
|
|
58
58
|
|
|
59
59
|
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
|
|
@@ -69,7 +69,7 @@ local function makeBoolArray(
|
|
|
69
69
|
Log.error(`payload {byteCount}B exceeds remaining buffer`)
|
|
70
70
|
end
|
|
71
71
|
local result = table.create(len, false)
|
|
72
|
-
unpackBoolArray(src, dataStart, len, result)
|
|
72
|
+
unpackBoolArray(src, dataStart, len, result, byteCount)
|
|
73
73
|
return result, lenBytes + byteCount
|
|
74
74
|
end,
|
|
75
75
|
}
|
|
@@ -157,6 +157,11 @@ local function makeGenericArray(
|
|
|
157
157
|
if len == 0 then
|
|
158
158
|
return {}, lenBytes
|
|
159
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
|
|
160
165
|
|
|
161
166
|
local result = table.create(len)
|
|
162
167
|
local total = lenBytes
|
|
@@ -415,22 +420,28 @@ function Array.deltaArray(
|
|
|
415
420
|
if cLen == 0 then
|
|
416
421
|
Log.error("truncated deltaArray change count")
|
|
417
422
|
end
|
|
418
|
-
-- changeCount is also bounded by newLen: every index appears at most once.
|
|
419
423
|
if changeCount > newLen then
|
|
420
424
|
Log.error(`deltaArray changeCount {changeCount} exceeds newLen {newLen}`)
|
|
421
425
|
end
|
|
422
426
|
total += cLen
|
|
423
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
|
+
|
|
424
434
|
for _ = 1, changeCount do
|
|
425
435
|
local idx, iLen = varintRead(src, pos + total)
|
|
426
436
|
if iLen == 0 then
|
|
427
437
|
Log.error("truncated deltaArray index")
|
|
428
438
|
end
|
|
429
|
-
-- Reject out-of-range idx so the wire can't poison the cache with
|
|
430
|
-
-- arbitrary keys that survive truncation.
|
|
431
439
|
if idx < 1 or idx > newLen then
|
|
432
440
|
Log.error(`deltaArray index {idx} out of range [1, {newLen}]`)
|
|
433
441
|
end
|
|
442
|
+
if idx > cachedLen then
|
|
443
|
+
appendedSeen += 1
|
|
444
|
+
end
|
|
434
445
|
total += iLen
|
|
435
446
|
local v, vc = element.read(src, pos + total, refs)
|
|
436
447
|
if vc == 0 then
|
|
@@ -440,6 +451,12 @@ function Array.deltaArray(
|
|
|
440
451
|
result[idx] = v
|
|
441
452
|
end
|
|
442
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
|
+
|
|
443
460
|
for i = newLen + 1, cachedLen do
|
|
444
461
|
result[i] = nil
|
|
445
462
|
end
|
|
@@ -21,9 +21,11 @@ local DM_DIFF = 2
|
|
|
21
21
|
--[[
|
|
22
22
|
Depth-stacked key scratch. A nested Map.write (Map<K, Map<K2, V>>) would
|
|
23
23
|
clobber a single shared array mid-iteration; the stack gives each frame
|
|
24
|
-
its own slot.
|
|
24
|
+
its own slot. _keyTails parallels _keyStacks because `#keys` after
|
|
25
|
+
`keys[count] = k` is undefined when count < the prior count.
|
|
25
26
|
]]
|
|
26
27
|
local _keyStacks: { { any } } = {}
|
|
28
|
+
local _keyTails: { number } = {}
|
|
27
29
|
local _keyDepth = 0
|
|
28
30
|
|
|
29
31
|
-- Private ----------------------------------------------------------------
|
|
@@ -73,23 +75,23 @@ local function sortKeys(keys: { any }, canSortDirect: boolean): ()
|
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
--[[
|
|
77
|
-
Acquire a scratch keys array, populate it from `value`, nil any leftover
|
|
78
|
-
tail entries from a prior reuse, then sort. Returns the keys array and
|
|
79
|
-
the populated count. Caller MUST releaseKeys() after consuming the keys.
|
|
80
|
-
]]
|
|
81
78
|
local function collectAndSortKeys(value: { [any]: any }, canSortDirect: boolean): ({ any }, number)
|
|
82
79
|
local keys = acquireKeys()
|
|
80
|
+
local prevTail = _keyTails[_keyDepth] or 0
|
|
83
81
|
local count = 0
|
|
84
82
|
for k in value do
|
|
85
83
|
count += 1
|
|
86
84
|
keys[count] = k
|
|
87
85
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
if count < prevTail then
|
|
87
|
+
for i = count + 1, prevTail do
|
|
88
|
+
keys[i] = nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
_keyTails[_keyDepth] = count
|
|
92
|
+
if count > 1 then
|
|
93
|
+
sortKeys(keys, canSortDirect)
|
|
91
94
|
end
|
|
92
|
-
sortKeys(keys, canSortDirect)
|
|
93
95
|
return keys, count
|
|
94
96
|
end
|
|
95
97
|
|
|
@@ -163,6 +165,11 @@ function Map.map(
|
|
|
163
165
|
if payloadBytes > bufLen(src) - (pos + lenBytes) then
|
|
164
166
|
Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
|
|
165
167
|
end
|
|
168
|
+
elseif count > 0 then
|
|
169
|
+
-- Variable-size: each pair is at least 2 bytes (key + value).
|
|
170
|
+
if count * 2 > bufLen(src) - (pos + lenBytes) then
|
|
171
|
+
Log.error(`map count {count} exceeds remaining buffer`)
|
|
172
|
+
end
|
|
166
173
|
end
|
|
167
174
|
|
|
168
175
|
local result: { [any]: any } = {}
|
|
@@ -419,14 +426,19 @@ function Map.deltaMap(
|
|
|
419
426
|
local result: { [any]: any } = table.clone(cached)
|
|
420
427
|
local total = 1
|
|
421
428
|
|
|
429
|
+
local cachedSize = 0
|
|
430
|
+
for _ in cached do
|
|
431
|
+
cachedSize += 1
|
|
432
|
+
end
|
|
433
|
+
|
|
422
434
|
local removeCount, rcLen = varintRead(src, pos + total)
|
|
423
435
|
if rcLen == 0 then
|
|
424
436
|
Log.error("truncated deltaMap remove count")
|
|
425
437
|
end
|
|
426
|
-
-- Both counts bounded by maxCount; otherwise a malicious DIFF
|
|
427
|
-
-- could spin the loop arbitrarily long before erroring on
|
|
428
|
-
-- buffer overrun.
|
|
429
438
|
enforceMaxCount(removeCount, maxCount, "removeCount")
|
|
439
|
+
if removeCount > cachedSize then
|
|
440
|
+
Log.error(`deltaMap removeCount {removeCount} exceeds cached size {cachedSize}`)
|
|
441
|
+
end
|
|
430
442
|
total += rcLen
|
|
431
443
|
for _ = 1, removeCount do
|
|
432
444
|
local k, kc = keyCodec.read(src, pos + total, refs)
|
|
@@ -442,6 +454,10 @@ function Map.deltaMap(
|
|
|
442
454
|
Log.error("truncated deltaMap change count")
|
|
443
455
|
end
|
|
444
456
|
enforceMaxCount(changeCount, maxCount, "changeCount")
|
|
457
|
+
-- Joint bound: post-DIFF size can't exceed maxCount either.
|
|
458
|
+
if maxCount and (cachedSize - removeCount + changeCount) > maxCount then
|
|
459
|
+
Log.error(`deltaMap DIFF would grow map past maxCount {maxCount}`)
|
|
460
|
+
end
|
|
445
461
|
total += ccLen
|
|
446
462
|
for _ = 1, changeCount do
|
|
447
463
|
local k, kc = keyCodec.read(src, pos + total, refs)
|
|
@@ -97,7 +97,9 @@ end
|
|
|
97
97
|
--[[
|
|
98
98
|
Stack-based scratch ChannelState pool. Re-entrant codec serialization
|
|
99
99
|
(a codec writing into scratch that triggers another codec's scratch
|
|
100
|
-
acquire) works without aliasing.
|
|
100
|
+
acquire) works without aliasing. Full scrub on reuse so any codec that
|
|
101
|
+
reads framing fields (lastId/singleMode/itemCount/countPos) on scratch
|
|
102
|
+
sees a clean state, not stale bytes from the previous owner.
|
|
101
103
|
]]
|
|
102
104
|
function Shared.acquireScratch(): Types.ChannelState
|
|
103
105
|
_scratchDepth += 1
|
|
@@ -105,6 +107,15 @@ function Shared.acquireScratch(): Types.ChannelState
|
|
|
105
107
|
if ch then
|
|
106
108
|
ch.cursor = 0
|
|
107
109
|
ch.refCount = 0
|
|
110
|
+
ch.lastId = -1
|
|
111
|
+
ch.countPos = -1
|
|
112
|
+
ch.itemCount = 0
|
|
113
|
+
ch.singleMode = false
|
|
114
|
+
ch.singlePos = 0
|
|
115
|
+
ch.currentPacket = nil
|
|
116
|
+
if next(ch.deltas) ~= nil then
|
|
117
|
+
table.clear(ch.deltas)
|
|
118
|
+
end
|
|
108
119
|
return ch
|
|
109
120
|
end
|
|
110
121
|
ch = Channel.create()
|
|
@@ -171,25 +182,27 @@ function Shared.extractFields(
|
|
|
171
182
|
return dataKeys, dataCodecs, boolKeys
|
|
172
183
|
end
|
|
173
184
|
|
|
185
|
+
-- `byteCount` is `(boolCount + 7) // 8`; callers with it cached pass it through.
|
|
174
186
|
function Shared.packBools(
|
|
175
187
|
ch: Types.ChannelState,
|
|
176
188
|
value: { [string]: any },
|
|
177
189
|
boolKeys: { string },
|
|
178
|
-
boolCount: number
|
|
190
|
+
boolCount: number,
|
|
191
|
+
byteCount: number?
|
|
179
192
|
): ()
|
|
180
193
|
if boolCount == 0 then
|
|
181
194
|
return
|
|
182
195
|
end
|
|
183
196
|
|
|
184
|
-
local
|
|
197
|
+
local bc = byteCount or (boolCount + 7) // 8
|
|
185
198
|
local cursor = ch.cursor
|
|
186
|
-
if cursor +
|
|
187
|
-
alloc(ch,
|
|
199
|
+
if cursor + bc > ch.size then
|
|
200
|
+
alloc(ch, bc)
|
|
188
201
|
end
|
|
189
202
|
|
|
190
203
|
local idx = 1
|
|
191
204
|
local b = ch.buff
|
|
192
|
-
for byteIdx = 0,
|
|
205
|
+
for byteIdx = 0, bc - 1 do
|
|
193
206
|
local packed = 0
|
|
194
207
|
local remaining = boolCount - idx + 1
|
|
195
208
|
local limit = if remaining >= 8 then 8 else remaining
|
|
@@ -202,27 +215,32 @@ function Shared.packBools(
|
|
|
202
215
|
writeu8(b, cursor + byteIdx, packed)
|
|
203
216
|
end
|
|
204
217
|
|
|
205
|
-
ch.cursor = cursor +
|
|
218
|
+
ch.cursor = cursor + bc
|
|
206
219
|
end
|
|
207
220
|
|
|
208
221
|
--[[
|
|
209
222
|
Integer-indexed bool-array pack. Split from packBools to skip the
|
|
210
223
|
per-bit string-key getter on the bool-array hot path.
|
|
211
224
|
]]
|
|
212
|
-
function Shared.packBoolArray(
|
|
225
|
+
function Shared.packBoolArray(
|
|
226
|
+
ch: Types.ChannelState,
|
|
227
|
+
value: { boolean },
|
|
228
|
+
count: number,
|
|
229
|
+
byteCount: number?
|
|
230
|
+
): ()
|
|
213
231
|
if count == 0 then
|
|
214
232
|
return
|
|
215
233
|
end
|
|
216
234
|
|
|
217
|
-
local
|
|
235
|
+
local bc = byteCount or (count + 7) // 8
|
|
218
236
|
local cursor = ch.cursor
|
|
219
|
-
if cursor +
|
|
220
|
-
alloc(ch,
|
|
237
|
+
if cursor + bc > ch.size then
|
|
238
|
+
alloc(ch, bc)
|
|
221
239
|
end
|
|
222
240
|
|
|
223
241
|
local idx = 1
|
|
224
242
|
local b = ch.buff
|
|
225
|
-
for byteIdx = 0,
|
|
243
|
+
for byteIdx = 0, bc - 1 do
|
|
226
244
|
local packed = 0
|
|
227
245
|
local remaining = count - idx + 1
|
|
228
246
|
local limit = if remaining >= 8 then 8 else remaining
|
|
@@ -235,7 +253,7 @@ function Shared.packBoolArray(ch: Types.ChannelState, value: { boolean }, count:
|
|
|
235
253
|
writeu8(b, cursor + byteIdx, packed)
|
|
236
254
|
end
|
|
237
255
|
|
|
238
|
-
ch.cursor = cursor +
|
|
256
|
+
ch.cursor = cursor + bc
|
|
239
257
|
end
|
|
240
258
|
|
|
241
259
|
function Shared.unpackBools(
|
|
@@ -243,15 +261,16 @@ function Shared.unpackBools(
|
|
|
243
261
|
pos: number,
|
|
244
262
|
boolKeys: { string },
|
|
245
263
|
boolCount: number,
|
|
246
|
-
result: { [string]: any }
|
|
264
|
+
result: { [string]: any },
|
|
265
|
+
byteCount: number?
|
|
247
266
|
): number
|
|
248
267
|
if boolCount == 0 then
|
|
249
268
|
return 0
|
|
250
269
|
end
|
|
251
270
|
|
|
252
|
-
local
|
|
271
|
+
local bc = byteCount or (boolCount + 7) // 8
|
|
253
272
|
local idx = 1
|
|
254
|
-
for byteIdx = 0,
|
|
273
|
+
for byteIdx = 0, bc - 1 do
|
|
255
274
|
local byte = readu8(src, pos + byteIdx)
|
|
256
275
|
local remaining = boolCount - idx + 1
|
|
257
276
|
local limit = if remaining >= 8 then 8 else remaining
|
|
@@ -260,7 +279,7 @@ function Shared.unpackBools(
|
|
|
260
279
|
idx += 1
|
|
261
280
|
end
|
|
262
281
|
end
|
|
263
|
-
return
|
|
282
|
+
return bc
|
|
264
283
|
end
|
|
265
284
|
|
|
266
285
|
-- Fills `result[1..count]` in place.
|
|
@@ -268,15 +287,16 @@ function Shared.unpackBoolArray(
|
|
|
268
287
|
src: buffer,
|
|
269
288
|
pos: number,
|
|
270
289
|
count: number,
|
|
271
|
-
result: { boolean }
|
|
290
|
+
result: { boolean },
|
|
291
|
+
byteCount: number?
|
|
272
292
|
): number
|
|
273
293
|
if count == 0 then
|
|
274
294
|
return 0
|
|
275
295
|
end
|
|
276
296
|
|
|
277
|
-
local
|
|
297
|
+
local bc = byteCount or (count + 7) // 8
|
|
278
298
|
local idx = 1
|
|
279
|
-
for byteIdx = 0,
|
|
299
|
+
for byteIdx = 0, bc - 1 do
|
|
280
300
|
local byte = readu8(src, pos + byteIdx)
|
|
281
301
|
local remaining = count - idx + 1
|
|
282
302
|
local limit = if remaining >= 8 then 8 else remaining
|
|
@@ -285,7 +305,7 @@ function Shared.unpackBoolArray(
|
|
|
285
305
|
idx += 1
|
|
286
306
|
end
|
|
287
307
|
end
|
|
288
|
-
return
|
|
308
|
+
return bc
|
|
289
309
|
end
|
|
290
310
|
|
|
291
311
|
return table.freeze(Shared)
|
|
@@ -165,7 +165,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
165
165
|
result[dataKeys[i]] = directReads[i](b, off + offsets[i])
|
|
166
166
|
end
|
|
167
167
|
if boolCount > 0 then
|
|
168
|
-
unpackBools(b, off + fixedSize, boolKeys, boolCount, result)
|
|
168
|
+
unpackBools(b, off + fixedSize, boolKeys, boolCount, result, boolBytes)
|
|
169
169
|
end
|
|
170
170
|
return result
|
|
171
171
|
end,
|
|
@@ -181,7 +181,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
181
181
|
end
|
|
182
182
|
ch.cursor = cursor + fixedSize
|
|
183
183
|
if boolCount > 0 then
|
|
184
|
-
packBools(ch, value, boolKeys, boolCount)
|
|
184
|
+
packBools(ch, value, boolKeys, boolCount, boolBytes)
|
|
185
185
|
end
|
|
186
186
|
end,
|
|
187
187
|
|
|
@@ -191,7 +191,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
191
191
|
result[dataKeys[i]] = directReads[i](src, pos + offsets[i])
|
|
192
192
|
end
|
|
193
193
|
if boolCount > 0 then
|
|
194
|
-
unpackBools(src, pos + fixedSize, boolKeys, boolCount, result)
|
|
194
|
+
unpackBools(src, pos + fixedSize, boolKeys, boolCount, result, boolBytes)
|
|
195
195
|
end
|
|
196
196
|
return result, totalFixed
|
|
197
197
|
end,
|
|
@@ -213,7 +213,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
213
213
|
dataCodecs[i].write(ch, value[dataKeys[i]])
|
|
214
214
|
end
|
|
215
215
|
if boolCount > 0 then
|
|
216
|
-
packBools(ch, value, boolKeys, boolCount)
|
|
216
|
+
packBools(ch, value, boolKeys, boolCount, boolBytes)
|
|
217
217
|
end
|
|
218
218
|
end,
|
|
219
219
|
|
|
@@ -226,7 +226,7 @@ function Struct.struct(schema: { [string]: Types.InternalCodec<any> }): Types.In
|
|
|
226
226
|
total += consumed
|
|
227
227
|
end
|
|
228
228
|
if boolCount > 0 then
|
|
229
|
-
total += unpackBools(src, pos + total, boolKeys, boolCount, result)
|
|
229
|
+
total += unpackBools(src, pos + total, boolKeys, boolCount, result, boolBytes)
|
|
230
230
|
end
|
|
231
231
|
return result, total
|
|
232
232
|
end,
|
|
@@ -316,7 +316,7 @@ function Struct.deltaStruct(
|
|
|
316
316
|
refs: { Instance }?
|
|
317
317
|
): number
|
|
318
318
|
if hasBoolSeg and segIdx == boolSegIdx then
|
|
319
|
-
return unpackBools(src, pos, boolKeys, boolCount, target)
|
|
319
|
+
return unpackBools(src, pos, boolKeys, boolCount, target, boolBytes)
|
|
320
320
|
end
|
|
321
321
|
local v, consumed = dataCodecs[segIdx].read(src, pos, refs)
|
|
322
322
|
target[dataKeys[segIdx]] = v
|
|
@@ -361,7 +361,7 @@ function Struct.deltaStruct(
|
|
|
361
361
|
prev = cur
|
|
362
362
|
end
|
|
363
363
|
if hasBoolSeg then
|
|
364
|
-
packBools(scratch, value, boolKeys, boolCount)
|
|
364
|
+
packBools(scratch, value, boolKeys, boolCount, boolBytes)
|
|
365
365
|
segOff[boolSegIdx] = prev
|
|
366
366
|
segLen[boolSegIdx] = scratch.cursor - prev
|
|
367
367
|
end
|
|
@@ -117,11 +117,24 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
|
|
|
117
117
|
local packed = 0
|
|
118
118
|
for i = 1, fieldCount do
|
|
119
119
|
local v = value[fieldKeys[i]]
|
|
120
|
-
|
|
120
|
+
local ft = fieldTypes[i]
|
|
121
|
+
if ft == TYPE_BOOL then
|
|
121
122
|
if v then
|
|
122
123
|
packed = bor(packed, lshift(1, fieldOffsets[i]))
|
|
123
124
|
end
|
|
125
|
+
elseif ft == TYPE_UINT then
|
|
126
|
+
local mask = fieldMasks[i]
|
|
127
|
+
if v < 0 or v > mask or v % 1 ~= 0 then
|
|
128
|
+
Log.error(`field "{fieldKeys[i]}" value {v} out of uint range [0, {mask}]`)
|
|
129
|
+
end
|
|
130
|
+
packed = bor(packed, lshift(v, fieldOffsets[i]))
|
|
124
131
|
else
|
|
132
|
+
local signBit = fieldSignBit[i]
|
|
133
|
+
if v < -signBit or v >= signBit or v % 1 ~= 0 then
|
|
134
|
+
Log.error(
|
|
135
|
+
`field "{fieldKeys[i]}" value {v} out of int range [{-signBit}, {signBit - 1}]`
|
|
136
|
+
)
|
|
137
|
+
end
|
|
125
138
|
packed = bor(packed, lshift(band(v, fieldMasks[i]), fieldOffsets[i]))
|
|
126
139
|
end
|
|
127
140
|
end
|
|
@@ -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
|
|
@@ -50,7 +49,6 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
|
|
|
50
49
|
local alloc = Base.alloc
|
|
51
50
|
local ensure = Base.ensure
|
|
52
51
|
local band = bit32.band
|
|
53
|
-
local bor = bit32.bor
|
|
54
52
|
local bufCopy = buffer.copy
|
|
55
53
|
local writeu8 = buffer.writeu8
|
|
56
54
|
local writeu16 = buffer.writeu16
|
|
@@ -58,15 +56,7 @@ local writef64 = buffer.writef64
|
|
|
58
56
|
local round = math.round
|
|
59
57
|
local osClock = os.clock
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
if mode == TS_FRAME then
|
|
63
|
-
writeu8(b, off, _frameCounter)
|
|
64
|
-
elseif mode == TS_OFFSET then
|
|
65
|
-
writeu16(b, off, _offsetMs)
|
|
66
|
-
elseif mode == TS_FULL then
|
|
67
|
-
writef64(b, off, _clock)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
59
|
+
-- id is in [1, 127] (Registry.MAX_ID), so id + 128 == bor(FRAME_MSB, id).
|
|
70
60
|
|
|
71
61
|
--[[
|
|
72
62
|
Single -> multi upgrade. Patches the previously-emitted single-item
|
|
@@ -111,25 +101,47 @@ local function spliceEncoded(ch: Types.ChannelState, payload: buffer, payloadLen
|
|
|
111
101
|
end
|
|
112
102
|
|
|
113
103
|
--[[
|
|
114
|
-
Hot-swapped by enableStats()
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
107
|
]]
|
|
118
108
|
local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
|
|
119
109
|
local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
|
|
120
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
|
+
|
|
121
122
|
writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
122
123
|
openOrUpgrade(ch, reg)
|
|
123
|
-
|
|
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)
|
|
130
|
+
end
|
|
124
131
|
ch.itemCount += 1
|
|
125
132
|
end
|
|
126
133
|
|
|
127
134
|
writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
128
135
|
openOrUpgrade(ch, reg)
|
|
129
|
-
local
|
|
130
|
-
|
|
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
|
|
131
143
|
ch.itemCount += 1
|
|
132
|
-
reg.bytesSent += ch.cursor -
|
|
144
|
+
reg.bytesSent += ch.cursor - rewindCursor
|
|
133
145
|
end
|
|
134
146
|
|
|
135
147
|
--[[
|
|
@@ -146,7 +158,12 @@ writeBatchEncodedPlain = function(
|
|
|
146
158
|
payloadLen: number
|
|
147
159
|
): ()
|
|
148
160
|
openOrUpgrade(ch, reg)
|
|
149
|
-
|
|
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)
|
|
166
|
+
end
|
|
150
167
|
ch.itemCount += 1
|
|
151
168
|
end
|
|
152
169
|
|
|
@@ -157,7 +174,12 @@ writeBatchEncodedStats = function(
|
|
|
157
174
|
payloadLen: number
|
|
158
175
|
): ()
|
|
159
176
|
openOrUpgrade(ch, reg)
|
|
160
|
-
|
|
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
|
|
161
183
|
ch.itemCount += 1
|
|
162
184
|
reg.bytesSent += payloadLen
|
|
163
185
|
end
|
|
@@ -241,48 +263,114 @@ function Channel.fullReset(ch: Types.ChannelState): ()
|
|
|
241
263
|
Channel.reset(ch)
|
|
242
264
|
ch.prevDump = nil
|
|
243
265
|
ch.prevDumpLen = 0
|
|
244
|
-
|
|
266
|
+
if next(ch.deltas) ~= nil then
|
|
267
|
+
table.clear(ch.deltas)
|
|
268
|
+
end
|
|
245
269
|
end
|
|
246
270
|
|
|
247
271
|
--[[
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
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.
|
|
252
275
|
]]
|
|
253
|
-
function
|
|
254
|
-
timestampMode: number,
|
|
255
|
-
name: string
|
|
256
|
-
): (ch: Types.ChannelState, id: number) -> ()
|
|
257
|
-
local headerLen = HEADER_BYTE + TS_BYTES[timestampMode]
|
|
258
|
-
|
|
276
|
+
local function makeOpenNoTs(name: string): (Types.ChannelState, number) -> ()
|
|
259
277
|
return function(ch: Types.ChannelState, id: number): ()
|
|
260
|
-
--[[
|
|
261
|
-
Inline sealCount: this is the only call site that can take the
|
|
262
|
-
multi-mode branch on a non-fresh channel.
|
|
263
|
-
]]
|
|
264
278
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
265
279
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
266
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
|
|
267
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
|
|
268
300
|
ch.currentPacket = name
|
|
269
301
|
ch.lastId = id
|
|
270
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
|
|
271
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
|
|
272
324
|
local cursor = ch.cursor
|
|
273
|
-
if cursor +
|
|
274
|
-
alloc(ch,
|
|
325
|
+
if cursor + 3 > ch.size then
|
|
326
|
+
alloc(ch, 3)
|
|
275
327
|
end
|
|
276
328
|
local b = ch.buff
|
|
277
|
-
writeu8(b, cursor,
|
|
278
|
-
|
|
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
|
|
279
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)
|
|
280
352
|
ch.singleMode = true
|
|
281
353
|
ch.singlePos = cursor
|
|
282
|
-
ch.cursor = cursor +
|
|
354
|
+
ch.cursor = cursor + 9
|
|
283
355
|
end
|
|
284
356
|
end
|
|
285
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)
|
|
367
|
+
end
|
|
368
|
+
if timestampMode == TS_OFFSET then
|
|
369
|
+
return makeOpenOffsetTs(name)
|
|
370
|
+
end
|
|
371
|
+
return makeOpenFullTs(name)
|
|
372
|
+
end
|
|
373
|
+
|
|
286
374
|
Channel.writeBatch = writeBatchPlain
|
|
287
375
|
Channel.writeBatchEncoded = writeBatchEncodedPlain
|
|
288
376
|
|
|
@@ -315,7 +403,8 @@ function Channel.writeQuery(
|
|
|
315
403
|
id: number,
|
|
316
404
|
correlationId: number,
|
|
317
405
|
codec: Types.InternalCodec<any>?,
|
|
318
|
-
data: any
|
|
406
|
+
data: any,
|
|
407
|
+
name: string?
|
|
319
408
|
): ()
|
|
320
409
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
321
410
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
@@ -327,9 +416,14 @@ function Channel.writeQuery(
|
|
|
327
416
|
here would never be observed before the next reset overwrites it.
|
|
328
417
|
]]
|
|
329
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
|
|
330
424
|
|
|
331
425
|
local hasPayload = codec ~= nil and data ~= nil
|
|
332
|
-
local headerByte = if hasPayload then id else
|
|
426
|
+
local headerByte = if hasPayload then id else id + 128
|
|
333
427
|
|
|
334
428
|
local cursor = ch.cursor
|
|
335
429
|
if cursor + 1 > ch.size then
|
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
|
|
@@ -209,6 +209,7 @@ function Reader.process(
|
|
|
209
209
|
local regName = reg.name
|
|
210
210
|
-- Lync.nothing reads zero bytes; bypass the pos/consumed gates that assume forward progress.
|
|
211
211
|
local isZeroSize = sizeMeta == 0
|
|
212
|
+
local accepted = 0
|
|
212
213
|
|
|
213
214
|
for _ = 1, count do
|
|
214
215
|
if not isZeroSize and pos >= incomingLen then
|
|
@@ -249,15 +250,14 @@ function Reader.process(
|
|
|
249
250
|
value = runReceive(value, regName, player)
|
|
250
251
|
end
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
reg.recvFires += 1
|
|
254
|
-
end
|
|
253
|
+
accepted += 1
|
|
255
254
|
|
|
256
255
|
-- tsValue is nil when mode == TS_NONE; pass through to skip a per-item branch.
|
|
257
256
|
regSignal:fire(value, player, tsValue)
|
|
258
257
|
end
|
|
259
258
|
|
|
260
259
|
if statsEnabled then
|
|
260
|
+
reg.recvFires += accepted
|
|
261
261
|
reg.bytesReceived += (pos - frameStart)
|
|
262
262
|
end
|
|
263
263
|
elseif kind == KIND_REQUEST or kind == KIND_RESPONSE then
|
|
@@ -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 }
|