@axpecter/lync 2.0.0 → 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 +475 -426
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  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 +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  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 +28 -21
  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 -36
  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 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,567 +1,217 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Server-side transport with batching, delta state, frame XOR.
3
+ -- Server-side transport: per-player channels, broadcast, flush.
4
4
 
5
5
  local Players = game:GetService("Players")
6
- local RunService = game:GetService("RunService")
7
6
 
8
7
  local Baseline = require(script.Parent.Parent.internal.Baseline)
9
- local Bridge = require(script.Parent.Bridge)
8
+ local Bridge = require(script.Parent.Parent.transport.Bridge)
10
9
  local Channel = require(script.Parent.Parent.internal.Channel)
11
- local Gate = require(script.Parent.Gate)
12
- local Middleware = require(script.Parent.Parent.internal.Middleware)
10
+ local Gate = require(script.Parent.Parent.transport.Gate)
13
11
  local Pool = require(script.Parent.Parent.internal.Pool)
14
- local Reader = require(script.Parent.Reader)
15
- local Registry = require(script.Parent.Parent.internal.Registry)
12
+ local Reader = require(script.Parent.Parent.transport.Reader)
16
13
  local Types = require(script.Parent.Parent.Types)
17
14
 
18
- type ChannelState = Types.ChannelState
19
- type Codec<T> = Types.Codec<T>
20
- type Registration = Types.Registration
21
-
22
- local channelWriteBatch = Channel.writeBatch
23
- local channelWriteBatchRaw = Channel.writeBatchRaw
24
- local channelUpdateTimestamp = Channel.updateTimestamp
25
- local channelStatsEnabled = Channel.statsEnabled
26
-
27
15
  -- State ---------------------------------------------------------------
28
16
 
29
- type PlayerStats = {
30
- bytesSent: number,
31
- bytesReceived: number,
32
- }
33
-
34
- type PlayerState = {
35
- reliable: ChannelState,
36
- unreliable: ChannelState,
37
- stats: PlayerStats,
38
- }
39
-
40
- local _players = {} :: { [Player]: PlayerState }
41
- local _prevRecv = {} :: { [Player]: buffer }
42
- local _isStarted = false
43
- local _broadScratch = Channel.create()
44
-
45
- -- Flush control
46
- local _flushRate = 60
47
- local _flushInterval = 1 / 60
48
- local _useAccumulator = false
49
- local _flushAccum = 0
50
- local _flushedThisFrame = false
51
-
52
- -- Timestamp
53
- local _frameCounter: number = 0
54
-
55
- -- Bandwidth strike counter
56
- local _strikes = {} :: { [Player]: number }
57
- local _softLimit = 16384
58
- local _maxStrikes = 10
59
-
60
- -- Packet capture
61
- local _captureEnabled = false
62
- local _captureLabel = ""
63
- local _captureFrame = 0
64
- local _captures = {} :: { any }
65
-
66
- local MAX_HEX = 512 -- cap hex dump to first 512 bytes to keep capture size sane
67
-
68
- local function bufToHex(buf: buffer, len: number): string
69
- local cap = math.min(len, MAX_HEX)
70
- local parts = table.create(cap)
71
- for i = 0, cap - 1 do
72
- parts[i + 1] = string.format("%02x", buffer.readu8(buf, i))
73
- end
74
- return table.concat(parts)
75
- end
17
+ local _reliableChannels: { [Player]: Types.ChannelState } = {}
18
+ local _unreliableChannels: { [Player]: Types.ChannelState } = {}
19
+ local _playerStats: { [Player]: Types.PlayerStats } = {}
20
+ local _statsEnabled = false
76
21
 
77
22
  -- Private -------------------------------------------------------------
78
23
 
79
- local function onPlayerAdded(player: Player): ()
80
- _players[player] = {
81
- reliable = Pool.acquire(),
82
- unreliable = Pool.acquire(),
83
- stats = { bytesSent = 0, bytesReceived = 0 },
84
- }
85
- _strikes[player] = 0
86
- Gate.onPlayerAdded(player)
87
- end
88
-
89
- local function onPlayerRemoving(player: Player): ()
90
- local state = _players[player]
91
- if not state then
92
- return
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
93
29
  end
