@axpecter/lync 2.2.1 → 2.3.2

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 (65) hide show
  1. package/README.md +109 -147
  2. package/package.json +1 -1
  3. package/src/Types.luau +34 -22
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +140 -66
  6. package/src/api/Query.luau +103 -65
  7. package/src/api/Scope.luau +26 -10
  8. package/src/api/Signal.luau +43 -52
  9. package/src/codec/Base.luau +28 -14
  10. package/src/codec/composite/Array.luau +296 -115
  11. package/src/codec/composite/Map.luau +377 -57
  12. package/src/codec/composite/Optional.luau +10 -2
  13. package/src/codec/composite/Shared.luau +134 -64
  14. package/src/codec/composite/Struct.luau +294 -43
  15. package/src/codec/composite/Tagged.luau +21 -16
  16. package/src/codec/composite/Tuple.luau +32 -15
  17. package/src/codec/datatype/Buffer.luau +7 -7
  18. package/src/codec/datatype/CFrame.luau +34 -122
  19. package/src/codec/datatype/Color.luau +10 -10
  20. package/src/codec/datatype/Instance.luau +15 -12
  21. package/src/codec/datatype/IntVector.luau +2 -2
  22. package/src/codec/datatype/NumberRange.luau +15 -8
  23. package/src/codec/datatype/Ray.luau +13 -14
  24. package/src/codec/datatype/Rect.luau +12 -12
  25. package/src/codec/datatype/Region.luau +13 -14
  26. package/src/codec/datatype/Sequence.luau +87 -65
  27. package/src/codec/datatype/String.luau +17 -10
  28. package/src/codec/datatype/UDim.luau +10 -8
  29. package/src/codec/datatype/Vector.luau +20 -59
  30. package/src/codec/meta/Auto.luau +94 -126
  31. package/src/codec/meta/Bitfield.luau +12 -14
  32. package/src/codec/meta/Custom.luau +3 -1
  33. package/src/codec/meta/DeltaScalar.luau +390 -0
  34. package/src/codec/meta/Enum.luau +9 -9
  35. package/src/codec/meta/Float.luau +6 -28
  36. package/src/codec/meta/Nothing.luau +1 -1
  37. package/src/codec/meta/Unknown.luau +10 -7
  38. package/src/codec/primitive/Bool.luau +6 -4
  39. package/src/codec/primitive/Float16.luau +5 -2
  40. package/src/codec/primitive/Int.luau +5 -6
  41. package/src/codec/primitive/Number.luau +6 -4
  42. package/src/codec/primitive/Signed.luau +2 -2
  43. package/src/codec/primitive/Varint.luau +76 -39
  44. package/src/codec/primitive/Zint.luau +99 -0
  45. package/src/index.d.ts +207 -53
  46. package/src/init.luau +116 -103
  47. package/src/internal/Baseline.luau +9 -1
  48. package/src/internal/Channel.luau +191 -157
  49. package/src/internal/Middleware.luau +22 -6
  50. package/src/internal/Pool.luau +12 -4
  51. package/src/internal/Registry.luau +25 -16
  52. package/src/internal/Transport.luau +1 -1
  53. package/src/transport/Bridge.luau +47 -33
  54. package/src/transport/Client.luau +25 -21
  55. package/src/transport/Gate.luau +227 -172
  56. package/src/transport/Reader.luau +174 -106
  57. package/src/transport/Server.luau +46 -57
  58. package/src/util/Array.luau +18 -0
  59. package/src/util/Buffer.luau +90 -0
  60. package/src/util/Constants.luau +30 -0
  61. package/src/util/Log.luau +68 -0
  62. package/src/util/Player.luau +14 -0
  63. package/src/util/Quantize.luau +84 -0
  64. package/src/util/Quat.luau +124 -0
  65. package/src/internal/Util.luau +0 -26
@@ -1,10 +1,11 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Named player sets with __iter, auto-cleanup on PlayerRemoving.
3
+ -- Named player sets with __iter and auto-cleanup on PlayerRemoving.
4
4
 
5
5
  local Players = game:GetService("Players")
6
6
 
