@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.
Files changed (56) hide show
  1. package/README.md +241 -349
  2. package/package.json +1 -1
  3. package/src/Types.luau +53 -13
  4. package/src/api/Group.luau +41 -51
  5. package/src/api/Packet.luau +145 -143
  6. package/src/api/Query.luau +204 -202
  7. package/src/api/Scope.luau +36 -38
  8. package/src/api/Signal.luau +68 -94
  9. package/src/codec/Base.luau +43 -23
  10. package/src/codec/composite/Array.luau +161 -152
  11. package/src/codec/composite/Map.luau +37 -53
  12. package/src/codec/composite/Optional.luau +15 -21
  13. package/src/codec/composite/Shared.luau +78 -41
  14. package/src/codec/composite/Struct.luau +64 -125
  15. package/src/codec/composite/Tagged.luau +15 -25
  16. package/src/codec/composite/Tuple.luau +15 -20
  17. package/src/codec/datatype/Buffer.luau +44 -51
  18. package/src/codec/datatype/CFrame.luau +72 -104
  19. package/src/codec/datatype/Color.luau +14 -10
  20. package/src/codec/datatype/Instance.luau +32 -42
  21. package/src/codec/datatype/IntVector.luau +24 -22
  22. package/src/codec/datatype/NumberRange.luau +21 -12
  23. package/src/codec/datatype/Ray.luau +13 -7
  24. package/src/codec/datatype/Rect.luau +13 -7
  25. package/src/codec/datatype/Region.luau +25 -22
  26. package/src/codec/datatype/Sequence.luau +144 -118
  27. package/src/codec/datatype/String.luau +29 -59
  28. package/src/codec/datatype/UDim.luau +29 -30
  29. package/src/codec/datatype/Vector.luau +89 -105
  30. package/src/codec/meta/Auto.luau +194 -246
  31. package/src/codec/meta/Bitfield.luau +37 -36
  32. package/src/codec/meta/Custom.luau +10 -10
  33. package/src/codec/meta/Enum.luau +8 -11
  34. package/src/codec/meta/Float.luau +16 -20
  35. package/src/codec/meta/Nothing.luau +2 -2
  36. package/src/codec/meta/Unknown.luau +22 -32
  37. package/src/codec/primitive/Bool.luau +9 -5
  38. package/src/codec/primitive/Float16.luau +13 -23
  39. package/src/codec/primitive/Int.luau +20 -28
  40. package/src/codec/primitive/Number.luau +6 -10
  41. package/src/codec/primitive/Signed.luau +32 -0
  42. package/src/codec/primitive/Varint.luau +59 -28
  43. package/src/index.d.ts +8 -2
  44. package/src/init.luau +118 -143
  45. package/src/internal/Baseline.luau +17 -18
  46. package/src/internal/Channel.luau +143 -212
  47. package/src/internal/Middleware.luau +37 -47
  48. package/src/internal/Pool.luau +10 -10
  49. package/src/internal/Registry.luau +38 -34
  50. package/src/internal/Transport.luau +28 -0
  51. package/src/internal/Util.luau +26 -0
  52. package/src/transport/Bridge.luau +32 -24
  53. package/src/transport/Client.luau +51 -53
  54. package/src/transport/Gate.luau +183 -113
  55. package/src/transport/Reader.luau +95 -66
  56. package/src/transport/Server.luau +130 -125
@@ -10,28 +10,60 @@ local Registry = require(script.Parent.Parent.internal.Registry)
10
10
  local Types = require(script.Parent.Parent.Types)
11
11
  local Varint = require(script.Parent.Parent.codec.primitive.Varint)
12
12
 
13
- -- Constants -----------------------------------------------------------
13
+ -- Constants --------------------------------------------------------------
14
14
 
15
15
  local KIND_PACKET = Registry.KIND_PACKET
16
16
  local KIND_REQUEST = Registry.KIND_REQUEST
17
17
  local KIND_RESPONSE = Registry.KIND_RESPONSE
18
18
 
19
- -- Private -------------------------------------------------------------
19
+ local TS_FRAME = Channel.TS_FRAME
20
+ local TS_OFFSET = Channel.TS_OFFSET
21
+ local TS_FULL = Channel.TS_FULL
22
+
23
+ local MAX_ITEMS_PER_FRAME = 0xFFFF
24
+
25
+ -- Private ----------------------------------------------------------------
20
26
 
21
27
  local band = bit32.band
