@axpecter/lync 2.2.1 → 2.3.2
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 +109 -147
- package/package.json +1 -1
- package/src/Types.luau +34 -22
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +140 -66
- package/src/api/Query.luau +103 -65
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +28 -14
- package/src/codec/composite/Array.luau +296 -115
- package/src/codec/composite/Map.luau +377 -57
- package/src/codec/composite/Optional.luau +10 -2
- package/src/codec/composite/Shared.luau +134 -64
- package/src/codec/composite/Struct.luau +294 -43
- package/src/codec/composite/Tagged.luau +21 -16
- package/src/codec/composite/Tuple.luau +32 -15
- package/src/codec/datatype/Buffer.luau +7 -7
- package/src/codec/datatype/CFrame.luau +34 -122
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +76 -39
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +207 -53
- package/src/init.luau +116 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +191 -157
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +174 -106
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +90 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +84 -0
- package/src/util/Quat.luau +124 -0
- package/src/internal/Util.luau +0 -26
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
--!strict
|
|
2
|
-
--!
|
|
3
|
-
--
|
|
2
|
+
--!native
|
|
3
|
+
-- Inbound buffer parser. Returns false on malformed wire so the transport can drop its XOR baseline.
|
|
4
4
|
|
|
5
5
|
local Baseline = require(script.Parent.Parent.internal.Baseline)
|
|
6
6
|
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
7
|
+
local Constants = require(script.Parent.Parent.util.Constants)
|
|
7
8
|
local Gate = require(script.Parent.Parent.transport.Gate)
|
|
9
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
8
10
|
local Middleware = require(script.Parent.Parent.internal.Middleware)
|
|
9
11
|
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
10
12
|
local Types = require(script.Parent.Parent.Types)
|
|
@@ -20,12 +22,22 @@ local TS_FRAME = Channel.TS_FRAME
|
|
|
20
22
|
local TS_OFFSET = Channel.TS_OFFSET
|
|
21
23
|
local TS_FULL = Channel.TS_FULL
|
|
22
24
|
|
|
23
|
-
local
|
|
25
|
+
local FRAME_MSB = Constants.FRAME_MSB
|
|
26
|
+
local FRAME_ID_MASK = Constants.FRAME_ID_MASK
|
|
24
27
|
|
|
25
28
|
-- Private ----------------------------------------------------------------
|
|
26
29
|
|
|
27
30
|
local band = bit32.band
|
|
28
31
|
local readu8 = buffer.readu8
|
|
32
|
+
local readu16 = buffer.readu16
|
|
33
|
+
local readf64 = buffer.readf64
|
|
34
|
+
local registryGet = Registry.get
|
|
35
|
+
local checkGlobal = Gate.checkGlobalRateLimit
|
|
36
|
+
local checkRate = Gate.checkRateLimit
|
|
37
|
+
local validateGate = Gate.validate
|
|
38
|
+
local fireDrop = Middleware.fireDrop
|
|
39
|
+
local runReceive = Middleware.runReceive
|
|
40
|
+
local varintRead = Varint.read
|
|
29
41
|
|
|
30
42
|
local function recordDrop(
|
|
31
43
|
reg: Types.Registration,
|
|
@@ -38,25 +50,60 @@ local function recordDrop(
|
|
|
38
50
|
reg.drops += 1
|
|
39
51
|
end
|
|
40
52
|
if player then
|
|
41
|
-
|
|
53
|
+
fireDrop(player, reason, reg.name, value)
|
|
42
54
|
end
|
|
43
55
|
end
|
|
44
56
|
|
|
57
|
+
--[[
|
|
58
|
+
Server gate pipeline. Returns true to deliver, false on rate-limit,
|
|
59
|
+
schema-validate, or custom-validate rejection. `runGlobal` lets the
|
|
60
|
+
response path skip the global per-player limit.
|
|
61
|
+
]]
|
|
62
|
+
local function gateValue(
|
|
63
|
+
reg: Types.Registration,
|
|
64
|
+
regCodec: Types.InternalCodec<any>,
|
|
65
|
+
value: any,
|
|
66
|
+
player: Player,
|
|
67
|
+
statsEnabled: boolean,
|
|
68
|
+
runGlobal: boolean
|
|
69
|
+
): boolean
|
|
70
|
+
-- Both global and per-packet drops use the "rate" reason; collapsed into one branch.
|
|
71
|
+
if (runGlobal and not checkGlobal(player)) or not checkRate(reg, player) then
|
|
72
|
+
recordDrop(reg, statsEnabled, player, "rate", value)
|
|
73
|
+
return false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
local validateReason = validateGate(value, regCodec)
|
|
77
|
+
if validateReason then
|
|
78
|
+
recordDrop(reg, statsEnabled, player, `validation: {validateReason}`, value)
|
|
79
|
+
return false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if reg.validate then
|
|
83
|
+
local valid, validReason = reg.validate(value, player)
|
|
84
|
+
if not valid then
|
|
85
|
+
recordDrop(
|
|
86
|
+
reg,
|
|
87
|
+
statsEnabled,
|
|
88
|
+
player,
|
|
89
|
+
if validReason then `validate: {validReason}` else "validate",
|
|
90
|
+
value
|
|
91
|
+
)
|
|
92
|
+
return false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
return true
|
|
96
|
+
end
|
|
97
|
+
|
|
45
98
|
-- Public -----------------------------------------------------------------
|
|
46
99
|
|
|
47
100
|
local Reader = {}
|
|
48
101
|
|
|
49
102
|
--[[
|
|
50
|
-
Sanity-check an inbound
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
them caused server frames larger than the receiver's local default to
|
|
55
|
-
be silently dropped, which permanently desynced the XOR baseline.
|
|
56
|
-
|
|
57
|
-
Roblox enforces a per-RemoteEvent payload ceiling around 1MB; if the
|
|
58
|
-
payload reached us at all, it's already within Roblox's limit. Trust
|
|
59
|
-
that and accept anything non-empty.
|
|
103
|
+
Sanity-check an inbound payload. Size cap is delegated to Roblox's ~1MB
|
|
104
|
+
RemoteEvent ceiling: the receiver may not have called Lync.configure
|
|
105
|
+
with the same channelMaxSize as the sender (independent VMs, independent
|
|
106
|
+
module state), so a local cap here would reject legitimate frames.
|
|
60
107
|
]]
|
|
61
108
|
function Reader.decodeIncoming(data: any): buffer?
|
|
62
109
|
if typeof(data) ~= "buffer" then
|
|
@@ -69,13 +116,19 @@ function Reader.decodeIncoming(data: any): buffer?
|
|
|
69
116
|
return b
|
|
70
117
|
end
|
|
71
118
|
|
|
119
|
+
--[[
|
|
120
|
+
Returns true on full success, false if a malformed frame aborted the
|
|
121
|
+
parse. Transport callers use this to decide whether to keep the per-peer
|
|
122
|
+
XOR baseline: false means the buffer is suspect and would permanently
|
|
123
|
+
desync the channel if reused as the next XOR mask.
|
|
124
|
+
]]
|
|
72
125
|
function Reader.process(
|
|
73
126
|
incoming: buffer,
|
|
74
127
|
incomingLen: number,
|
|
75
128
|
refs: { Instance }?,
|
|
76
129
|
player: Player?,
|
|
77
130
|
isServer: boolean
|
|
78
|
-
):
|
|
131
|
+
): boolean
|
|
79
132
|
if Baseline.hasDelta() then
|
|
80
133
|
Baseline.setReadKey(player or false)
|
|
81
134
|
end
|
|
@@ -89,115 +142,130 @@ function Reader.process(
|
|
|
89
142
|
local raw = readu8(incoming, pos)
|
|
90
143
|
pos += 1
|
|
91
144
|
|
|
92
|
-
local msb = band(raw,
|
|
93
|
-
local id = band(raw,
|
|
145
|
+
local msb = band(raw, FRAME_MSB) ~= 0
|
|
146
|
+
local id = band(raw, FRAME_ID_MASK)
|
|
94
147
|
|
|
95
|
-
local reg =
|
|
148
|
+
local reg = registryGet(id)
|
|
96
149
|
if not reg then
|
|
97
|
-
warn(`
|
|
98
|
-
return
|
|
150
|
+
Log.warn(`unknown packet ID {id} at byte {pos - 1}`)
|
|
151
|
+
return false
|
|
99
152
|
end
|
|
100
153
|
|
|
101
|
-
|
|
154
|
+
local kind = reg.kind
|
|
155
|
+
if kind == KIND_PACKET then
|
|
102
156
|
local count: number
|
|
103
157
|
if msb then
|
|
104
158
|
count = 1
|
|
105
159
|
else
|
|
106
160
|
if pos + 2 > incomingLen then
|
|
107
|
-
warn("
|
|
108
|
-
return
|
|
161
|
+
Log.warn("truncated multi-frame header")
|
|
162
|
+
return false
|
|
109
163
|
end
|
|
110
|
-
count =
|
|
164
|
+
count = readu16(incoming, pos)
|
|
111
165
|
pos += 2
|
|
112
166
|
end
|
|
113
167
|
|
|
114
168
|
local tsMode = reg.timestampMode
|
|
115
169
|
local tsValue: number? = nil
|
|
116
170
|
if tsMode == TS_FRAME then
|
|
171
|
+
if pos + 1 > incomingLen then
|
|
172
|
+
Log.warn("truncated timestamp")
|
|
173
|
+
return false
|
|
174
|
+
end
|
|
117
175
|
tsValue = readu8(incoming, pos)
|
|
118
176
|
pos += 1
|
|
119
177
|
elseif tsMode == TS_OFFSET then
|
|
120
|
-
|
|
178
|
+
if pos + 2 > incomingLen then
|
|
179
|
+
Log.warn("truncated timestamp")
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
tsValue = readu16(incoming, pos)
|
|
121
183
|
pos += 2
|
|
122
184
|
elseif tsMode == TS_FULL then
|
|
123
|
-
|
|
185
|
+
if pos + 8 > incomingLen then
|
|
186
|
+
Log.warn("truncated timestamp")
|
|
187
|
+
return false
|
|
188
|
+
end
|
|
189
|
+
tsValue = readf64(incoming, pos)
|
|
124
190
|
pos += 8
|
|
125
191
|
end
|
|
126
192
|
|
|
127
|
-
|
|
128
|
-
count
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
193
|
+
--[[
|
|
194
|
+
Fixed-size codec: a count exceeding remaining bytes is
|
|
195
|
+
unambiguously corrupt. Abort to keep the cursor on a frame
|
|
196
|
+
boundary so the transport can reset cleanly.
|
|
197
|
+
]]
|
|
198
|
+
local regCodec = reg.codec
|
|
199
|
+
local sizeMeta = regCodec._size
|
|
200
|
+
if sizeMeta and count * sizeMeta > incomingLen - pos then
|
|
201
|
+
Log.warn(`"{reg.name}" count {count} exceeds remaining bytes; aborting`)
|
|
202
|
+
return false
|
|
136
203
|
end
|
|
137
204
|
|
|
138
205
|
local maxPayload = reg.maxPayloadBytes
|
|
139
206
|
local payloadStart = pos
|
|
207
|
+
local doGate = isServer and reg.needsGate and player ~= nil
|
|
208
|
+
local regSignal = reg.signal
|
|
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
|
|
140
212
|
|
|
141
213
|
for _ = 1, count do
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
214
|
+
if not isZeroSize and pos >= incomingLen then
|
|
215
|
+
Log.warn(`codec for "{regName}" ran past frame end; aborting`)
|
|
216
|
+
return false
|
|
217
|
+
end
|
|
218
|
+
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
219
|
+
if not isZeroSize and consumed == 0 then
|
|
220
|
+
Log.warn(`codec for "{regName}" reported 0 bytes; aborting`)
|
|
221
|
+
return false
|
|
222
|
+
end
|
|
223
|
+
-- Fixed-size codecs MUST report consumed == _size or `pos` desyncs silently.
|
|
224
|
+
if sizeMeta and consumed ~= sizeMeta then
|
|
225
|
+
Log.warn(
|
|
226
|
+
`codec for "{regName}" consumed {consumed} bytes, expected {sizeMeta}; aborting`
|
|
146
227
|
)
|
|
147
|
-
return
|
|
228
|
+
return false
|
|
148
229
|
end
|
|
149
230
|
pos += consumed
|
|
231
|
+
if pos > incomingLen then
|
|
232
|
+
Log.warn(`codec for "{regName}" overran frame end; aborting`)
|
|
233
|
+
return false
|
|
234
|
+
end
|
|
150
235
|
|
|
151
236
|
if maxPayload and (pos - payloadStart) > maxPayload then
|
|
152
237
|
recordDrop(reg, statsEnabled, player, "maxPayloadBytes", value)
|
|
153
|
-
return
|
|
238
|
+
return false
|
|
154
239
|
end
|
|
155
240
|
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if player and not Gate.checkRateLimit(reg, player) then
|
|
162
|
-
recordDrop(reg, statsEnabled, player, "rate", value)
|
|
163
|
-
continue
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
local ok = Gate.validate(value, reg.codec)
|
|
167
|
-
if not ok then
|
|
168
|
-
recordDrop(reg, statsEnabled, player, "validation", value)
|
|
169
|
-
continue
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
if reg.validate and player then
|
|
173
|
-
local valid, validReason = reg.validate(value, player)
|
|
174
|
-
if not valid then
|
|
175
|
-
recordDrop(reg, statsEnabled, player, validReason or "validate", value)
|
|
176
|
-
continue
|
|
177
|
-
end
|
|
178
|
-
end
|
|
241
|
+
if
|
|
242
|
+
doGate
|
|
243
|
+
and not gateValue(reg, regCodec, value, player :: Player, statsEnabled, true)
|
|
244
|
+
then
|
|
245
|
+
continue
|
|
179
246
|
end
|
|
180
247
|
|
|
181
248
|
if hasRecvHooks then
|
|
182
|
-
value =
|
|
249
|
+
value = runReceive(value, regName, player)
|
|
183
250
|
end
|
|
184
251
|
|
|
185
252
|
if statsEnabled then
|
|
186
253
|
reg.recvFires += 1
|
|
187
254
|
end
|
|
188
255
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else
|
|
192
|
-
reg.signal:fire(value, player)
|
|
193
|
-
end
|
|
256
|
+
-- tsValue is nil when mode == TS_NONE; pass through to skip a per-item branch.
|
|
257
|
+
regSignal:fire(value, player, tsValue)
|
|
194
258
|
end
|
|
195
259
|
|
|
196
260
|
if statsEnabled then
|
|
197
261
|
reg.bytesReceived += (pos - frameStart)
|
|
198
262
|
end
|
|
199
|
-
elseif
|
|
200
|
-
local corrId, corrBytes =
|
|
263
|
+
elseif kind == KIND_REQUEST or kind == KIND_RESPONSE then
|
|
264
|
+
local corrId, corrBytes = varintRead(incoming, pos)
|
|
265
|
+
if corrBytes == 0 then
|
|
266
|
+
Log.warn(`truncated correlation id for "{reg.name}"; aborting`)
|
|
267
|
+
return false
|
|
268
|
+
end
|
|
201
269
|
pos += corrBytes
|
|
202
270
|
|
|
203
271
|
if msb then
|
|
@@ -207,47 +275,43 @@ function Reader.process(
|
|
|
207
275
|
end
|
|
208
276
|
reg.signal:fire(nil, player, corrId)
|
|
209
277
|
else
|
|
210
|
-
local
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
return
|
|
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
|
|
282
|
+
Log.warn(`query "{reg.name}" body missing; aborting`)
|
|
283
|
+
return false
|
|
284
|
+
end
|
|
285
|
+
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
286
|
+
if not qIsZeroSize and consumed == 0 then
|
|
287
|
+
Log.warn(`query codec for "{reg.name}" reported 0 bytes; aborting`)
|
|
288
|
+
return false
|
|
216
289
|
end
|
|
217
290
|
pos += consumed
|
|
291
|
+
if pos > incomingLen then
|
|
292
|
+
Log.warn(`query "{reg.name}" overran frame end; aborting`)
|
|
293
|
+
return false
|
|
294
|
+
end
|
|
218
295
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
reg.bytesReceived += (pos - frameStart)
|
|
233
|
-
end
|
|
234
|
-
continue
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
if reg.validate and player then
|
|
238
|
-
local valid, validReason = reg.validate(value, player)
|
|
239
|
-
if not valid then
|
|
240
|
-
recordDrop(reg, statsEnabled, player, validReason or "validate", value)
|
|
241
|
-
if statsEnabled then
|
|
242
|
-
reg.bytesReceived += (pos - frameStart)
|
|
243
|
-
end
|
|
244
|
-
continue
|
|
245
|
-
end
|
|
296
|
+
--[[
|
|
297
|
+
Server gates REQUEST and RESPONSE frames opted into
|
|
298
|
+
needsGate so a malicious client can't bypass via the
|
|
299
|
+
response channel.
|
|
300
|
+
]]
|
|
301
|
+
if
|
|
302
|
+
isServer
|
|
303
|
+
and reg.needsGate
|
|
304
|
+
and player
|
|
305
|
+
and not gateValue(reg, regCodec, value, player, statsEnabled, false)
|
|
306
|
+
then
|
|
307
|
+
if statsEnabled then
|
|
308
|
+
reg.bytesReceived += (pos - frameStart)
|
|
246
309
|
end
|
|
310
|
+
continue
|
|
247
311
|
end
|
|
248
312
|
|
|
249
313
|
if hasRecvHooks then
|
|
250
|
-
value =
|
|
314
|
+
value = runReceive(value, reg.name, player)
|
|
251
315
|
end
|
|
252
316
|
|
|
253
317
|
if statsEnabled then
|
|
@@ -258,6 +322,10 @@ function Reader.process(
|
|
|
258
322
|
end
|
|
259
323
|
end
|
|
260
324
|
end
|
|
325
|
+
return true
|
|
261
326
|
end
|
|
262
327
|
|
|
328
|
+
Reader.setSilent = Log.setSilent
|
|
329
|
+
Reader.reset = Log.reset
|
|
330
|
+
|
|
263
331
|
return table.freeze(Reader)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Server
|
|
4
|
-
-- Decodes inbound XOR delta against a per-player baseline.
|
|
3
|
+
-- Server transport: per-player channels, broadcast, flush.
|
|
5
4
|
|
|
6
5
|
local Players = game:GetService("Players")
|
|
7
6
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
@@ -26,10 +25,15 @@ local _playerStats: { [Player]: Types.PlayerStats } = {}
|
|
|
26
25
|
local _prevIncoming: { [Player]: buffer } = {}
|
|
27
26
|
local _prevIncomingLen: { [Player]: number } = {}
|
|
28
27
|
|
|
29
|
-
local _statsEnabled = false
|
|
30
|
-
|
|
31
28
|
-- Private ----------------------------------------------------------------
|
|
32
29
|
|
|
30
|
+
local statsEnabled = Channel.statsEnabled
|
|
31
|
+
local xorApply = Channel.xorApply
|
|
32
|
+
local sealAndDump = Channel.sealAndDump
|
|
33
|
+
local resetCh = Channel.reset
|
|
34
|
+
|
|
35
|
+
type StatsField = "bytesSent" | "bytesReceived"
|
|
36
|
+
|
|
33
37
|
local function getOrCreateChannel(
|
|
34
38
|
map: { [Player]: Types.ChannelState },
|
|
35
39
|
player: Player
|
|
@@ -42,26 +46,30 @@ local function getOrCreateChannel(
|
|
|
42
46
|
return ch
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
-- Skip the table read when stats are off; the hot path needs no branch.
|
|
50
|
+
local function addStat(player: Player, field: StatsField, byteLen: number): ()
|
|
51
|
+
if statsEnabled() then
|
|
52
|
+
local ps = _playerStats[player]
|
|
53
|
+
if ps then
|
|
54
|
+
ps[field] += byteLen
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
45
59
|
local function flushReliable(player: Player): ()
|
|
46
60
|
local rCh = _reliableChannels[player]
|
|
47
61
|
if not rCh or rCh.cursor == 0 then
|
|
48
62
|
return
|
|
49
63
|
end
|
|
50
64
|
|
|
51
|
-
local snapshot, refs, byteLen =
|
|
52
|
-
local xored =
|
|
65
|
+
local snapshot, refs, byteLen = sealAndDump(rCh)
|
|
66
|
+
local xored = xorApply(snapshot, byteLen, rCh.prevDump, rCh.prevDumpLen)
|
|
53
67
|
rCh.prevDump = snapshot
|
|
54
68
|
rCh.prevDumpLen = byteLen
|
|
55
69
|
|
|
56
70
|
Bridge.fireClient(player, xored, refs)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if _statsEnabled then
|
|
60
|
-
local ps = _playerStats[player]
|
|
61
|
-
if ps then
|
|
62
|
-
ps.bytesSent += byteLen
|
|
63
|
-
end
|
|
64
|
-
end
|
|
71
|
+
resetCh(rCh)
|
|
72
|
+
addStat(player, "bytesSent", byteLen)
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
local function flushUnreliable(player: Player): ()
|
|
@@ -70,16 +78,10 @@ local function flushUnreliable(player: Player): ()
|
|
|
70
78
|
return
|
|
71
79
|
end
|
|
72
80
|
|
|
73
|
-
local snapshot, refs, byteLen =
|
|
81
|
+
local snapshot, refs, byteLen = sealAndDump(uCh)
|
|
74
82
|
Bridge.fireClientUnreliable(player, snapshot, refs)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if _statsEnabled then
|
|
78
|
-
local ps = _playerStats[player]
|
|
79
|
-
if ps then
|
|
80
|
-
ps.bytesSent += byteLen
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
+
resetCh(uCh)
|
|
84
|
+
addStat(player, "bytesSent", byteLen)
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
local function flushPlayer(player: Player): ()
|
|
@@ -99,23 +101,22 @@ local function handleIncomingReliable(player: Player, data: any, refs: any?): ()
|
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
local prev = _prevIncoming[player]
|
|
102
|
-
local decoded =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
end
|
|
104
|
+
local decoded = xorApply(incoming, incomingLen, prev, _prevIncomingLen[player])
|
|
105
|
+
|
|
106
|
+
--[[
|
|
107
|
+
Only adopt `decoded` as the next XOR baseline on success. A failed
|
|
108
|
+
parse means the wire is suspect; reusing it would poison every
|
|
109
|
+
subsequent packet.
|
|
110
|
+
]]
|
|
111
|
+
if Reader.process(decoded, incomingLen, refs, player, true) then
|
|
112
|
+
_prevIncoming[player] = decoded
|
|
113
|
+
_prevIncomingLen[player] = incomingLen
|
|
114
|
+
else
|
|
115
|
+
_prevIncoming[player] = nil
|
|
116
|
+
_prevIncomingLen[player] = nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
addStat(player, "bytesReceived", incomingLen)
|
|
119
120
|
end
|
|
120
121
|
|
|
121
122
|
local function handleIncomingUnreliable(player: Player, data: any, refs: any?): ()
|
|
@@ -125,17 +126,8 @@ local function handleIncomingUnreliable(player: Player, data: any, refs: any?):
|
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
local incomingLen = buffer.len(incoming)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
warn(`[Lync] Server.receiveUnreliable: {player.Name}: {err}`)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
if _statsEnabled then
|
|
134
|
-
local ps = _playerStats[player]
|
|
135
|
-
if ps then
|
|
136
|
-
ps.bytesReceived += incomingLen
|
|
137
|
-
end
|
|
138
|
-
end
|
|
129
|
+
Reader.process(incoming, incomingLen, refs, player, true)
|
|
130
|
+
addStat(player, "bytesReceived", incomingLen)
|
|
139
131
|
end
|
|
140
132
|
|
|
141
133
|
local function clearPlayerState(player: Player): ()
|
|
@@ -191,23 +183,20 @@ function Server.start(): ()
|
|
|
191
183
|
Bridge.unreliable().OnServerEvent:Connect(handleIncomingUnreliable)
|
|
192
184
|
|
|
193
185
|
Players.PlayerAdded:Connect(function(player: Player)
|
|
194
|
-
if
|
|
186
|
+
if statsEnabled() then
|
|
195
187
|
_playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
|
|
196
188
|
end
|
|
197
189
|
end)
|
|
198
190
|
Players.PlayerRemoving:Connect(clearPlayerState)
|
|
199
191
|
|
|
200
|
-
|
|
192
|
+
-- Catch players who joined before Lync.start.
|
|
193
|
+
if statsEnabled() then
|
|
201
194
|
for _, player in Players:GetPlayers() do
|
|
202
195
|
_playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
|
|
203
196
|
end
|
|
204
197
|
end
|
|
205
198
|
end
|
|
206
199
|
|
|
207
|
-
function Server.enableStats(): ()
|
|
208
|
-
_statsEnabled = true
|
|
209
|
-
end
|
|
210
|
-
|
|
211
200
|
function Server.getPlayerStats(player: Player): Types.PlayerStats?
|
|
212
201
|
return _playerStats[player]
|
|
213
202
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!optimize 2
|
|
3
|
+
-- O(1) array swap-remove. Breaks insertion order.
|
|
4
|
+
|
|
5
|
+
-- Public -----------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
local Array = {}
|
|
8
|
+
|
|
9
|
+
-- Reorders. Caller-tracked counts must decrement separately.
|
|
10
|
+
function Array.swapRemove<T>(arr: { T }, idx: number): ()
|
|
11
|
+
local last = #arr
|
|
12
|
+
if idx ~= last then
|
|
13
|
+
arr[idx] = arr[last]
|
|
14
|
+
end
|
|
15
|
+
arr[last] = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
return table.freeze(Array)
|