@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
@@ -8,6 +8,7 @@ local CFrameC = require(script.Parent.Parent.datatype.CFrame)
8
8
  local Channel = require(script.Parent.Parent.Parent.internal.Channel)
9
9
  local ColorC = require(script.Parent.Parent.datatype.Color)
10
10
  local IVC = require(script.Parent.Parent.datatype.IntVector)
11
+ local Log = require(script.Parent.Parent.Parent.util.Log)
11
12
  local NRC = require(script.Parent.Parent.datatype.NumberRange)
12
13
  local NumberC = require(script.Parent.Parent.primitive.Number)
13
14
  local RayC = require(script.Parent.Parent.datatype.Ray)
@@ -53,6 +54,78 @@ local TAG_COLSEQ = 26
53
54
  local INLINE_MAX = Varint.INLINE_MAX
54
55
  local F32_SCRATCH = NumberC._f32Scratch
55
56
 
57
+ -- Tag -> numeric primitive dispatch tables.
58
+ local NUM_SIZE: { [number]: number } = table.freeze({
59
+ [TAG_U8] = 1,
60
+ [TAG_I8] = 1,
61
+ [TAG_U16] = 2,
62
+ [TAG_I16] = 2,
63
+ [TAG_U32] = 4,
64
+ [TAG_I32] = 4,
65
+ [TAG_F32] = 4,
66
+ [TAG_F64] = 8,
67
+ })
68
+
69
+ local NUM_WRITE: { [number]: (buffer, number, number) -> () } = table.freeze({
70
+ [TAG_U8] = buffer.writeu8,
71
+ [TAG_U16] = buffer.writeu16,
72
+ [TAG_U32] = buffer.writeu32,
73
+ [TAG_I8] = Signed.writeI8,
74
+ [TAG_I16] = Signed.writeI16,
75
+ [TAG_I32] = Signed.writeI32,
76
+ [TAG_F32] = buffer.writef32,
77
+ [TAG_F64] = buffer.writef64,
78
+ })
79
+
80
+ local NUM_READ: { [number]: (buffer, number) -> number } = table.freeze({
81
+ [TAG_U8] = buffer.readu8,
82
+ [TAG_U16] = buffer.readu16,
83
+ [TAG_U32] = buffer.readu32,
84
+ [TAG_I8] = buffer.readi8,
85
+ [TAG_I16] = buffer.readi16,
86
+ [TAG_I32] = buffer.readi32,
87
+ [TAG_F32] = buffer.readf32,
88
+ [TAG_F64] = buffer.readf64,
89
+ })
90
+
91
+ type TypeEntry = { tag: number, codec: Types.InternalCodec<any> }
92
+
93
+ local TYPE_DISPATCH: { [string]: TypeEntry } = table.freeze({
94
+ Vector2 = { tag = TAG_VEC2, codec = VecC.vec2 },
95
+ Vector3 = { tag = TAG_VEC3, codec = VecC.vec3 },
96
+ Color3 = { tag = TAG_COLOR3, codec = ColorC.color3 },
97
+ CFrame = { tag = TAG_CFRAME, codec = CFrameC.cframe },
98
+ UDim = { tag = TAG_UDIM, codec = UDimC.udim },
99
+ UDim2 = { tag = TAG_UDIM2, codec = UDimC.udim2 },
100
+ NumberRange = { tag = TAG_NUMRANGE, codec = NRC.numberRange },
101
+ Rect = { tag = TAG_RECT, codec = RectC.rect },
102
+ Vector2int16 = { tag = TAG_V2I16, codec = IVC.vec2int16 },
103
+ Vector3int16 = { tag = TAG_V3I16, codec = IVC.vec3int16 },
104
+ Region3 = { tag = TAG_REGION3, codec = RegionC.region3 },
105
+ Region3int16 = { tag = TAG_R3I16, codec = RegionC.region3int16 },
106
+ Ray = { tag = TAG_RAY, codec = RayC.ray },
107
+ NumberSequence = { tag = TAG_NUMSEQ, codec = SeqC.numberSequence },
108
+ ColorSequence = { tag = TAG_COLSEQ, codec = SeqC.colorSequence },
109
+ })
110
+
111
+ local READ_DISPATCH: { [number]: Types.InternalCodec<any> } = table.freeze({
112
+ [TAG_VEC2] = VecC.vec2,
113
+ [TAG_VEC3] = VecC.vec3,
114
+ [TAG_COLOR3] = ColorC.color3,
115
+ [TAG_CFRAME] = CFrameC.cframe,
116
+ [TAG_UDIM] = UDimC.udim,
117
+ [TAG_UDIM2] = UDimC.udim2,
118
+ [TAG_NUMRANGE] = NRC.numberRange,
119
+ [TAG_RECT] = RectC.rect,
120
+ [TAG_V2I16] = IVC.vec2int16,
121
+ [TAG_V3I16] = IVC.vec3int16,
122
+ [TAG_REGION3] = RegionC.region3,
123
+ [TAG_R3I16] = RegionC.region3int16,
124
+ [TAG_RAY] = RayC.ray,
125
+ [TAG_NUMSEQ] = SeqC.numberSequence,
126
+ [TAG_COLSEQ] = SeqC.colorSequence,
127
+ })
128
+
56
129
  -- Private ----------------------------------------------------------------
