@axpecter/lync 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +241 -349
  2. package/package.json +1 -1
  3. package/src/Types.luau +53 -13
  4. package/src/api/Group.luau +41 -51
  5. package/src/api/Packet.luau +145 -143
  6. package/src/api/Query.luau +204 -202
  7. package/src/api/Scope.luau +36 -38
  8. package/src/api/Signal.luau +68 -94
  9. package/src/codec/Base.luau +43 -23
  10. package/src/codec/composite/Array.luau +161 -152
  11. package/src/codec/composite/Map.luau +37 -53
  12. package/src/codec/composite/Optional.luau +15 -21
  13. package/src/codec/composite/Shared.luau +78 -41
  14. package/src/codec/composite/Struct.luau +64 -125
  15. package/src/codec/composite/Tagged.luau +15 -25
  16. package/src/codec/composite/Tuple.luau +15 -20
  17. package/src/codec/datatype/Buffer.luau +44 -51
  18. package/src/codec/datatype/CFrame.luau +72 -104
  19. package/src/codec/datatype/Color.luau +14 -10
  20. package/src/codec/datatype/Instance.luau +32 -42
  21. package/src/codec/datatype/IntVector.luau +24 -22
  22. package/src/codec/datatype/NumberRange.luau +21 -12
  23. package/src/codec/datatype/Ray.luau +13 -7
  24. package/src/codec/datatype/Rect.luau +13 -7
  25. package/src/codec/datatype/Region.luau +25 -22
  26. package/src/codec/datatype/Sequence.luau +144 -118
  27. package/src/codec/datatype/String.luau +29 -59
  28. package/src/codec/datatype/UDim.luau +29 -30
  29. package/src/codec/datatype/Vector.luau +89 -105
  30. package/src/codec/meta/Auto.luau +194 -246
  31. package/src/codec/meta/Bitfield.luau +37 -36
  32. package/src/codec/meta/Custom.luau +10 -10
  33. package/src/codec/meta/Enum.luau +8 -11
  34. package/src/codec/meta/Float.luau +16 -20
  35. package/src/codec/meta/Nothing.luau +2 -2
  36. package/src/codec/meta/Unknown.luau +22 -32
  37. package/src/codec/primitive/Bool.luau +9 -5
  38. package/src/codec/primitive/Float16.luau +13 -23
  39. package/src/codec/primitive/Int.luau +20 -28
  40. package/src/codec/primitive/Number.luau +6 -10
  41. package/src/codec/primitive/Signed.luau +32 -0
  42. package/src/codec/primitive/Varint.luau +59 -28
  43. package/src/index.d.ts +8 -2
  44. package/src/init.luau +118 -143
  45. package/src/internal/Baseline.luau +17 -18
  46. package/src/internal/Channel.luau +143 -212
  47. package/src/internal/Middleware.luau +37 -47
  48. package/src/internal/Pool.luau +10 -10
  49. package/src/internal/Registry.luau +38 -34
  50. package/src/internal/Transport.luau +28 -0
  51. package/src/internal/Util.luau +26 -0
  52. package/src/transport/Bridge.luau +32 -24
  53. package/src/transport/Client.luau +51 -53
  54. package/src/transport/Gate.luau +183 -113
  55. package/src/transport/Reader.luau +95 -66
  56. package/src/transport/Server.luau +130 -125
@@ -3,11 +3,10 @@
3
3
  -- Self-describing codec. u8 type tag + value payload.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
- local BoolC = require(script.Parent.Parent.primitive.Bool)
7
6
  local BufferC = require(script.Parent.Parent.datatype.Buffer)
8
7
  local CFrameC = require(script.Parent.Parent.datatype.CFrame)
8
+ local Channel = require(script.Parent.Parent.Parent.internal.Channel)
9
9
  local ColorC = require(script.Parent.Parent.datatype.Color)
10
- local Float16C = require(script.Parent.Parent.primitive.Float16)
11
10
  local IVC = require(script.Parent.Parent.datatype.IntVector)
12
11
  local NRC = require(script.Parent.Parent.datatype.NumberRange)
