@axpecter/lync 2.3.2 → 3.0.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/LICENSE +21 -0
- package/README.md +302 -299
- package/Types.luau +215 -0
- package/api/Define.luau +154 -0
- package/api/Lifecycle.luau +119 -0
- package/api/Packet.luau +108 -0
- package/api/Query.luau +149 -0
- package/api/Set.luau +189 -0
- package/channel/Blocks.luau +124 -0
- package/channel/Group.luau +118 -0
- package/channel/Link.luau +135 -0
- package/channel/Outbox.luau +261 -0
- package/channel/Recipients.luau +129 -0
- package/channel/Transport.luau +138 -0
- package/codec/Base.luau +236 -0
- package/codec/Composites.luau +128 -0
- package/codec/Datatypes.luau +162 -0
- package/codec/Legality.luau +61 -0
- package/codec/Scalars.luau +72 -0
- package/codec/Text.luau +89 -0
- package/codec/init.luau +39 -0
- package/index.d.ts +285 -0
- package/init.luau +91 -0
- package/kernel/Bits.luau +16 -0
- package/kernel/Catalog.luau +121 -0
- package/kernel/Heap.luau +80 -0
- package/kernel/Listeners.luau +95 -0
- package/kernel/Log.luau +197 -0
- package/kernel/Tally.luau +128 -0
- package/kernel/Threads.luau +44 -0
- package/kernel/Wide.luau +267 -0
- package/package.json +15 -32
- package/runtime/Boundary.luau +180 -0
- package/runtime/Dispatch.luau +91 -0
- package/runtime/Handles.luau +114 -0
- package/runtime/Handshake.luau +90 -0
- package/runtime/Pending.luau +196 -0
- package/runtime/Router.luau +379 -0
- package/runtime/Session.luau +731 -0
- package/shape/Compile.luau +340 -0
- package/shape/Folds.luau +332 -0
- package/shape/Frames.luau +170 -0
- package/shape/Grid.luau +169 -0
- package/shape/Ir.luau +540 -0
- package/shape/Joins.luau +125 -0
- package/shape/Legality.luau +273 -0
- package/shape/Lower.luau +419 -0
- package/shape/Order.luau +64 -0
- package/shape/Regions.luau +113 -0
- package/shape/Rotation.luau +472 -0
- package/shape/Serial.luau +258 -0
- package/shape/Words.luau +89 -0
- package/shape/emit/Block.luau +453 -0
- package/shape/emit/Column.luau +338 -0
- package/shape/emit/Leaf.luau +462 -0
- package/shape/emit/Word.luau +234 -0
- package/shape/emit/init.luau +222 -0
- package/shape/init.luau +78 -0
- package/state/Apply.luau +330 -0
- package/state/Audience.luau +126 -0
- package/state/Catchup.luau +218 -0
- package/state/Coalesce.luau +43 -0
- package/state/Fieldset.luau +177 -0
- package/state/Newest.luau +339 -0
- package/state/Plan.luau +441 -0
- package/state/Sections.luau +468 -0
- package/state/Store.luau +188 -0
- package/state/Sync.luau +762 -0
- package/wire/Bytes.luau +118 -0
- package/wire/Clock.luau +37 -0
- package/wire/Protocol.luau +110 -0
- package/wire/Sequence.luau +40 -0
- package/wire/Varint.luau +124 -0
- package/src/Types.luau +0 -180
- package/src/api/Group.luau +0 -156
- package/src/api/Packet.luau +0 -319
- package/src/api/Query.luau +0 -338
- package/src/api/Scope.luau +0 -90
- package/src/api/Signal.luau +0 -188
- package/src/codec/Base.luau +0 -136
- package/src/codec/composite/Array.luau +0 -456
- package/src/codec/composite/Map.luau +0 -469
- package/src/codec/composite/Optional.luau +0 -54
- package/src/codec/composite/Shared.luau +0 -291
- package/src/codec/composite/Struct.luau +0 -494
- package/src/codec/composite/Tagged.luau +0 -103
- package/src/codec/composite/Tuple.luau +0 -143
- package/src/codec/datatype/Buffer.luau +0 -66
- package/src/codec/datatype/CFrame.luau +0 -109
- package/src/codec/datatype/Color.luau +0 -32
- package/src/codec/datatype/Instance.luau +0 -52
- package/src/codec/datatype/IntVector.luau +0 -39
- package/src/codec/datatype/NumberRange.luau +0 -39
- package/src/codec/datatype/Ray.luau +0 -36
- package/src/codec/datatype/Rect.luau +0 -31
- package/src/codec/datatype/Region.luau +0 -62
- package/src/codec/datatype/Sequence.luau +0 -184
- package/src/codec/datatype/String.luau +0 -94
- package/src/codec/datatype/UDim.luau +0 -46
- package/src/codec/datatype/Vector.luau +0 -130
- package/src/codec/meta/Auto.luau +0 -318
- package/src/codec/meta/Bitfield.luau +0 -147
- package/src/codec/meta/Custom.luau +0 -33
- package/src/codec/meta/DeltaScalar.luau +0 -390
- package/src/codec/meta/Enum.luau +0 -50
- package/src/codec/meta/Float.luau +0 -38
- package/src/codec/meta/Nothing.luau +0 -13
- package/src/codec/meta/Unknown.luau +0 -41
- package/src/codec/primitive/Bool.luau +0 -26
- package/src/codec/primitive/Float16.luau +0 -99
- package/src/codec/primitive/Int.luau +0 -59
- package/src/codec/primitive/Number.luau +0 -24
- package/src/codec/primitive/Signed.luau +0 -32
- package/src/codec/primitive/Varint.luau +0 -165
- package/src/codec/primitive/Zint.luau +0 -99
- package/src/index.d.ts +0 -423
- package/src/init.luau +0 -362
- package/src/internal/Baseline.luau +0 -57
- package/src/internal/Channel.luau +0 -390
- package/src/internal/Middleware.luau +0 -100
- package/src/internal/Pool.luau +0 -65
- package/src/internal/Registry.luau +0 -119
- package/src/internal/Transport.luau +0 -28
- package/src/transport/Bridge.luau +0 -110
- package/src/transport/Client.luau +0 -97
- package/src/transport/Gate.luau +0 -488
- package/src/transport/Reader.luau +0 -331
- package/src/transport/Server.luau +0 -211
- package/src/util/Array.luau +0 -18
- package/src/util/Buffer.luau +0 -90
- package/src/util/Constants.luau +0 -30
- package/src/util/Log.luau +0 -68
- package/src/util/Player.luau +0 -14
- package/src/util/Quantize.luau +0 -84
- package/src/util/Quat.luau +0 -124
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--!optimize 2
|
|
3
|
-
-- RemoteEvent / UnreliableRemoteEvent wrapper.
|
|
4
|
-
|
|
5
|
-
local RunService = game:GetService("RunService")
|
|
6
|
-
|
|
7
|
-
local Log = require(script.Parent.Parent.util.Log)
|
|
8
|
-
|
|
9
|
-
-- Constants --------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
local RELIABLE_NAME = "LyncReliable"
|
|
12
|
-
local UNRELIABLE_NAME = "LyncUnreliable"
|
|
13
|
-
|
|
14
|
-
-- State ------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
local _reliable: RemoteEvent? = nil
|
|
17
|
-
local _unreliable: UnreliableRemoteEvent? = nil
|
|
18
|
-
local _isServer = RunService:IsServer()
|
|
19
|
-
|
|
20
|
-
-- Private ----------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
--[[
|
|
23
|
-
Captured to a local first so Log.error's `never` return narrows the
|
|
24
|
-
optional. Reading the module global directly wouldn't narrow.
|
|
25
|
-
]]
|
|
26
|
-
local function ensureReliable(): RemoteEvent
|
|
27
|
-
local r = _reliable
|
|
28
|
-
if not r then
|
|
29
|
-
Log.error("not set up; call Lync.start() first")
|
|
30
|
-
end
|
|
31
|
-
return r
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
local function ensureUnreliable(): UnreliableRemoteEvent
|
|
35
|
-
local u = _unreliable
|
|
36
|
-
if not u then
|
|
37
|
-
Log.error("not set up; call Lync.start() first")
|
|
38
|
-
end
|
|
39
|
-
return u
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
-- Drop the trailing arg when refs is empty: Roblox serializes empty tables as several wire bytes.
|
|
43
|
-
local function fireToServer(re: any, data: buffer, refs: { Instance }): ()
|
|
44
|
-
if #refs > 0 then
|
|
45
|
-
re:FireServer(data, refs)
|
|
46
|
-
else
|
|
47
|
-
re:FireServer(data)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
local function fireToClient(re: any, player: Player, data: buffer, refs: { Instance }): ()
|
|
52
|
-
if #refs > 0 then
|
|
53
|
-
re:FireClient(player, data, refs)
|
|
54
|
-
else
|
|
55
|
-
re:FireClient(player, data)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
-- Public -----------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
local Bridge = {}
|
|
62
|
-
|
|
63
|
-
function Bridge.setup(container: Instance): ()
|
|
64
|
-
if _isServer then
|
|
65
|
-
local re = Instance.new("RemoteEvent")
|
|
66
|
-
re.Name = RELIABLE_NAME
|
|
67
|
-
re.Parent = container
|
|
68
|
-
|
|
69
|
-
local ure = Instance.new("UnreliableRemoteEvent")
|
|
70
|
-
ure.Name = UNRELIABLE_NAME
|
|
71
|
-
ure.Parent = container
|
|
72
|
-
|
|
73
|
-
_reliable, _unreliable = re, ure
|
|
74
|
-
return
|
|
75
|
-
end
|
|
76
|
-
_reliable = container:WaitForChild(RELIABLE_NAME) :: RemoteEvent
|
|
77
|
-
_unreliable = container:WaitForChild(UNRELIABLE_NAME) :: UnreliableRemoteEvent
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
Bridge.reliable = ensureReliable
|
|
81
|
-
Bridge.unreliable = ensureUnreliable
|
|
82
|
-
|
|
83
|
-
function Bridge.fireServer(data: buffer, refs: { Instance }): ()
|
|
84
|
-
fireToServer(ensureReliable(), data, refs)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
function Bridge.fireClient(player: Player, data: buffer, refs: { Instance }): ()
|
|
88
|
-
fireToClient(ensureReliable(), player, data, refs)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
function Bridge.fireServerUnreliable(data: buffer, refs: { Instance }): ()
|
|
92
|
-
fireToServer(ensureUnreliable(), data, refs)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
function Bridge.fireClientUnreliable(player: Player, data: buffer, refs: { Instance }): ()
|
|
96
|
-
fireToClient(ensureUnreliable(), player, data, refs)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
-- Test injection point: harnesses substitute fake remotes.
|
|
100
|
-
function Bridge.attach(reliable: any, unreliable: any): ()
|
|
101
|
-
_reliable = reliable
|
|
102
|
-
_unreliable = unreliable
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
function Bridge.reset(): ()
|
|
106
|
-
_reliable = nil
|
|
107
|
-
_unreliable = nil
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
return table.freeze(Bridge)
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--!optimize 2
|
|
3
|
-
-- Client transport: single channel pair with outbound XOR delta.
|
|
4
|
-
|
|
5
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
6
|
-
|
|
7
|
-
local Bridge = require(script.Parent.Parent.transport.Bridge)
|
|
8
|
-
local Channel = require(script.Parent.Parent.internal.Channel)
|
|
9
|
-
local Reader = require(script.Parent.Parent.transport.Reader)
|
|
10
|
-
local Types = require(script.Parent.Parent.Types)
|
|
11
|
-
|
|
12
|
-
-- Constants --------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
local CONTAINER_NAME = "LyncRemotes"
|
|
15
|
-
|
|
16
|
-
-- State ------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
local _reliable: Types.ChannelState = Channel.create()
|
|
19
|
-
local _unreliable: Types.ChannelState = Channel.create()
|
|
20
|
-
local _prevIncoming: buffer? = nil
|
|
21
|
-
local _prevIncomingLen: number = 0
|
|
22
|
-
|
|
23
|
-
-- Private ----------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
local xorApply = Channel.xorApply
|
|
26
|
-
local sealAndDump = Channel.sealAndDump
|
|
27
|
-
local resetCh = Channel.reset
|
|
28
|
-
|
|
29
|
-
local function handleIncomingReliable(data: any, refs: any?): ()
|
|
30
|
-
local incoming = Reader.decodeIncoming(data)
|
|
31
|
-
if not incoming then
|
|
32
|
-
return
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
local incomingLen = buffer.len(incoming)
|
|
36
|
-
local decoded = xorApply(incoming, incomingLen, _prevIncoming, _prevIncomingLen)
|
|
37
|
-
|
|
38
|
-
--[[
|
|
39
|
-
Mirror Server: only adopt `decoded` as the next baseline on success.
|
|
40
|
-
A failed parse means the wire is suspect; reusing it would poison
|
|
41
|
-
every subsequent packet.
|
|
42
|
-
]]
|
|
43
|
-
if Reader.process(decoded, incomingLen, refs, nil, false) then
|
|
44
|
-
_prevIncoming = decoded
|
|
45
|
-
_prevIncomingLen = incomingLen
|
|
46
|
-
else
|
|
47
|
-
_prevIncoming = nil
|
|
48
|
-
_prevIncomingLen = 0
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
local function handleIncomingUnreliable(data: any, refs: any?): ()
|
|
53
|
-
local incoming = Reader.decodeIncoming(data)
|
|
54
|
-
if not incoming then
|
|
55
|
-
return
|
|
56
|
-
end
|
|
57
|
-
Reader.process(incoming, buffer.len(incoming), refs, nil, false)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
-- Public -----------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
local Client = {}
|
|
63
|
-
|
|
64
|
-
function Client.getChannel(isUnreliable: boolean): Types.ChannelState
|
|
65
|
-
return if isUnreliable then _unreliable else _reliable
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
function Client.flush(): ()
|
|
69
|
-
if Channel.hasTimestamps() then
|
|
70
|
-
Channel.updateTimestamp()
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
if _reliable.cursor > 0 then
|
|
74
|
-
local snapshot, refs, byteLen = sealAndDump(_reliable)
|
|
75
|
-
local xored = xorApply(snapshot, byteLen, _reliable.prevDump, _reliable.prevDumpLen)
|
|
76
|
-
_reliable.prevDump = snapshot
|
|
77
|
-
_reliable.prevDumpLen = byteLen
|
|
78
|
-
Bridge.fireServer(xored, refs)
|
|
79
|
-
resetCh(_reliable)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if _unreliable.cursor > 0 then
|
|
83
|
-
local snapshot, refs = sealAndDump(_unreliable)
|
|
84
|
-
Bridge.fireServerUnreliable(snapshot, refs)
|
|
85
|
-
resetCh(_unreliable)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
function Client.start(): ()
|
|
90
|
-
local container = ReplicatedStorage:WaitForChild(CONTAINER_NAME)
|
|
91
|
-
Bridge.setup(container)
|
|
92
|
-
|
|
93
|
-
Bridge.reliable().OnClientEvent:Connect(handleIncomingReliable)
|
|
94
|
-
Bridge.unreliable().OnClientEvent:Connect(handleIncomingUnreliable)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
return table.freeze(Client)
|
package/src/transport/Gate.luau
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--!native
|
|
3
|
-
-- Schema-aware validation and rate limiting.
|
|
4
|
-
|
|
5
|
-
local Log = require(script.Parent.Parent.util.Log)
|
|
6
|
-
local Types = require(script.Parent.Parent.Types)
|
|
7
|
-
|
|
8
|
-
-- Constants --------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
local DEFAULT_DEPTH = 16
|
|
11
|
-
local STRIKE_DECAY_SEC = 5
|
|
12
|
-
|
|
13
|
-
local VEC2_AXES = table.freeze({ "X", "Y" })
|
|
14
|
-
local VEC3_AXES = table.freeze({ "X", "Y", "Z" })
|
|
15
|
-
|
|
16
|
-
-- State ------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
local _depth = DEFAULT_DEPTH
|
|
19
|
-
local _globalMaxPerSecond: number? = nil
|
|
20
|
-
local _bandwidthSoftLimit: number? = nil
|
|
21
|
-
local _bandwidthMaxStrikes = 10
|
|
22
|
-
local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
|
|
23
|
-
local _globalLimits: { [Player]: Types.RateLimitState } = {}
|
|
24
|
-
local _bandwidthStrikes: { [Player]: number } = {}
|
|
25
|
-
local _bandwidthLastSeen: { [Player]: number } = {}
|
|
26
|
-
|
|
27
|
-
-- Private ----------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
local isfinite = math.isfinite
|
|
30
|
-
local floor = math.floor
|
|
31
|
-
local clock = os.clock
|
|
32
|
-
local minN = math.min
|
|
33
|
-
|
|
34
|
-
-- Validators return string?: nil = accepted, non-nil = rejection reason.
|
|
35
|
-
local validateWithCodec -- forward decl (recursive)
|
|
36
|
-
|
|
37
|
-
local function checkScalarBounds(value: number, minV: number?, maxV: number?): string?
|
|
38
|
-
if not isfinite(value) then
|
|
39
|
-
return "non-finite"
|
|
40
|
-
end
|
|
41
|
-
if minV and value < minV then
|
|
42
|
-
return `below min {minV}`
|
|
43
|
-
end
|
|
44
|
-
if maxV and value > maxV then
|
|
45
|
-
return `above max {maxV}`
|
|
46
|
-
end
|
|
47
|
-
return nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
local function checkVectorComponents(
|
|
51
|
-
value: any,
|
|
52
|
-
components: { string },
|
|
53
|
-
minV: number,
|
|
54
|
-
maxV: number
|
|
55
|
-
): string?
|
|
56
|
-
for _, axis in components do
|
|
57
|
-
local v = value[axis]
|
|
58
|
-
if not isfinite(v) then
|
|
59
|
-
return `component {axis} non-finite`
|
|
60
|
-
end
|
|
61
|
-
if v < minV or v > maxV then
|
|
62
|
-
return `component {axis} ({v}) out of [{minV}, {maxV}]`
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
return nil
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
--[[
|
|
69
|
-
Untyped fallback NaN/inf scan. Used when a codec lacks specific metadata
|
|
70
|
-
(e.g. user custom codec) and as the last step of validateWithCodec.
|
|
71
|
-
Bounded by `depth` so a self-referential table can't recurse forever.
|
|
72
|
-
]]
|
|
73
|
-
local function scanValue(value: any, depth: number): boolean
|
|
74
|
-
if depth <= 0 then
|
|
75
|
-
return true
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
local t = typeof(value)
|
|
79
|
-
if t == "number" then
|
|
80
|
-
return isfinite(value)
|
|
81
|
-
end
|
|
82
|
-
if t == "Vector2" then
|
|
83
|
-
return isfinite(value.X) and isfinite(value.Y)
|
|
84
|
-
end
|
|
85
|
-
if t == "Vector3" then
|
|
86
|
-
return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
|
|
87
|
-
end
|
|
88
|
-
if t == "CFrame" then
|
|
89
|
-
local pos = value.Position
|
|
90
|
-
if not (isfinite(pos.X) and isfinite(pos.Y) and isfinite(pos.Z)) then
|
|
91
|
-
return false
|
|
92
|
-
end
|
|
93
|
-
local rx, ry, rz = value:ToEulerAnglesXYZ()
|
|
94
|
-
return isfinite(rx) and isfinite(ry) and isfinite(rz)
|
|
95
|
-
end
|
|
96
|
-
if t == "UDim" then
|
|
97
|
-
return isfinite(value.Scale) and isfinite(value.Offset)
|
|
98
|
-
end
|
|
99
|
-
if t == "UDim2" then
|
|
100
|
-
return isfinite(value.X.Scale)
|
|
101
|
-
and isfinite(value.X.Offset)
|
|
102
|
-
and isfinite(value.Y.Scale)
|
|
103
|
-
and isfinite(value.Y.Offset)
|
|
104
|
-
end
|
|
105
|
-
if t == "Color3" then
|
|
106
|
-
return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
|
|
107
|
-
end
|
|
108
|
-
if t == "NumberRange" then
|
|
109
|
-
return isfinite(value.Min) and isfinite(value.Max)
|
|
110
|
-
end
|
|
111
|
-
if t == "Rect" then
|
|
112
|
-
return isfinite(value.Min.X)
|
|
113
|
-
and isfinite(value.Min.Y)
|
|
114
|
-
and isfinite(value.Max.X)
|
|
115
|
-
and isfinite(value.Max.Y)
|
|
116
|
-
end
|
|
117
|
-
if t == "Ray" then
|
|
118
|
-
local o, d = value.Origin, 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
|
-
if t == "table" then
|
|
127
|
-
local nextDepth = depth - 1
|
|
128
|
-
for _, v in value do
|
|
129
|
-
if not scanValue(v, nextDepth) then
|
|
130
|
-
return false
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
return true
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
local function validateString(value: any, codec: Types.InternalCodec<any>): string?
|
|
138
|
-
local maxLen = codec._maxStringLength
|
|
139
|
-
if maxLen and #(value :: string) > maxLen then
|
|
140
|
-
return `string length {#value} exceeds max {maxLen}`
|
|
141
|
-
end
|
|
142
|
-
return nil
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
local function validateEnum(value: any, codec: Types.InternalCodec<any>): string?
|
|
146
|
-
local valid = codec._enumValues
|
|
147
|
-
if valid and valid[value] == nil then
|
|
148
|
-
return `not a valid enum value: "{value}"`
|
|
149
|
-
end
|
|
150
|
-
return nil
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
local function validateInteger(value: any, codec: Types.InternalCodec<any>): string?
|
|
154
|
-
if not isfinite(value) then
|
|
155
|
-
return "non-finite"
|
|
156
|
-
end
|
|
157
|
-
if floor(value) ~= value then
|
|
158
|
-
return "non-integer"
|
|
159
|
-
end
|
|
160
|
-
return checkScalarBounds(value, codec._min, codec._max)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
local function validateTagged(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
164
|
-
if typeof(value) ~= "table" then
|
|
165
|
-
return "expected table for tagged variant"
|
|
166
|
-
end
|
|
167
|
-
local tagField = codec._tagField :: string
|
|
168
|
-
local tagName = value[tagField]
|
|
169
|
-
if tagName == nil then
|
|
170
|
-
return `missing tagField "{tagField}"`
|
|
171
|
-
end
|
|
172
|
-
local variants = codec._variants :: { [string]: Types.InternalCodec<any> }
|
|
173
|
-
local variant = variants[tagName]
|
|
174
|
-
if not variant then
|
|
175
|
-
return `unknown variant "{tagName}"`
|
|
176
|
-
end
|
|
177
|
-
return validateWithCodec(value, variant, depth - 1)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
local function validateArray(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
181
|
-
if typeof(value) ~= "table" then
|
|
182
|
-
return "expected array"
|
|
183
|
-
end
|
|
184
|
-
local maxCount = codec._maxCount
|
|
185
|
-
local len = #value
|
|
186
|
-
if maxCount and len > maxCount then
|
|
187
|
-
return `array length {len} exceeds max {maxCount}`
|
|
188
|
-
end
|
|
189
|
-
local element = codec._element :: Types.InternalCodec<any>
|
|
190
|
-
local nextDepth = depth - 1
|
|
191
|
-
for i = 1, len do
|
|
192
|
-
local reason = validateWithCodec(value[i], element, nextDepth)
|
|
193
|
-
if reason then
|
|
194
|
-
return `[{i}]: {reason}`
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
return nil
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
local function validateMap(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
201
|
-
if typeof(value) ~= "table" then
|
|
202
|
-
return "expected map"
|
|
203
|
-
end
|
|
204
|
-
local maxCount = codec._maxCount
|
|
205
|
-
local keyCodec = codec._keyCodec :: Types.InternalCodec<any>
|
|
206
|
-
local valCodec = codec._valueCodec :: Types.InternalCodec<any>
|
|
207
|
-
local nextDepth = depth - 1
|
|
208
|
-
local count = 0
|
|
209
|
-
for k, v in value do
|
|
210
|
-
count += 1
|
|
211
|
-
if maxCount and count > maxCount then
|
|
212
|
-
return `map size exceeds max {maxCount}`
|
|
213
|
-
end
|
|
214
|
-
local reason = validateWithCodec(k, keyCodec, nextDepth)
|
|
215
|
-
if reason then
|
|
216
|
-
return `key: {reason}`
|
|
217
|
-
end
|
|
218
|
-
reason = validateWithCodec(v, valCodec, nextDepth)
|
|
219
|
-
if reason then
|
|
220
|
-
return `value at key "{tostring(k)}": {reason}`
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
return nil
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
local function validateTuple(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
227
|
-
if typeof(value) ~= "table" then
|
|
228
|
-
return "expected tuple"
|
|
229
|
-
end
|
|
230
|
-
local elements = codec._elements :: { Types.InternalCodec<any> }
|
|
231
|
-
local nextDepth = depth - 1
|
|
232
|
-
for i = 1, #elements do
|
|
233
|
-
local reason = validateWithCodec(value[i], elements[i], nextDepth)
|
|
234
|
-
if reason then
|
|
235
|
-
return `[{i}]: {reason}`
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
return nil
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
local function validateStruct(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
242
|
-
if typeof(value) ~= "table" then
|
|
243
|
-
return "expected table for struct"
|
|
244
|
-
end
|
|
245
|
-
local nextDepth = depth - 1
|
|
246
|
-
for key, fieldCodec in codec._schema :: { [string]: Types.InternalCodec<any> } do
|
|
247
|
-
local fieldVal = value[key]
|
|
248
|
-
-- Bool fields default to false when missing; everything else must be present.
|
|
249
|
-
if fieldVal == nil and not fieldCodec._isBool then
|
|
250
|
-
return `missing field "{key}"`
|
|
251
|
-
end
|
|
252
|
-
local reason = validateWithCodec(fieldVal, fieldCodec, nextDepth)
|
|
253
|
-
if reason then
|
|
254
|
-
return `field "{key}": {reason}`
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
return nil
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
function validateWithCodec(value: any, codec: Types.InternalCodec<any>, depth: number): string?
|
|
261
|
-
-- Peel Optional layers in a loop so optional(optional(X)) chains stay flat.
|
|
262
|
-
while codec._isOptional do
|
|
263
|
-
if depth <= 0 then
|
|
264
|
-
return nil
|
|
265
|
-
end
|
|
266
|
-
if value == nil then
|
|
267
|
-
return nil
|
|
268
|
-
end
|
|
269
|
-
codec = codec._inner :: Types.InternalCodec<any>
|
|
270
|
-
depth -= 1
|
|
271
|
-
end
|
|
272
|
-
if depth <= 0 then
|
|
273
|
-
return nil
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
local typeCheck = codec._typeCheck
|
|
277
|
-
if typeCheck and typeof(value) ~= typeCheck then
|
|
278
|
-
return `expected {typeCheck}, got {typeof(value)}`
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
if typeCheck == "string" then
|
|
282
|
-
local reason = validateString(value, codec)
|
|
283
|
-
if reason then
|
|
284
|
-
return reason
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
if codec._isBool then
|
|
289
|
-
return nil
|
|
290
|
-
end
|
|
291
|
-
if codec._isEnum then
|
|
292
|
-
return validateEnum(value, codec)
|
|
293
|
-
end
|
|
294
|
-
if codec._isInteger then
|
|
295
|
-
return validateInteger(value, codec)
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
if (typeCheck == "Vector2" or typeCheck == "Vector3") and codec._min and codec._max then
|
|
299
|
-
local axes = if typeCheck == "Vector2" then VEC2_AXES else VEC3_AXES
|
|
300
|
-
return checkVectorComponents(value, axes, codec._min, codec._max)
|
|
301
|
-
end
|
|
302
|
-
if typeCheck == "number" and codec._min ~= nil and codec._max ~= nil then
|
|
303
|
-
return checkScalarBounds(value, codec._min, codec._max)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
if codec._isTagged then
|
|
307
|
-
return validateTagged(value, codec, depth)
|
|
308
|
-
end
|
|
309
|
-
if codec._isArray then
|
|
310
|
-
return validateArray(value, codec, depth)
|
|
311
|
-
end
|
|
312
|
-
if codec._isMap then
|
|
313
|
-
return validateMap(value, codec, depth)
|
|
314
|
-
end
|
|
315
|
-
if codec._isTuple then
|
|
316
|
-
return validateTuple(value, codec, depth)
|
|
317
|
-
end
|
|
318
|
-
if codec._schema then
|
|
319
|
-
return validateStruct(value, codec, depth)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
-- No specific metadata: fall back to a depth-bounded NaN/inf scan.
|
|
323
|
-
if not scanValue(value, depth) then
|
|
324
|
-
return "NaN/inf detected"
|
|
325
|
-
end
|
|
326
|
-
return nil
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
local function getOrCreateBucket(
|
|
330
|
-
map: { [Player]: Types.RateLimitState },
|
|
331
|
-
player: Player,
|
|
332
|
-
burst: number,
|
|
333
|
-
now: number
|
|
334
|
-
): Types.RateLimitState
|
|
335
|
-
local state = map[player]
|
|
336
|
-
if not state then
|
|
337
|
-
state = { tokens = burst, lastRefill = now, lastAccepted = 0 }
|
|
338
|
-
map[player] = state
|
|
339
|
-
end
|
|
340
|
-
return state
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
local function consumeToken(
|
|
344
|
-
state: Types.RateLimitState,
|
|
345
|
-
now: number,
|
|
346
|
-
capacity: number,
|
|
347
|
-
rate: number
|
|
348
|
-
): boolean
|
|
349
|
-
local elapsed = now - state.lastRefill
|
|
350
|
-
state.lastRefill = now
|
|
351
|
-
state.tokens = minN(capacity, state.tokens + elapsed * rate)
|
|
352
|
-
|
|
353
|
-
if state.tokens >= 1 then
|
|
354
|
-
state.tokens -= 1
|
|
355
|
-
state.lastAccepted = now
|
|
356
|
-
return true
|
|
357
|
-
end
|
|
358
|
-
return false
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
-- Public -----------------------------------------------------------------
|
|
362
|
-
|
|
363
|
-
local Gate = {}
|
|
364
|
-
|
|
365
|
-
function Gate.setDepth(depth: number): ()
|
|
366
|
-
if depth < 1 then
|
|
367
|
-
Log.error(`depth must be >= 1, got {depth}`)
|
|
368
|
-
end
|
|
369
|
-
_depth = depth
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
function Gate.validate(value: any, codec: Types.InternalCodec<any>): string?
|
|
373
|
-
return validateWithCodec(value, codec, _depth)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
function Gate.scan(value: any): boolean
|
|
377
|
-
return scanValue(value, _depth)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
|
|
381
|
-
local config = reg.rateLimit
|
|
382
|
-
if not config then
|
|
383
|
-
return true
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
local now = clock()
|
|
387
|
-
|
|
388
|
-
local perPacket = _rateLimits[reg.id]
|
|
389
|
-
if not perPacket then
|
|
390
|
-
perPacket = {}
|
|
391
|
-
_rateLimits[reg.id] = perPacket
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
local burst = config.burst or 1
|
|
395
|
-
local state = getOrCreateBucket(perPacket, player, burst, now)
|
|
396
|
-
|
|
397
|
-
-- Cooldown form: reject anything within `cooldown` seconds of the last accept.
|
|
398
|
-
if config.cooldown then
|
|
399
|
-
if now - state.lastAccepted < config.cooldown then
|
|
400
|
-
return false
|
|
401
|
-
end
|
|
402
|
-
state.lastAccepted = now
|
|
403
|
-
return true
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
local maxPerSecond = config.maxPerSecond
|
|
407
|
-
if not maxPerSecond then
|
|
408
|
-
return true
|
|
409
|
-
end
|
|
410
|
-
return consumeToken(state, now, burst, maxPerSecond)
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
function Gate.checkGlobalRateLimit(player: Player): boolean
|
|
414
|
-
local maxPS = _globalMaxPerSecond
|
|
415
|
-
if not maxPS then
|
|
416
|
-
return true
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
local now = clock()
|
|
420
|
-
local state = getOrCreateBucket(_globalLimits, player, maxPS, now)
|
|
421
|
-
return consumeToken(state, now, maxPS, maxPS)
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
--[[
|
|
425
|
-
Bandwidth check. Frames at or below softLimit refund a strike; frames
|
|
426
|
-
above add one. Strikes decay with idle time so a quiet player isn't
|
|
427
|
-
penalized indefinitely after a noisy burst.
|
|
428
|
-
]]
|
|
429
|
-
function Gate.checkBandwidth(player: Player, bytes: number): boolean
|
|
430
|
-
if not _bandwidthSoftLimit then
|
|
431
|
-
return true
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
local now = clock()
|
|
435
|
-
local last = _bandwidthLastSeen[player]
|
|
436
|
-
local strikes = _bandwidthStrikes[player] or 0
|
|
437
|
-
|
|
438
|
-
if last then
|
|
439
|
-
local decay = floor((now - last) / STRIKE_DECAY_SEC)
|
|
440
|
-
if decay > 0 then
|
|
441
|
-
strikes = if decay >= strikes then 0 else strikes - decay
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
_bandwidthLastSeen[player] = now
|
|
445
|
-
|
|
446
|
-
if bytes <= _bandwidthSoftLimit then
|
|
447
|
-
if strikes > 0 then
|
|
448
|
-
strikes -= 1
|
|
449
|
-
end
|
|
450
|
-
_bandwidthStrikes[player] = strikes
|
|
451
|
-
return true
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
strikes += 1
|
|
455
|
-
_bandwidthStrikes[player] = strikes
|
|
456
|
-
return strikes <= _bandwidthMaxStrikes
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
|
|
460
|
-
_globalMaxPerSecond = maxPerSecond
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
|
|
464
|
-
_bandwidthSoftLimit = softLimit
|
|
465
|
-
_bandwidthMaxStrikes = maxStrikes
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
function Gate.clearPlayer(player: Player): ()
|
|
469
|
-
_globalLimits[player] = nil
|
|
470
|
-
_bandwidthStrikes[player] = nil
|
|
471
|
-
_bandwidthLastSeen[player] = nil
|
|
472
|
-
for _, perPacket in _rateLimits do
|
|
473
|
-
perPacket[player] = nil
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
function Gate.reset(): ()
|
|
478
|
-
_depth = DEFAULT_DEPTH
|
|
479
|
-
_globalMaxPerSecond = nil
|
|
480
|
-
_bandwidthSoftLimit = nil
|
|
481
|
-
_bandwidthMaxStrikes = 10
|
|
482
|
-
table.clear(_rateLimits)
|
|
483
|
-
table.clear(_globalLimits)
|
|
484
|
-
table.clear(_bandwidthStrikes)
|
|
485
|
-
table.clear(_bandwidthLastSeen)
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
return table.freeze(Gate)
|