57
130
 
58
131
  local alloc = Base.alloc
@@ -62,10 +135,12 @@ local readu8 = buffer.readu8
62
135
  local floor = math.floor
63
136
  local isnan = math.isnan
64
137
 
65
- local writeI8 = Signed.writeI8
66
- local writeI16 = Signed.writeI16
67
- local writeI32 = Signed.writeI32
68
-
138
+ --[[
139
+ Pick the narrowest tag for a number. NaN forces F64 (no integer or F32
140
+ can represent it). Integers walk u8 -> u16 -> u32 / i8 -> i16 -> i32; non-
141
+ integers try f32 by round-tripping through scratch (see Number.luau) and
142
+ fall through to f64 if precision is lost.
143
+ ]]
69
144
  local function selectNumberTag(value: number): number
70
145
  if isnan(value) then
71
146
  return TAG_F64
@@ -95,7 +170,6 @@ local function selectNumberTag(value: number): number
95
170
  end
96
171
  end
97
172
 
98
- -- Roblox is single-threaded so a module scratch cannot be aliased.
99
173
  buffer.writef32(F32_SCRATCH, 0, value)
100
174
  if buffer.readf32(F32_SCRATCH, 0) == value then
101
175
  return TAG_F32
@@ -103,114 +177,6 @@ local function selectNumberTag(value: number): number
103
177
  return TAG_F64
104
178
  end
105
179
 
