@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.
Files changed (55) hide show
  1. package/README.md +499 -357
  2. package/package.json +2 -2
  3. package/src/Types.luau +69 -27
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +191 -157
  6. package/src/api/Query.luau +223 -282
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +189 -201
  11. package/src/codec/composite/Map.luau +97 -341
  12. package/src/codec/composite/Optional.luau +27 -23
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +201 -339
  15. package/src/codec/composite/Tagged.luau +64 -178
  16. package/src/codec/composite/Tuple.luau +81 -95
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +29 -19
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -35
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +249 -306
  43. package/src/init.luau +332 -173
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +346 -161
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +61 -101
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +60 -117
  51. package/src/transport/Gate.luau +282 -133
  52. package/src/transport/Reader.luau +159 -100
  53. package/src/transport/Server.luau +147 -292
  54. package/src/api/Namespace.luau +0 -251
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,172 +1,231 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Deserializes incoming buffers and routes by registration kind.
3
+ -- Deserializes incoming buffers with MSB batch dispatch.
4
4
 
5
5
  local Baseline = require(script.Parent.Parent.internal.Baseline)
6
- local Gate = require(script.Parent.Gate)
6
+ local Channel = require(script.Parent.Parent.internal.Channel)
7
+ local Gate = require(script.Parent.Parent.transport.Gate)
7
8
  local Middleware = require(script.Parent.Parent.internal.Middleware)
8
9
  local Registry = require(script.Parent.Parent.internal.Registry)
9
-
10
- local registryGet = Registry.get
11
- local gateCheck = Gate.check
12
- local gateReportDrop = Gate.reportDrop
10
+ local Types = require(script.Parent.Parent.Types)
11
+ local Varint = require(script.Parent.Parent.codec.primitive.Varint)
13
12
 
14
13
  -- Constants -----------------------------------------------------------
15
14
 
16
15
  local KIND_PACKET = Registry.KIND_PACKET
17
16
  local KIND_REQUEST = Registry.KIND_REQUEST
17
+ local KIND_RESPONSE = Registry.KIND_RESPONSE
18
+
19
+ -- Private -------------------------------------------------------------
18
20
 
19
- local MAX_INCOMING = 65536
21
+ local band = bit32.band
22
+ local readu8 = buffer.readu8
20
23
 
21
24
  -- Public --------------------------------------------------------------
22
25
 
23
26
  local Reader = {}
24
27
 
25
- -- Shared incoming-data validation used by both Client and Server receive.
26
28
  function Reader.decodeIncoming(data: any): buffer?
27
29
  if typeof(data) ~= "buffer" then
28
30
  return nil
29
31
  end
30
- if buffer.len(data) > MAX_INCOMING then
32
+ local b = data :: buffer
33
+ local len = buffer.len(b)
34
+ if len == 0 or len > 65536 then
31
35
  return nil
32
36
  end
33
- return data
37
+ return b
34
38
  end
35
39
 
36
- -- Callers wrap this in pcall. Any throw aborts the entire buffer from this source.
37
40
  function Reader.process(
38
41
  incoming: buffer,
42
+ incomingLen: number,
39
43
  refs: { Instance }?,
40
44
  player: Player?,
41
- startOffset: number
45
+ isServer: boolean
42
46
  ): ()
43
- local length = buffer.len(incoming)
44
- local pos = startOffset
45
-
46
- if Baseline.hasDelta then
47
+ -- Set baseline read key for delta codecs
48
+ if Baseline.hasDelta() then
47
49
  Baseline.setReadKey(player or false)
48
50
  end
49
51
 
50
- -- Cache flags once per frame-buffer, not per packet
51
- local hasReceiveMiddleware = Middleware.hasReceive
52
- local isServer = player ~= nil
52
+ local statsEnabled = Channel.statsEnabled()
53
+ local hasRecvHooks = Middleware.hasReceiveHooks()
54
+ local pos = 0
53
55
 
54
- while pos < length do
55
- local id = buffer.readu8(incoming, pos)
56
+ while pos < incomingLen do
57
+ local raw = readu8(incoming, pos)
56
58
  pos += 1
57
59
 