13
12
  local NumberC = require(script.Parent.Parent.primitive.Number)
@@ -15,12 +14,13 @@ local RayC = require(script.Parent.Parent.datatype.Ray)
15
14
  local RectC = require(script.Parent.Parent.datatype.Rect)
16
15
  local RegionC = require(script.Parent.Parent.datatype.Region)
17
16
  local SeqC = require(script.Parent.Parent.datatype.Sequence)
17
+ local Signed = require(script.Parent.Parent.primitive.Signed)
18
18
  local Types = require(script.Parent.Parent.Parent.Types)
19
19
  local UDimC = require(script.Parent.Parent.datatype.UDim)
20
20
  local Varint = require(script.Parent.Parent.primitive.Varint)
21
21
  local VecC = require(script.Parent.Parent.datatype.Vector)
22
22
 
23
- -- Constants -----------------------------------------------------------
23
+ -- Constants --------------------------------------------------------------
24
24
 
25
25
  local TAG_NIL = 0
26
26
  local TAG_BOOL = 1
@@ -51,68 +51,26 @@ local TAG_NUMSEQ = 25
51
51
  local TAG_COLSEQ = 26
52
52
 
53
53
  local INLINE_MAX = Varint.INLINE_MAX
54
+ local F32_SCRATCH = NumberC._f32Scratch
54
55
 
55
- -- Private -------------------------------------------------------------
56
+ -- Private ----------------------------------------------------------------
56
57
 
57
58
  local alloc = Base.alloc
59
+ local writeByte = Channel.writeByte
58
60
  local writeu8 = buffer.writeu8
59
61
  local readu8 = buffer.readu8
60
62
  local floor = math.floor
61
- local abs = math.abs
62
-
63
- local F32_SCRATCH = NumberC._f32Scratch
63
+ local isnan = math.isnan
64
64
 
65
- --[[
66
- Write dispatch tables: tag → { directWrite, payloadSize }.
67
- Used for integer/float types where the codec has _directWrite + _size.
68
- ]]
69
- local NUM_WRITE: { [number]: (buffer, number, number) -> () } = {
70
- [TAG_U8] = buffer.writeu8,
71
- [TAG_U16] = buffer.writeu16,
72
- [TAG_U32] = buffer.writeu32,
73
- [TAG_F32] = buffer.writef32,
74
- [TAG_F64] = buffer.writef64,
75
- }
76
-
77
- local NUM_SIZE: { [number]: number } = {
78
- [TAG_U8] = 1,
79
- [TAG_U16] = 2,
80
- [TAG_U32] = 4,
81
- [TAG_F32] = 4,
82
- [TAG_F64] = 8,
83
- }
84
-
85
- -- writei8/i16/i32 are NOT FASTCALL, so we use writeu* with two's complement conversion
86
- local function writeI8(b: buffer, off: number, v: number): ()
87
- writeu8(b, off, if v < 0 then v + 0x100 else v)
88
- end
89
-
90
- local function writeI16(b: buffer, off: number, v: number): ()
91
- buffer.writeu16(b, off, if v < 0 then v + 0x10000 else v)
92
- end
65
+ local writeI8 = Signed.writeI8
66
+ local writeI16 = Signed.writeI16
67
+ local writeI32 = Signed.writeI32
93
68
 
94
- local function writeI32(b: buffer, off: number, v: number): ()
95
- buffer.writeu32(b, off, if v < 0 then v + 0x100000000 else v)
96
- end
97
-
98
- NUM_WRITE[TAG_I8] = writeI8
99
- NUM_WRITE[TAG_I16] = writeI16
100
- NUM_WRITE[TAG_I32] = writeI32
101
- NUM_SIZE[TAG_I8] = 1
102
- NUM_SIZE[TAG_I16] = 2
103
- NUM_SIZE[TAG_I32] = 4
104
-
105
- table.freeze(NUM_WRITE)
106
- table.freeze(NUM_SIZE)
107
-
108
- -- Selects the smallest integer/float tag for a number value
109
69
  local function selectNumberTag(value: number): number
