@axpecter/lync 2.2.1 → 2.3.1

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 (62) hide show
  1. package/README.md +95 -143
  2. package/package.json +1 -1
  3. package/src/Types.luau +22 -16
  4. package/src/api/Group.luau +40 -33
  5. package/src/api/Packet.luau +68 -54
  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 +21 -14
  10. package/src/codec/composite/Array.luau +56 -134
  11. package/src/codec/composite/Map.luau +103 -72
  12. package/src/codec/composite/Optional.luau +6 -2
  13. package/src/codec/composite/Shared.luau +160 -69
  14. package/src/codec/composite/Struct.luau +283 -42
  15. package/src/codec/composite/Tagged.luau +13 -16
  16. package/src/codec/composite/Tuple.luau +21 -15
  17. package/src/codec/datatype/Buffer.luau +6 -4
  18. package/src/codec/datatype/CFrame.luau +56 -44
  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/Enum.luau +9 -9
  34. package/src/codec/meta/Float.luau +6 -28
  35. package/src/codec/meta/Nothing.luau +1 -1
  36. package/src/codec/meta/Unknown.luau +10 -7
  37. package/src/codec/primitive/Bool.luau +6 -4
  38. package/src/codec/primitive/Float16.luau +5 -2
  39. package/src/codec/primitive/Int.luau +5 -6
  40. package/src/codec/primitive/Number.luau +6 -4
  41. package/src/codec/primitive/Signed.luau +2 -2
  42. package/src/codec/primitive/Varint.luau +69 -38
  43. package/src/index.d.ts +161 -53
  44. package/src/init.luau +109 -103
  45. package/src/internal/Baseline.luau +9 -1
  46. package/src/internal/Channel.luau +153 -154
  47. package/src/internal/Middleware.luau +22 -6
  48. package/src/internal/Pool.luau +12 -4
  49. package/src/internal/Registry.luau +25 -16
  50. package/src/internal/Transport.luau +1 -1
  51. package/src/transport/Bridge.luau +47 -33
  52. package/src/transport/Client.luau +25 -21
  53. package/src/transport/Gate.luau +227 -172
  54. package/src/transport/Reader.luau +162 -105
  55. package/src/transport/Server.luau +46 -57
  56. package/src/util/Array.luau +18 -0
  57. package/src/util/Buffer.luau +92 -0
  58. package/src/util/Constants.luau +30 -0
  59. package/src/util/Log.luau +68 -0
  60. package/src/util/Player.luau +14 -0
  61. package/src/util/Quantize.luau +60 -0
  62. 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
@@ -1,8 +1,9 @@
1
1
  --!strict
2
- --!optimize 2
2
+ --!native
3
3
  -- String-keyed enum codec. Maps values to u8 indices at definition time.
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
  -- Public -----------------------------------------------------------------
@@ -12,11 +13,9 @@ local EnumCodec = {}
12
13
  function EnumCodec.define(...: string): Types.InternalCodec<string>
13
14
  local values = { ... }
14
15
  local count = #values
15
- if count == 0 then
16
- error("[Lync] Lync.enum: requires at least one value")
17
- end
16
+ Log.assert(count > 0, "requires at least one variant")
18
17
  if count > 256 then
19
- error("[Lync] Lync.enum: exceeds 256 variants")
18
+ Log.error(`exceeds 256 variants ({count})`)
20
19
  end
21
20
 
22
21
  local toIndex: { [string]: number } = {}
@@ -24,7 +23,7 @@ function EnumCodec.define(...: string): Types.InternalCodec<string>
24
23
  for i = 1, count do
25
24
  local val = values[i]
26
25
  if toIndex[val] then
27
- error(`[Lync] Lync.enum: duplicate value "{val}"`)
26
+ Log.error(`duplicate value "{val}"`)
28
27
  end
29
28
  toIndex[val] = i - 1
30
29
  toValue[i] = val
@@ -35,13 +34,14 @@ function EnumCodec.define(...: string): Types.InternalCodec<string>
35
34
  return Base.define(1, function(b: buffer, off: number, value: string): ()
36
35
  local idx = toIndex[value]
37
36
  if idx == nil then
38
- error(`[Lync] Enum.write: invalid value "{value}"`)
37
+ Log.error(`invalid value "{value}"`)
39
38
  end
40
39
  buffer.writeu8(b, off, idx)
41
40
  end, function(b: buffer, off: number): string
42
- local val = toValue[buffer.readu8(b, off) + 1]
41
+ local raw = buffer.readu8(b, off)
42
+ local val = toValue[raw + 1]
43
43
  if val == nil then
44
- error(`[Lync] Enum.read: invalid index`)
44
+ Log.error(`invalid index {raw}`)
45
45
  end
46
46
  return val
47
47
  end, { _typeCheck = "string", _isEnum = true, _enumValues = toIndex })