94
-
95
- Pool.release(state.reliable)
96
- Pool.release(state.unreliable)
97
- _players[player] = nil
98
- _prevRecv[player] = nil
99
- _strikes[player] = nil
100
-
101
- Gate.onPlayerRemoved(player)
102
- Baseline.clearSource(player)
30
+ return ch
103
31
  end
104
32
 
105
- local function receive(player: Player, data: any, refs: any, applyXor: boolean): ()
106
- local buf = Reader.decodeIncoming(data)
107
- if not buf then
108
- return
109
- end
110
-
111
- local len = buffer.len(buf)
112
- local strikes = _strikes[player] or 0
113
- if len > _softLimit then
114
- strikes += 1
115
- else
116
- strikes = math.max(0, strikes - 1)
117
- end
118
- _strikes[player] = strikes
119
- if strikes > _maxStrikes then
120
- Gate.reportDrop(player, "bandwidth", "frame", nil)
121
- return
122
- end
123
-
124
- local raw: buffer
125
- if applyXor then
126
- raw = Channel.xorApply(buf, _prevRecv[player])
127
- _prevRecv[player] = raw
128
- else
129
- raw = buf
130
- end
131
-
132
- local safeRefs: { Instance }? = if typeof(refs) == "table" then refs else nil
133
- local ok, err = pcall(Reader.process, raw, safeRefs, player, 0)
134
- if not ok then
135
- warn(`[Lync] Read failed from {player.Name} ({player.UserId}): {err}`)
136
- strikes = (_strikes[player] or 0) + 1
137
- _strikes[player] = strikes
138
- end
139
-
140
- if channelStatsEnabled() then
141
- local state = _players[player]
142
- if state then
143
- state.stats.bytesReceived += buffer.len(raw)
144
- end
33
+ local function getUnreliable(player: Player): Types.ChannelState
34
+ local ch = _unreliableChannels[player]
35
+ if not ch then
36
+ ch = Pool.acquire()
37
+ _unreliableChannels[player] = ch
145
38
  end
39
+ return ch
146
40
  end
147
41
 
148
- local function flushChannel(remote: any, ch: ChannelState, target: Player, applyXor: boolean): ()
149
- if ch.cursor == 0 then
150
- return
151
- end
42
+ local function flushPlayer(player: Player): ()
43
+ -- Reliable channel
44
+ local rCh = _reliableChannels[player]
45
+ if rCh and rCh.cursor > 0 then
46
+ local snapshot, refs, byteLen, refCount = Channel.sealAndDump(rCh)
152
47
 
153
- local raw, refs = Channel.sealAndDump(ch)
48
+ -- XOR against previous frame
49
+ local xored = Channel.xorApply(snapshot, byteLen, rCh.prevDump, rCh.prevDumpLen)
50
+ rCh.prevDump = snapshot
51
+ rCh.prevDumpLen = byteLen
154
52
 
155
- local xored: buffer? = nil
156
- if applyXor then
157
- xored = Channel.xorApply(raw, ch.prevDump)
158
- ch.prevDump = raw
159
- remote:FireClient(target, xored, refs)
160
- else
161
- remote:FireClient(target, raw, refs)
162
- end
163
-
164
- if _captureEnabled then
165
- _captureFrame += 1
166
- local rawLen = buffer.len(raw)
167
- table.insert(_captures, {
168
- label = _captureLabel,
169
- frame = _captureFrame,
170
- raw = bufToHex(raw, rawLen),
171
- xored = if xored then bufToHex(xored :: buffer, buffer.len(xored :: buffer)) else nil,
172
- bytes = rawLen,
173
- refs = #refs,
174
- })
175
- end
53
+ Bridge.fireClient(player, xored, refs)
54
+ Channel.reset(rCh)
176
55
 
177
- if channelStatsEnabled() then
178
- local state = _players[target]
179
- if state then
180
- state.stats.bytesSent += buffer.len(raw)
56
+ if _statsEnabled then
57
+ local ps = _playerStats[player]
58
+ if ps then
59
+ ps.bytesSent += byteLen
60
+ end
181
61
  end
