@axpecter/lync 2.0.0 → 2.1.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 +342 -428
- package/package.json +1 -1
- package/src/Types.luau +63 -31
- package/src/api/Group.luau +101 -106
- package/src/api/Packet.luau +187 -186
- package/src/api/Query.luau +223 -284
- package/src/api/Scope.luau +46 -57
- package/src/api/Signal.luau +104 -137
- package/src/codec/Base.luau +69 -20
- package/src/codec/composite/Array.luau +179 -270
- package/src/codec/composite/Map.luau +97 -347
- package/src/codec/composite/Optional.luau +27 -24
- package/src/codec/composite/Shared.luau +96 -65
- package/src/codec/composite/Struct.luau +200 -360
- package/src/codec/composite/Tagged.luau +62 -187
- package/src/codec/composite/Tuple.luau +79 -103
- package/src/codec/datatype/Buffer.luau +49 -21
- package/src/codec/datatype/CFrame.luau +207 -30
- package/src/codec/datatype/Color.luau +21 -11
- package/src/codec/datatype/Instance.luau +28 -21
- package/src/codec/datatype/IntVector.luau +33 -21
- package/src/codec/datatype/NumberRange.luau +19 -10
- package/src/codec/datatype/Ray.luau +26 -22
- package/src/codec/datatype/Rect.luau +20 -16
- package/src/codec/datatype/Region.luau +51 -45
- package/src/codec/datatype/Sequence.luau +64 -56
- package/src/codec/datatype/String.luau +89 -56
- package/src/codec/datatype/UDim.luau +40 -22
- package/src/codec/datatype/Vector.luau +181 -17
- package/src/codec/meta/Auto.luau +295 -253
- package/src/codec/meta/Bitfield.luau +104 -148
- package/src/codec/meta/Custom.luau +8 -4
- package/src/codec/meta/Enum.luau +29 -57
- package/src/codec/meta/Float.luau +64 -0
- package/src/codec/meta/Nothing.luau +9 -5
- package/src/codec/meta/Unknown.luau +44 -36
- package/src/codec/primitive/Bool.luau +16 -26
- package/src/codec/primitive/Float16.luau +58 -64
- package/src/codec/primitive/Int.luau +68 -0
- package/src/codec/primitive/Number.luau +21 -43
- package/src/codec/primitive/Varint.luau +67 -46
- package/src/index.d.ts +251 -335
- package/src/init.luau +319 -207
- package/src/internal/Baseline.luau +29 -24
- package/src/internal/Channel.luau +333 -278
- package/src/internal/Middleware.luau +60 -85
- package/src/internal/Pool.luau +19 -30
- package/src/internal/Registry.luau +56 -113
- package/src/transport/Bridge.luau +64 -43
- package/src/transport/Client.luau +59 -170
- package/src/transport/Gate.luau +282 -142
- package/src/transport/Reader.luau +148 -140
- package/src/transport/Server.luau +138 -488
- package/src/api/Namespace.luau +0 -256
- package/src/codec/meta/Quantized.luau +0 -174
package/src/transport/Gate.luau
CHANGED
|
@@ -1,223 +1,363 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!native
|
|
3
|
-
--
|
|
3
|
+
-- Schema-aware validation and rate limiting.
|
|
4
4
|
|
|
5
|
+
local Middleware = require(script.Parent.Parent.internal.Middleware)
|
|
6
|
+
local Registry = require(script.Parent.Parent.internal.Registry)
|
|
5
7
|
local Types = require(script.Parent.Parent.Types)
|
|
6
8
|
|
|
7
|
-
type Registration = Types.Registration
|
|
8
|
-
type RateLimitConfig = Types.RateLimitConfig
|
|
9
|
-
|
|
10
9
|
-- Constants -----------------------------------------------------------
|
|
11
10
|
|
|
12
|
-
local
|
|
13
|
-
|
|
14
|
-
local isfinite = math.isfinite
|
|
15
|
-
local min = math.min
|
|
16
|
-
local clock = os.clock
|
|
11
|
+
local DEFAULT_DEPTH = 16
|
|
17
12
|
|
|
18
13
|
-- State ---------------------------------------------------------------
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
local
|
|
26
|
-
local
|
|
27
|
-
local _dropHandlers =
|
|
28
|
-
{} :: { (player: Player, reason: string, packetName: string, data: any?) -> () }
|
|
15
|
+
local _depth = DEFAULT_DEPTH
|
|
16
|
+
local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
|
|
17
|
+
local _globalLimits: { [Player]: Types.RateLimitState } = {}
|
|
18
|
+
local _globalMaxPerSecond: number? = nil
|
|
19
|
+
local _bandwidthSoftLimit: number? = nil
|
|
20
|
+
local _bandwidthMaxStrikes: number? = nil
|
|
21
|
+
local _bandwidthStrikes: { [Player]: number } = {}
|
|
29
22
|
|
|
30
23
|
-- Private -------------------------------------------------------------
|
|
31
24
|
|
|
32
|
-
local
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
local isfinite = math.isfinite
|
|
26
|
+
local floor = math.floor
|
|
27
|
+
local abs = math.abs
|
|
28
|
+
local clock = os.clock
|
|
29
|
+
|
|
30
|
+
--[[
|
|
31
|
+
Checks a number value for NaN/inf and optionally range + integrality.
|
|
32
|
+
Returns true if the value passes all checks.
|
|
33
|
+
]]
|
|
34
|
+
local function validateNumber(
|
|
35
|
+
value: number,
|
|
36
|
+
isInteger: boolean?,
|
|
37
|
+
minVal: number?,
|
|
38
|
+
maxVal: number?
|
|
39
|
+
): boolean
|
|
40
|
+
if not isfinite(value) then
|
|
41
|
+
return false
|
|
35
42
|
end
|
|
36
|
-
end
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
if depth > _maxDepth then
|
|
44
|
+
if isInteger and floor(value) ~= value then
|
|
40
45
|
return false
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
if minVal and value < minVal then
|
|
49
|
+
return false
|
|
50
|
+
end
|
|
51
|
+
if maxVal and value > maxVal then
|
|
52
|
+
return false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
--[[
|
|
59
|
+
Recursive NaN/inf scan for values without schema metadata.
|
|
60
|
+
Used as fallback for f32, f64, auto, and Roblox types.
|
|
61
|
+
]]
|
|
62
|
+
local function scanValue(value: any, depth: number): boolean
|
|
63
|
+
if depth <= 0 then
|
|
64
|
+
return true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
local t = typeof(value)
|
|
44
68
|
|
|
45
69
|
if t == "number" then
|
|
46
70
|
return isfinite(value)
|
|
47
71
|
end
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return true
|
|
73
|
+
if t == "Vector2" then
|
|
74
|
+
return isfinite(value.X) and isfinite(value.Y)
|
|
52
75
|
end
|
|
53
76
|
|
|
54
|
-
if t == "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
if t == "Vector3" then
|
|
78
|
+
return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if t == "CFrame" then
|
|
82
|
+
local pos = value.Position
|
|
83
|
+
if not isfinite(pos.X) or not isfinite(pos.Y) or not isfinite(pos.Z) then
|
|
84
|
+
return false
|
|
60
85
|
end
|
|
61
|
-
|
|
86
|
+
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
|
87
|
+
return isfinite(rx) and isfinite(ry) and isfinite(rz)
|
|
62
88
|
end
|
|
63
89
|
|
|
64
|
-
if t == "
|
|
65
|
-
return isfinite(value.
|
|
90
|
+
if t == "UDim" then
|
|
91
|
+
return isfinite(value.Scale) and isfinite(value.Offset)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if t == "UDim2" then
|
|
95
|
+
return isfinite(value.X.Scale)
|
|
96
|
+
and isfinite(value.X.Offset)
|
|
97
|
+
and isfinite(value.Y.Scale)
|
|
98
|
+
and isfinite(value.Y.Offset)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if t == "Color3" then
|
|
102
|
+
return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if t == "NumberRange" then
|
|
106
|
+
return isfinite(value.Min) and isfinite(value.Max)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if t == "Rect" then
|
|
110
|
+
return isfinite(value.Min.X)
|
|
111
|
+
and isfinite(value.Min.Y)
|
|
112
|
+
and isfinite(value.Max.X)
|
|
113
|
+
and isfinite(value.Max.Y)
|
|
66
114
|
end
|
|
67
115
|
|
|
68
|
-
if t == "
|
|
69
|
-
local
|
|
116
|
+
if t == "Ray" then
|
|
117
|
+
local o = value.Origin
|
|
118
|
+
local d = value.Direction
|
|
119
|
+
return isfinite(o.X)
|
|
120
|
+
and isfinite(o.Y)
|
|
121
|
+
and isfinite(o.Z)
|
|
122
|
+
and isfinite(d.X)
|
|
123
|
+
and isfinite(d.Y)
|
|
124
|
+
and isfinite(d.Z)
|
|
125
|
+
end
|
|
70
126
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
elseif ut == "CFrame" then
|
|
76
|
-
local position = value.Position
|
|
77
|
-
if not isfinite(position.X) or not isfinite(position.Y) or not isfinite(position.Z) then
|
|
127
|
+
if t == "table" then
|
|
128
|
+
local nextDepth = depth - 1
|
|
129
|
+
for _, v in value do
|
|
130
|
+
if not scanValue(v, nextDepth) then
|
|
78
131
|
return false
|
|
79
132
|
end
|
|
80
|
-
local axis, angle = value:ToAxisAngle()
|
|
81
|
-
return isfinite(angle) and isfinite(axis.X) and isfinite(axis.Y) and isfinite(axis.Z)
|
|
82
|
-
elseif ut == "UDim" then
|
|
83
|
-
return isfinite(value.Scale)
|
|
84
|
-
elseif ut == "UDim2" then
|
|
85
|
-
return isfinite(value.X.Scale) and isfinite(value.Y.Scale)
|
|
86
|
-
elseif ut == "NumberRange" then
|
|
87
|
-
return isfinite(value.Min) and isfinite(value.Max)
|
|
88
|
-
elseif ut == "Rect" then
|
|
89
|
-
return isfinite(value.Min.X)
|
|
90
|
-
and isfinite(value.Min.Y)
|
|
91
|
-
and isfinite(value.Max.X)
|
|
92
|
-
and isfinite(value.Max.Y)
|
|
93
|
-
elseif ut == "Ray" then
|
|
94
|
-
local origin = value.Origin
|
|
95
|
-
local direction = value.Direction
|
|
96
|
-
return isfinite(origin.X)
|
|
97
|
-
and isfinite(origin.Y)
|
|
98
|
-
and isfinite(origin.Z)
|
|
99
|
-
and isfinite(direction.X)
|
|
100
|
-
and isfinite(direction.Y)
|
|
101
|
-
and isfinite(direction.Z)
|
|
102
|
-
elseif ut == "Region3" then
|
|
103
|
-
local position = value.CFrame.Position
|
|
104
|
-
return isfinite(position.X) and isfinite(position.Y) and isfinite(position.Z)
|
|
105
133
|
end
|
|
134
|
+
return true
|
|
106
135
|
end
|
|
107
136
|
|
|
137
|
+
-- string, boolean, nil, buffer, Instance, etc. are always clean
|
|
108
138
|
return true
|
|
109
139
|
end
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
141
|
+
--[[
|
|
142
|
+
Schema-aware validation. Dispatches based on codec metadata.
|
|
143
|
+
Returns true if valid, false + reason if not.
|
|
144
|
+
]]
|
|
145
|
+
local function validateWithCodec(
|
|
146
|
+
value: any,
|
|
147
|
+
codec: Types.InternalCodec<any>,
|
|
148
|
+
depth: number
|
|
149
|
+
): (boolean, string?)
|
|
150
|
+
if depth <= 0 then
|
|
151
|
+
return true, nil
|
|
115
152
|
end
|
|
116
153
|
|
|
117
|
-
local
|
|
118
|
-
|
|
119
|
-
|
|
154
|
+
local codecAny = codec :: any
|
|
155
|
+
|
|
156
|
+
-- Type check
|
|
157
|
+
local typeCheck = codecAny._typeCheck
|
|
158
|
+
if typeCheck then
|
|
159
|
+
if typeof(value) ~= typeCheck then
|
|
160
|
+
return false, `expected {typeCheck}, got {typeof(value)}`
|
|
161
|
+
end
|
|
120
162
|
end
|
|
121
163
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
-- Boolean: typeof check is sufficient
|
|
165
|
+
if codecAny._isBool then
|
|
166
|
+
return true, nil
|
|
167
|
+
end
|
|
126
168
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
-- Integer + range
|
|
170
|
+
if codecAny._isInteger then
|
|
171
|
+
if not isfinite(value) then
|
|
172
|
+
return false, "non-finite"
|
|
173
|
+
end
|
|
174
|
+
if floor(value) ~= value then
|
|
175
|
+
return false, "non-integer"
|
|
176
|
+
end
|
|
177
|
+
local minV = codecAny._min
|
|
178
|
+
local maxV = codecAny._max
|
|
179
|
+
if minV and value < minV then
|
|
180
|
+
return false, `below min {minV}`
|
|
181
|
+
end
|
|
182
|
+
if maxV and value > maxV then
|
|
183
|
+
return false, `above max {maxV}`
|
|
184
|
+
end
|
|
185
|
+
return true, nil
|
|
133
186
|
end
|
|
134
187
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
-- Float with range (quantized float)
|
|
189
|
+
if codecAny._min ~= nil and codecAny._max ~= nil and not codecAny._isInteger then
|
|
190
|
+
if not isfinite(value) then
|
|
191
|
+
return false, "non-finite"
|
|
192
|
+
end
|
|
193
|
+
return true, nil
|
|
194
|
+
end
|
|
138
195
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
196
|
+
-- Struct schema: recursive per-field validation
|
|
197
|
+
if codecAny._schema then
|
|
198
|
+
if typeof(value) ~= "table" then
|
|
199
|
+
return false, "expected table for struct"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
local schema = codecAny._schema
|
|
203
|
+
for key, fieldCodec in schema do
|
|
204
|
+
local fieldVal = value[key]
|
|
205
|
+
if fieldVal == nil and not (fieldCodec :: any)._isBool then
|
|
206
|
+
return false, `missing field "{key}"`
|
|
207
|
+
end
|
|
208
|
+
local ok, reason = validateWithCodec(fieldVal, fieldCodec, depth - 1)
|
|
209
|
+
if not ok then
|
|
210
|
+
return false, `field "{key}": {reason}`
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
return true, nil
|
|
142
214
|
end
|
|
143
215
|
|
|
144
|
-
|
|
216
|
+
-- Fallback: generic NaN scan for f32, f64, auto, and Roblox types
|
|
217
|
+
if not scanValue(value, depth) then
|
|
218
|
+
return false, "NaN/inf detected"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return true, nil
|
|
145
222
|
end
|
|
146
223
|
|
|
147
224
|
-- Public --------------------------------------------------------------
|
|
148
225
|
|
|
149
226
|
local Gate = {}
|
|
150
227
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if depth < 4 or depth > 32 then
|
|
154
|
-
error(`[Lync] Gate max depth must be 4–32, got {depth}`)
|
|
155
|
-
end
|
|
156
|
-
_maxDepth = depth
|
|
228
|
+
function Gate.setDepth(depth: number): ()
|
|
229
|
+
_depth = depth
|
|
157
230
|
end
|
|
158
231
|
|
|
159
|
-
function Gate.
|
|
160
|
-
return
|
|
232
|
+
function Gate.validate(value: any, codec: Types.InternalCodec<any>): (boolean, string?)
|
|
233
|
+
return validateWithCodec(value, codec, _depth)
|
|
161
234
|
end
|
|
162
235
|
|
|
163
|
-
function Gate.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
function Gate.scan(value: any): boolean
|
|
237
|
+
return scanValue(value, _depth)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
-- Rate limiting -------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
243
|
+
local config = reg.rateLimit
|
|
244
|
+
if not config then
|
|
245
|
+
return true
|
|
170
246
|
end
|
|
171
247
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
local now = clock()
|
|
249
|
+
|
|
250
|
+
-- Get or create per-player state
|
|
251
|
+
local perPacket = _rateLimits[reg.id]
|
|
252
|
+
if not perPacket then
|
|
253
|
+
perPacket = {}
|
|
254
|
+
_rateLimits[reg.id] = perPacket
|
|
178
255
|
end
|
|
179
256
|
|
|
180
|
-
local
|
|
181
|
-
if
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
257
|
+
local state = perPacket[player]
|
|
258
|
+
if not state then
|
|
259
|
+
state = {
|
|
260
|
+
tokens = config.burst or 1,
|
|
261
|
+
lastRefill = now,
|
|
262
|
+
lastAccepted = 0,
|
|
263
|
+
}
|
|
264
|
+
perPacket[player] = state
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
-- Cooldown mode
|
|
268
|
+
if config.cooldown then
|
|
269
|
+
if now - state.lastAccepted < config.cooldown then
|
|
188
270
|
return false
|
|
189
271
|
end
|
|
272
|
+
state.lastAccepted = now
|
|
273
|
+
return true
|
|
190
274
|
end
|
|
191
275
|
|
|
192
|
-
|
|
276
|
+
-- Token bucket mode
|
|
277
|
+
local maxPerSecond = config.maxPerSecond
|
|
278
|
+
if not maxPerSecond then
|
|
279
|
+
return true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
local elapsed = now - state.lastRefill
|
|
283
|
+
state.lastRefill = now
|
|
284
|
+
|
|
285
|
+
local burst = config.burst or 1
|
|
286
|
+
state.tokens = math.min(burst, state.tokens + elapsed * maxPerSecond)
|
|
287
|
+
|
|
288
|
+
if state.tokens >= 1 then
|
|
289
|
+
state.tokens -= 1
|
|
290
|
+
state.lastAccepted = now
|
|
291
|
+
return true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
return false
|
|
193
295
|
end
|
|
194
296
|
|
|
195
|
-
function Gate.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
297
|
+
function Gate.checkGlobalRateLimit(player: Player): boolean
|
|
298
|
+
local maxPS = _globalMaxPerSecond
|
|
299
|
+
if not maxPS then
|
|
300
|
+
return true
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
local now = clock()
|
|
304
|
+
local state = _globalLimits[player]
|
|
305
|
+
if not state then
|
|
306
|
+
state = { tokens = maxPS, lastRefill = now, lastAccepted = 0 }
|
|
307
|
+
_globalLimits[player] = state
|
|
308
|
+
end
|
|
199
309
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
310
|
+
local elapsed = now - state.lastRefill
|
|
311
|
+
state.lastRefill = now
|
|
312
|
+
state.tokens = math.min(maxPS, state.tokens + elapsed * maxPS)
|
|
313
|
+
|
|
314
|
+
if state.tokens >= 1 then
|
|
315
|
+
state.tokens -= 1
|
|
316
|
+
return true
|
|
205
317
|
end
|
|
318
|
+
|
|
319
|
+
return false
|
|
206
320
|
end
|
|
207
321
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
322
|
+
function Gate.checkBandwidth(player: Player, bytes: number): boolean
|
|
323
|
+
if not _bandwidthSoftLimit then
|
|
324
|
+
return true
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
if bytes <= _bandwidthSoftLimit then
|
|
328
|
+
local strikes = _bandwidthStrikes[player]
|
|
329
|
+
if strikes and strikes > 0 then
|
|
330
|
+
_bandwidthStrikes[player] = strikes - 1
|
|
331
|
+
end
|
|
332
|
+
return true
|
|
212
333
|
end
|
|
334
|
+
|
|
335
|
+
local current = _bandwidthStrikes[player]
|
|
336
|
+
local strikes = (if current then current else 0) + 1
|
|
337
|
+
_bandwidthStrikes[player] = strikes
|
|
338
|
+
|
|
339
|
+
local maxStrikes = if _bandwidthMaxStrikes then _bandwidthMaxStrikes else 10
|
|
340
|
+
return strikes <= maxStrikes
|
|
213
341
|
end
|
|
214
342
|
|
|
215
|
-
|
|
216
|
-
|
|
343
|
+
-- Configuration -------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
|
|
346
|
+
_globalMaxPerSecond = maxPerSecond
|
|
217
347
|
end
|
|
218
348
|
|
|
219
|
-
function Gate.
|
|
220
|
-
|
|
349
|
+
function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
|
|
350
|
+
_bandwidthSoftLimit = softLimit
|
|
351
|
+
_bandwidthMaxStrikes = maxStrikes
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
function Gate.clearPlayer(player: Player): ()
|
|
355
|
+
_globalLimits[player] = nil
|
|
356
|
+
_bandwidthStrikes[player] = nil
|
|
357
|
+
|
|
358
|
+
for _, perPacket in _rateLimits do
|
|
359
|
+
perPacket[player] = nil
|
|
360
|
+
end
|
|
221
361
|
end
|
|
222
362
|
|
|
223
363
|
return table.freeze(Gate)
|