110
- -- NaN always f64
111
- if value ~= value then
70
+ if isnan(value) then
112
71
  return TAG_F64
113
72
  end
114
73
 
115
- -- Integer path
116
74
  if floor(value) == value then
117
75
  if value >= 0 then
118
76
  if value <= 0xFF then
@@ -137,23 +95,85 @@ local function selectNumberTag(value: number): number
137
95
  end
138
96
  end
139
97
 
140
- -- Float path: try f32 round-trip
98
+ -- Roblox is single-threaded so a module scratch cannot be aliased.
141
99
  buffer.writef32(F32_SCRATCH, 0, value)
142
100
  if buffer.readf32(F32_SCRATCH, 0) == value then
143
101
  return TAG_F32
144
102
  end
145
-
146
103
  return TAG_F64
147
104
  end
148
105
 
149
- --[[
150
- Roblox type dispatch: typeof string { tag, codec }.
151
- For types with _directWrite + _size, we use the fast path.
152
- ]]
153
- type TypeEntry = {
154
- tag: number,
155
- codec: Types.InternalCodec<any>,
156
- }
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> }
157
177
 
158
178
  local TYPE_DISPATCH: { [string]: TypeEntry } = table.freeze({
159
179
  Vector2 = { tag = TAG_VEC2, codec = VecC.vec2 },
@@ -171,9 +191,8 @@ local TYPE_DISPATCH: { [string]: TypeEntry } = table.freeze({
171
191
  Ray = { tag = TAG_RAY, codec = RayC.ray },
172
192
  NumberSequence = { tag = TAG_NUMSEQ, codec = SeqC.numberSequence },
173
193
  ColorSequence = { tag = TAG_COLSEQ, codec = SeqC.colorSequence },
174
- } :: any)
194
+ })
175
195
 
