@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
@@ -0,0 +1,390 @@
1
+ --!strict
2
+ --!native
3
+ -- Delta scalars: zigzag-varint diff against previous frame for ints, floats, vec3, CFrame.
4
+
5
+ local Base = require(script.Parent.Parent.Base)
6
+ local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
7
+ local Log = require(script.Parent.Parent.Parent.util.Log)
8
+ local Quantize = require(script.Parent.Parent.Parent.util.Quantize)
9
+ local Quat = require(script.Parent.Parent.Parent.util.Quat)
10
+ local Shared = require(script.Parent.Parent.composite.Shared)
11
+ local Types = require(script.Parent.Parent.Parent.Types)
12
+ local Varint = require(script.Parent.Parent.primitive.Varint)
13
+ local Zint = require(script.Parent.Parent.primitive.Zint)
14
+
15
+ -- Constants --------------------------------------------------------------
16
+
17
+ local I32_MIN = -0x80000000
18
+ local I32_MAX = 0x7FFFFFFF
19
+
20
+ -- deltaCFrame header bits: which components changed since the last frame.
21
+ local CF_X = 0x01
22
+ local CF_Y = 0x02
23
+ local CF_Z = 0x04
24
+ local CF_ROT = 0x08
25
+ local CF_ALL = 0x0F
26
+
27
+ -- Private ----------------------------------------------------------------
28
+
29
+ local ensure = Base.ensure
30
+ local allocDeltaId = Shared.allocDeltaId
31
+ local floor = math.floor
32
+ local clamp = math.clamp
33
+ local round = math.round
34
+ local band = bit32.band
35
+ local bor = bit32.bor
36
+ local readu8 = buffer.readu8
37
+ local writeu8 = buffer.writeu8
38
+ local readu32 = buffer.readu32
39
+ local writeu32 = buffer.writeu32
40
+ local bufLen = buffer.len
41
+ local varintWrite = Varint.write
42
+ local varintRead = Varint.read
43
+ local zigzagEncode = Zint.encode
44
+ local zigzagDecode = Zint.decode
45
+ local quatPack = Quat.pack
46
+ local quatUnpack = Quat.unpack
47
+ local quatFromCFrame = Quat.fromCFrame
48
+
49
+ local function quantizeVec3(
50
+ v: Vector3,
51
+ min: number,
52
+ delta: number,
53
+ scale: number
54
+ ): (number, number, number)
55
+ return round(clamp(v.X - min, 0, delta) * scale),
56
+ round(clamp(v.Y - min, 0, delta) * scale),
57
+ round(clamp(v.Z - min, 0, delta) * scale)
58
+ end
59
+
60
+ -- Reject out-of-range components rather than silently snapping via clamp().
61
+ local function validateVec3(v: Vector3, min: number, max: number, label: string): ()
62
+ if v.X < min or v.X > max or v.Y < min or v.Y > max or v.Z < min or v.Z > max then
63
+ Log.error(`{label} ({v.X}, {v.Y}, {v.Z}) has component out of range [{min}, {max}]`)
64
+ end
65
+ end
66
+
67
+ local function getScalarPrev(ch: Types.ChannelState, id: number, fallback: number): number
68
+ local cache = ch.deltas[id] :: any
69
+ return if cache then cache.scalar :: number else fallback
70
+ end
71
+
72
+ local function setScalarCache(ch: Types.ChannelState, id: number, value: number): ()
73
+ local cache = ch.deltas[id] :: any
74
+ if cache then
75
+ cache.scalar = value
76
+ else
77
+ ch.deltas[id] = { scalar = value } :: any
78
+ end
79
+ end
80
+
81
+ -- Public -----------------------------------------------------------------
82
+
83
+ local DeltaScalar = {}
84
+
85
+ --[[
86
+ Variable-length integer that emits zigzag varint of (current - previous).
87
+ First frame uses `min` as the implicit prev so the initial value encodes
88
+ as (value - min). Reliable transport only — a dropped frame desyncs prev.
89
+
90
+ Wire: 1 byte for unchanged or [-96, 95] mutations; up to 5 bytes for full i32.
91
+ ]]
92
+ function DeltaScalar.deltaInt(min: number, max: number): Types.InternalCodec<number>
93
+ if min > max then
94
+ Log.error(`min ({min}) must be <= max ({max})`)
95
+ end
96
+ if floor(min) ~= min or floor(max) ~= max then
97
+ Log.error("bounds must be integers")
98
+ end
99
+ if max - min > I32_MAX then
100
+ Log.error(`range too wide; (max - min) must fit in i32, got {max - min}`)
101
+ end
102
+
103
+ local deltaId = allocDeltaId()
104
+
105
+ return table.freeze({
106
+ _isDelta = true,
107
+ _typeCheck = "number",
108
+ _isInteger = true,
109
+ _min = min,
110
+ _max = max,
111
+
112
+ write = function(ch: Types.ChannelState, value: number): ()
113
+ if value < min or value > max then
114
+ Log.error(`value {value} out of range [{min}, {max}]`)
115
+ end
116
+ if floor(value) ~= value then
117
+ Log.error(`value {value} must be an integer`)
118
+ end
119
+ varintWrite(ch, zigzagEncode(value - getScalarPrev(ch, deltaId, min)))
120
+ setScalarCache(ch, deltaId, value)
121
+ end,
122
+
123
+ read = function(src: buffer, pos: number, _refs: { Instance }?): (number, number)
124
+ local raw, consumed = varintRead(src, pos)
125
+ if consumed == 0 then
126
+ Log.error("truncated deltaInt")
127
+ end
128
+ local cached = Baseline.getCache(deltaId)
129
+ local value = (if cached ~= nil then cached :: number else min) + zigzagDecode(raw)
130
+ if value < min or value > max then
131
+ Log.error(`deltaInt decoded value {value} out of range [{min}, {max}]`)
132
+ end
133
+ Baseline.setCache(deltaId, value)
134
+ return value, consumed
135
+ end,
136
+ }) :: Types.InternalCodec<number>
137
+ end
138
+
139
+ --[[
140
+ Quantized float with diffs in integer wire space (post-quantize). Storing
141
+ the quantized representative — not the float — across frames means
142
+ accumulating diffs over thousands of frames cannot drift past the grid.
143
+ ]]
144
+ function DeltaScalar.deltaFloat(
145
+ min: number,
146
+ max: number,
147
+ precision: number
148
+ ): Types.InternalCodec<number>
149
+ if max - min == 0 then
150
+ return Base.constant(min)
151
+ end
152
+
153
+ local _, _, _, scale, invScale, delta = Quantize.setup("Lync.deltaFloat", min, max, precision)
154
+ local deltaId = allocDeltaId()
155
+
156
+ return table.freeze({
157
+ _isDelta = true,
158
+ _typeCheck = "number",
159
+ _min = min,
160
+ _max = max,
161
+
162
+ write = function(ch: Types.ChannelState, value: number): ()
163
+ local q = round(clamp(value - min, 0, delta) * scale)
164
+ varintWrite(ch, zigzagEncode(q - getScalarPrev(ch, deltaId, 0)))
165
+ setScalarCache(ch, deltaId, q)
166
+ end,
167
+
168
+ read = function(src: buffer, pos: number, _refs: { Instance }?): (number, number)
169
+ local raw, consumed = varintRead(src, pos)
170
+ if consumed == 0 then
171
+ Log.error("truncated deltaFloat")
172
+ end
173
+ local cached = Baseline.getCache(deltaId)
174
+ local q = (if cached ~= nil then cached :: number else 0) + zigzagDecode(raw)
175
+ Baseline.setCache(deltaId, q)
176
+ return q * invScale + min, consumed
177
+ end,
178
+ }) :: Types.InternalCodec<number>
179
+ end
180
+
181
+ --[[
182
+ Quantized Vector3 with per-axis zigzag varint diffs. Static value: 3 bytes
183
+ (three zero zigzags); typical < 1 stud motion at 0.01-stud precision: 3-6
184
+ bytes. Baseline vec3 = 12 bytes, so 4x reduction at the static end.
185
+ ]]
186
+ function DeltaScalar.deltaVec3(
187
+ min: number,
188
+ max: number,
189
+ precision: number
190
+ ): Types.InternalCodec<Vector3>
191
+ if max - min == 0 then
192
+ return Base.constant(Vector3.new(min, min, min)) :: Types.InternalCodec<Vector3>
193
+ end
194
+
195
+ local _, _, _, scale, invScale, delta = Quantize.setup("Lync.deltaVec3", min, max, precision)
196
+ local deltaId = allocDeltaId()
197
+
198
+ return table.freeze({
199
+ _isDelta = true,
200
+ _typeCheck = "Vector3",
201
+ _min = min,
202
+ _max = max,
203
+
204
+ write = function(ch: Types.ChannelState, value: Vector3): ()
205
+ validateVec3(value, min, max, "Vector3")
206
+ local qx, qy, qz = quantizeVec3(value, min, delta, scale)
207
+ local cache = ch.deltas[deltaId] :: any
208
+ local px, py, pz = 0, 0, 0
209
+ if cache then
210
+ px, py, pz = cache.qx :: number, cache.qy :: number, cache.qz :: number
211
+ end
212
+
213
+ varintWrite(ch, zigzagEncode(qx - px))
214
+ varintWrite(ch, zigzagEncode(qy - py))
215
+ varintWrite(ch, zigzagEncode(qz - pz))
216
+
217
+ if cache then
218
+ cache.qx, cache.qy, cache.qz = qx, qy, qz
219
+ else
220
+ ch.deltas[deltaId] = { qx = qx, qy = qy, qz = qz } :: any
221
+ end
222
+ end,
223
+
224
+ read = function(src: buffer, pos: number, _refs: { Instance }?): (Vector3, number)
225
+ local rx, cx = varintRead(src, pos)
226
+ if cx == 0 then
227
+ Log.error("truncated deltaVec3 X")
228
+ end
229
+ local ry, cy = varintRead(src, pos + cx)
230
+ if cy == 0 then
231
+ Log.error("truncated deltaVec3 Y")
232
+ end
233
+ local rz, cz = varintRead(src, pos + cx + cy)
234
+ if cz == 0 then
235
+ Log.error("truncated deltaVec3 Z")
236
+ end
237
+
238
+ local cached = Baseline.getCache(deltaId) :: any
239
+ local px, py, pz = 0, 0, 0
240
+ if cached then
241
+ px, py, pz = cached.qx, cached.qy, cached.qz
242
+ end
243
+ local qx = px + zigzagDecode(rx)
244
+ local qy = py + zigzagDecode(ry)
245
+ local qz = pz + zigzagDecode(rz)
246
+ Baseline.setCache(deltaId, { qx = qx, qy = qy, qz = qz })
247
+
248
+ return Vector3.new(qx * invScale + min, qy * invScale + min, qz * invScale + min),
249
+ cx + cy + cz
250
+ end,
251
+ }) :: Types.InternalCodec<Vector3>
252
+ end
253
+
254
+ --[[
255
+ Quantized position + smallest-three quaternion rotation. Wire layout:
256
+ byte 0 header bits: bit0=Δx, bit1=Δy, bit2=Δz, bit3=Δrot
257
+ For each set position bit: zigzag varint of quantized diff
258
+ If rotation bit set: 4-byte packed quat
259
+
260
+ Static CFrame: 1 byte. Pos-only motion: 1 + 3 zint bytes (~4-7).
261
+ Full update: 1 + 3 zint + 4 quat (~8-13). Baseline lossless: 24 bytes.
262
+ ]]
263
+ function DeltaScalar.deltaCFrame(
264
+ posMin: number,
265
+ posMax: number,
266
+ posPrecision: number
267
+ ): Types.InternalCodec<CFrame>
268
+ -- Quantize.setup itself rejects min == max; the redundant guard is gone.
269
+ local _, _, _, scale, invScale, delta =
270
+ Quantize.setup("Lync.deltaCFrame", posMin, posMax, posPrecision)
271
+ local deltaId = allocDeltaId()
272
+
273
+ return table.freeze({
274
+ _isDelta = true,
275
+ _typeCheck = "CFrame",
276
+
277
+ write = function(ch: Types.ChannelState, value: CFrame): ()
278
+ local pos = value.Position
279
+ validateVec3(pos, posMin, posMax, "CFrame position")
280
+ local qx, qy, qz = quantizeVec3(pos, posMin, delta, scale)
281
+ local packed = quatPack(quatFromCFrame(value))
282
+
283
+ -- prev = 0 so the first frame writes absolute values; receiver also defaults to 0.
284
+ local cache = ch.deltas[deltaId] :: any
285
+ local flag: number
286
+ local px, py, pz = 0, 0, 0
287
+ if cache then
288
+ px = cache.qx :: number
289
+ py = cache.qy :: number
290
+ pz = cache.qz :: number
291
+ local ppacked = cache.qrot :: number
292
+ flag = 0
293
+ if qx ~= px then
294
+ flag = bor(flag, CF_X)
295
+ end
296
+ if qy ~= py then
297
+ flag = bor(flag, CF_Y)
298
+ end
299
+ if qz ~= pz then
300
+ flag = bor(flag, CF_Z)
301
+ end
302
+ if packed ~= ppacked then
303
+ flag = bor(flag, CF_ROT)
304
+ end
305
+ else
306
+ -- First frame: emit everything so the receiver has a baseline.
307
+ flag = CF_ALL
308
+ end
309
+
310
+ ensure(ch, 1)
311
+ writeu8(ch.buff, ch.cursor, flag)
312
+ ch.cursor += 1
313
+
314
+ if band(flag, CF_X) ~= 0 then
315
+ varintWrite(ch, zigzagEncode(qx - px))
316
+ end
317
+ if band(flag, CF_Y) ~= 0 then
318
+ varintWrite(ch, zigzagEncode(qy - py))
319
+ end
320
+ if band(flag, CF_Z) ~= 0 then
321
+ varintWrite(ch, zigzagEncode(qz - pz))
322
+ end
323
+ if band(flag, CF_ROT) ~= 0 then
324
+ ensure(ch, 4)
325
+ writeu32(ch.buff, ch.cursor, packed)
326
+ ch.cursor += 4
327
+ end
328
+
329
+ if cache then
330
+ cache.qx, cache.qy, cache.qz, cache.qrot = qx, qy, qz, packed
331
+ else
332
+ ch.deltas[deltaId] = { qx = qx, qy = qy, qz = qz, qrot = packed } :: any
333
+ end
334
+ end,
335
+
336
+ read = function(src: buffer, pos: number, _refs: { Instance }?): (CFrame, number)
337
+ if pos >= bufLen(src) then
338
+ Log.error("truncated deltaCFrame header")
339
+ end
340
+ local flag = readu8(src, pos)
341
+ local total = 1
342
+
343
+ local cached = Baseline.getCache(deltaId) :: any
344
+ local qx, qy, qz, packed = 0, 0, 0, 0
345
+ if cached then
346
+ qx, qy, qz, packed = cached.qx, cached.qy, cached.qz, cached.qrot
347
+ end
348
+
349
+ if band(flag, CF_X) ~= 0 then
350
+ local r, c = varintRead(src, pos + total)
351
+ if c == 0 then
352
+ Log.error("truncated deltaCFrame X")
353
+ end
354
+ qx += zigzagDecode(r)
355
+ total += c
356
+ end
357
+ if band(flag, CF_Y) ~= 0 then
358
+ local r, c = varintRead(src, pos + total)
359
+ if c == 0 then
360
+ Log.error("truncated deltaCFrame Y")
361
+ end
362
+ qy += zigzagDecode(r)
363
+ total += c
364
+ end
365
+ if band(flag, CF_Z) ~= 0 then
366
+ local r, c = varintRead(src, pos + total)
367
+ if c == 0 then
368
+ Log.error("truncated deltaCFrame Z")
369
+ end
370
+ qz += zigzagDecode(r)
371
+ total += c
372
+ end
373
+ if band(flag, CF_ROT) ~= 0 then
374
+ if pos + total + 4 > bufLen(src) then
375
+ Log.error("truncated deltaCFrame rotation")
376
+ end
377
+ packed = readu32(src, pos + total)
378
+ total += 4
379
+ end
380
+
381
+ Baseline.setCache(deltaId, { qx = qx, qy = qy, qz = qz, qrot = packed })
382
+
383
+ local position =
384
+ Vector3.new(qx * invScale + posMin, qy * invScale + posMin, qz * invScale + posMin)
385
+ return CFrame.new(position) * quatUnpack(packed), total
386
+ end,
387
+ }) :: Types.InternalCodec<CFrame>
388
+ end
389
+
390
+ return table.freeze(DeltaScalar)
@@ -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 -----------------------------------------------------------------
@@ -1,7 +1,6 @@
1
1
  --!strict
2
2
  --!native
3
- -- Signed integer buffer helpers. Reads use FASTCALL2 readi*; writes
4
- -- emulate via writeu* + two's complement (writei* is not FASTCALL).
3
+ -- Signed integer writers via writeu* + two's complement (writei* is not FASTCALL).
5
4
 
6
5
  -- Private ----------------------------------------------------------------
7
6
 
@@ -25,6 +24,7 @@ function Signed.writeI32(b: buffer, off: number, value: number): ()
25
24
  writeu32(b, off, if value < 0 then value + 0x100000000 else value)
26
25
  end
27
26
 
27
+ -- Reads stay on buffer.readi* (already FASTCALL).
28
28
  Signed.readI8 = buffer.readi8
29
29
  Signed.readI16 = buffer.readi16
30
30
  Signed.readI32 = buffer.readi32