58
- local reg = registryGet(id)
60
+ local msb = band(raw, 0x80) ~= 0
61
+ local id = band(raw, 0x7F)
62
+
63
+ local reg = Registry.get(id)
59
64
  if not reg then
60
- warn(`[Lync] Unknown packet ID {id}`)
65
+ warn(`[Lync] Reader: unknown packet ID {id} at byte {pos - 1}`)
61
66
  break
62
67
  end
63
68
 
64
- local kind = reg.kind
69
+ if reg.kind == KIND_PACKET then
70
+ local count: number
65
71
 
66
- if kind == KIND_PACKET then
67
- if pos + 2 > length then
68
- break
72
+ if msb then
73
+ count = 1
74
+ else
75
+ count = buffer.readu16(incoming, pos)
76
+ pos += 2
69
77
  end
70
78
 
71
- local count = buffer.readu16(incoming, pos)
72
- pos += 2
73
-
74
- if count == 0 then
75
- continue
79
+ -- Read timestamp if this packet has one configured
80
+ local tsMode = reg.timestampMode
81
+ local tsValue: any = nil
82
+
83
+ if tsMode == Channel.TS_FRAME then
84
+ tsValue = readu8(incoming, pos)
85
+ pos += 1
86
+ elseif tsMode == Channel.TS_OFFSET then
87
+ tsValue = buffer.readu16(incoming, pos)
88
+ pos += 2
89
+ elseif tsMode == Channel.TS_FULL then
90
+ tsValue = buffer.readf64(incoming, pos)
91
+ pos += 8
76
92
  end
77
93
 
78
- local codec = reg.codec
79
- local signal = reg.signal
80
- local batchStart = pos
81
- local maxBytes = reg.maxPayloadBytes
82
-
83
- -- Hot path: no gate, no middleware, no byte budget
84
- if not (isServer and reg.needsGate) and not hasReceiveMiddleware and not maxBytes then
85
- for _ = 1, count do
86
- if pos >= length then
87
- break
88
- end
89
-
90
- local value, consumed = codec.read(incoming, pos, refs)
91
- pos += consumed
92
- signal:fire(value, player)
94
+ -- Prevent a malicious count from reading past the buffer
95
+ if reg.codec._size then
96
+ local maxItems = (incomingLen - pos) // (reg.codec._size :: number)
97
+ if count > maxItems then
98
+ count = maxItems
93
99
  end
94
- else
95
- -- Cold path: gate and/or middleware and/or byte budget active
96
- local baseName = reg.baseName
97
- local needsGate = isServer and reg.needsGate
100
+ end
98
101
 
99
- for _ = 1, count do
100
- if pos >= length then
101
- break
102
- end
102
+ for _ = 1, count do
103
+ local value, consumed = reg.codec.read(incoming, pos, refs)
104
+ pos += consumed
103
105
 
104
- if maxBytes and (pos - batchStart) > maxBytes then
105
- gateReportDrop(player, "size", reg.name, nil)
106
- break
106
+ if isServer and reg.needsGate then
107
+ if player and not Gate.checkGlobalRateLimit(player) then
108
+ if statsEnabled then
109
+ reg.drops += 1
110
+ end
111
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
112
+ continue
107
113
  end
108
114
 
109
- local value, consumed = codec.read(incoming, pos, refs)
110
- pos += consumed
115
+ if player and not Gate.checkRateLimit(reg, player) then
116
+ if statsEnabled then
117
+ reg.drops += 1
118
+ end
119
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
120
+ continue
121
+ end
111
122
 
112
- if needsGate and not gateCheck(value, reg, player :: Player) then
123
+ local ok, reason = Gate.validate(value, reg.codec)
124
+ if not ok then
125
+ if statsEnabled then
126
+ reg.drops += 1
127
+ end
128
+ if player then
129
+ Middleware.fireDrop(player, "validation", reg.name, value)
130
+ end
113
131
  continue
114
132
  end
115
133
 
116
- if hasReceiveMiddleware then
117
- local final = Middleware.runReceive(value, baseName, player)
118
- if final == nil then
134
+ if reg.validate then
135
+ local valid, validReason = reg.validate(value, player :: Player)
136
+ if not valid then
137
+ if statsEnabled then
138
+ reg.drops += 1
139
+ end
140
+ Middleware.fireDrop(
141
+ player :: Player,
142
+ validReason or "validate",
143
+ reg.name,
144
+ value
145
+ )
119
146
  continue