7
- local Util = require(script.Parent.Parent.internal.Util)
7
+ local Array = require(script.Parent.Parent.util.Array)
8
+ local Log = require(script.Parent.Parent.util.Log)
8
9
 
9
10
  -- Public types -----------------------------------------------------------
10
11
 
@@ -14,22 +15,24 @@ export type GroupHandle = {
14
15
  has: (self: GroupHandle, player: Player) -> boolean,
15
16
  count: (self: GroupHandle) -> number,
16
17
  destroy: (self: GroupHandle) -> (),
17
- _lyncGroup: boolean,
18
+ _lyncKind: "group",
18
19
  _members: { [Player]: boolean },
19
20
  }
20
21
 
21
- type Group = GroupHandle & { _count: number, _name: string }
22
+ type GroupInternal = GroupHandle & { _count: number, _name: string }
22
23
 
23
24
  -- State ------------------------------------------------------------------
24
25
 
25
- local _groups: { [string]: Group } = {}
26
+ local _groups: { [string]: GroupInternal } = {}
27
+ -- Reverse index used by PlayerRemoving so we don't scan every group on disconnect.
26
28
  local _playerCleanup: { [Player]: { Group } } = {}
27
29
  local _cleanupConnected = false
28
30
 
29
31
  -- Private ----------------------------------------------------------------
30
32
 
31
- local swapRemove = Util.swapRemove
33
+ local swapRemove = Array.swapRemove
32
34
 
35
+ -- Connect once on the first Group.create; subsequent creates reuse the same connection.
33
36
  local function ensureCleanup(): ()
34
37
  if _cleanupConnected then
35
38
  return
@@ -49,6 +52,17 @@ local function ensureCleanup(): ()
49
52
  end)
50
53
  end
51
54
 
55
+ local function untrackCleanup(player: Player, group: GroupInternal): ()
56
+ local cleanup = _playerCleanup[player]
57
+ if not cleanup then
58
+ return
59
+ end
60
+ local idx = table.find(cleanup, group)
61
+ if idx then
62
+ swapRemove(cleanup, idx)
63
+ end
64
+ end
65
+
52
66
  -- Public -----------------------------------------------------------------
53
67
 
54
68
  local Group = {}
@@ -56,15 +70,12 @@ local Group = {}
56
70
  local GroupMeta = {}
57
71
  GroupMeta.__index = GroupMeta
58
72
 
59
- --[[
60
- __iter returns the standard 3-tuple iterator so `for player in group do`
61
- uses Luau's builtin path (luaH_next) with zero closure allocation.
62
- ]]
63
- function GroupMeta.__iter(self: Group): (any, any, any)
73
+ -- __iter lets `for player in group do` skip the explicit :members() call.
74
+ function GroupMeta.__iter(self: GroupInternal): (any, any, any)
64
75
  return next, self._members, nil
65
76
  end
66
77
 
67
- function GroupMeta.add(self: Group, player: Player): boolean
78
+ function GroupMeta.add(self: GroupInternal, player: Player): boolean
68
79
  if self._members[player] then
69
80
  return false
70
81
  end
@@ -81,45 +92,32 @@ function GroupMeta.add(self: Group, player: Player): boolean
81
92
  return true
82
93
  end
83
94
 
84
- function GroupMeta.remove(self: Group, player: Player): boolean
95
+ function GroupMeta.remove(self: GroupInternal, player: Player): boolean
85
96
  if not self._members[player] then
86
97
  return false
87
98
  end
88
99
 
89
100
  self._members[player] = nil
90
101
  self._count -= 1
91
-
92
- local cleanup = _playerCleanup[player]
93
- if cleanup then
94
- local idx = table.find(cleanup, self)
95
- if idx then
96
- swapRemove(cleanup, idx)
97
- end
98
- end
102
+ untrackCleanup(player, self)
99
103
  return true
100
104
  end
101
105
 
102
- function GroupMeta.has(self: Group, player: Player): boolean
106
+ function GroupMeta.has(self: GroupInternal, player: Player): boolean
103
107
  return self._members[player] == true
104
108
  end
105
109
 
106
- function GroupMeta.count(self: Group): number
110
+ function GroupMeta.count(self: GroupInternal): number
107
111
  return self._count