106
- local function numberSize(tag: number): number
107
- if tag == TAG_U8 or tag == TAG_I8 then
108
- return 1
109
- end
110
- if tag == TAG_U16 or tag == TAG_I16 then
111
- return 2
112
- end
113
- if tag == TAG_U32 or tag == TAG_I32 or tag == TAG_F32 then
114
- return 4
115
- end
116
- return 8
117
- end
118
-
119
- local function writeNumber(b: buffer, off: number, tag: number, value: number): ()
120
- if tag == TAG_U8 then
121
- buffer.writeu8(b, off, value)
122
- return
123
- end
124
- if tag == TAG_U16 then
125
- buffer.writeu16(b, off, value)
126
- return
127
- end
128
- if tag == TAG_U32 then
129
- buffer.writeu32(b, off, value)
130
- return
131
- end
132
- if tag == TAG_I8 then
133
- writeI8(b, off, value)
134
- return
135
- end
136
- if tag == TAG_I16 then
137
- writeI16(b, off, value)
138
- return
139
- end
140
- if tag == TAG_I32 then
141
- writeI32(b, off, value)
142
- return
143
- end
144
- if tag == TAG_F32 then
145
- buffer.writef32(b, off, value)
146
- return
147
- end
148
- buffer.writef64(b, off, value)
149
- end
150
-
151
- local function readNumber(b: buffer, off: number, tag: number): number
152
- if tag == TAG_U8 then
153
- return buffer.readu8(b, off)
154
- end
155
- if tag == TAG_U16 then
156
- return buffer.readu16(b, off)
157
- end
158
- if tag == TAG_U32 then
159
- return buffer.readu32(b, off)
160
- end
161
- if tag == TAG_I8 then
162
- return buffer.readi8(b, off)
163
- end
164
- if tag == TAG_I16 then
165
- return buffer.readi16(b, off)
166
- end
167
- if tag == TAG_I32 then
168
- return buffer.readi32(b, off)
169
- end
170
- if tag == TAG_F32 then
171
- return buffer.readf32(b, off)
172
- end
173
- return buffer.readf64(b, off)
174
- end
175
-
176
- type TypeEntry = { tag: number, codec: Types.InternalCodec<any> }
177
-
178
- local TYPE_DISPATCH: { [string]: TypeEntry } = table.freeze({
179
- Vector2 = { tag = TAG_VEC2, codec = VecC.vec2 },
180
- Vector3 = { tag = TAG_VEC3, codec = VecC.vec3 },
181
- Color3 = { tag = TAG_COLOR3, codec = ColorC.color3 },
182
- CFrame = { tag = TAG_CFRAME, codec = CFrameC.cframe },
183
- UDim = { tag = TAG_UDIM, codec = UDimC.udim },
184
- UDim2 = { tag = TAG_UDIM2, codec = UDimC.udim2 },
185
- NumberRange = { tag = TAG_NUMRANGE, codec = NRC.numberRange },
186
- Rect = { tag = TAG_RECT, codec = RectC.rect },
187
- Vector2int16 = { tag = TAG_V2I16, codec = IVC.vec2int16 },
188
- Vector3int16 = { tag = TAG_V3I16, codec = IVC.vec3int16 },
189
- Region3 = { tag = TAG_REGION3, codec = RegionC.region3 },
190
- Region3int16 = { tag = TAG_R3I16, codec = RegionC.region3int16 },
191
- Ray = { tag = TAG_RAY, codec = RayC.ray },
192
- NumberSequence = { tag = TAG_NUMSEQ, codec = SeqC.numberSequence },
193
- ColorSequence = { tag = TAG_COLSEQ, codec = SeqC.colorSequence },
194
- })
195
-
196
- local READ_DISPATCH: { [number]: Types.InternalCodec<any> } = table.freeze({
197
- [TAG_VEC2] = VecC.vec2,
198
- [TAG_VEC3] = VecC.vec3,
199
- [TAG_COLOR3] = ColorC.color3,
200
- [TAG_CFRAME] = CFrameC.cframe,
201
- [TAG_UDIM] = UDimC.udim,
202
- [TAG_UDIM2] = UDimC.udim2,
203
- [TAG_NUMRANGE] = NRC.numberRange,
204
- [TAG_RECT] = RectC.rect,
205
- [TAG_V2I16] = IVC.vec2int16,
206
- [TAG_V3I16] = IVC.vec3int16,
207
- [TAG_REGION3] = RegionC.region3,
208
- [TAG_R3I16] = RegionC.region3int16,
209
- [TAG_RAY] = RayC.ray,
210
- [TAG_NUMSEQ] = SeqC.numberSequence,
211
- [TAG_COLSEQ] = SeqC.colorSequence,
212
- })
213
-
214
180
  local function writeAuto(ch: Types.ChannelState, value: any): ()
215
181
  if value == nil then
216
182
  writeByte(ch, TAG_NIL)
@@ -233,15 +199,16 @@ local function writeAuto(ch: Types.ChannelState, value: any): ()
233
199
 
234
200
  if t == "number" then
235
201
  local tag = selectNumberTag(value)
236
- local totalSize = 1 + numberSize(tag)
202
+ local size = NUM_SIZE[tag]
203
+ local total = 1 + size
237
204
  local cursor = ch.cursor
238
- if cursor + totalSize > ch.size then
239
- alloc(ch, totalSize)
205
+ if cursor + total > ch.size then
206
+ alloc(ch, total)
240
207
  end
