@axpecter/lync 1.5.2 → 2.1.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 +499 -357
- package/package.json +2 -2
- package/src/Types.luau +69 -27
- package/src/api/Group.luau +101 -106
- package/src/api/Packet.luau +191 -157
- package/src/api/Query.luau +223 -282
- 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 +189 -201
- package/src/codec/composite/Map.luau +97 -341
- package/src/codec/composite/Optional.luau +27 -23
- package/src/codec/composite/Shared.luau +96 -65
- package/src/codec/composite/Struct.luau +201 -339
- package/src/codec/composite/Tagged.luau +64 -178
- package/src/codec/composite/Tuple.luau +81 -95
- 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 +29 -19
- 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 -35
- 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 +249 -306
- package/src/init.luau +332 -173
- package/src/internal/Baseline.luau +29 -24
- package/src/internal/Channel.luau +346 -161
- package/src/internal/Middleware.luau +60 -85
- package/src/internal/Pool.luau +19 -30
- package/src/internal/Registry.luau +61 -101
- package/src/transport/Bridge.luau +64 -43
- package/src/transport/Client.luau +60 -117
- package/src/transport/Gate.luau +282 -133
- package/src/transport/Reader.luau +159 -100
- package/src/transport/Server.luau +147 -292
- package/src/api/Namespace.luau +0 -251
- package/src/codec/meta/Quantized.luau +0 -174
package/src/transport/Gate.luau
CHANGED
|
@@ -1,214 +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
|
-
|
|
44
|
+
if isInteger and floor(value) ~= value then
|
|
45
|
+
return false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if minVal and value < minVal then
|
|
49
|
+
return false
|
|
50
|
+
end
|
|
51
|
+
if maxVal and value > maxVal then
|
|
40
52
|
return false
|
|
41
53
|
end
|
|
42
54
|
|
|
43
|
-
|
|
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)
|
|
66
103
|
end
|
|
67
104
|
|
|
68
|
-
if t == "
|
|
69
|
-
|
|
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)
|
|
114
|
+
end
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
126
|
+
|
|
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
|
|
186
|
+
end
|
|
187
|
+
|
|
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
|
|
133
194
|
end
|
|
134
195
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
138
201
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
167
246
|
end
|
|
168
247
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
255
|
+
end
|
|
256
|
+
|
|
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
|
|
172
265
|
end
|
|
173
266
|
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
if not ok then
|
|
178
|
-
fireDrop(player, reason or "validate", reg.name, value)
|
|
267
|
+
-- Cooldown mode
|
|
268
|
+
if config.cooldown then
|
|
269
|
+
if now - state.lastAccepted < config.cooldown then
|
|
179
270
|
return false
|
|
180
271
|
end
|
|
272
|
+
state.lastAccepted = now
|
|
273
|
+
return true
|
|
181
274
|
end
|
|
182
275
|
|
|
183
|
-
|
|
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
|
|
184
295
|
end
|
|
185
296
|
|
|
186
|
-
function Gate.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
309
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
196
317
|
end
|
|
318
|
+
|
|
319
|
+
return false
|
|
197
320
|
end
|
|
198
321
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
fireDrop(player, reason, packetName, data)
|
|
322
|
+
function Gate.checkBandwidth(player: Player, bytes: number): boolean
|
|
323
|
+
if not _bandwidthSoftLimit then
|
|
324
|
+
return true
|
|
203
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
|
|
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
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
-- Configuration -------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
|
|
346
|
+
_globalMaxPerSecond = maxPerSecond
|
|
204
347
|
end
|
|
205
348
|
|
|
206
|
-
function Gate.
|
|
207
|
-
|
|
349
|
+
function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
|
|
350
|
+
_bandwidthSoftLimit = softLimit
|
|
351
|
+
_bandwidthMaxStrikes = maxStrikes
|
|
208
352
|
end
|
|
209
353
|
|
|
210
|
-
function Gate.
|
|
211
|
-
|
|
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
|
|
212
361
|
end
|
|
213
362
|
|
|
214
363
|
return table.freeze(Gate)
|