182
62
  end
183
63
 
184
- Channel.reset(ch)
185
- end
64
+ -- Unreliable channel (no XOR, no delta)
65
+ local uCh = _unreliableChannels[player]
66
+ if uCh and uCh.cursor > 0 then
67
+ local snapshot, refs, byteLen, _ = Channel.sealAndDump(uCh)
186
68
 
187
- local function flush(): ()
188
- if Channel.hasTimestamps() then
189
- _frameCounter = (_frameCounter + 1) % 256
190
- channelUpdateTimestamp(_frameCounter, 0, os.clock())
191
- end
69
+ Bridge.fireClientUnreliable(player, snapshot, refs)
70
+ Channel.reset(uCh)
192
71
 
193
- local reliable = Bridge.reliable
194
- local unreliable = Bridge.unreliable
195
-
196
- for player, state in _players do
197
- local ok, err = pcall(flushChannel, reliable, state.reliable, player, true)
198
- if not ok then
199
- warn(`[Lync] Flush failed for {player.Name} ({player.UserId}): {err}`)
200
- Channel.reset(state.reliable)
201
- end
202
-
203
- local ok2, err2 = pcall(flushChannel, unreliable, state.unreliable, player, false)
204
- if not ok2 then
205
- warn(`[Lync] Flush failed for {player.Name} ({player.UserId}): {err2}`)
206
- Channel.reset(state.unreliable)
72
+ if _statsEnabled then
73
+ local ps = _playerStats[player]
74
+ if ps then
75
+ ps.bytesSent += byteLen
76
+ end
207
77
  end
208
78
  end
209
79
  end
210
80
 
211
- -- No timestamp here: writeBatchRaw handles it in the batch header.
212
- local function serializeBroadcast(reg: Registration, data: any): (number, { Instance }?)
213
- local scratch = _broadScratch
214
- scratch.cursor = 0
215
- table.clear(scratch.refs)
216
- Channel.setCurrentPacket(reg.name)
217
- reg.codec.write(scratch, data)
218
-
219
- local snapRefs = if #scratch.refs > 0 then table.clone(scratch.refs) else nil
220
- return scratch.cursor, snapRefs
221
- end
81
+ -- Public --------------------------------------------------------------
222
82
 
223
- local function prepareBroadcast(reg: Registration, data: any): (any, number, { Instance }?)
224
- local final: any
225
- if Middleware.hasSend then
226
- final = Middleware.runSend(data, reg.name, nil)
227
- if final == nil then
228
- return nil, 0, nil
229
- end
230
- else
231
- final = data
232
- end
83
+ local Server = {}
233
84
 
234
- local snapLen, snapRefs = serializeBroadcast(reg, final)
235
- return final, snapLen, snapRefs
85
+ function Server.getChannel(player: Player, isUnreliable: boolean): Types.ChannelState
86
+ return if isUnreliable then getUnreliable(player) else getReliable(player)
236
87
  end
237
88
 