176
- -- Read dispatch: tag → codec
177
196
  local READ_DISPATCH: { [number]: Types.InternalCodec<any> } = table.freeze({
178
197
  [TAG_VEC2] = VecC.vec2,
179
198
  [TAG_VEC3] = VecC.vec3,
@@ -190,213 +209,142 @@ local READ_DISPATCH: { [number]: Types.InternalCodec<any> } = table.freeze({
190
209
  [TAG_RAY] = RayC.ray,
191
210
  [TAG_NUMSEQ] = SeqC.numberSequence,
192
211
  [TAG_COLSEQ] = SeqC.colorSequence,
193
- } :: any)
194
-
195
- -- Numeric read helpers
196
- local function readI8(b: buffer, off: number): number
197
- local raw = readu8(b, off)
198
- return if raw >= 0x80 then raw - 0x100 else raw
199
- end
200
-
201
- local function readI16(b: buffer, off: number): number
202
- local raw = buffer.readu16(b, off)
203
- return if raw >= 0x8000 then raw - 0x10000 else raw
204
- end
205
-
206
- local function readI32(b: buffer, off: number): number
207
- local raw = buffer.readu32(b, off)
208
- return if raw >= 0x80000000 then raw - 0x100000000 else raw
209
- end
210
-
211
- local NUM_READ: { [number]: (buffer, number) -> number } = table.freeze({
212
- [TAG_U8] = buffer.readu8,
213
- [TAG_U16] = buffer.readu16,
214
- [TAG_U32] = buffer.readu32,
215
- [TAG_I8] = readI8,
216
- [TAG_I16] = readI16,
217
- [TAG_I32] = readI32,
218
- [TAG_F32] = buffer.readf32,
219
- [TAG_F64] = buffer.readf64,
220
212
  })
221
213
 
222
- -- Public --------------------------------------------------------------
214
+ local function writeAuto(ch: Types.ChannelState, value: any): ()
215
+ if value == nil then
216
+ writeByte(ch, TAG_NIL)
217
+ return
218
+ end
223
219
 
224
- local AutoModule = {}
220
+ local t = typeof(value)
225
221
 
226
- AutoModule.auto = table.freeze({
227
- write = function(ch: Types.ChannelState, value: any): ()
228
- if value == nil then
229
- local cursor = ch.cursor
230
- if cursor + 1 > ch.size then
231
- alloc(ch, 1)
232
- end
233
- writeu8(ch.buff, cursor, TAG_NIL)
234
- ch.cursor = cursor + 1
235
- return
222
+ if t == "boolean" then
223
+ local cursor = ch.cursor
224
+ if cursor + 2 > ch.size then
225
+ alloc(ch, 2)
236
226
  end
227
+ local b = ch.buff
228
+ writeu8(b, cursor, TAG_BOOL)
229
+ writeu8(b, cursor + 1, if value then 1 else 0)
230
+ ch.cursor = cursor + 2
231
+ return
232
+ end
237
233
 
238
- local t = typeof(value)
239
-
240
- if t == "boolean" then
241
- local cursor = ch.cursor
242
- if cursor + 2 > ch.size then
243
- alloc(ch, 2)
244
- end
245
- local b = ch.buff
246
- writeu8(b, cursor, TAG_BOOL)
247
- writeu8(b, cursor + 1, if value then 1 else 0)
248
- ch.cursor = cursor + 2
249
- return
234
+ if t == "number" then
235
+ local tag = selectNumberTag(value)
236
+ local totalSize = 1 + numberSize(tag)
237
+ local cursor = ch.cursor
238
+ if cursor + totalSize > ch.size then
239
+ alloc(ch, totalSize)
250
240
  end
241
+ local b = ch.buff
242
+ writeu8(b, cursor, tag)
243
+ writeNumber(b, cursor + 1, tag, value)
244
+ ch.cursor = cursor + totalSize
245
+ return
246
+ end
251
247
 
252
- if t == "number" then
253
- local tag = selectNumberTag(value)
254
- local payloadSize = NUM_SIZE[tag]
255
- local totalSize = 1 + payloadSize
256
- local cursor = ch.cursor
257
- if cursor + totalSize > ch.size then
258
- alloc(ch, totalSize)
259
- end
260
- local b = ch.buff
261
- writeu8(b, cursor, tag)
262
- NUM_WRITE[tag](b, cursor + 1, value)
263
- ch.cursor = cursor + totalSize
264
- return
265
- end
248
+ if t == "string" then
249
+ writeByte(ch, TAG_STRING)
266
250
 
267
- if t == "string" then
268
- local cursor = ch.cursor
269
- if cursor + 1 > ch.size then
270
- alloc(ch, 1)
271
- end
272
- writeu8(ch.buff, cursor, TAG_STRING)
273
- ch.cursor = cursor + 1
274
- -- Inline string write (same as String codec)
275
- local len = #value
276
- cursor = ch.cursor
277
- if len <= INLINE_MAX then
278
- if cursor + 1 + len > ch.size then
279
- alloc(ch, 1 + len)
280
- end
281
- local b = ch.buff
282
- writeu8(b, cursor, len)
283
- if len > 0 then
284
- buffer.writestring(b, cursor + 1, value)
285
- end
286
- ch.cursor = cursor + 1 + len
287
- else
288
- Varint.write(ch, len)
289
- cursor = ch.cursor
290
- if cursor + len > ch.size then
291
- alloc(ch, len)
292
- end
293
- buffer.writestring(ch.buff, cursor, value)
294
- ch.cursor = cursor + len
251
+ local len = #value
252
+ local cursor = ch.cursor
253
+ if len <= INLINE_MAX then
254
+ if cursor + 1 + len > ch.size then
255
+ alloc(ch, 1 + len)
295
256
  end
296
- return
297
- end
298
-
299
- if t == "buffer" then
300
- local cursor = ch.cursor
301
- if cursor + 1 > ch.size then
302
- alloc(ch, 1)
257
+ local b = ch.buff
258
+ writeu8(b, cursor, len)
259
+ if len > 0 then
260
+ buffer.writestring(b, cursor + 1, value)
303
261
  end
304
- writeu8(ch.buff, cursor, TAG_BUFFER)
305
- ch.cursor = cursor + 1
306
- BufferC.buff.write(ch, value)
262
+ ch.cursor = cursor + 1 + len
307
263
  return
308
264
  end
309
-
310
- -- Roblox type dispatch
311
- local entry = TYPE_DISPATCH[t]
312
- if entry then
313
- local codec = entry.codec
314
- local size = codec._size
315
- local dw = codec._directWrite
316
-
317
- if size and dw then
318
- -- Fast path: direct write
319
- local totalSize = 1 + size
320
- local cursor = ch.cursor
321
- if cursor + totalSize > ch.size then
322
- alloc(ch, totalSize)
323
- end
324
- local b = ch.buff
325
- writeu8(b, cursor, entry.tag)
326
- dw(b, cursor + 1, value)
327
- ch.cursor = cursor + totalSize
328
- else
329
- -- Generic path
330
- local cursor = ch.cursor
331
- if cursor + 1 > ch.size then
332
- alloc(ch, 1)
333
- end
334
- writeu8(ch.buff, cursor, entry.tag)
335
- ch.cursor = cursor + 1
336
- codec.write(ch, value)
337
- end
338
- return
265
+ Varint.write(ch, len)
266
+ cursor = ch.cursor
267
+ if cursor + len > ch.size then
268
+ alloc(ch, len)
339
269
  end
270
+ buffer.writestring(ch.buff, cursor, value)
271
+ ch.cursor = cursor + len
272
+ return
273
+ end
340
274
 
341
- error(`[Lync] Auto: unsupported type "{t}"`)
342
- end,
275
+ if t == "buffer" then
276
+ writeByte(ch, TAG_BUFFER)
277
+ BufferC.buff.write(ch, value)
278
+ return
279
+ end
343
280
 
344
- read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
345
- local tag = readu8(src, pos)
281
+ local entry = TYPE_DISPATCH[t]
282
+ if not entry then
283
+ error(`[Lync] Auto.write: unsupported type "{t}"`)
284
+ end
346
285
 
347
- -- Nil
348
- if tag == TAG_NIL then
349
- return nil, 1
286
+ local codec = entry.codec
287
+ local size = codec._size
288
+ local dw = codec._directWrite
289
+ if size and dw then
290
+ local totalSize = 1 + size
291
+ local cursor = ch.cursor
292
+ if cursor + totalSize > ch.size then
293
+ alloc(ch, totalSize)
350
294
  end
295
+ local b = ch.buff
296
+ writeu8(b, cursor, entry.tag)
297
+ dw(b, cursor + 1, value)
298
+ ch.cursor = cursor + totalSize
299
+ return
300
+ end
351
301
 
352
- -- Bool
353
- if tag == TAG_BOOL then
354
- return readu8(src, pos + 1) ~= 0, 2
355
- end
302
+ writeByte(ch, entry.tag)
303
+ codec.write(ch, value)
304
+ end
356
305
 
357
- -- Numeric types (tags 2-9)
358
- local numRead = NUM_READ[tag]
359
- if numRead then
360
- local payloadSize = NUM_SIZE[tag]
361
- return numRead(src, pos + 1), 1 + payloadSize
362
- end
306
+ local function readAuto(src: buffer, pos: number, refs: { Instance }?): (any, number)
307
+ local tag = readu8(src, pos)
363
308
 
364
- -- String
365
- if tag == TAG_STRING then
366
- local b0 = readu8(src, pos + 1)
367
- local len: number
368
- local lenBytes: number
369
-
370
- if b0 <= INLINE_MAX then
371
- len = b0
372
- lenBytes = 1
373
- else
374
- len, lenBytes = Varint.read(src, pos + 1)
375
- end
309
+ if tag == TAG_NIL then
310
+ return nil, 1
311
+ end
312
+ if tag == TAG_BOOL then
313
+ return readu8(src, pos + 1) ~= 0, 2
314
+ end
376
315
 
377
- if len == 0 then
378
- return "", 1 + lenBytes
379
- end
316
+ if tag >= TAG_U8 and tag <= TAG_F64 then
317
+ return readNumber(src, pos + 1, tag), 1 + numberSize(tag)
318
+ end
380
319
 
381
- local dataStart = pos + 1 + lenBytes
382
- return buffer.readstring(src, dataStart, len), 1 + lenBytes + len
320
+ if tag == TAG_STRING then
321
+ local len, lenBytes = Varint.readLengthPrefix(src, pos + 1)
322
+ if len == 0 then
323
+ return "", 1 + lenBytes
383
324
  end
325
+ return buffer.readstring(src, pos + 1 + lenBytes, len), 1 + lenBytes + len
326
+ end
384
327
 
385
- -- Buffer
386
- if tag == TAG_BUFFER then
387
- local value, consumed = BufferC.buff.read(src, pos + 1, refs)
388
- return value, 1 + consumed
389
- end
328
+ if tag == TAG_BUFFER then
329
+ local value, consumed = BufferC.buff.read(src, pos + 1, refs)
330
+ return value, 1 + consumed
331
+ end
390
332
 
391
- -- Roblox types
392
- local codec = READ_DISPATCH[tag]
393
- if codec then
394
- local value, consumed = codec.read(src, pos + 1, refs)
395
- return value, 1 + consumed
396
- end
333
+ local codec = READ_DISPATCH[tag]
334
+ if not codec then
335
+ error(`[Lync] Auto.read: unknown tag {tag}`)
336
+ end
337
+ local value, consumed = codec.read(src, pos + 1, refs)
338
+ return value, 1 + consumed
339
+ end
397
340
 
398
- error(`[Lync] Auto: unknown tag {tag}`)
399
- end,
400
- } :: any)
341
+ -- Public -----------------------------------------------------------------
342
+
343
+ local AutoModule = {}
344
+
345
+ AutoModule.auto = table.freeze({
346
+ write = writeAuto,
347
+ read = readAuto,
348
+ })
401
349
 
402
350
  return table.freeze(AutoModule)
@@ -1,38 +1,54 @@
1
1
  --!strict
2
2
  --!native
3
- -- Sub-byte packing. 1 to 32 bits across bool/uint/int fields.
3
+ -- Sub-byte packing. 1 to 32 bits across bool / uint / int fields.
4
4
 
5
5
  local Base = require(script.Parent.Parent.Base)
6
+ local Types = require(script.Parent.Parent.Parent.Types)
6
7
 
7
- -- Private -------------------------------------------------------------
8
+ -- Constants --------------------------------------------------------------
9
+
10
+ local TYPE_BOOL = 0
11
+ local TYPE_UINT = 1
12
+ local TYPE_INT = 2
13
+
14
+ -- Private ----------------------------------------------------------------
8
15
 
9
16
  local band = bit32.band
10
17
  local bor = bit32.bor
11
18
  local lshift = bit32.lshift
12
19
  local rshift = bit32.rshift
13
20
 
14
- -- Field type constants
15
- local TYPE_BOOL = 0
16
- local TYPE_UINT = 1
17
- local TYPE_INT = 2
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
+ ]]
27
+ local function widthMask(width: number): number
28
+ if width >= 32 then
29
+ return 0xFFFFFFFF
30
+ end
31
+ return lshift(1, width) - 1
32
+ end
18
33
 