@@ -1,17 +1,15 @@
1
1
  --!strict
2
2
  --!native
3
- -- Fixed-point quantization codec. Maps a float range to u8/u16/u32.
3
+ -- Fixed-point float quantizer. Maps a float range to u8/u16/u32.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Quantize = require(script.Parent.Parent.Parent.util.Quantize)
6
7
  local Types = require(script.Parent.Parent.Parent.Types)
7
8
 
8
9
  -- Private ----------------------------------------------------------------
9
10
 
10
- local ceil = math.ceil
11
11
  local clamp = math.clamp
12
12
  local round = math.round
13
- local maxN = math.max
14
- local minN = math.min
15
13
 
16
14
  -- Public -----------------------------------------------------------------
17
15
 
@@ -22,33 +20,13 @@ function Float.float(
22
20
  rangeMax: number,
23
21
  precision: number
24
22
  ): Types.InternalCodec<number>
25
- if rangeMin > rangeMax then
26
- error(`[Lync] Lync.float: min ({rangeMin}) must be <= max ({rangeMax})`)
27
- end
28
- if precision <= 0 then
29
- error("[Lync] Lync.float: precision must be positive")
30
- end
31
-
32
- local delta = rangeMax - rangeMin
33
- if delta == 0 then
23
+ -- Degenerate range: emit nothing, decode to the constant.
24
+ if rangeMax - rangeMin == 0 then
34
25
  return Base.constant(rangeMin)
35
26
  end
36
27
 
37
- local maxInt = minN(0xFFFFFFFF, maxN(1, ceil(delta / precision)))
38
-
39
- local compBytes: number
40
- local wfn: (buffer, number, number) -> ()
41
- local rfn: (buffer, number) -> number
42
- if maxInt <= 0xFF then
43
- compBytes, wfn, rfn = 1, buffer.writeu8, buffer.readu8
44
- elseif maxInt <= 0xFFFF then
45
- compBytes, wfn, rfn = 2, buffer.writeu16, buffer.readu16
46
- else
47
- compBytes, wfn, rfn = 4, buffer.writeu32, buffer.readu32
48
- end
49
-
50
- local scale = maxInt / delta
51
- local invScale = delta / maxInt
28
+ local compBytes, wfn, rfn, scale, invScale, delta =
29
+ Quantize.setup("Lync.float", rangeMin, rangeMax, precision)
52
30
 