238
- -- Broadcast fan-out: stats computed once from snapLen, not per-player (#7)
239
- local function broadcastToAll(
240
- reg: Registration,
241
- data: any,
242
- isUnreliable: boolean,
243
- exceptSet: { [Player]: true }?
244
- ): ()
245
- local final, snapLen, snapRefs = prepareBroadcast(reg, data)
246
- if final == nil and data ~= nil then
247
- return
248
- end
249
-
250
- local scratchBuff = _broadScratch.buff
251
- local count = 0
252
- for player, state in _players do
253
- if exceptSet and exceptSet[player] then
254
- continue
255
- end
256
- local ch = if isUnreliable then state.unreliable else state.reliable
257
- channelWriteBatchRaw(ch, reg, scratchBuff, snapLen, snapRefs)
258
- count += 1
259
- end
260
- if channelStatsEnabled() and count > 0 then
261
- -- Header (3 bytes for id+count) + timestamp + payload per player
262
- local tsBytes = if reg.timestampMode == 1
263
- then 1
264
- elseif reg.timestampMode == 2 then 2
265
- elseif reg.timestampMode == 3 then 8
266
- else 0
267
- reg.bytesSent += (3 + tsBytes + snapLen) * count
268
- reg.fires += count
89
+ function Server.flush(): ()
90
+ if Channel.hasTimestamps() then
91
+ Channel.updateTimestamp()
269
92
  end
270
- end
271
93
 
272
- local function broadcastToList(
273
- players: { Player },
274
- reg: Registration,
275
- data: any,
276
- isUnreliable: boolean
277
- ): ()
278
- local final, snapLen, snapRefs = prepareBroadcast(reg, data)
279
- if final == nil and data ~= nil then
280
- return
94
+ for player in _reliableChannels do
95
+ flushPlayer(player)
281
96
  end
282
97
 
283
- local scratchBuff = _broadScratch.buff
284
- local count = 0
285
- for _, player in players do
286
- local state = _players[player]
287
- if not state then
288
- continue
98
+ for player in _unreliableChannels do
99
+ if not _reliableChannels[player] then
100
+ flushPlayer(player)
289
101
  end
290
- local ch = if isUnreliable then state.unreliable else state.reliable
291
- channelWriteBatchRaw(ch, reg, scratchBuff, snapLen, snapRefs)
292
- count += 1
293
- end
294
- if channelStatsEnabled() and count > 0 then
295
- local tsBytes = if reg.timestampMode == 1
296
- then 1
297
- elseif reg.timestampMode == 2 then 2
298
- elseif reg.timestampMode == 3 then 8
299
- else 0
300
- reg.bytesSent += (3 + tsBytes + snapLen) * count
301
- reg.fires += count
302
102
  end
303
103
  end
304
104
 
305
- local function broadcastToSet(
306
- set: { [Player]: true },
307
- reg: Registration,
308
- data: any,
309
- isUnreliable: boolean
310
- ): ()
311
- local final, snapLen, snapRefs = prepareBroadcast(reg, data)
312
- if final == nil and data ~= nil then
313
- return
314
- end
315
-
316
- local scratchBuff = _broadScratch.buff
317
- local count = 0
318
- for player in set do
319
- local state = _players[player]
320
- if not state then
321
- continue
322
- end
323
- local ch = if isUnreliable then state.unreliable else state.reliable
324
- channelWriteBatchRaw(ch, reg, scratchBuff, snapLen, snapRefs)
325
- count += 1
326
- end
327
- if channelStatsEnabled() and count > 0 then
328
- local tsBytes = if reg.timestampMode == 1
329
- then 1
330
- elseif reg.timestampMode == 2 then 2
331
- elseif reg.timestampMode == 3 then 8
332
- else 0
333
- reg.bytesSent += (3 + tsBytes + snapLen) * count
334
- reg.fires += count
335
- end
105
+ function Server.flushPlayer(player: Player): ()
106
+ flushPlayer(player)
336
107
  end
337
108
 
338
- -- Public --------------------------------------------------------------
339
-
340
- local Server = {}
341
-
342
109
  function Server.start(): ()
343
- if _isStarted then
344
- return
345
- end
346
- _isStarted = true
347
-
348
- Bridge.setup()
349
-
350
- Players.PlayerAdded:Connect(onPlayerAdded)
351
- Players.PlayerRemoving:Connect(onPlayerRemoving)
352
- for _, player in Players:GetPlayers() do
353
- onPlayerAdded(player)
354
- end
110
+ local container = Instance.new("Folder")
111
+ container.Name = "LyncRemotes"
112
+ container.Parent = game:GetService("ReplicatedStorage")
355
113
 
356
- Bridge.reliable.OnServerEvent:Connect(function(player: Player, data: any, refs: any): ()
357
- receive(player, data, refs, true)
358
- end)
359
- Bridge.unreliable.OnServerEvent:Connect(function(player: Player, data: any, refs: any): ()
360
- receive(player, data, refs, false)
361
- end)
114
+ Bridge.setup(container)
362
115
 
363
- if _useAccumulator then
364
- RunService.Heartbeat:Connect(function(dt: number)
365
- if _flushedThisFrame then
366
- _flushedThisFrame = false
367
- return
368
- end
369
- _flushAccum += dt
370
- if _flushAccum < _flushInterval then
371
- return
372
- end
373
- _flushAccum -= _flushInterval
374
- flush()
375
- end)
376
- else
377
- RunService.Heartbeat:Connect(function()
378
- if _flushedThisFrame then
379
- _flushedThisFrame = false
380
- return
381
- end
382
- flush()
383
- end)
384
- end
385
- end
116
+ local reliable = Bridge.reliable()
117
+ local unreliable = Bridge.unreliable()
386
118
 
387
- function Server.writeTo(player: Player, reg: Registration, data: any, isUnreliable: boolean): ()
388
- local state = _players[player]
389
- if not state then
390
- return
391
- end
392
-
393
- local final: any
394
- if Middleware.hasSend then
395
- final = Middleware.runSend(data, reg.name, player)
396
- if final == nil then
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
397
123
  return
398
124
  end
399
- else
400
- final = data
401
- end
402
125
 
403
- local ch = if isUnreliable then state.unreliable else state.reliable
404
- if channelStatsEnabled() then
405
- local before = ch.cursor
406
- channelWriteBatch(ch, reg, final)
407
- reg.bytesSent += ch.cursor - before
408
- reg.fires += 1
409
- else
410
- channelWriteBatch(ch, reg, final)
411
- end
412
- end
413
-
414
- function Server.writeToAll(reg: Registration, data: any, isUnreliable: boolean): ()
415
- broadcastToAll(reg, data, isUnreliable, nil)
416
- end
126
+ local incomingLen = buffer.len(incoming)
417
127
 
418
- function Server.writeToList(
419
- players: { Player },
420
- reg: Registration,
421
- data: any,
422
- isUnreliable: boolean
423
- ): ()
424
- broadcastToList(players, reg, data, isUnreliable)
425
- end
426
-
427
- function Server.writeToAllExcept(
428
- exceptSet: { [Player]: true },
429
- reg: Registration,
430
- data: any,
431
- isUnreliable: boolean
432
- ): ()
433
- broadcastToAll(reg, data, isUnreliable, exceptSet)
434
- end
128
+ -- Bandwidth check
129
+ if not Gate.checkBandwidth(player, incomingLen) then
130
+ return
131
+ end
435
132
 
436
- function Server.writeToSet(
437
- set: { [Player]: true },
438
- reg: Registration,
439
- data: any,
440
- isUnreliable: boolean
441
- ): ()
442
- broadcastToSet(set, reg, data, isUnreliable)
443
- end
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
444
138
 
445
- function Server.writeQuery(
446
- player: Player,
447
- id: number,
448
- name: string,
449
- correlationId: number,
450
- codec: Codec<any>,
451
- data: any
452
- ): ()
453
- local state = _players[player]
454
- if not state then
455
- return
456
- end
139
+ if _statsEnabled then
140
+ local ps = _playerStats[player]
141
+ if ps then
142
+ ps.bytesReceived += incomingLen
143
+ end
144
+ end
145
+ end)
457
146
 