19
- -- Public --------------------------------------------------------------
34
+ -- Public -----------------------------------------------------------------
20
35
 
21
36
  local Bitfield = {}
22
37
 
23
- function Bitfield.define(schema: { [string]: { type: string, width: number? } }): any
24
- -- Collect and sort keys alphabetically
25
- local keys = {}
38
+ export type FieldSpec = { type: "bool" | "uint" | "int", width: number? }
39
+
40
+ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<any>
41
+ local keys: { string } = {}
26
42
  for key in schema do
27
43
  table.insert(keys, key)
28
44
  end
29
45
  table.sort(keys)
30
46
 
31
- if #keys == 0 then
47
+ local fieldCount = #keys
48
+ if fieldCount == 0 then
32
49
  error("[Lync] Lync.bitfield: requires at least one field")
33
50
  end
34
51
 
35
- local fieldCount = #keys
36
52
  local fieldKeys = table.create(fieldCount)
37
53
  local fieldTypes = table.create(fieldCount)
38
54
  local fieldOffsets = table.create(fieldCount)
@@ -59,7 +75,7 @@ function Bitfield.define(schema: { [string]: { type: string, width: number? } })
59
75
  error(`[Lync] Lync.bitfield: field "{key}" uint width must be 1-32`)
60
76
  end
