@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,90 @@
1
+ --!strict
2
+ --!native
3
+ -- Word-aligned XOR and equality helpers for Channel and composite deltas.
4
+
5
+ -- Private ----------------------------------------------------------------
6
+
7
+ local band = bit32.band
8
+ local bxor = bit32.bxor
9
+ local readu8 = buffer.readu8
10
+ local readu32 = buffer.readu32
11
+ local writeu8 = buffer.writeu8
12
+ local writeu32 = buffer.writeu32
13
+ local bufCopy = buffer.copy
14
+ local bufCreate = buffer.create
15
+ local minN = math.min
16
+
17
+ -- Public -----------------------------------------------------------------
18
+
19
+ local Buf = {}
20
+
21
+ -- 8-byte unrolled compare; 4-byte step then byte tail for remainder.
22
+ function Buf.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: number): boolean
23
+ local aligned8 = band(len, -8)
24
+ local i = 0
25
+ while i < aligned8 do
26
+ if
27
+ readu32(a, offA + i) ~= readu32(b, offB + i)
28
+ or readu32(a, offA + i + 4) ~= readu32(b, offB + i + 4)
29
+ then
30
+ return false
31
+ end
32
+ i += 8
33
+ end
34
+ if i + 4 <= len then
35
+ if readu32(a, offA + i) ~= readu32(b, offB + i) then
36
+ return false
37
+ end
38
+ i += 4
39
+ end
40
+ while i < len do
41
+ if readu8(a, offA + i) ~= readu8(b, offB + i) then
42
+ return false
43
+ end
44
+ i += 1
45
+ end
46
+ return true
47
+ end
48
+
49
+ function Buf.snapshot(src: buffer, len: number, srcOff: number?): buffer
50
+ local out = bufCreate(len)
51
+ bufCopy(out, 0, src, srcOff or 0, len)
52
+ return out
53
+ end
54
+
55
+ --[[
56
+ XOR `current` against `previous` into a fresh curLen buffer. Returns
57
+ `current` unchanged when previous is nil so first-frame callers don't
58
+ branch. Overlap is 8-byte unrolled; tail is bulk-copied.
59
+ ]]
60
+ function Buf.xorApply(current: buffer, curLen: number, previous: buffer?, prevLen: number?): buffer
61
+ if not previous then
62
+ return current
63
+ end
64
+ local pLen = prevLen or 0
65
+
66
+ local result = bufCreate(curLen)
67
+ local overlap = minN(curLen, pLen)
68
+
69
+ local aligned8 = band(overlap, -8)
70
+ local i = 0
71
+ while i < aligned8 do
72
+ writeu32(result, i, bxor(readu32(current, i), readu32(previous, i)))
73
+ writeu32(result, i + 4, bxor(readu32(current, i + 4), readu32(previous, i + 4)))
74
+ i += 8
75
+ end
76
+ if i + 4 <= overlap then
77
+ writeu32(result, i, bxor(readu32(current, i), readu32(previous, i)))
78
+ i += 4
79
+ end
80
+ while i < overlap do
81
+ writeu8(result, i, bxor(readu8(current, i), readu8(previous, i)))
82
+ i += 1
83
+ end
84
+ if curLen > overlap then
85
+ bufCopy(result, overlap, current, overlap, curLen - overlap)
86
+ end
87
+ return result
88
+ end
89
+
90
+ return table.freeze(Buf)
@@ -0,0 +1,30 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Wire-format constants and integer limits shared across codecs.
4
+
5
+ -- Public -----------------------------------------------------------------
6
+
7
+ local Constants = {}
8
+
9
+ Constants.U8_MAX = 0xFF
10
+ Constants.U16_MAX = 0xFFFF
11
+ Constants.U32_MAX = 0xFFFFFFFF
12
+
13
+ Constants.I8_MIN = -0x80
14
+ Constants.I8_MAX = 0x7F
15
+ Constants.I16_MIN = -0x8000
16
+ Constants.I16_MAX = 0x7FFF
17
+ Constants.I32_MIN = -0x80000000
18
+ Constants.I32_MAX = 0x7FFFFFFF
19
+
20
+ --[[
21
+ Frame header byte: MSB = single-item (no count cell); cleared = multi-item
22
+ (u16 count follows). Low 7 bits = registration id (caps registry at 128).
23
+ ]]
24
+ Constants.FRAME_MSB = 0x80
25
+ Constants.FRAME_ID_MASK = 0x7F
26
+
27
+ -- u16 count cell.
28
+ Constants.MAX_BATCH_ITEMS = 0xFFFF
29
+
30
+ return table.freeze(Constants)
@@ -0,0 +1,68 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Centralized warn/error/assert. Project name + caller source come from here.
4
+
5
+ -- State ------------------------------------------------------------------
6
+
7
+ -- Module-level so tests mute the whole library without threading a flag.
8
+ local _silent = false
9
+
10
+ -- Constants --------------------------------------------------------------
11
+
12
+ local PREFIX = "[Lync]"
13
+
14
+ -- Private ----------------------------------------------------------------
15
+
16
+ --[[
17
+ Stack levels: 1=this fn, 2=Log.warn/error/assert, 3=user. We want #3 so
18
+ the source/line/name point at the actual caller, not at Log.
19
+
20
+ Source comes back as a dotted Roblox path (e.g. "ServerScriptService.
21
+ MainModule.Lync.codec.primitive.Int" or "...test.unit.api.Packet.spec").
22
+ `.spec` is a literal dot in test ModuleScript names, so split-on-dot
23
+ would yield the meaningless tail "spec"; check the .spec suffix first
24
+ and recover the meaningful segment, then fall back to the last segment.
25
+ ]]
26
+ local function format(message: string): string
27
+ local source, line, name = debug.info(3, "sln")
28
+ local file = source:match("([^.]+)%.spec$") or source:match("[^./\\]+$") or source
29
+ if name and name ~= "" then
30
+ return `{PREFIX} {file}:{line} ({name}) {message}`
31
+ end
32
+ return `{PREFIX} {file}:{line} {message}`
33
+ end
34
+
35
+ -- Public -----------------------------------------------------------------
36
+
37
+ local Log = {}
38
+
39
+ function Log.warn(message: string): ()
40
+ if not _silent then
41
+ warn(format(message))
42
+ end
43
+ end
44
+
45
+ -- level 2 so the runtime traceback points at the caller, not at Log.
46
+ function Log.error(message: string): never
47
+ error(format(message), 2)
48
+ end
49
+
50
+ --[[
51
+ NOTE: Luau evaluates args eagerly. `format(message)` runs even when cond
52
+ is true, paying one debug.info call per Log.assert call. Use only with
53
+ LITERAL strings (no interpolation). For interpolated messages prefer
54
+ `if cond then Log.error(...) end`.
55
+ ]]
56
+ function Log.assert<T>(cond: T, message: string): T
57
+ return assert(cond, format(message))
58
+ end
59
+
60
+ function Log.setSilent(silent: boolean): ()
61
+ _silent = silent
62
+ end
63
+
64
+ function Log.reset(): ()
65
+ _silent = false
66
+ end
67
+
68
+ return table.freeze(Log)
@@ -0,0 +1,14 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Player Instance type guard.
4
+
5
+ -- Public -----------------------------------------------------------------
6
+
7
+ local Player = {}
8
+
9
+ -- typeof first: :IsA errors on non-Instances.
10
+ function Player.is(value: any): boolean
11
+ return typeof(value) == "Instance" and value:IsA("Player")
12
+ end
13
+
14
+ return table.freeze(Player)
@@ -0,0 +1,84 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Fixed-point range quantizer setup shared by float and vector codecs.
4
+
5
+ local Log = require(script.Parent.Log)
6
+
7
+ -- Private ----------------------------------------------------------------
8
+
9
+ local ceil = math.ceil
10
+ local maxN = math.max
11
+ local minN = math.min
12
+
13
+ local writeu8 = buffer.writeu8
14
+ local writeu16 = buffer.writeu16
15
+ local readu8 = buffer.readu8
16
+ local readu16 = buffer.readu16
17
+ local band = bit32.band
18
+ local bor = bit32.bor
19
+ local rshift = bit32.rshift
20
+ local lshift = bit32.lshift
21
+
22
+ --[[
23
+ 24-bit wire form. Splits into u16 + u8 since the buffer API has no native
24
+ u24. Saves 1 byte over u32 for ranges needing 17–24 bits of precision.
25
+ ]]
26
+ local function writeu24(b: buffer, off: number, value: number): ()
27
+ writeu16(b, off, band(value, 0xFFFF))
28
+ writeu8(b, off + 2, band(rshift(value, 16), 0xFF))
29
+ end
30
+
31
+ local function readu24(b: buffer, off: number): number
32
+ return bor(readu16(b, off), lshift(readu8(b, off + 2), 16))
33
+ end
34
+
35
+ -- Public -----------------------------------------------------------------
36
+
37
+ local Quantize = {}
38
+
39
+ --[[
40
+ Pick the narrowest u8/u16/u24/u32 wire form for [rangeMin, rangeMax] at
41
+ `precision`. Returns (compBytes, wfn, rfn, scale, invScale, delta).
42
+ `apiName` shows in error messages so callers see their public surface.
43
+ ]]
44
+ function Quantize.setup(
45
+ apiName: string,
46
+ rangeMin: number,
47
+ rangeMax: number,
48
+ precision: number
49
+ ): (
50
+ number,
51
+ (buffer, number, number) -> (),
52
+ (buffer, number) -> number,
53
+ number,
54
+ number,
55
+ number
56
+ )
57
+ if rangeMin >= rangeMax then
58
+ Log.error(`{apiName}: min ({rangeMin}) must be < max ({rangeMax})`)
59
+ end
60
+ if precision <= 0 then
61
+ Log.error(`{apiName}: precision must be positive, got {precision}`)
62
+ end
63
+
64
+ local delta = rangeMax - rangeMin
65
+ -- Clamp to [1, U32_MAX] so wire-form selection has a valid value.
66
+ local maxInt = minN(0xFFFFFFFF, maxN(1, ceil(delta / precision)))
67
+
68
+ local compBytes: number
69
+ local wfn: (buffer, number, number) -> ()
70
+ local rfn: (buffer, number) -> number
71
+ if maxInt <= 0xFF then
72
+ compBytes, wfn, rfn = 1, buffer.writeu8, buffer.readu8
73
+ elseif maxInt <= 0xFFFF then
74
+ compBytes, wfn, rfn = 2, buffer.writeu16, buffer.readu16
75
+ elseif maxInt <= 0xFFFFFF then
76
+ compBytes, wfn, rfn = 3, writeu24, readu24
77
+ else
78
+ compBytes, wfn, rfn = 4, buffer.writeu32, buffer.readu32
79
+ end
80
+
81
+ return compBytes, wfn, rfn, maxInt / delta, delta / maxInt, delta
82
+ end
83
+
84
+ return table.freeze(Quantize)
@@ -0,0 +1,124 @@
1
+ --!strict
2
+ --!native
3
+ -- Smallest-three quaternion packing into a single u32 for rotation wire forms.
4
+
5
+ -- Constants --------------------------------------------------------------
6
+
7
+ local INV_SQRT2 = 1 / math.sqrt(2)
8
+ local Q_SCALE = 1023 / (2 * INV_SQRT2)
9
+ local Q_INV = (2 * INV_SQRT2) / 1023
10
+ local EPSILON_SQ = 5.76e-14
11
+
12
+ -- Private ----------------------------------------------------------------
13
+
14
+ local abs = math.abs
15
+ local clamp = math.clamp
16
+ local round = math.round
17
+ local sin = math.sin
18
+ local cos = math.cos
19
+ local acos = math.acos
20
+ local sqrt = math.sqrt
21
+ local band = bit32.band
22
+ local bor = bit32.bor
23
+ local lshift = bit32.lshift
24
+ local rshift = bit32.rshift
25
+
26
+ -- Public -----------------------------------------------------------------
27
+
28
+ local Quat = {}
29
+
30
+ --[[
31
+ Quaternion from a CFrame's rotation portion. Returns (qx, qy, qz, qw)
32
+ so callers can pack without rebuilding the conversion table.
33
+ ]]
34
+ function Quat.fromCFrame(cf: CFrame): (number, number, number, number)
35
+ local axis, angle = cf:ToAxisAngle()
36
+ local h = angle * 0.5
37
+ local s = sin(h)
38
+ return axis.X * s, axis.Y * s, axis.Z * s, cos(h)
39
+ end
40
+
41
+ --[[
42
+ Pack a unit quaternion into a single u32:
43
+ [largest:2][a:10][b:10][c:10]
44
+ Drops the largest |component|; reconstructed from the other three with
45
+ the constraint qx²+qy²+qz²+qw² = 1. Forces the dropped value
46
+ non-negative so the +sqrt path on decode yields the correct sign.
47
+ ]]
48
+ function Quat.pack(qx: number, qy: number, qz: number, qw: number): number
49
+ local ax, ay, az, aw = abs(qx), abs(qy), abs(qz), abs(qw)
50
+ local largest, maxVal = 3, aw
51
+ if ax > maxVal then
52
+ largest, maxVal = 0, ax
53
+ end
54
+ if ay > maxVal then
55
+ largest, maxVal = 1, ay
56
+ end
57
+ if az > maxVal then
58
+ largest, maxVal = 2, az
59
+ end
60
+
61
+ local a: number, b: number, c: number
62
+ if largest == 0 then
63
+ if qx < 0 then
64
+ qy, qz, qw = -qy, -qz, -qw
65
+ end
66
+ a, b, c = qy, qz, qw
67
+ elseif largest == 1 then
68
+ if qy < 0 then
69
+ qx, qz, qw = -qx, -qz, -qw
70
+ end
71
+ a, b, c = qx, qz, qw
72
+ elseif largest == 2 then
73
+ if qz < 0 then
74
+ qx, qy, qw = -qx, -qy, -qw
75
+ end
76
+ a, b, c = qx, qy, qw
77
+ else
78
+ if qw < 0 then
79
+ qx, qy, qz = -qx, -qy, -qz
80
+ end
81
+ a, b, c = qx, qy, qz
82
+ end
83
+
84
+ local qa = clamp(round((a + INV_SQRT2) * Q_SCALE), 0, 1023)
85
+ local qb = clamp(round((b + INV_SQRT2) * Q_SCALE), 0, 1023)
86
+ local qc = clamp(round((c + INV_SQRT2) * Q_SCALE), 0, 1023)
87
+ return bor(lshift(largest, 30), lshift(qa, 20), lshift(qb, 10), qc)
88
+ end
89
+
90
+ -- Reverse Quat.pack into a rotation-only CFrame. Identity for near-zero magnitudes.
91
+ function Quat.unpack(packed: number): CFrame
92
+ local largest = rshift(packed, 30)
93
+ local qa = band(rshift(packed, 20), 0x3FF)
94
+ local qb = band(rshift(packed, 10), 0x3FF)
95
+ local qc = band(packed, 0x3FF)
96
+
97
+ local a = qa * Q_INV - INV_SQRT2
98
+ local b = qb * Q_INV - INV_SQRT2
99
+ local c = qc * Q_INV - INV_SQRT2
100
+
101
+ local sqSum = a * a + b * b + c * c
102
+ local d = sqrt(if sqSum < 1 then 1 - sqSum else 0)
103
+
104
+ local qx: number, qy: number, qz: number, qw: number
105
+ if largest == 0 then
106
+ qx, qy, qz, qw = d, a, b, c
107
+ elseif largest == 1 then
108
+ qx, qy, qz, qw = a, d, b, c
109
+ elseif largest == 2 then
110
+ qx, qy, qz, qw = a, b, d, c
111
+ else
112
+ qx, qy, qz, qw = a, b, c, d
113
+ end
114
+
115
+ local sqLen = qx * qx + qy * qy + qz * qz
116
+ if sqLen < EPSILON_SQ then
117
+ return CFrame.identity
118
+ end
119
+ local angle = 2 * acos(clamp(qw, -1, 1))
120
+ local inv = 1 / sqrt(sqLen)
121
+ return CFrame.fromAxisAngle(Vector3.new(qx * inv, qy * inv, qz * inv), angle)
122
+ end
123
+
124
+ return table.freeze(Quat)
@@ -1,26 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- Cross-module utility helpers.
4
-
5
- -- Public -----------------------------------------------------------------
6
-
7
- local Util = {}
8
-
9
- function Util.isPlayer(value: any): boolean
10
- return typeof(value) == "Instance" and (value :: Instance):IsA("Player")
11
- end
12
-
13
- --[[
14
- O(1) array remove via swap with last element. The slot becomes nil
15
- after this returns; callers that track an external count must
16
- decrement it themselves.
17
- ]]
18
- function Util.swapRemove<T>(arr: { T }, idx: number): ()
19
- local last = #arr
20
- if idx ~= last then
21
- arr[idx] = arr[last]
22
- end
23
- arr[last] = nil
24
- end
25
-
26
- return table.freeze(Util)