22
28
  local readu8 = buffer.readu8
23
29
 
24
- -- Public --------------------------------------------------------------
30
+ local function recordDrop(
31
+ reg: Types.Registration,
32
+ statsEnabled: boolean,
33
+ player: Player?,
34
+ reason: string,
35
+ value: any
36
+ ): ()
37
+ if statsEnabled then
38
+ reg.drops += 1
39
+ end
40
+ if player then
41
+ Middleware.fireDrop(player, reason, reg.name, value)
42
+ end
43
+ end
44
+
45
+ -- Public -----------------------------------------------------------------
25
46
 
26
47
  local Reader = {}
27
48
 
49
+ --[[
50
+ Sanity-check an inbound RemoteEvent payload. The size cap MUST NOT be
51
+ tied to the local `Channel.maxSize()` — sender and receiver run in
52
+ separate Lua VMs with independent module state, so the receiver may
53
+ not have called Lync.configure with the same channelMaxSize. Coupling
54
+ them caused server frames larger than the receiver's local default to
55
+ be silently dropped, which permanently desynced the XOR baseline.
56
+
57
+ Roblox enforces a per-RemoteEvent payload ceiling around 1MB; if the
58
+ payload reached us at all, it's already within Roblox's limit. Trust
59
+ that and accept anything non-empty.
60
+ ]]
28
61
  function Reader.decodeIncoming(data: any): buffer?
29
62
  if typeof(data) ~= "buffer" then
30
63
  return nil
31
64
  end
32
65
  local b = data :: buffer
33
- local len = buffer.len(b)
34
- if len == 0 or len > 65536 then
66
+ if buffer.len(b) == 0 then
35
67
  return nil
36
68
  end
37
69
  return b
@@ -44,7 +76,6 @@ function Reader.process(
44
76
  player: Player?,
45
77
  isServer: boolean
46
78
  ): ()
47
- -- Set baseline read key for delta codecs
48
79
  if Baseline.hasDelta() then
49
80
  Baseline.setReadKey(player or false)
50
81
  end
@@ -54,6 +85,7 @@ function Reader.process(
54
85
  local pos = 0
55
86
 
56
87
  while pos < incomingLen do
88
+ local frameStart = pos
57
89
  local raw = readu8(incoming, pos)
58
90
  pos += 1
59
91
 
@@ -62,87 +94,85 @@ function Reader.process(
62
94
 
63
95
  local reg = Registry.get(id)
64
96
  if not reg then
65
- warn(`[Lync] Reader: unknown packet ID {id} at byte {pos - 1}`)
66
- break
97
+ warn(`[Lync] Reader.process: unknown packet ID {id} at byte {pos - 1}`)
98
+ return
67
99
  end
68
100
 
69
101
  if reg.kind == KIND_PACKET then
70
102
  local count: number
71
-
72
103
  if msb then
73
104
  count = 1
74
105
  else
106
+ if pos + 2 > incomingLen then
107
+ warn("[Lync] Reader.process: truncated multi-frame header")
108
+ return
109
+ end
75
110
  count = buffer.readu16(incoming, pos)
76
111
  pos += 2
77
112
  end
78
113
 
79
- -- Read timestamp if this packet has one configured
80
114
  local tsMode = reg.timestampMode
81
- local tsValue: any = nil
82
-
83
- if tsMode == Channel.TS_FRAME then
115
+ local tsValue: number? = nil
116
+ if tsMode == TS_FRAME then
84
117
  tsValue = readu8(incoming, pos)
85
118
  pos += 1
86
- elseif tsMode == Channel.TS_OFFSET then
119
+ elseif tsMode == TS_OFFSET then
87
120
  tsValue = buffer.readu16(incoming, pos)
88
121
  pos += 2
89
- elseif tsMode == Channel.TS_FULL then
122
+ elseif tsMode == TS_FULL then
90
123
  tsValue = buffer.readf64(incoming, pos)
91
124
  pos += 8
92
125
  end
93
126
 
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
127
+ if count > MAX_ITEMS_PER_FRAME then
128
+ count = MAX_ITEMS_PER_FRAME
129
+ end
130
+ local sizeMeta = reg.codec._size
131
+ if sizeMeta then
132
+ local maxBySize = (incomingLen - pos) // sizeMeta
133
+ if count > maxBySize then
134
+ count = maxBySize
99
135
  end
100
136
  end
101
137
 
138
+ local maxPayload = reg.maxPayloadBytes
139
+ local payloadStart = pos
140
+
102
141
  for _ = 1, count do
103
142
  local value, consumed = reg.codec.read(incoming, pos, refs)
143
+ if consumed == 0 then
144
+ warn(
145
+ `[Lync] Reader.process: codec for "{reg.name}" reported 0 bytes; aborting frame`
146
+ )
147
+ return
148
+ end
104
149
  pos += consumed
105
150
 
151
+ if maxPayload and (pos - payloadStart) > maxPayload then
152
+ recordDrop(reg, statsEnabled, player, "maxPayloadBytes", value)
153
+ return
154
+ end
155
+
106
156
  if isServer and reg.needsGate then
107
157
  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)
158
+ recordDrop(reg, statsEnabled, player, "rate", value)
112
159
  continue
113
160
  end
114
-
115
161
  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)
