@axpecter/lync 2.1.2 → 2.2.0
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 +241 -349
- package/package.json +1 -1
- package/src/Types.luau +53 -13
- package/src/api/Group.luau +41 -51
- package/src/api/Packet.luau +145 -143
- package/src/api/Query.luau +204 -202
- package/src/api/Scope.luau +36 -38
- package/src/api/Signal.luau +68 -94
- package/src/codec/Base.luau +43 -23
- package/src/codec/composite/Array.luau +161 -152
- package/src/codec/composite/Map.luau +37 -53
- package/src/codec/composite/Optional.luau +15 -21
- package/src/codec/composite/Shared.luau +78 -41
- package/src/codec/composite/Struct.luau +64 -125
- package/src/codec/composite/Tagged.luau +15 -25
- package/src/codec/composite/Tuple.luau +15 -20
- package/src/codec/datatype/Buffer.luau +44 -51
- package/src/codec/datatype/CFrame.luau +72 -104
- package/src/codec/datatype/Color.luau +14 -10
- package/src/codec/datatype/Instance.luau +32 -42
- package/src/codec/datatype/IntVector.luau +24 -22
- package/src/codec/datatype/NumberRange.luau +21 -12
- package/src/codec/datatype/Ray.luau +13 -7
- package/src/codec/datatype/Rect.luau +13 -7
- package/src/codec/datatype/Region.luau +25 -22
- package/src/codec/datatype/Sequence.luau +144 -118
- package/src/codec/datatype/String.luau +29 -59
- package/src/codec/datatype/UDim.luau +29 -30
- package/src/codec/datatype/Vector.luau +89 -105
- package/src/codec/meta/Auto.luau +194 -246
- package/src/codec/meta/Bitfield.luau +37 -36
- package/src/codec/meta/Custom.luau +10 -10
- package/src/codec/meta/Enum.luau +8 -11
- package/src/codec/meta/Float.luau +16 -20
- package/src/codec/meta/Nothing.luau +2 -2
- package/src/codec/meta/Unknown.luau +22 -32
- package/src/codec/primitive/Bool.luau +9 -5
- package/src/codec/primitive/Float16.luau +13 -23
- package/src/codec/primitive/Int.luau +20 -28
- package/src/codec/primitive/Number.luau +6 -10
- package/src/codec/primitive/Signed.luau +32 -0
- package/src/codec/primitive/Varint.luau +59 -28
- package/src/index.d.ts +8 -2
- package/src/init.luau +118 -143
- package/src/internal/Baseline.luau +17 -18
- package/src/internal/Channel.luau +143 -212
- package/src/internal/Middleware.luau +37 -47
- package/src/internal/Pool.luau +10 -10
- package/src/internal/Registry.luau +38 -34
- package/src/internal/Transport.luau +28 -0
- package/src/internal/Util.luau +26 -0
- package/src/transport/Bridge.luau +32 -24
- package/src/transport/Client.luau +51 -53
- package/src/transport/Gate.luau +183 -113
- package/src/transport/Reader.luau +95 -66
- package/src/transport/Server.luau +130 -125
package/src/transport/Gate.luau
CHANGED
|
@@ -2,120 +2,111 @@
|
|
|
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)
|
|
7
5
|
local Types = require(script.Parent.Parent.Types)
|
|
8
6
|
|
|
9
|
-
-- Constants
|
|
7
|
+
-- Constants --------------------------------------------------------------
|
|
10
8
|
|
|
11
9
|
local DEFAULT_DEPTH = 16
|
|
10
|
+
local STRIKE_DECAY_SEC = 5
|
|
12
11
|
|
|
13
|
-
-- State
|
|
12
|
+
-- State ------------------------------------------------------------------
|
|
14
13
|
|
|
15
14
|
local _depth = DEFAULT_DEPTH
|
|
16
|
-
local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
|
|
17
|
-
local _globalLimits: { [Player]: Types.RateLimitState } = {}
|
|
18
15
|
local _globalMaxPerSecond: number? = nil
|
|
19
16
|
local _bandwidthSoftLimit: number? = nil
|
|
20
|
-
local _bandwidthMaxStrikes
|
|
17
|
+
local _bandwidthMaxStrikes = 10
|
|
18
|
+
local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
|
|
19
|
+
local _globalLimits: { [Player]: Types.RateLimitState } = {}
|
|
21
20
|
local _bandwidthStrikes: { [Player]: number } = {}
|
|
21
|
+
local _bandwidthLastSeen: { [Player]: number } = {}
|
|
22
22
|
|
|
23
|
-
-- Private
|
|
23
|
+
-- Private ----------------------------------------------------------------
|
|
24
24
|
|
|
25
25
|
local isfinite = math.isfinite
|
|
26
26
|
local floor = math.floor
|
|
27
|
-
local abs = math.abs
|
|
28
27
|
local clock = os.clock
|
|
28
|
+
local minN = math.min
|
|
29
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
|
|
30
|
+
local function checkScalarBounds(value: number, minV: number?, maxV: number?): (boolean, string?)
|
|
40
31
|
if not isfinite(value) then
|
|
41
|
-
return false
|
|
32
|
+
return false, "non-finite"
|
|
42
33
|
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return false
|
|
34
|
+
if minV and value < minV then
|
|
35
|
+
return false, `below min {minV}`
|
|
46
36
|
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return false
|
|
50
|
-
end
|
|
51
|
-
if maxVal and value > maxVal then
|
|
52
|
-
return false
|
|
37
|
+
if maxV and value > maxV then
|
|
38
|
+
return false, `above max {maxV}`
|
|
53
39
|
end
|
|
40
|
+
return true, nil
|
|
41
|
+
end
|
|
54
42
|
|
|
55
|
-
|
|
43
|
+
local function checkVectorComponents(
|
|
44
|
+
value: any,
|
|
45
|
+
components: { string },
|
|
46
|
+
minV: number,
|
|
47
|
+
maxV: number
|
|
48
|
+
): (boolean, string?)
|
|
49
|
+
for _, axis in components do
|
|
50
|
+
local v = value[axis]
|
|
51
|
+
if not isfinite(v) then
|
|
52
|
+
return false, `component {axis} non-finite`
|
|
53
|
+
end
|
|
54
|
+
if v < minV or v > maxV then
|
|
55
|
+
return false, `component {axis} ({v}) out of [{minV}, {maxV}]`
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return true, nil
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
]]
|
|
61
|
+
local VEC2_AXES = table.freeze({ "X", "Y" })
|
|
62
|
+
local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
|
|
63
|
+
|
|
62
64
|
local function scanValue(value: any, depth: number): boolean
|
|
63
65
|
if depth <= 0 then
|
|
64
66
|
return true
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
local t = typeof(value)
|
|
68
|
-
|
|
69
70
|
if t == "number" then
|
|
70
71
|
return isfinite(value)
|
|
71
72
|
end
|
|
72
|
-
|
|
73
73
|
if t == "Vector2" then
|
|
74
74
|
return isfinite(value.X) and isfinite(value.Y)
|
|
75
75
|
end
|
|
76
|
-
|
|
77
76
|
if t == "Vector3" then
|
|
78
77
|
return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
|
|
79
78
|
end
|
|
80
|
-
|
|
81
79
|
if t == "CFrame" then
|
|
82
80
|
local pos = value.Position
|
|
83
|
-
if not isfinite(pos.X)
|
|
81
|
+
if not (isfinite(pos.X) and isfinite(pos.Y) and isfinite(pos.Z)) then
|
|
84
82
|
return false
|
|
85
83
|
end
|
|
86
84
|
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
|
87
85
|
return isfinite(rx) and isfinite(ry) and isfinite(rz)
|
|
88
86
|
end
|
|
89
|
-
|
|
90
87
|
if t == "UDim" then
|
|
91
88
|
return isfinite(value.Scale) and isfinite(value.Offset)
|
|
92
89
|
end
|
|
93
|
-
|
|
94
90
|
if t == "UDim2" then
|
|
95
91
|
return isfinite(value.X.Scale)
|
|
96
92
|
and isfinite(value.X.Offset)
|
|
97
93
|
and isfinite(value.Y.Scale)
|
|
98
94
|
and isfinite(value.Y.Offset)
|
|
99
95
|
end
|
|
100
|
-
|
|
101
96
|
if t == "Color3" then
|
|
102
97
|
return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
|
|
103
98
|
end
|
|
104
|
-
|
|
105
99
|
if t == "NumberRange" then
|
|
106
100
|
return isfinite(value.Min) and isfinite(value.Max)
|
|
107
101
|
end
|
|
108
|
-
|
|
109
102
|
if t == "Rect" then
|
|
110
103
|
return isfinite(value.Min.X)
|
|
111
104
|
and isfinite(value.Min.Y)
|
|
112
105
|
and isfinite(value.Max.X)
|
|
113
106
|
and isfinite(value.Max.Y)
|
|
114
107
|
end
|
|
115
|
-
|
|
116
108
|
if t == "Ray" then
|
|
117
|
-
local o = value.Origin
|
|
118
|
-
local d = value.Direction
|
|
109
|
+
local o, d = value.Origin, value.Direction
|
|
119
110
|
return isfinite(o.X)
|
|
120
111
|
and isfinite(o.Y)
|
|
121
112
|
and isfinite(o.Z)
|
|
@@ -123,7 +114,6 @@ local function scanValue(value: any, depth: number): boolean
|
|
|
123
114
|
and isfinite(d.Y)
|
|
124
115
|
and isfinite(d.Z)
|
|
125
116
|
end
|
|
126
|
-
|
|
127
117
|
if t == "table" then
|
|
128
118
|
local nextDepth = depth - 1
|
|
129
119
|
for _, v in value do
|
|
@@ -131,17 +121,10 @@ local function scanValue(value: any, depth: number): boolean
|
|
|
131
121
|
return false
|
|
132
122
|
end
|
|
133
123
|
end
|
|
134
|
-
return true
|
|
135
124
|
end
|
|
136
|
-
|
|
137
|
-
-- string, boolean, nil, buffer, Instance, etc. are always clean
|
|
138
125
|
return true
|
|
139
126
|
end
|
|
140
127
|
|
|
141
|
-
--[[
|
|
142
|
-
Schema-aware validation. Dispatches based on codec metadata.
|
|
143
|
-
Returns true if valid, false + reason if not.
|
|
144
|
-
]]
|
|
145
128
|
local function validateWithCodec(
|
|
146
129
|
value: any,
|
|
147
130
|
codec: Types.InternalCodec<any>,
|
|
@@ -151,61 +134,146 @@ local function validateWithCodec(
|
|
|
151
134
|
return true, nil
|
|
152
135
|
end
|
|
153
136
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if typeCheck then
|
|
159
|
-
if typeof(value) ~= typeCheck then
|
|
160
|
-
return false, `expected {typeCheck}, got {typeof(value)}`
|
|
137
|
+
-- Optional: branch on nil before any other check.
|
|
138
|
+
if codec._isOptional then
|
|
139
|
+
if value == nil then
|
|
140
|
+
return true, nil
|
|
161
141
|
end
|
|
142
|
+
local inner = codec._inner :: Types.InternalCodec<any>
|
|
143
|
+
return validateWithCodec(value, inner, depth - 1)
|
|
162
144
|
end
|
|
163
145
|
|
|
164
|
-
--
|
|
165
|
-
|
|
146
|
+
-- typeCheck (cheap fail-fast for non-composite codecs).
|
|
147
|
+
local typeCheck = codec._typeCheck
|
|
148
|
+
if typeCheck and typeof(value) ~= typeCheck then
|
|
149
|
+
return false, `expected {typeCheck}, got {typeof(value)}`
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if codec._isBool then
|
|
153
|
+
return true, nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
-- Enum: value is a string (typeCheck already validated typeof);
|
|
157
|
+
-- decoded enums are guaranteed to be in the value set, but on
|
|
158
|
+
-- send-side a caller may pass an arbitrary string.
|
|
159
|
+
if codec._isEnum then
|
|
160
|
+
local valid = codec._enumValues
|
|
161
|
+
if valid and valid[value] == nil then
|
|
162
|
+
return false, `not a valid enum value: "{value}"`
|
|
163
|
+
end
|
|
166
164
|
return true, nil
|
|
167
165
|
end
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
if codecAny._isInteger then
|
|
167
|
+
if codec._isInteger then
|
|
171
168
|
if not isfinite(value) then
|
|
172
169
|
return false, "non-finite"
|
|
173
170
|
end
|
|
174
171
|
if floor(value) ~= value then
|
|
175
172
|
return false, "non-integer"
|
|
176
173
|
end
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
return checkScalarBounds(value, codec._min, codec._max)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
-- Vector quantized: per-component range check.
|
|
178
|
+
if (typeCheck == "Vector2" or typeCheck == "Vector3") and codec._min and codec._max then
|
|
179
|
+
local axes = if typeCheck == "Vector2" then VEC2_AXES else VEC3_AXES
|
|
180
|
+
return checkVectorComponents(value, axes, codec._min, codec._max)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
-- Scalar quantized float (number with explicit range).
|
|
184
|
+
if typeCheck == "number" and codec._min ~= nil and codec._max ~= nil then
|
|
185
|
+
return checkScalarBounds(value, codec._min, codec._max)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if codec._isTagged then
|
|
189
|
+
if typeof(value) ~= "table" then
|
|
190
|
+
return false, "expected table for tagged variant"
|
|
191
|
+
end
|
|
192
|
+
local tagField = codec._tagField :: string
|
|
193
|
+
local tagName = value[tagField]
|
|
194
|
+
if tagName == nil then
|
|
195
|
+
return false, `missing tagField "{tagField}"`
|
|
196
|
+
end
|
|
197
|
+
local variants = codec._variants :: { [string]: Types.InternalCodec<any> }
|
|
198
|
+
local variant = variants[tagName]
|
|
199
|
+
if not variant then
|
|
200
|
+
return false, `unknown variant "{tagName}"`
|
|
201
|
+
end
|
|
202
|
+
return validateWithCodec(value, variant, depth - 1)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if codec._isArray then
|
|
206
|
+
if typeof(value) ~= "table" then
|
|
207
|
+
return false, "expected array"
|
|
208
|
+
end
|
|
209
|
+
local maxCount = codec._maxCount
|
|
210
|
+
local len = #value
|
|
211
|
+
if maxCount and len > maxCount then
|
|
212
|
+
return false, `array length {len} exceeds max {maxCount}`
|
|
181
213
|
end
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
local element = codec._element :: Types.InternalCodec<any>
|
|
215
|
+
local nextDepth = depth - 1
|
|
216
|
+
for i = 1, len do
|
|
217
|
+
local ok, reason = validateWithCodec(value[i], element, nextDepth)
|
|
218
|
+
if not ok then
|
|
219
|
+
return false, `[{i}]: {reason}`
|
|
220
|
+
end
|
|
184
221
|
end
|
|
185
222
|
return true, nil
|
|
186
223
|
end
|
|
187
224
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
225
|
+
if codec._isMap then
|
|
226
|
+
if typeof(value) ~= "table" then
|
|
227
|
+
return false, "expected map"
|
|
228
|
+
end
|
|
229
|
+
local maxCount = codec._maxCount
|
|
230
|
+
local keyCodec = codec._keyCodec :: Types.InternalCodec<any>
|
|
231
|
+
local valCodec = codec._valueCodec :: Types.InternalCodec<any>
|
|
232
|
+
local nextDepth = depth - 1
|
|
233
|
+
local count = 0
|
|
234
|
+
for k, v in value do
|
|
235
|
+
count += 1
|
|
236
|
+
if maxCount and count > maxCount then
|
|
237
|
+
return false, `map size exceeds max {maxCount}`
|
|
238
|
+
end
|
|
239
|
+
local ok, reason = validateWithCodec(k, keyCodec, nextDepth)
|
|
240
|
+
if not ok then
|
|
241
|
+
return false, `key: {reason}`
|
|
242
|
+
end
|
|
243
|
+
ok, reason = validateWithCodec(v, valCodec, nextDepth)
|
|
244
|
+
if not ok then
|
|
245
|
+
return false, `value at key "{tostring(k)}": {reason}`
|
|
246
|
+
end
|
|
192
247
|
end
|
|
193
248
|
return true, nil
|
|
194
249
|
end
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
if codecAny._schema then
|
|
251
|
+
if codec._isTuple then
|
|
198
252
|
if typeof(value) ~= "table" then
|
|
199
|
-
return false, "expected
|
|
253
|
+
return false, "expected tuple"
|
|
254
|
+
end
|
|
255
|
+
local elements = codec._elements :: { Types.InternalCodec<any> }
|
|
256
|
+
local nextDepth = depth - 1
|
|
257
|
+
for i = 1, #elements do
|
|
258
|
+
local ok, reason = validateWithCodec(value[i], elements[i], nextDepth)
|
|
259
|
+
if not ok then
|
|
260
|
+
return false, `[{i}]: {reason}`
|
|
261
|
+
end
|
|
200
262
|
end
|
|
263
|
+
return true, nil
|
|
264
|
+
end
|
|
201
265
|
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
if codec._schema then
|
|
267
|
+
if typeof(value) ~= "table" then
|
|
268
|
+
return false, "expected table for struct"
|
|
269
|
+
end
|
|
270
|
+
local nextDepth = depth - 1
|
|
271
|
+
for key, fieldCodec in codec._schema do
|
|
204
272
|
local fieldVal = value[key]
|
|
205
|
-
if fieldVal == nil and not
|
|
273
|
+
if fieldVal == nil and not fieldCodec._isBool then
|
|
206
274
|
return false, `missing field "{key}"`
|
|
207
275
|
end
|
|
208
|
-
local ok, reason = validateWithCodec(fieldVal, fieldCodec,
|
|
276
|
+
local ok, reason = validateWithCodec(fieldVal, fieldCodec, nextDepth)
|
|
209
277
|
if not ok then
|
|
210
278
|
return false, `field "{key}": {reason}`
|
|
211
279
|
end
|
|
@@ -213,19 +281,22 @@ local function validateWithCodec(
|
|
|
213
281
|
return true, nil
|
|
214
282
|
end
|
|
215
283
|
|
|
216
|
-
-- Fallback:
|
|
284
|
+
-- Fallback: NaN/inf scan for codecs without specific metadata
|
|
285
|
+
-- (raw f32/f64, untagged Roblox types, buffer, etc.).
|
|
217
286
|
if not scanValue(value, depth) then
|
|
218
287
|
return false, "NaN/inf detected"
|
|
219
288
|
end
|
|
220
|
-
|
|
221
289
|
return true, nil
|
|
222
290
|
end
|
|
223
291
|
|
|
224
|
-
-- Public
|
|
292
|
+
-- Public -----------------------------------------------------------------
|
|
225
293
|
|
|
226
294
|
local Gate = {}
|
|
227
295
|
|
|
228
296
|
function Gate.setDepth(depth: number): ()
|
|
297
|
+
if depth < 1 then
|
|
298
|
+
error(`[Lync] Gate.setDepth: depth must be >= 1, got {depth}`)
|
|
299
|
+
end
|
|
229
300
|
_depth = depth
|
|
230
301
|
end
|
|
231
302
|
|
|
@@ -237,8 +308,6 @@ function Gate.scan(value: any): boolean
|
|
|
237
308
|
return scanValue(value, _depth)
|
|
238
309
|
end
|
|
239
310
|
|
|
240
|
-
-- Rate limiting -------------------------------------------------------
|
|
241
|
-
|
|
242
311
|
function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
243
312
|
local config = reg.rateLimit
|
|
244
313
|
if not config then
|
|
@@ -247,7 +316,6 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
|
247
316
|
|
|
248
317
|
local now = clock()
|
|
249
318
|
|
|
250
|
-
-- Get or create per-player state
|
|
251
319
|
local perPacket = _rateLimits[reg.id]
|
|
252
320
|
if not perPacket then
|
|
253
321
|
perPacket = {}
|
|
@@ -256,15 +324,10 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
|
256
324
|
|
|
257
325
|
local state = perPacket[player]
|
|
258
326
|
if not state then
|
|
259
|
-
state = {
|
|
260
|
-
tokens = config.burst or 1,
|
|
261
|
-
lastRefill = now,
|
|
262
|
-
lastAccepted = 0,
|
|
263
|
-
}
|
|
327
|
+
state = { tokens = config.burst or 1, lastRefill = now, lastAccepted = 0 }
|
|
264
328
|
perPacket[player] = state
|
|
265
329
|
end
|
|
266
330
|
|
|
267
|
-
-- Cooldown mode
|
|
268
331
|
if config.cooldown then
|
|
269
332
|
if now - state.lastAccepted < config.cooldown then
|
|
270
333
|
return false
|
|
@@ -273,7 +336,6 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
|
273
336
|
return true
|
|
274
337
|
end
|
|
275
338
|
|
|
276
|
-
-- Token bucket mode
|
|
277
339
|
local maxPerSecond = config.maxPerSecond
|
|
278
340
|
if not maxPerSecond then
|
|
279
341
|
return true
|
|
@@ -281,16 +343,13 @@ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
|
281
343
|
|
|
282
344
|
local elapsed = now - state.lastRefill
|
|
283
345
|
state.lastRefill = now
|
|
284
|
-
|
|
285
|
-
local burst = config.burst or 1
|
|
286
|
-
state.tokens = math.min(burst, state.tokens + elapsed * maxPerSecond)
|
|
346
|
+
state.tokens = minN(config.burst or 1, state.tokens + elapsed * maxPerSecond)
|
|
287
347
|
|
|
288
348
|
if state.tokens >= 1 then
|
|
289
349
|
state.tokens -= 1
|
|
290
350
|
state.lastAccepted = now
|
|
291
351
|
return true
|
|
292
352
|
end
|
|
293
|
-
|
|
294
353
|
return false
|
|
295
354
|
end
|
|
296
355
|
|
|
@@ -309,39 +368,50 @@ function Gate.checkGlobalRateLimit(player: Player): boolean
|
|
|
309
368
|
|
|
310
369
|
local elapsed = now - state.lastRefill
|
|
311
370
|
state.lastRefill = now
|
|
312
|
-
state.tokens =
|
|
371
|
+
state.tokens = minN(maxPS, state.tokens + elapsed * maxPS)
|
|
313
372
|
|
|
314
373
|
if state.tokens >= 1 then
|
|
315
374
|
state.tokens -= 1
|
|
316
375
|
return true
|
|
317
376
|
end
|
|
318
|
-
|
|
319
377
|
return false
|
|
320
378
|
end
|
|
321
379
|
|
|
380
|
+
--[[
|
|
381
|
+
Bandwidth check: frames at or below softLimit refund a strike;
|
|
382
|
+
over-soft frames add one. Strikes also decay with idle time so a
|
|
383
|
+
quiet player isn't penalized indefinitely.
|
|
384
|
+
]]
|
|
322
385
|
function Gate.checkBandwidth(player: Player, bytes: number): boolean
|
|
323
386
|
if not _bandwidthSoftLimit then
|
|
324
387
|
return true
|
|
325
388
|
end
|
|
326
389
|
|
|
390
|
+
local now = clock()
|
|
391
|
+
local last = _bandwidthLastSeen[player]
|
|
392
|
+
local strikes = _bandwidthStrikes[player] or 0
|
|
393
|
+
|
|
394
|
+
if last then
|
|
395
|
+
local decay = floor((now - last) / STRIKE_DECAY_SEC)
|
|
396
|
+
if decay > 0 then
|
|
397
|
+
strikes = if decay >= strikes then 0 else strikes - decay
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
_bandwidthLastSeen[player] = now
|
|
401
|
+
|
|
327
402
|
if bytes <= _bandwidthSoftLimit then
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
_bandwidthStrikes[player] = strikes - 1
|
|
403
|
+
if strikes > 0 then
|
|
404
|
+
strikes -= 1
|
|
331
405
|
end
|
|
406
|
+
_bandwidthStrikes[player] = strikes
|
|
332
407
|
return true
|
|
333
408
|
end
|
|
334
409
|
|
|
335
|
-
|
|
336
|
-
local strikes = (if current then current else 0) + 1
|
|
410
|
+
strikes += 1
|
|
337
411
|
_bandwidthStrikes[player] = strikes
|
|
338
|
-
|
|
339
|
-
local maxStrikes = if _bandwidthMaxStrikes then _bandwidthMaxStrikes else 10
|
|
340
|
-
return strikes <= maxStrikes
|
|
412
|
+
return strikes <= _bandwidthMaxStrikes
|
|
341
413
|
end
|
|
342
414
|
|
|
343
|
-
-- Configuration -------------------------------------------------------
|
|
344
|
-
|
|
345
415
|
function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
|
|
346
416
|
_globalMaxPerSecond = maxPerSecond
|
|
347
417
|
end
|
|
@@ -354,7 +424,7 @@ end
|
|
|
354
424
|
function Gate.clearPlayer(player: Player): ()
|
|
355
425
|
_globalLimits[player] = nil
|
|
356
426
|
_bandwidthStrikes[player] = nil
|
|
357
|
-
|
|
427
|
+
_bandwidthLastSeen[player] = nil
|
|
358
428
|
for _, perPacket in _rateLimits do
|
|
359
429
|
perPacket[player] = nil
|
|
360
430
|
end
|