@axpecter/lync 2.2.1 → 2.3.1
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 +95 -143
- package/package.json +1 -1
- package/src/Types.luau +22 -16
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +68 -54
- 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 +21 -14
- package/src/codec/composite/Array.luau +56 -134
- package/src/codec/composite/Map.luau +103 -72
- package/src/codec/composite/Optional.luau +6 -2
- package/src/codec/composite/Shared.luau +160 -69
- package/src/codec/composite/Struct.luau +283 -42
- package/src/codec/composite/Tagged.luau +13 -16
- package/src/codec/composite/Tuple.luau +21 -15
- package/src/codec/datatype/Buffer.luau +6 -4
- package/src/codec/datatype/CFrame.luau +56 -44
- 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/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 +69 -38
- package/src/index.d.ts +161 -53
- package/src/init.luau +109 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +153 -154
- 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 +162 -105
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +92 -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 +60 -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,121 @@ 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
|
|
140
210
|
|
|
141
211
|
for _ = 1, count do
|
|
142
|
-
|
|
212
|
+
if pos >= incomingLen then
|
|
213
|
+
Log.warn(`codec for "{regName}" ran past frame end; aborting`)
|
|
214
|
+
return false
|
|
215
|
+
end
|
|
216
|
+
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
143
217
|
if consumed == 0 then
|
|
144
|
-
warn(
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
return
|
|
218
|
+
Log.warn(`codec for "{regName}" reported 0 bytes; aborting`)
|
|
219
|
+
return false
|
|
148
220
|
end
|
|
149
221
|
pos += consumed
|
|
222
|
+
if pos > incomingLen then
|
|
223
|
+
Log.warn(`codec for "{regName}" overran frame end; aborting`)
|
|
224
|
+
return false
|
|
225
|
+
end
|
|
150
226
|
|
|
151
227
|
if maxPayload and (pos - payloadStart) > maxPayload then
|
|
152
228
|
recordDrop(reg, statsEnabled, player, "maxPayloadBytes", value)
|
|
153
|
-
return
|
|
229
|
+
return false
|
|
154
230
|
end
|
|
155
231
|
|
|
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
|
|
232
|
+
if
|
|
233
|
+
doGate
|
|
234
|
+
and not gateValue(reg, regCodec, value, player :: Player, statsEnabled, true)
|
|
235
|
+
then
|
|
236
|
+
continue
|
|
179
237
|
end
|
|
180
238
|
|
|
181
239
|
if hasRecvHooks then
|
|
182
|
-
value =
|
|
240
|
+
value = runReceive(value, regName, player)
|
|
183
241
|
end
|
|
184
242
|
|
|
185
243
|
if statsEnabled then
|
|
186
244
|
reg.recvFires += 1
|
|
187
245
|
end
|
|
188
246
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else
|
|
192
|
-
reg.signal:fire(value, player)
|
|
193
|
-
end
|
|
247
|
+
-- tsValue is nil when mode == TS_NONE; pass through to skip a per-item branch.
|
|
248
|
+
regSignal:fire(value, player, tsValue)
|
|
194
249
|
end
|
|
195
250
|
|
|
196
251
|
if statsEnabled then
|
|
197
252
|
reg.bytesReceived += (pos - frameStart)
|
|
198
253
|
end
|
|
199
|
-
elseif
|
|
200
|
-
local corrId, corrBytes =
|
|
254
|
+
elseif kind == KIND_REQUEST or kind == KIND_RESPONSE then
|
|
255
|
+
local corrId, corrBytes = varintRead(incoming, pos)
|
|
256
|
+
if corrBytes == 0 then
|
|
257
|
+
Log.warn(`truncated correlation id for "{reg.name}"; aborting`)
|
|
258
|
+
return false
|
|
259
|
+
end
|
|
201
260
|
pos += corrBytes
|
|
202
261
|
|
|
203
262
|
if msb then
|
|
@@ -207,47 +266,41 @@ function Reader.process(
|
|
|
207
266
|
end
|
|
208
267
|
reg.signal:fire(nil, player, corrId)
|
|
209
268
|
else
|
|
210
|
-
|
|
269
|
+
if pos >= incomingLen then
|
|
270
|
+
Log.warn(`query "{reg.name}" body missing; aborting`)
|
|
271
|
+
return false
|
|
272
|
+
end
|
|
273
|
+
local regCodec = reg.codec
|
|
274
|
+
local value, consumed = regCodec.read(incoming, pos, refs)
|
|
211
275
|
if consumed == 0 then
|
|
212
|
-
warn(
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
return
|
|
276
|
+
Log.warn(`query codec for "{reg.name}" reported 0 bytes; aborting`)
|
|
277
|
+
return false
|
|
216
278
|
end
|
|
217
279
|
pos += consumed
|
|
280
|
+
if pos > incomingLen then
|
|
281
|
+
Log.warn(`query "{reg.name}" overran frame end; aborting`)
|
|
282
|
+
return false
|
|
283
|
+
end
|
|
218
284
|
|
|
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
|
|
285
|
+
--[[
|
|
286
|
+
Server gates REQUEST and RESPONSE frames opted into
|
|
287
|
+
needsGate so a malicious client can't bypass via the
|
|
288
|
+
response channel.
|
|
289
|
+
]]
|
|
290
|
+
if
|
|
291
|
+
isServer
|
|
292
|
+
and reg.needsGate
|
|
293
|
+
and player
|
|
294
|
+
and not gateValue(reg, regCodec, value, player, statsEnabled, false)
|
|
295
|
+
then
|
|
296
|
+
if statsEnabled then
|
|
297
|
+
reg.bytesReceived += (pos - frameStart)
|
|
246
298
|
end
|
|
299
|
+
continue
|
|
247
300
|
end
|
|
248
301
|
|
|
249
302
|
if hasRecvHooks then
|
|
250
|
-
value =
|
|
303
|
+
value = runReceive(value, reg.name, player)
|
|
251
304
|
end
|
|
252
305
|
|
|
253
306
|
if statsEnabled then
|
|
@@ -258,6 +311,10 @@ function Reader.process(
|
|
|
258
311
|
end
|
|
259
312
|
end
|
|
260
313
|
end
|
|
314
|
+
return true
|
|
261
315
|
end
|
|
262
316
|
|
|
317
|
+
Reader.setSilent = Log.setSilent
|
|
318
|
+
Reader.reset = Log.reset
|
|
319
|
+
|
|
263
320
|
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)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--!native
|
|
3
|
+
-- Word-aligned XOR and equality helpers for Channel and composite deltas.
|
|
4
|
+
|
|
5
|
+
-- Private ----------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
local band = bit32.band
|
|
8
|
+
local bxor = bit32.bxor
|
|
9
|
+
local readu8 = buffer.readu8
|
|
10
|
+
local readu32 = buffer.readu32
|
|
11
|
+
local writeu8 = buffer.writeu8
|
|
12
|
+
local writeu32 = buffer.writeu32
|
|
13
|
+
local bufCopy = buffer.copy
|
|
14
|
+
local bufCreate = buffer.create
|
|
15
|
+
local minN = math.min
|
|
16
|
+
|
|
17
|
+
-- Public -----------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
local Buf = {}
|
|
20
|
+
|
|
21
|
+
-- 8-byte unrolled compare; 4-byte step then byte tail for remainder.
|
|
22
|
+
function Buf.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: number): boolean
|
|
23
|
+
local aligned8 = band(len, -8)
|
|
24
|
+
local i = 0
|
|
25
|
+
while i < aligned8 do
|
|
26
|
+
if
|
|
27
|
+
readu32(a, offA + i) ~= readu32(b, offB + i)
|
|
28
|
+
or readu32(a, offA + i + 4) ~= readu32(b, offB + i + 4)
|
|
29
|
+
then
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
i += 8
|
|
33
|
+
end
|
|
34
|
+
if i + 4 <= len then
|
|
35
|
+
if readu32(a, offA + i) ~= readu32(b, offB + i) then
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
i += 4
|
|
39
|
+
end
|
|
40
|
+
while i < len do
|
|
41
|
+
if readu8(a, offA + i) ~= readu8(b, offB + i) then
|
|
42
|
+
return false
|
|
43
|
+
end
|
|
44
|
+
i += 1
|
|
45
|
+
end
|
|
46
|
+
return true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
function Buf.snapshot(src: buffer, len: number): buffer
|
|
50
|
+
local out = bufCreate(len)
|
|
51
|
+
if len > 0 then
|
|
52
|
+
bufCopy(out, 0, src, 0, len)
|
|
53
|
+
end
|
|
54
|
+
return out
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
--[[
|
|
58
|
+
XOR `current` against `previous` into a fresh curLen buffer. Returns
|
|
59
|
+
`current` unchanged when previous is nil so first-frame callers don't
|
|
60
|
+
branch. Overlap is 8-byte unrolled; tail is bulk-copied.
|
|
61
|
+
]]
|
|
62
|
+
function Buf.xorApply(current: buffer, curLen: number, previous: buffer?, prevLen: number?): buffer
|
|
63
|
+
if not previous then
|
|
64
|
+
return current
|
|
65
|
+
end
|
|
66
|
+
local pLen = prevLen or 0
|
|
67
|
+
|
|
68
|
+
local result = bufCreate(curLen)
|
|
69
|
+
local overlap = minN(curLen, pLen)
|
|
70
|
+
|
|
71
|
+
local aligned8 = band(overlap, -8)
|
|
72
|
+
local i = 0
|
|
73
|
+
while i < aligned8 do
|
|
74
|
+
writeu32(result, i, bxor(readu32(current, i), readu32(previous, i)))
|
|
75
|
+
writeu32(result, i + 4, bxor(readu32(current, i + 4), readu32(previous, i + 4)))
|
|
76
|
+
i += 8
|
|
77
|
+
end
|
|
78
|
+
if i + 4 <= overlap then
|
|
79
|
+
writeu32(result, i, bxor(readu32(current, i), readu32(previous, i)))
|
|
80
|
+
i += 4
|
|
81
|
+
end
|
|
82
|
+
while i < overlap do
|
|
83
|
+
writeu8(result, i, bxor(readu8(current, i), readu8(previous, i)))
|
|
84
|
+
i += 1
|
|
85
|
+
end
|
|
86
|
+
if curLen > overlap then
|
|
87
|
+
bufCopy(result, overlap, current, overlap, curLen - overlap)
|
|
88
|
+
end
|
|
89
|
+
return result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return table.freeze(Buf)
|