61
77
  fieldTypes[i] = TYPE_UINT
62
- fieldMasks[i] = lshift(1, width) - 1
78
+ fieldMasks[i] = widthMask(width)
63
79
  fieldSignBit[i] = 0
64
80
  fieldSignExt[i] = 0
65
81
  totalBits += width
@@ -69,12 +85,12 @@ function Bitfield.define(schema: { [string]: { type: string, width: number? } })
69
85
  error(`[Lync] Lync.bitfield: field "{key}" int width must be 2-32`)
70
86
  end
71
87
  fieldTypes[i] = TYPE_INT
72
- fieldMasks[i] = lshift(1, width) - 1
73
- fieldSignBit[i] = lshift(1, width - 1)
74
- fieldSignExt[i] = lshift(1, width)
88
+ fieldMasks[i] = widthMask(width)
89
+ fieldSignBit[i] = 2 ^ (width - 1)
90
+ fieldSignExt[i] = 2 ^ width
75
91
  totalBits += width
76
92
  else
77
- error(`[Lync] Lync.bitfield: unknown field type "{spec.type}"`)
93
+ error(`[Lync] Lync.bitfield: field "{key}" has unknown type "{spec.type}"`)
78
94
  end
79
95
  end
80
96
 
@@ -82,26 +98,17 @@ function Bitfield.define(schema: { [string]: { type: string, width: number? } })
82
98
  error(`[Lync] Lync.bitfield: total exceeds 32 bits ({totalBits})`)