458
- local final: any
459
- if Middleware.hasSend then
460
- final = Middleware.runSend(data, name, player)
461
- if final == nil then
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
462
151
  return
463
152
  end
464
- else
465
- final = data
466
- end
467
153
 
468
- Channel.writeQuery(state.reliable, id, name, correlationId, codec, final)
469
- end
154
+ local incomingLen = buffer.len(incoming)
470
155
 
471
- function Server.writeQueryNil(player: Player, id: number, name: string, correlationId: number): ()
472
- local state = _players[player]
473
- if not state then
474
- return
475
- end
476
-
477
- Channel.writeQuery(state.reliable, id, name, correlationId, nil, nil)
478
- end
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
479
160
 
480
- function Server.flush(): ()
481
- _flushAccum = 0
482
- _flushedThisFrame = true
483
- flush()
484
- end
161
+ if _statsEnabled then
162
+ local ps = _playerStats[player]
163
+ if ps then
164
+ ps.bytesReceived += incomingLen
165
+ end
166
+ end
167
+ end)
485
168
 
486
- function Server.setFlushRate(hz: number): ()
487
- _flushRate = math.clamp(hz, 1, 60)
488
- _flushInterval = 1 / _flushRate
489
- _useAccumulator = _flushRate < 60
490
- end
169
+ -- Player lifecycle
170
+ Players.PlayerAdded:Connect(function(player: Player)
171
+ if _statsEnabled then
172
+ _playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
173
+ end
174
+ end)
491
175
 