120
147
  end
121
- signal:fire(final, player)
122
- else
123
- signal:fire(value, player)
124
148
  end
125
149
  end
126
- end
127
- else
128
- -- Query frame: id(u8, already read) + correlationId(u16) + status(u8) = 3 more bytes
129
- if pos + 3 > length then
130
- break
131
- end
132
150
 
133
- local queryCodec = reg.codec
134
- local querySignal = reg.signal
151
+ if hasRecvHooks then
152
+ value = Middleware.runReceive(value, reg.name, player)
153
+ end
135
154
 
136
- local correlationId = buffer.readu16(incoming, pos)
137
- local status = buffer.readu8(incoming, pos + 2)
138
- pos += 3
155
+ if statsEnabled then
156
+ reg.recvFires += 1
157
+ end
139
158
 
140
- if status == 1 then
141
- if kind == KIND_REQUEST then
142
- querySignal:fire(nil, player, correlationId)
159
+ if tsValue ~= nil then
160
+ reg.signal:fire(value, player, tsValue)
143
161
  else
144
- querySignal:fire(nil, player, correlationId)
162
+ reg.signal:fire(value, player)
145
163
  end
146
- continue
147
164
  end
148
165
 
149
- local value, consumed = queryCodec.read(incoming, pos, refs)
150
- pos += consumed
151
-
152
- if isServer and reg.needsGate and not gateCheck(value, reg, player :: Player) then
153
- continue
166
+ if statsEnabled then
167
+ -- Approximate: counts all bytes consumed in this frame, not just payload
168
+ reg.bytesReceived += pos
154
169
  end
155
-
156
- local final: any
157
- if hasReceiveMiddleware then
158
- final = Middleware.runReceive(value, reg.baseName, player)
159
- if final == nil then
160
- continue
170
+ elseif reg.kind == KIND_REQUEST or reg.kind == KIND_RESPONSE then
171
+ -- MSB distinguishes nil responses (no payload) from data responses
172
+ local corrId, corrBytes = Varint.read(incoming, pos)
173
+ pos += corrBytes
174
+
175
+ if msb then
176
+ if statsEnabled then
177
+ reg.recvFires += 1
161
178
  end
179
+ reg.signal:fire(nil, player, corrId)
162
180
  else
163
- final = value
164
- end
181
+ local value, consumed = reg.codec.read(incoming, pos, refs)
182
+ pos += consumed
165
183
 
166
- if kind == KIND_REQUEST then
167
- querySignal:fire(final, player, correlationId)
168
- else
169
- querySignal:fire(final, player, correlationId)
184
+ if isServer and reg.needsGate and reg.kind == KIND_REQUEST then
185
+ if player and not Gate.checkRateLimit(reg, player) then
186
+ if statsEnabled then
187
+ reg.drops += 1
188
+ end
189
+ Middleware.fireDrop(player :: Player, "rate", reg.name, value)
190
+ continue
191
+ end
192
+
193
+ local ok, reason = Gate.validate(value, reg.codec)
194
+ if not ok then
195
+ if statsEnabled then
196
+ reg.drops += 1
197
+ end
198
+ if player then
199
+ Middleware.fireDrop(player, "validation", reg.name, value)
200
+ end
201
+ continue
202
+ end
203
+
204
+ if reg.validate then
205
+ local valid, validReason = reg.validate(value, player :: Player)
206
+ if not valid then
207
+ if statsEnabled then
208
+ reg.drops += 1
209
+ end
210
+ Middleware.fireDrop(
211
+ player :: Player,
212
+ validReason or "validate",
213
+ reg.name,
214
+ value
215
+ )
216
+ continue
217
+ end
218
+ end
219
+ end
220
+
221
+ if hasRecvHooks then
222
+ value = Middleware.runReceive(value, reg.name, player)
223
+ end
224
+
225
+ if statsEnabled then
226
+ reg.recvFires += 1
227
+ end
228
+ reg.signal:fire(value, player, corrId)
170
229
  end
171
230
  end
172
231
  end