83
99
  end
84
100
 
85
- -- Determine wire size
86
101
  local wireBytes: number
87
102
  local wfn: (buffer, number, number) -> ()
88
103
  local rfn: (buffer, number) -> number
89
-
90
104
  if totalBits <= 8 then
91
- wireBytes = 1
92
- wfn = buffer.writeu8
93
- rfn = buffer.readu8
105
+ wireBytes, wfn, rfn = 1, buffer.writeu8, buffer.readu8
94
106
  elseif totalBits <= 16 then
95
- wireBytes = 2
96
- wfn = buffer.writeu16
97
- rfn = buffer.readu16
107
+ wireBytes, wfn, rfn = 2, buffer.writeu16, buffer.readu16
98
108
  else
99
- wireBytes = 4
100
- wfn = buffer.writeu32
101
- rfn = buffer.readu32
109
+ wireBytes, wfn, rfn = 4, buffer.writeu32, buffer.readu32
102
110
  end
103
111
 
104
- -- Freeze all descriptor arrays
105
112
  table.freeze(fieldKeys)
106
113
  table.freeze(fieldTypes)
107
114
  table.freeze(fieldOffsets)
@@ -125,22 +132,16 @@ function Bitfield.define(schema: { [string]: { type: string, width: number? } })
125
132
  end, function(b: buffer, off: number): any
126
133
  local packed = rfn(b, off)
127
134
  local result: { [string]: any } = {}
128
-
129
135
  for i = 1, fieldCount do
130
136
  local raw = band(rshift(packed, fieldOffsets[i]), fieldMasks[i])
131
137
  if fieldTypes[i] == TYPE_BOOL then
132
138
  result[fieldKeys[i]] = raw ~= 0
133
139
  elseif fieldTypes[i] == TYPE_INT then
134
- if raw >= fieldSignBit[i] then
135
- result[fieldKeys[i]] = raw - fieldSignExt[i]
136
- else
137
- result[fieldKeys[i]] = raw
138
- end
140
+ result[fieldKeys[i]] = if raw >= fieldSignBit[i] then raw - fieldSignExt[i] else raw
139
141
  else
140
142
  result[fieldKeys[i]] = raw
141
143
  end
142
144
  end
143
-
144
145
  return result
145
146
  end)
146
147
  end