492
- function Server.getFlushRate(): number
493
- return _flushRate
494
- end
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
495
182
 
496
- function Server.setBandwidthLimit(softLimit: number, maxStrikes: number): ()
497
- _softLimit = softLimit
498
- _maxStrikes = maxStrikes
499
- end
183
+ local uCh = _unreliableChannels[player]
184
+ if uCh then
185
+ _unreliableChannels[player] = nil
186
+ Pool.release(uCh)
187
+ end
500
188
 
501
- function Server.getPlayerStats(player: Player): PlayerStats?
502
- local state = _players[player]
503
- return if state then state.stats else nil
504
- end
189
+ _playerStats[player] = nil
190
+ Gate.clearPlayer(player)
191
+ Baseline.clearPlayer(player)
192
+ end)
505
193
 
506
- function Server.resetStats(): ()
507
- for _, state in _players do
508
- state.stats.bytesSent = 0
509
- state.stats.bytesReceived = 0
194
+ -- Initialize stats for existing players
195
+ if _statsEnabled then
196
+ for _, player in Players:GetPlayers() do
197
+ _playerStats[player] = { bytesSent = 0, bytesReceived = 0 }
198
+ end
510
199
  end
511
- Registry.forEach(function(reg: Registration)
512
- reg.bytesSent = 0
513
- reg.bytesReceived = 0
514
- reg.fires = 0
515
- reg.recvFires = 0
516
- reg.drops = 0
517
- end)
518
200
  end
519
201
 
520
- function Server.startCapture(label: string?): ()
521
- _captureEnabled = true
522
- _captureLabel = label or "unknown"
523
- _captureFrame = 0
202
+ function Server.enableStats(): ()
203
+ _statsEnabled = true
524
204
  end
525
205
 
526
- function Server.stopCapture(): ()
527
- _captureEnabled = false
206
+ function Server.getPlayerStats(player: Player): Types.PlayerStats?
207
+ return _playerStats[player]
528
208
  end
529
209
 
530
- function Server.dumpCaptures(): ()
531
- local json = game:GetService("HttpService"):JSONEncode(_captures)
532
- local storage = game:GetService("ServerStorage")
533
-
534
- -- Clean up previous captures
535
- local old = storage:FindFirstChild("LyncCapture")
536
- if old then
537
- old:Destroy()
538
- end
539
-
540
- -- Split across StringValues if needed (200K limit per value)
541
- local CHUNK = 199000
542
- if #json <= CHUNK then
543
- local sv = Instance.new("StringValue")
544
- sv.Name = "LyncCapture"
545
- sv.Value = json
546
- sv.Parent = storage
547
- else
548
- local folder = Instance.new("Folder")
549
- folder.Name = "LyncCapture"
550
- local parts = math.ceil(#json / CHUNK)
551
- for i = 1, parts do
552
- local sv = Instance.new("StringValue")
553
- sv.Name = `Part{i}`
554
- sv.Value = string.sub(json, (i - 1) * CHUNK + 1, i * CHUNK)
555
- sv.Parent = folder
556
- end
557
- folder.Parent = storage
210
+ function Server.resetStats(): ()
211
+ for player, ps in _playerStats do
212
+ ps.bytesSent = 0
213
+ ps.bytesReceived = 0
558
214
  end
559
-
560
- local count = #_captures
561
- table.clear(_captures)
562
- print(
563
- `[Lync] Capture saved: {count} entries, {#json} chars -> ServerStorage.LyncCapture (open in script editor, copy contents)`
564
- )
565
215
  end
566
216
 
567
217
  return table.freeze(Server)