53
31
  return Base.define(compBytes, function(b: buffer, off: number, value: number): ()
54
32
  wfn(b, off, round(clamp(value - rangeMin, 0, delta) * scale))
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Zero-byte codec. Reads nil. For fire-and-forget signals.
3
+ -- 0-byte codec. Reads nil. For fire-and-forget signals.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
6
 
@@ -1,8 +1,9 @@
1
1
  --!strict
2
- --!optimize 2
3
- -- Bypasses buffer serialization. Uses Roblox remote sidecar refs.
2
+ --!native
3
+ -- Bypass codec. Stashes the value in the channel sidecar refs (no schema).
4
4
 
5
5
  local Channel = require(script.Parent.Parent.Parent.internal.Channel)
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 ----------------------------------------------------------------
@@ -15,20 +16,22 @@ local function writeUnknown(ch: Types.ChannelState, value: any): ()
15
16
  end
16
17
 
17
18
  local function readUnknown(src: buffer, pos: number, refs: { Instance }?): (any, number)
18
- if not refs then
19
- error("[Lync] Unknown.read: refs array is required")
19
+ local r = refs
20
+ if not r then
21
+ Log.error("refs array is required")
20
22
  end
21
23
  local idx = readu16(src, pos)
22
- if idx < 1 or idx > #refs then
23
- error(`[Lync] Unknown.read: ref index {idx} out of bounds`)
24
+ if idx < 1 or idx > #r then
25
+ Log.error(`ref index {idx} out of bounds`)
24
26
  end
25
- return refs[idx], 2
27
+ return r[idx], 2
26
28
  end
27
29
 
28
30
  -- Public -----------------------------------------------------------------
29
31
 
30
32
  local Unknown = {}
31
33
 
34
+ -- _hasUnknown propagates upward so containers warn when used without validate.
32
35
  Unknown.unknown = table.freeze({
33
36
  _hasUnknown = true,
34
37
  write = writeUnknown,
@@ -1,12 +1,10 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Single-byte boolean codec. _isBool flag enables struct/array bit packing.
3
+ -- 1-byte boolean codec. _isBool flag opts struct/array into bit packing.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
6
 
7
- -- Public -----------------------------------------------------------------
8
-
9
- local Bool = {}
7
+ -- Private ----------------------------------------------------------------
10
8
 
11
9
  local function writeBool(b: buffer, off: number, value: boolean): ()
12
10
  buffer.writeu8(b, off, if value then 1 else 0)
@@ -16,6 +14,10 @@ local function readBool(b: buffer, off: number): boolean
16
14
  return buffer.readu8(b, off) ~= 0
17
15
  end
18
16
 
17
+ -- Public -----------------------------------------------------------------
18
+
19
+ local Bool = {}
20
+
19
21
  Bool.bool = Base.define(1, writeBool, readBool, {
20
22
  _isBool = true,
21
23
  _typeCheck = "boolean",
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --!native
3
- -- IEEE 754 binary16 codec. 2 bytes, +/- 65504, ~3 decimal digits of precision.
3
+ -- IEEE 754 binary16 codec. 2 bytes, +/-65504 finite range, ~3 decimal digits.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
6
 
@@ -20,9 +20,10 @@ local frexp = math.frexp
20
20
  local ldexp = math.ldexp
21
21
  local round = math.round
22
22
  local huge = math.huge
23
+ local isnan = math.isnan
23
24
 
24
25
  local function encode(value: number): number
25
- if value ~= value then
26
+ if isnan(value) then
26
27
  return NAN_BITS
27
28
  end
28
29
  if value == 0 then
@@ -42,6 +43,7 @@ local function encode(value: number): number
42
43
  local mantissa, exponent = frexp(value)
43
44
  local biasedExp = exponent + 14
44
45
 
46
+ -- Subnormal: shift mantissa into the 10-bit fraction with the implicit bit gone.
45
47
  if biasedExp <= 0 then
46
48
  local frac = round(ldexp(mantissa, biasedExp + 10))
47
49
  if frac >= 1024 then
@@ -54,6 +56,7 @@ local function encode(value: number): number
54
56
  end
55
57
 
56
58
  local frac = round((mantissa * 2 - 1) * 1024)
59
+ -- Rounding can carry into the exponent; absorb it.
57
60
  if frac >= 1024 then
58
61
  frac = 0
59
62
  biasedExp += 1
@@ -1,8 +1,9 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Range-typed integer codec. Picks the narrowest u8/u16/u32/i8/i16/i32 wire form.
3
+ -- Range-typed integer codec. Picks the narrowest u8/u16/u32/i8/i16/i32 form.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
7
  local Signed = require(script.Parent.Signed)
7
8
  local Types = require(script.Parent.Parent.Parent.Types)
8
9
 
@@ -19,11 +20,9 @@ local Int = {}
19
20
 
20
21
  function Int.int(min: number, max: number): Types.InternalCodec<number>
21
22
  if min > max then
22
- error(`[Lync] Lync.int: min ({min}) must be <= max ({max})`)
23
- end
24
- if floor(min) ~= min or floor(max) ~= max then
25
- error("[Lync] Lync.int: bounds must be integers")
23
+ Log.error(`min ({min}) must be <= max ({max})`)
26
24
  end
25
+ Log.assert(floor(min) == min and floor(max) == max, "bounds must be integers")
27
26
 
28
27
  local meta = {
29
28
  _min = min,
@@ -54,7 +53,7 @@ function Int.int(min: number, max: number): Types.InternalCodec<number>
54
53
  end
55
54
  end
56
55
 
57
- error(`[Lync] Lync.int: range [{min}, {max}] exceeds i32 bounds`)
56
+ Log.error(`range [{min}, {max}] exceeds i32 bounds`)
58
57
  end
59
58
 
60
59
  return table.freeze(Int)
@@ -1,13 +1,15 @@
1
1
  --!strict
2
2
  --!optimize 2
3
- -- Explicit-precision float codecs: f32 and f64.
3
+ -- Explicit-precision float codecs: f32 (4B) and f64 (8B).
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
6
 
7
- -- Constants --------------------------------------------------------------
7
+ -- State ------------------------------------------------------------------
8
8
 
9
- -- Shared scratch for Auto codec's f32 round-trip test. Roblox is
10
- -- single-threaded so a module-level scratch cannot be aliased.
9
+ --[[
10
+ Roblox is single-threaded, so a module-level scratch is safe to alias.
11
+ Auto codec uses this to round-trip a float through f32 and check fidelity.
12
+ ]]
11
13
  local F32_SCRATCH = buffer.create(4)
12
14
 
13
15
  -- Public -----------------------------------------------------------------