108
112
  end
109
113
 
110
- function GroupMeta.destroy(self: Group): ()
114
+ function GroupMeta.destroy(self: GroupInternal): ()
111
115
  if _groups[self._name] == self then
112
116
  _groups[self._name] = nil
113
117
  end
114
118
 
115
119
  for player in self._members do
116
- local cleanup = _playerCleanup[player]
117
- if cleanup then
118
- local idx = table.find(cleanup, self)
119
- if idx then
120
- swapRemove(cleanup, idx)
121
- end
122
- end
120
+ untrackCleanup(player, self)
123
121
  end
124
122
 
125
123
  table.clear(self._members)
@@ -130,13 +128,13 @@ table.freeze(GroupMeta)
130
128
 
131
129
  function Group.create(name: string): GroupHandle
132
130
  if _groups[name] then
133
- error(`[Lync] Lync.group: "{name}" already exists`)
131
+ Log.error(`"{name}" already exists`)
134
132
  end
135
133
 
136
134
  ensureCleanup()
137
135
 
138
136
  local group = setmetatable({
139
- _lyncGroup = true,
137
+ _lyncKind = "group" :: "group",
140
138
  _members = {} :: { [Player]: boolean },
141
139
  _count = 0,
142
140
  _name = name,
@@ -146,4 +144,13 @@ function Group.create(name: string): GroupHandle
146
144
  return group
147
145
  end
148
146
 
147
+ function Group.reset(): ()
148
+ for _, group in _groups do
149
+ table.clear(group._members)
150
+ group._count = 0
151
+ end
152
+ table.clear(_groups)
153
+ table.clear(_playerCleanup)
154
+ end
155
+
149
156
  return table.freeze(Group)
@@ -7,34 +7,38 @@ local RunService = game:GetService("RunService")
7
7
 
8
8
  local Baseline = require(script.Parent.Parent.internal.Baseline)
9
9
  local Channel = require(script.Parent.Parent.internal.Channel)
10
+ local Log = require(script.Parent.Parent.util.Log)
10
11
  local Middleware = require(script.Parent.Parent.internal.Middleware)
12
+ local Player = require(script.Parent.Parent.util.Player)
11
13
  local Registry = require(script.Parent.Parent.internal.Registry)
14
+ local Shared = require(script.Parent.Parent.codec.composite.Shared)
12
15
  local Transport = require(script.Parent.Parent.internal.Transport)
13
16
  local Types = require(script.Parent.Parent.Types)
14
- local Util = require(script.Parent.Parent.internal.Util)
15
17
 
16
18
  -- Constants --------------------------------------------------------------
17
19
 
18
20
  local IS_SERVER = RunService:IsServer()
19
21
 
22
+ -- mode: TS_* tag; headerSize: 1 (frame byte) + timestamp bytes (1, 2, or 8).
23
+ local TS_MODES: { [string]: { mode: number, headerSize: number } } = table.freeze({
24
+ frame = table.freeze({ mode = Channel.TS_FRAME, headerSize = 2 }),
25
+ offset = table.freeze({ mode = Channel.TS_OFFSET, headerSize = 3 }),
26
+ full = table.freeze({ mode = Channel.TS_FULL, headerSize = 9 }),
27
+ })
28
+
29
+ local EMPTY_OPTIONS: Types.PacketOptions = table.freeze({})
30
+
20
31
  -- Private ----------------------------------------------------------------
21
32
 
22
- local isPlayer = Util.isPlayer
33
+ local isPlayer = Player.is
23
34
 
24
35
  local function resolveTimestampMode(ts: ("frame" | "offset" | "full")?): (number, number)
25
- if ts == "frame" then
26
- Channel.enableTimestamps()
27
- return Channel.TS_FRAME, 2
28
- end
29
- if ts == "offset" then
30
- Channel.enableTimestamps()
31
- return Channel.TS_OFFSET, 3
32
- end
33
- if ts == "full" then
34
- Channel.enableTimestamps()
35
- return Channel.TS_FULL, 9
36
+ if ts == nil then
37
+ return Channel.TS_NONE, 1
36
38
  end
37
- return Channel.TS_NONE, 1
39
+ local entry = TS_MODES[ts]
40
+ Channel.enableTimestamps()
41
+ return entry.mode, entry.headerSize
38
42
  end
39
43
 
40
44
  -- Public types -----------------------------------------------------------
@@ -63,17 +67,18 @@ function Packet.define<T>(
63
67
  codec: Types.InternalCodec<T>,
64
68
  options: Types.PacketOptions?
65
69
  ): PacketHandle<T>
66
- local opts = options or ({} :: Types.PacketOptions)
70
+ local opts = options or EMPTY_OPTIONS
67
71
  local isUnreliable = opts.unreliable or false
68
72
 
73
+ -- Delta + unreliable is unsafe: a dropped frame permanently desyncs the receiver baseline.
69
74
  if isUnreliable and codec._isDelta then
70
- error(`[Lync] Lync.packet: "{name}" cannot use delta codecs with unreliable transport`)
75
+ Log.error(
76
+ `"{name}" delta codecs need reliable transport (lost packets desync the baseline)`
77
+ )
71
78
  end
72
79
 
73
80
  if codec._hasUnknown and not opts.validate then
74
- warn(
75
- `[Lync] Lync.packet: "{name}" uses unknown codec without validate; data bypasses schema validation`
76
- )
81
+ Log.warn(`"{name}" uses unknown codec without validate; data bypasses schema validation`)
77
82
  end
78
83
 
79
84
  if codec._isDelta then
@@ -109,85 +114,154 @@ function Packet.define<T>(
109
114
  end
110
115
  end
111
116
 
112
- local function applySend(data: any, player: Player?): any?
117
+ --[[
118
+ Run the send middleware chain. Returns (dropped, value). A nil value
119
+ is a legitimate payload (Lync.nothing); only the explicit Lync.DROP
120
+ sentinel or a hook returning DROP triggers the drop branch.
121
+ ]]
122
+ local function applySend(data: any, player: Player?): (boolean, any)
113
123
  if not Middleware.hasSendHooks() then
114
- return data
124
+ return false, data
115
125
  end
116
-
117
126
  local sendData = Middleware.runSend(data, name, player)
118
- if typeof(sendData) == "table" and sendData._lyncDrop then
119
- return nil
127
+ if typeof(sendData) == "table" and sendData._lyncKind == "drop" then
128
+ return true, nil
120
129
  end
121
- return sendData
130
+ return false, sendData
122
131
  end
123
132
 
124
- local function sendFromServer(data: any, target: any): ()
125
- if target == nil then
126
- error(`[Lync] Packet.send: "{name}" server send requires a target`)
127
- end
133
+ -- Non-delta codecs are stateless wire-encoders, so broadcast can encode
134
+ -- once and bufCopy the payload to every player. Falls through if the
135
+ -- encoded payload includes ref indices (the indices would need rewriting
136
+ -- per channel, which no codec exposes).
137
+ local canEncodeOnce = not codec._isDelta and not codec._hasDelta
128
138
 
129
- if isPlayer(target) then
130
- local sendData = applySend(data, target :: Player)
131
- if sendData == nil then
132
- return
133
- end
134
- sendToPlayer(target :: Player, sendData)
139
+ local function broadcast(players: { Player }, data: any): ()
140
+ local count = #players
141
+ if count == 0 then
135
142
  bumpFires()
136
143
  return
137
144
  end
138
145
 
139
- if typeof(target) == "table" then
140
- local sendData = applySend(data, nil)
141
- if sendData == nil then
142
- return
143
- end
144
-
145
- if target._lyncAll then
146
- for _, player in Players:GetPlayers() do
147
- sendToPlayer(player, sendData)
146
+ if canEncodeOnce then
147
+ local scratch = Shared.acquireScratch()
148
+ scratch.currentPacket = name
149
+ codec.write(scratch, data)
150
+ scratch.currentPacket = nil
151
+ if scratch.refCount == 0 then
152
+ local payload = scratch.buff
153
+ local payloadLen = scratch.cursor
154
+ local server = Transport.server()
155
+ -- Channel.writeBatchEncoded is hot-swapped by enableStats; re-read per call.
156
+ local writeBatchEncoded = Channel.writeBatchEncoded
157
+ for i = 1, count do
158
+ writeBatchEncoded(
159
+ server.getChannel(players[i], isUnreliable),
160
+ reg,
161
+ payload,
162
+ payloadLen
163
+ )
148
164
  end
165
+ Shared.releaseScratch()
149
166
  bumpFires()
150
167
  return
151
168
  end
169
+ Shared.releaseScratch()
170
+ end
152
171
 
153
- if target._lyncExcept then
154
- local excluded = target._excluded :: { [Player]: boolean }
155
- for _, player in Players:GetPlayers() do
156
- if not excluded[player] then
157
- sendToPlayer(player, sendData)
158
- end
172
+ for i = 1, count do
173
+ sendToPlayer(players[i], data)
174
+ end
175
+ bumpFires()
176
+ end
177
+
178
+ --[[
179
+ Resolve a target table into a { Player } list. Returns nil for unknown
180
+ sentinel kinds; the caller errors with the original target type.
181
+ ]]
182
+ local function resolveAudience(target: any): { Player }?
183
+ local kind = target._lyncKind
184
+ if kind == "all" then
185
+ return Players:GetPlayers()
186
+ end
187
+
188
+ if kind == "except" then
189
+ local excluded = target._excluded :: { [Player]: boolean }
190
+ local all = Players:GetPlayers()
191
+ local list = table.create(#all)
192
+ local n = 0
193
+ for _, player in all do
194
+ if not excluded[player] then
195
+ n += 1
196
+ list[n] = player
159
197
  end
160
- bumpFires()
161
- return
162
198
  end
199
+ return list
200
+ end
163
201
 
164
- if target._lyncGroup then
165
- for player in target._members :: { [Player]: boolean } do
166
- sendToPlayer(player, sendData)
167
- end
168
- bumpFires()
169
- return
202
+ if kind == "group" then
203
+ local members = target._members :: { [Player]: boolean }
204
+ local list: { Player } = {}
205
+ local n = 0
206
+ for player in members do
207
+ n += 1
208
+ list[n] = player
170
209
  end
210
+ return list
211
+ end
171
212
 
213
+ if kind == nil then
214
+ -- Plain { Player } array. Validate entries on the way to a clean list.
215
+ local list: { Player } = {}
216
+ local n = 0
172
217
  for _, player in target do
173
218
  if isPlayer(player) then
174
- sendToPlayer(player :: Player, sendData)
219
+ n += 1
220
+ list[n] = player :: Player
175
221
  end
176
222
  end
223
+ return list
224
+ end
225
+
226
+ return nil
227
+ end
228
+
229
+ local function sendToTarget(data: any, target: any): ()
230
+ if isPlayer(target) then
231
+ local dropped, sendData = applySend(data, target :: Player)
232
+ if dropped then
233
+ return
234
+ end
235
+ sendToPlayer(target :: Player, sendData)
177
236
  bumpFires()
178
237
  return
179
238
  end
180
239
 
181
- error(
182
- `[Lync] Packet.send: "{name}" expected Player, table, Group, or Lync.all as target, got {typeof(
183
- target
184
- )}`
185
- )
240
+ if typeof(target) == "table" then
241
+ local dropped, sendData = applySend(data, nil)
242
+ if dropped then
243
+ return
244
+ end
245
+ local audience = resolveAudience(target)
246
+ if audience then
247
+ broadcast(audience, sendData)
248
+ return
249
+ end
250
+ end
251
+
252
+ Log.error(`"{name}" expected Player, table, Group, or Lync.all, got {typeof(target)}`)
253
+ end
254
+
255
+ local function sendFromServer(data: any, target: any): ()
256
+ if target == nil then
257
+ Log.error(`"{name}" server send requires a target`)
258
+ end
259
+ sendToTarget(data, target)
186
260
  end
187
261
 
188
262
  local function sendFromClient(data: any): ()
189
- local sendData = applySend(data, nil)
190
- if sendData == nil then
263
+ local dropped, sendData = applySend(data, nil)
264
+ if dropped then
191
265
  return
192
266
  end
193
267
  local ch = Transport.client().getChannel(isUnreliable)