241
208
  local b = ch.buff
242
209
  writeu8(b, cursor, tag)
243
- writeNumber(b, cursor + 1, tag, value)
244
- ch.cursor = cursor + totalSize
210
+ NUM_WRITE[tag](b, cursor + 1, value)
211
+ ch.cursor = cursor + total
245
212
  return
246
213
  end
247
214
 
@@ -250,6 +217,7 @@ local function writeAuto(ch: Types.ChannelState, value: any): ()
250
217
 
251
218
  local len = #value
252
219
  local cursor = ch.cursor
220
+ -- Inline single-byte length fast path; matches String.luau.
253
221
  if len <= INLINE_MAX then
254
222
  if cursor + 1 + len > ch.size then
255
223
  alloc(ch, 1 + len)
@@ -280,22 +248,22 @@ local function writeAuto(ch: Types.ChannelState, value: any): ()
280
248
 
281
249
  local entry = TYPE_DISPATCH[t]
282
250
  if not entry then
283
- error(`[Lync] Auto.write: unsupported type "{t}"`)
251
+ Log.error(`unsupported type "{t}"`)
284
252
  end
285
-
286
253
  local codec = entry.codec
287
254
  local size = codec._size
288
255
  local dw = codec._directWrite
256
+ -- Fixed-size + direct writer: single alloc + tag + payload.
289
257
  if size and dw then
290
- local totalSize = 1 + size
258
+ local total = 1 + size
291
259
  local cursor = ch.cursor
292
- if cursor + totalSize > ch.size then
293
- alloc(ch, totalSize)
260
+ if cursor + total > ch.size then
261
+ alloc(ch, total)
294
262
  end
295
263
  local b = ch.buff
296
264
  writeu8(b, cursor, entry.tag)
297
265
  dw(b, cursor + 1, value)
298
- ch.cursor = cursor + totalSize
266
+ ch.cursor = cursor + total
299
267
  return
300
268
  end
301
269
 