162
+ recordDrop(reg, statsEnabled, player, "rate", value)
120
163
  continue
121
164
  end
122
165
 
123
- local ok, reason = Gate.validate(value, reg.codec)
166
+ local ok = Gate.validate(value, reg.codec)
124
167
  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
168
+ recordDrop(reg, statsEnabled, player, "validation", value)
131
169
  continue
132
170
  end
133
171
 
134
- if reg.validate then
135
- local valid, validReason = reg.validate(value, player :: Player)
172
+ if reg.validate and player then
173
+ local valid, validReason = reg.validate(value, player)
136
174
  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
- )
175
+ recordDrop(reg, statsEnabled, player, validReason or "validate", value)
146
176
  continue
147
177
  end
148
178
  end
@@ -164,55 +194,53 @@ function Reader.process(
164
194
  end
165
195
 
166
196
  if statsEnabled then
167
- -- Approximate: counts all bytes consumed in this frame, not just payload
168
- reg.bytesReceived += pos
197
+ reg.bytesReceived += (pos - frameStart)
169
198
  end
170
199
  elseif reg.kind == KIND_REQUEST or reg.kind == KIND_RESPONSE then
171
- -- MSB distinguishes nil responses (no payload) from data responses
172
200
  local corrId, corrBytes = Varint.read(incoming, pos)
173
201
  pos += corrBytes
174
202
 
175
203
  if msb then
176
204
  if statsEnabled then
177
205
  reg.recvFires += 1
206
+ reg.bytesReceived += (pos - frameStart)
178
207
  end
179
208
  reg.signal:fire(nil, player, corrId)
180
209
  else
181
210
  local value, consumed = reg.codec.read(incoming, pos, refs)
211
+ if consumed == 0 then
212
+ warn(
213
+ `[Lync] Reader.process: query codec for "{reg.name}" reported 0 bytes; aborting frame`
214
+ )
215
+ return
216
+ end
182
217
  pos += consumed
183
218
 
184
219
  if isServer and reg.needsGate and reg.kind == KIND_REQUEST then
185
220
  if player and not Gate.checkRateLimit(reg, player) then
221
+ recordDrop(reg, statsEnabled, player, "rate", value)
186
222
  if statsEnabled then
187
- reg.drops += 1
223
+ reg.bytesReceived += (pos - frameStart)
188
224
  end
189
- Middleware.fireDrop(player :: Player, "rate", reg.name, value)
190
225
  continue
191
226
  end
192
227
 
193
- local ok, reason = Gate.validate(value, reg.codec)
228
+ local ok = Gate.validate(value, reg.codec)
194
229
  if not ok then
230
+ recordDrop(reg, statsEnabled, player, "validation", value)
195
231
  if statsEnabled then
196
- reg.drops += 1
197
- end
198
- if player then
199
- Middleware.fireDrop(player, "validation", reg.name, value)
232
+ reg.bytesReceived += (pos - frameStart)
200
233
  end
201
234
  continue
202
235
  end
203
236
 
204
- if reg.validate then
205
- local valid, validReason = reg.validate(value, player :: Player)
237
+ if reg.validate and player then
238
+ local valid, validReason = reg.validate(value, player)
206
239
  if not valid then
240
+ recordDrop(reg, statsEnabled, player, validReason or "validate", value)
207
241
  if statsEnabled then
208
- reg.drops += 1
242
+ reg.bytesReceived += (pos - frameStart)
209
243
  end
210
- Middleware.fireDrop(
211
- player :: Player,
212
- validReason or "validate",
213
- reg.name,
214
- value
215
- )
216
244
  continue
217
245
  end
218
246
  end
@@ -224,6 +252,7 @@ function Reader.process(
224
252
 
225
253
  if statsEnabled then
226
254
  reg.recvFires += 1
255
+ reg.bytesReceived += (pos - frameStart)
227
256
  end
228
257
  reg.signal:fire(value, player, corrId)
229
258
  end
@@ -1,8 +1,10 @@
1
1
  --!strict
2
2
  --!optimize 2
3
3
  -- Server-side transport: per-player channels, broadcast, flush.
4
+ -- Decodes inbound XOR delta against a per-player baseline.
4
5
 
5
6
  local Players = game:GetService("Players")
7
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
6
8
 
7
9
  local Baseline = require(script.Parent.Parent.internal.Baseline)
8
10
  local Bridge = require(script.Parent.Parent.transport.Bridge)
@@ -12,186 +14,189 @@ local Pool = require(script.Parent.Parent.internal.Pool)
12
14
  local Reader = require(script.Parent.Parent.transport.Reader)
13
15
  local Types = require(script.Parent.Parent.Types)
14
16
 
15
- -- State ---------------------------------------------------------------
17
+ -- Constants --------------------------------------------------------------
18
+
19
+ local CONTAINER_NAME = "LyncRemotes"
20
+
21
+ -- State ------------------------------------------------------------------
16
22
 
17
23
  local _reliableChannels: { [Player]: Types.ChannelState } = {}
18
24
  local _unreliableChannels: { [Player]: Types.ChannelState } = {}
19
25
  local _playerStats: { [Player]: Types.PlayerStats } = {}
20
- local _statsEnabled = false
26
+ local _prevIncoming: { [Player]: buffer } = {}
27
+ local _prevIncomingLen: { [Player]: number } = {}
21
28
 
22
- -- Private -------------------------------------------------------------
29
+ local _statsEnabled = false
23
30
 
24
- local function getReliable(player: Player): Types.ChannelState
25
- local ch = _reliableChannels[player]
26
- if not ch then
27
- ch = Pool.acquire()
28
- _reliableChannels[player] = ch
29
- end
30
- return ch
31
- end
31
+ -- Private ----------------------------------------------------------------
32
32
 
33
- local function getUnreliable(player: Player): Types.ChannelState
34
- local ch = _unreliableChannels[player]
33
+ local function getOrCreateChannel(
34
+ map: { [Player]: Types.ChannelState },
35
+ player: Player
36
+ ): Types.ChannelState
37
+ local ch = map[player]
35
38
  if not ch then
36
39
  ch = Pool.acquire()
37
- _unreliableChannels[player] = ch
40
+ map[player] = ch
38
41
  end
39
42
  return ch
40
43
  end
41
44
 
42
- local function flushPlayer(player: Player): ()
43
- -- Reliable channel
45
+ local function flushReliable(player: Player): ()
44
46
  local rCh = _reliableChannels[player]
45
- if rCh and rCh.cursor > 0 then
46
- local snapshot, refs, byteLen, refCount = Channel.sealAndDump(rCh)
47
+ if not rCh or rCh.cursor == 0 then
48
+ return
49
+ end
47
50
 
48
- -- XOR against previous frame
49
- local xored = Channel.xorApply(snapshot, byteLen, rCh.prevDump, rCh.prevDumpLen)
50
- rCh.prevDump = snapshot
51
- rCh.prevDumpLen = byteLen
51
+ local snapshot, refs, byteLen = Channel.sealAndDump(rCh)
52
+ local xored = Channel.xorApply(snapshot, byteLen, rCh.prevDump, rCh.prevDumpLen)
53
+ rCh.prevDump = snapshot
54
+ rCh.prevDumpLen = byteLen
52
55
 
53
- Bridge.fireClient(player, xored, refs)
54
- Channel.reset(rCh)
56
+ Bridge.fireClient(player, xored, refs)
57
+ Channel.reset(rCh)
55
58
 
56
- if _statsEnabled then
57
- local ps = _playerStats[player]
58
- if ps then
59
- ps.bytesSent += byteLen
60
- end
59
+ if _statsEnabled then
60
+ local ps = _playerStats[player]
61
+ if ps then
62
+ ps.bytesSent += byteLen
61
63
  end
62
64
  end
65
+ end
63
66
 
64
- -- Unreliable channel (no XOR, no delta)
67
+ local function flushUnreliable(player: Player): ()
65
68
  local uCh = _unreliableChannels[player]
66
- if uCh and uCh.cursor > 0 then
67
- local snapshot, refs, byteLen, _ = Channel.sealAndDump(uCh)
69
+ if not uCh or uCh.cursor == 0 then
70
+ return
71
+ end
68
72
 
69
- Bridge.fireClientUnreliable(player, snapshot, refs)
70
- Channel.reset(uCh)
73
+ local snapshot, refs, byteLen = Channel.sealAndDump(uCh)
74
+ Bridge.fireClientUnreliable(player, snapshot, refs)
75
+ Channel.reset(uCh)
71
76
 
72
- if _statsEnabled then
73
- local ps = _playerStats[player]
74
- if ps then
75
- ps.bytesSent += byteLen
76
- end
77
+ if _statsEnabled then
78
+ local ps = _playerStats[player]
79
+ if ps then
80
+ ps.bytesSent += byteLen
77
81
  end
78
82
  end
79
83
  end
80
84
 
81
- -- Public --------------------------------------------------------------
82
-
83
- local Server = {}
84
-
85
- function Server.getChannel(player: Player, isUnreliable: boolean): Types.ChannelState
86
- return if isUnreliable then getUnreliable(player) else getReliable(player)
85
+ local function flushPlayer(player: Player): ()
86
+ flushReliable(player)
87
+ flushUnreliable(player)
87
88
  end
88
89
 
89
- function Server.flush(): ()
90
- if Channel.hasTimestamps() then
91
- Channel.updateTimestamp()
90
+ local function handleIncomingReliable(player: Player, data: any, refs: any?): ()
91
+ local incoming = Reader.decodeIncoming(data)
92
+ if not incoming then
93
+ return
92
94
  end
93
95
 
94
- for player in _reliableChannels do
95
- flushPlayer(player)
96
+ local incomingLen = buffer.len(incoming)
97
+ if not Gate.checkBandwidth(player, incomingLen) then
98
+ return
99
+ end
100
+
101
+ local prev = _prevIncoming[player]
102
+ local decoded = if prev
103
+ then Channel.xorApply(incoming, incomingLen, prev, _prevIncomingLen[player])
104
+ else incoming
105
+ _prevIncoming[player] = decoded
106
+ _prevIncomingLen[player] = incomingLen
107
+
108
+ local ok, err = pcall(Reader.process, decoded, incomingLen, refs, player, true)
109
+ if not ok then
110
+ warn(`[Lync] Server.receive: {player.Name}: {err}`)
96
111
  end
97
112
 
98
- for player in _unreliableChannels do
99
- if not _reliableChannels[player] then
100
- flushPlayer(player)
113
+ if _statsEnabled then
114
+ local ps = _playerStats[player]
115
+ if ps then
116
+ ps.bytesReceived += incomingLen
101
117
  end
102
118
  end
103
119
  end
104
120
 
105
- function Server.flushPlayer(player: Player): ()
106
- flushPlayer(player)
107
- end
121
+ local function handleIncomingUnreliable(player: Player, data: any, refs: any?): ()
122
+ local incoming = Reader.decodeIncoming(data)
123
+ if not incoming then
124
+ return
125
+ end
108
126
 
109
- function Server.start(): ()
110
- local container = Instance.new("Folder")
111
- container.Name = "LyncRemotes"
112
- container.Parent = game:GetService("ReplicatedStorage")
127
+ local incomingLen = buffer.len(incoming)
128
+ local ok, err = pcall(Reader.process, incoming, incomingLen, refs, player, true)
129
+ if not ok then
130
+ warn(`[Lync] Server.receiveUnreliable: {player.Name}: {err}`)
131
+ end
113
132
 
114
- Bridge.setup(container)
133
+ if _statsEnabled then
134
+ local ps = _playerStats[player]
135
+ if ps then
136
+ ps.bytesReceived += incomingLen
137
+ end
138
+ end
139
+ end
115
140
 
116
- local reliable = Bridge.reliable()
117
- local unreliable = Bridge.unreliable()
141
+ local function clearPlayerState(player: Player): ()
142
+ local rCh = _reliableChannels[player]
143
+ if rCh then
144
+ _reliableChannels[player] = nil
145
+ Pool.release(rCh)
146
+ end
147
+ local uCh = _unreliableChannels[player]
148
+ if uCh then
149
+ _unreliableChannels[player] = nil
150
+ Pool.release(uCh)
151
+ end
118
152
 
119
- -- Incoming reliable
120
- reliable.OnServerEvent:Connect(function(player: Player, data: any, refs: any?)
121
- local incoming = Reader.decodeIncoming(data)
122
- if not incoming then
123
- return
124
- end
153
+ _playerStats[player] = nil
154
+ _prevIncoming[player] = nil
155
+ _prevIncomingLen[player] = nil
125
156
 
126
- local incomingLen = buffer.len(incoming)
157
+ Gate.clearPlayer(player)
158
+ Baseline.clearPlayer(player)
159
+ end
127
160
 
128
- -- Bandwidth check
129
- if not Gate.checkBandwidth(player, incomingLen) then
130
- return
131
- end
161
+ -- Public -----------------------------------------------------------------
132
162
 
133
- -- XOR decode against player's previous outbound (client→server has no XOR)
134
- local ok, err = pcall(Reader.process, incoming, incomingLen, refs :: any, player, true)
135
- if not ok then
136
- warn(`[Lync] Server: read error from {player.Name}: {err}`)
137
- end
163
+ local Server = {}
138
164
 
139
- if _statsEnabled then
140
- local ps = _playerStats[player]
141
- if ps then
142
- ps.bytesReceived += incomingLen
143
- end
144
- end
145
- end)
165
+ function Server.getChannel(player: Player, isUnreliable: boolean): Types.ChannelState
166
+ return getOrCreateChannel(
167
+ if isUnreliable then _unreliableChannels else _reliableChannels,
168
+ player
169
+ )
170
+ end
146
171
 
147
- -- Incoming unreliable
148
- unreliable.OnServerEvent:Connect(function(player: Player, data: any, refs: any?)
149
- local incoming = Reader.decodeIncoming(data)
150
- if not incoming then
151
- return
152
- end
172
+ function Server.flush(): ()
173
+ if Channel.hasTimestamps() then
174
+ Channel.updateTimestamp()
175
+ end
176
+ for _, player in Players:GetPlayers() do
177
+ flushPlayer(player)
178
+ end
179
+ end
153
180
 
154
- local incomingLen = buffer.len(incoming)
181
+ Server.flushPlayer = flushPlayer
155
182
 
156
- local ok, err = pcall(Reader.process, incoming, incomingLen, refs :: any, player, true)
157
- if not ok then
158
- warn(`[Lync] Server: read error (unreliable) from {player.Name}: {err}`)
159
- end
183
+ function Server.start(): ()
184
+ local container = Instance.new("Folder")
185
+ container.Name = CONTAINER_NAME
186
+ container.Parent = ReplicatedStorage
160
187
 
161
- if _statsEnabled then
162
- local ps = _playerStats[player]
163
- if ps then
164
- ps.bytesReceived += incomingLen
165
- end
166
- end
167
- end)
188
+ Bridge.setup(container)
189
+
190
+ Bridge.reliable().OnServerEvent:Connect(handleIncomingReliable)
191
+ Bridge.unreliable().OnServerEvent:Connect(handleIncomingUnreliable)
168
192
 
169
- -- Player lifecycle
170
193
  Players.PlayerAdded:Connect(function(player: Player)
171
194
  if _statsEnabled then
172
195
  _playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
173
196
  end
174
197
  end)
198
+ Players.PlayerRemoving:Connect(clearPlayerState)
175
199
 
176
- Players.PlayerRemoving:Connect(function(player: Player)
177
- local rCh = _reliableChannels[player]
178
- if rCh then
179
- _reliableChannels[player] = nil
180
- Pool.release(rCh)
181
- end
182
-
183
- local uCh = _unreliableChannels[player]
184
- if uCh then
185
- _unreliableChannels[player] = nil
186
- Pool.release(uCh)
187
- end
188
-
189
- _playerStats[player] = nil
190
- Gate.clearPlayer(player)
191
- Baseline.clearPlayer(player)
192
- end)
193
-
194
- -- Initialize stats for existing players
195
200
  if _statsEnabled then
196
201
  for _, player in Players:GetPlayers() do
197
202
  _playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
@@ -208,7 +213,7 @@ function Server.getPlayerStats(player: Player): Types.PlayerStats?
208
213
  end
209
214
 
210
215
  function Server.resetStats(): ()
211
- for player, ps in _playerStats do
216
+ for _, ps in _playerStats do
212
217
  ps.bytesSent = 0
213
218
  ps.bytesReceived = 0
214
219
  end