@@ -314,7 +282,7 @@ local function readAuto(src: buffer, pos: number, refs: { Instance }?): (any, nu
314
282
  end
315
283
 
316
284
  if tag >= TAG_U8 and tag <= TAG_F64 then
317
- return readNumber(src, pos + 1, tag), 1 + numberSize(tag)
285
+ return NUM_READ[tag](src, pos + 1), 1 + NUM_SIZE[tag]
318
286
  end
319
287
 
320
288
  if tag == TAG_STRING then
@@ -332,7 +300,7 @@ local function readAuto(src: buffer, pos: number, refs: { Instance }?): (any, nu
332
300
 
333
301
  local codec = READ_DISPATCH[tag]
334
302
  if not codec then
335
- error(`[Lync] Auto.read: unknown tag {tag}`)
303
+ Log.error(`unknown tag {tag}`)
336
304
  end
337
305
  local value, consumed = codec.read(src, pos + 1, refs)
338
306
  return value, 1 + consumed
@@ -1,8 +1,9 @@
1
1
  --!strict
2
2
  --!native
3
- -- Sub-byte packing. 1 to 32 bits across bool / uint / int fields.
3
+ -- Sub-byte packed bool/uint/int fields. Up to 32 bits total per record.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
 
8
9
  -- Constants --------------------------------------------------------------
@@ -18,12 +19,7 @@ local bor = bit32.bor
18
19
  local lshift = bit32.lshift
19
20
  local rshift = bit32.rshift
20
21
 
21
- --[[
22
- Width-aware mask: bit32.lshift(1, 32) returns 0, so 32-bit fields
23
- need the all-ones mask explicitly. Sign-extension constants for int
24
- fields use 2^(width-1) and 2^width via numeric exponentiation so
25
- width=32 still produces 0x80000000 / 0x100000000 correctly.
26
- ]]
22
+ -- bit32.lshift(1, 32) wraps to 0; force all-ones in the 32-bit case.
27
23
  local function widthMask(width: number): number
28
24
  if width >= 32 then
29
25
  return 0xFFFFFFFF
@@ -38,6 +34,7 @@ local Bitfield = {}
38
34
  export type FieldSpec = { type: "bool" | "uint" | "int", width: number? }
39
35
 
40
36
  function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<any>
37
+ -- Sort field names so bit positions are stable across runs.
41
38
  local keys: { string } = {}
42
39
  for key in schema do
43
40
  table.insert(keys, key)
@@ -45,9 +42,7 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
45
42
  table.sort(keys)
46
43
 
47
44
  local fieldCount = #keys
48
- if fieldCount == 0 then
49
- error("[Lync] Lync.bitfield: requires at least one field")
50
- end
45
+ Log.assert(fieldCount > 0, "requires at least one field")
51
46
 
52
47
  local fieldKeys = table.create(fieldCount)
53
48
  local fieldTypes = table.create(fieldCount)
@@ -72,7 +67,7 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
72
67
  elseif spec.type == "uint" then
73
68
  local width = spec.width
74
69
  if not width or width < 1 or width > 32 then
75
- error(`[Lync] Lync.bitfield: field "{key}" uint width must be 1-32`)
70
+ Log.error(`field "{key}" uint width must be 1-32`)
76
71
  end
77
72
  fieldTypes[i] = TYPE_UINT
78
73
  fieldMasks[i] = widthMask(width)
@@ -81,8 +76,9 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
81
76
  totalBits += width
82
77
  elseif spec.type == "int" then
83
78
  local width = spec.width
79
+ -- Min int width is 2: 1 bit can't represent both a sign and a value.
84
80
  if not width or width < 2 or width > 32 then
85
- error(`[Lync] Lync.bitfield: field "{key}" int width must be 2-32`)
81
+ Log.error(`field "{key}" int width must be 2-32`)
86
82
  end
87
83
  fieldTypes[i] = TYPE_INT
88
84
  fieldMasks[i] = widthMask(width)
@@ -90,14 +86,15 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
90
86
  fieldSignExt[i] = 2 ^ width
91
87
  totalBits += width
92
88
  else
93
- error(`[Lync] Lync.bitfield: field "{key}" has unknown type "{spec.type}"`)
89
+ Log.error(`field "{key}" has unknown type "{spec.type}"`)
94
90
  end
95
91
  end
96
92
 
97
93
  if totalBits > 32 then
98
- error(`[Lync] Lync.bitfield: total exceeds 32 bits ({totalBits})`)
94
+ Log.error(`total exceeds 32 bits ({totalBits})`)
99
95
  end
100
96
 
97
+ -- Pick the narrowest u8/u16/u32 wire form.
101
98
  local wireBytes: number
102
99
  local wfn: (buffer, number, number) -> ()
103
100
  local rfn: (buffer, number) -> number
@@ -137,6 +134,7 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
137
134
  if fieldTypes[i] == TYPE_BOOL then
138
135
  result[fieldKeys[i]] = raw ~= 0
139
136
  elseif fieldTypes[i] == TYPE_INT then
137
+ -- Sign-extend: subtract 2^width when the sign bit is set.
140
138
  result[fieldKeys[i]] = if raw >= fieldSignBit[i] then raw - fieldSignExt[i] else raw
141
139
  else
142
140
  result[fieldKeys[i]] = raw
@@ -3,6 +3,7 @@
3
3
  -- User-defined fixed-size codec.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
 
8
9
  -- Private ----------------------------------------------------------------
@@ -20,8 +21,9 @@ function Custom.define<T>(
20
21
  typeCheck: string?
21
22
  ): Types.InternalCodec<T>
22
23
  if type(size) ~= "number" or size < 0 or floor(size) ~= size then
23
- error("[Lync] Lync.custom: size must be a non-negative integer")
24
+ Log.error(`size must be a non-negative integer, got {size}`)
24
25
  end
26
+ -- size == 0 collapses to constant(nil): user has no payload to encode.
25
27
  if size == 0 then
26
28
  return Base.constant(nil :: any) :: Types.InternalCodec<T>
27
29
  end