@axpecter/lync 2.3.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.
@@ -0,0 +1,99 @@
1
+ --!strict
2
+ --!native
3
+ -- Signed varint via zigzag mapping. Small magnitudes pack in 1 byte.
4
+
5
+ local Log = require(script.Parent.Parent.Parent.util.Log)
6
+ local Types = require(script.Parent.Parent.Parent.Types)
7
+ local Varint = require(script.Parent.Parent.primitive.Varint)
8
+
9
+ -- Constants --------------------------------------------------------------
10
+
11
+ --[[
12
+ Zigzag fold maps signed values onto unsigned varint space:
13
+ n >= 0 -> 2n
14
+ n < 0 -> -2n - 1
15
+ Inverse: low bit selects sign, shift down 1 bit.
16
+
17
+ Wire ranges (using Varint dense form):
18
+ 1 byte: [-96, 95]
19
+ 2 bytes: [-4192, 4191]
20
+ 3 bytes: [-528480, 528479]
21
+ 5 bytes: full i32
22
+ ]]
23
+ local I32_MIN = -0x80000000
24
+ local I32_MAX = 0x7FFFFFFF
25
+
26
+ -- Private ----------------------------------------------------------------
27
+
28
+ local floor = math.floor
29
+ local band = bit32.band
30
+ local varintWrite = Varint.write
31
+ local varintRead = Varint.read
32
+
33
+ -- Signed -> unsigned. Hot path; called per write of every delta scalar.
34
+ local function encode(value: number): number
35
+ if value >= 0 then
36
+ return value * 2
37
+ end
38
+ return value * -2 - 1
39
+ end
40
+
41
+ -- Inverse of encode. Even -> n/2; odd -> -((n+1)/2). Branchless on the half.
42
+ local function decode(raw: number): number
43
+ local low = band(raw, 1)
44
+ local half = (raw - low) / 2
45
+ return if low == 1 then -half - 1 else half
46
+ end
47
+
48
+ -- Public -----------------------------------------------------------------
49
+
50
+ local Zint = {}
51
+
52
+ Zint.encode = encode
53
+ Zint.decode = decode
54
+
55
+ --[[
56
+ Variable-length signed integer in [I32_MIN, I32_MAX]. Optional [min, max]
57
+ bounds gate input at write time so out-of-range payloads error early
58
+ rather than corrupting downstream decoders.
59
+ ]]
60
+ function Zint.zint(min: number?, max: number?): Types.InternalCodec<number>
61
+ local lo = min or I32_MIN
62
+ local hi = max or I32_MAX
63
+ if lo > hi then
64
+ Log.error(`min ({lo}) must be <= max ({hi})`)
65
+ end
66
+ if floor(lo) ~= lo or floor(hi) ~= hi then
67
+ Log.error("bounds must be integers")
68
+ end
69
+ if lo < I32_MIN or hi > I32_MAX then
70
+ Log.error(`range [{lo}, {hi}] exceeds i32 bounds`)
71
+ end
72
+
73
+ return table.freeze({
74
+ _typeCheck = "number",
75
+ _isInteger = true,
76
+ _min = lo,
77
+ _max = hi,
78
+
79
+ write = function(ch: Types.ChannelState, value: number): ()
80
+ if value < lo or value > hi then
81
+ Log.error(`value {value} out of range [{lo}, {hi}]`)
82
+ end
83
+ if floor(value) ~= value then
84
+ Log.error(`value {value} must be an integer`)
85
+ end
86
+ varintWrite(ch, encode(value))
87
+ end,
88
+
89
+ read = function(src: buffer, pos: number, _refs: { Instance }?): (number, number)
90
+ local raw, consumed = varintRead(src, pos)
91
+ if consumed == 0 then
92
+ Log.error("truncated zint")
93
+ end
94
+ return decode(raw), consumed
95
+ end,
96
+ }) :: Types.InternalCodec<number>
97
+ end
98
+
99
+ return table.freeze(Zint)
package/src/index.d.ts CHANGED
@@ -265,12 +265,58 @@ interface LyncModule {
265
265
  // ── Number codecs ───────────────────────────────────────────────────
266
266
 
267
267
  int(this: void, min: number, max: number): Lync.Codec<number>;
268
+ /**
269
+ * Variable-length signed int via zigzag varint. 1 byte for values in
270
+ * [-96, 95]; up to 5 bytes for full i32. Optional bounds gate input.
271
+ */
272
+ zint(this: void, min?: number, max?: number): Lync.Codec<number>;
268
273
  float(this: void, min: number, max: number, precision: number): Lync.Codec<number>;
269
274
  readonly f16: Lync.Codec<number>;
270
275
  readonly f32: Lync.Codec<number>;
271
276
  readonly f64: Lync.Codec<number>;
272
277
  readonly bool: Lync.Codec<boolean>;
273
278
 
279
+ // ── Delta scalars (reliable transport only) ─────────────────────────
280
+
281
+ /**
282
+ * Integer that emits zigzag varint of (current - previous). Reliable
283
+ * transport only; a dropped frame desyncs the receiver. Best for ints
284
+ * mutating slowly across a wide range (saves vs fixed u16/u24/u32).
285
+ */
286
+ deltaInt(this: void, min: number, max: number): Lync.Codec<number>;
287
+ /**
288
+ * Quantized float with per-frame diff in integer wire space (no drift).
289
+ * Same wire-size profile as deltaInt. Reliable transport only.
290
+ */
291
+ deltaFloat(
292
+ this: void,
293
+ min: number,
294
+ max: number,
295
+ precision: number,
296
+ ): Lync.Codec<number>;
297
+ /**
298
+ * Quantized Vector3 with per-axis zigzag varint diffs. ~3 bytes for
299
+ * unchanged, 3-15 bytes for typical motion vs 12 bytes baseline.
300
+ * Reliable transport only.
301
+ */
302
+ deltaVec3(
303
+ this: void,
304
+ min: number,
305
+ max: number,
306
+ precision: number,
307
+ ): Lync.Codec<Vector3>;
308
+ /**
309
+ * Quantized position + smallest-three quaternion rotation. 1 byte for
310
+ * fully static; 4-7 bytes for position-only motion; up to 13 bytes
311
+ * for full pose changes vs 24 bytes baseline. Reliable transport only.
312
+ */
313
+ deltaCFrame(
314
+ this: void,
315
+ posMin: number,
316
+ posMax: number,
317
+ posPrecision: number,
318
+ ): Lync.Codec<CFrame>;
319
+
274
320
  // ── String & buffer ─────────────────────────────────────────────────
275
321
 
276
322
  readonly string: Lync.StringCodec;
package/src/init.luau CHANGED
@@ -27,6 +27,7 @@ local BufferC = require(script.codec.datatype.Buffer)
27
27
  local CFrameC = require(script.codec.datatype.CFrame)
28
28
  local ColorC = require(script.codec.datatype.Color)
29
29
  local CustomC = require(script.codec.meta.Custom)
30
+ local DeltaScalarC = require(script.codec.meta.DeltaScalar)
30
31
  local EnumC = require(script.codec.meta.Enum)
31
32
  local Float16C = require(script.codec.primitive.Float16)
32
33
  local FloatC = require(script.codec.meta.Float)
@@ -49,6 +50,7 @@ local TupleC = require(script.codec.composite.Tuple)
49
50
  local UDimC = require(script.codec.datatype.UDim)
50
51
  local UnknownC = require(script.codec.meta.Unknown)
51
52
  local VectorC = require(script.codec.datatype.Vector)
53
+ local ZintC = require(script.codec.primitive.Zint)
52
54
 
53
55
  -- Public types -----------------------------------------------------------
54
56
 
@@ -313,7 +315,12 @@ Lync.debug = table.freeze({
313
315
  })
314
316
 
315
317
  Lync.int = IntC.int
318
+ Lync.zint = ZintC.zint
316
319
  Lync.float = FloatC.float
320
+ Lync.deltaInt = DeltaScalarC.deltaInt
321
+ Lync.deltaFloat = DeltaScalarC.deltaFloat
322
+ Lync.deltaVec3 = DeltaScalarC.deltaVec3
323
+ Lync.deltaCFrame = DeltaScalarC.deltaCFrame
317
324
  Lync.f16 = Float16C.f16
318
325
  Lync.f32 = NumberC.f32
319
326
  Lync.f64 = NumberC.f64
@@ -48,6 +48,7 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
48
48
  -- Private ----------------------------------------------------------------
49
49
 
50
50
  local alloc = Base.alloc
51
+ local ensure = Base.ensure
51
52
  local band = bit32.band
52
53
  local bor = bit32.bor
53
54
  local bufCopy = buffer.copy
@@ -57,12 +58,6 @@ local writef64 = buffer.writef64
57
58
  local round = math.round
58
59
  local osClock = os.clock
59
60
 
60
- local function ensure(ch: Types.ChannelState, n: number): ()
61
- if ch.cursor + n > ch.size then
62
- alloc(ch, n)
63
- end
64
- end
65
-
66
61
  local function writeTimestamp(b: buffer, off: number, mode: number): ()
67
62
  if mode == TS_FRAME then
68
63
  writeu8(b, off, _frameCounter)
@@ -89,10 +84,7 @@ local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, heade
89
84
  ensure(ch, 2)
90
85
  -- Re-bind: ensure() may have realloced the buffer.
91
86
  local b = ch.buff
92
-
93
- if payloadLen > 0 then
94
- bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
95
- end
87
+ bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
96
88
 
97
89
  ch.countPos = afterTs
98
90
  writeu16(b, afterTs, 0)
@@ -100,36 +92,76 @@ local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, heade
100
92
  ch.singleMode = false
101
93
  end
102
94
 
95
+ -- itemCount bump is left to the caller, AFTER the body write succeeds.
96
+ local function openOrUpgrade(ch: Types.ChannelState, reg: Types.Registration): ()
97
+ if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
98
+ reg._openFn(ch, reg.id)
99
+ elseif ch.singleMode then
100
+ upgradeSingleToMulti(ch, reg.id, reg.headerSize)
101
+ end
102
+ end
103
+
104
+ local function spliceEncoded(ch: Types.ChannelState, payload: buffer, payloadLen: number): ()
105
+ local cursor = ch.cursor
106
+ if cursor + payloadLen > ch.size then
107
+ alloc(ch, payloadLen)
108
+ end
109
+ bufCopy(ch.buff, cursor, payload, 0, payloadLen)
110
+ ch.cursor = cursor + payloadLen
111
+ end
112
+
103
113
  --[[
104
- Two writeBatch implementations, swapped at startup by enableStats(). For
105
- high-rate channels this saves ~1 register read + 1 compare + 1 jump per
106
- item versus branching on `_statsEnabled` inside the loop.
114
+ Hot-swapped by enableStats() so the per-item dispatch avoids a runtime
115
+ branch on the stats flag. itemCount bumps AFTER codec.write so a
116
+ throwing codec leaves the count consistent with the buffer.
107
117
  ]]
108
118
  local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
109
119
  local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
110
120
 
111
121
  writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
112
- if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
113
- reg._openFn(ch, reg.id)
114
- elseif ch.singleMode then
115
- upgradeSingleToMulti(ch, reg.id, reg.headerSize)
116
- end
117
- ch.itemCount += 1
122
+ openOrUpgrade(ch, reg)
118
123
  reg.codec.write(ch, data)
124
+ ch.itemCount += 1
119
125
  end
120
126
 
121
127
  writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
122
- if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
123
- reg._openFn(ch, reg.id)
124
- elseif ch.singleMode then
125
- upgradeSingleToMulti(ch, reg.id, reg.headerSize)
126
- end
127
- ch.itemCount += 1
128
+ openOrUpgrade(ch, reg)
128
129
  local before = ch.cursor
129
130
  reg.codec.write(ch, data)
131
+ ch.itemCount += 1
130
132
  reg.bytesSent += ch.cursor - before
131
133
  end
132
134
 
135
+ --[[
136
+ Splice a pre-encoded payload into the channel. Used by Packet.broadcast
137
+ to encode the codec output once per fanout instead of N times.
138
+ ]]
139
+ local writeBatchEncodedPlain: (Types.ChannelState, Types.Registration, buffer, number) -> ()
140
+ local writeBatchEncodedStats: (Types.ChannelState, Types.Registration, buffer, number) -> ()
141
+
142
+ writeBatchEncodedPlain = function(
143
+ ch: Types.ChannelState,
144
+ reg: Types.Registration,
145
+ payload: buffer,
146
+ payloadLen: number
147
+ ): ()
148
+ openOrUpgrade(ch, reg)
149
+ spliceEncoded(ch, payload, payloadLen)
150
+ ch.itemCount += 1
151
+ end
152
+
153
+ writeBatchEncodedStats = function(
154
+ ch: Types.ChannelState,
155
+ reg: Types.Registration,
156
+ payload: buffer,
157
+ payloadLen: number
158
+ ): ()
159
+ openOrUpgrade(ch, reg)
160
+ spliceEncoded(ch, payload, payloadLen)
161
+ ch.itemCount += 1
162
+ reg.bytesSent += payloadLen
163
+ end
164
+
133
165
  -- Public -----------------------------------------------------------------
134
166
 
135
167
  -- Module table is intentionally unfrozen: enableStats() hot-swaps writeBatch.
@@ -252,6 +284,7 @@ function Channel.resolveOpenFn(
252
284
  end
253
285
 
254
286
  Channel.writeBatch = writeBatchPlain
287
+ Channel.writeBatchEncoded = writeBatchEncodedPlain
255
288
 
256
289
  --[[
257
290
  Every caller (Server.flushReliable / flushUnreliable, Client.flush)
@@ -335,6 +368,7 @@ end
335
368
  function Channel.enableStats(): ()
336
369
  _statsEnabled = true
337
370
  Channel.writeBatch = writeBatchStats
371
+ Channel.writeBatchEncoded = writeBatchEncodedStats
338
372
  end
339
373
 
340
374
  function Channel.statsEnabled(): boolean
@@ -350,6 +384,7 @@ function Channel.resetGlobals(): ()
350
384
  _offsetMs = 0
351
385
  _clock = 0.0
352
386
  Channel.writeBatch = writeBatchPlain
387
+ Channel.writeBatchEncoded = writeBatchEncodedPlain
353
388
  end
354
389
 
355
390
  return Channel
@@ -207,17 +207,26 @@ function Reader.process(
207
207
  local doGate = isServer and reg.needsGate and player ~= nil
208
208
  local regSignal = reg.signal
209
209
  local regName = reg.name
210
+ -- Lync.nothing reads zero bytes; bypass the pos/consumed gates that assume forward progress.
211
+ local isZeroSize = sizeMeta == 0
210
212
 
211
213
  for _ = 1, count do
212
- if pos >= incomingLen then
214
+ if not isZeroSize and pos >= incomingLen then
213
215
  Log.warn(`codec for "{regName}" ran past frame end; aborting`)
214
216
  return false
215
217
  end
216
218
  local value, consumed = regCodec.read(incoming, pos, refs)
217
- if consumed == 0 then
219
+ if not isZeroSize and consumed == 0 then
218
220
  Log.warn(`codec for "{regName}" reported 0 bytes; aborting`)
219
221
  return false
220
222
  end
223
+ -- Fixed-size codecs MUST report consumed == _size or `pos` desyncs silently.
224
+ if sizeMeta and consumed ~= sizeMeta then
225
+ Log.warn(
226
+ `codec for "{regName}" consumed {consumed} bytes, expected {sizeMeta}; aborting`
227
+ )
228
+ return false
229
+ end
221
230
  pos += consumed
222
231
  if pos > incomingLen then
223
232
  Log.warn(`codec for "{regName}" overran frame end; aborting`)
@@ -266,13 +275,15 @@ function Reader.process(
266
275
  end
267
276
  reg.signal:fire(nil, player, corrId)
268
277
  else
269
- if pos >= incomingLen then
278
+ local regCodec = reg.codec
279
+ -- Mirror the packet path: 0-byte response codecs (Lync.nothing) bypass the gates.
280
+ local qIsZeroSize = regCodec._size == 0
281
+ if not qIsZeroSize and pos >= incomingLen then
270
282
  Log.warn(`query "{reg.name}" body missing; aborting`)
271
283
  return false
272
284
  end
273
- local regCodec = reg.codec
274
285
  local value, consumed = regCodec.read(incoming, pos, refs)
275
- if consumed == 0 then
286
+ if not qIsZeroSize and consumed == 0 then
276
287
  Log.warn(`query codec for "{reg.name}" reported 0 bytes; aborting`)
277
288
  return false
278
289
  end
@@ -46,11 +46,9 @@ function Buf.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: n
46
46
  return true
47
47
  end
48
48
 
49
- function Buf.snapshot(src: buffer, len: number): buffer
49
+ function Buf.snapshot(src: buffer, len: number, srcOff: number?): buffer
50
50
  local out = bufCreate(len)
51
- if len > 0 then
52
- bufCopy(out, 0, src, 0, len)
53
- end
51
+ bufCopy(out, 0, src, srcOff or 0, len)
54
52
  return out
55
53
  end
56
54
 
@@ -10,12 +10,34 @@ local ceil = math.ceil
10
10
  local maxN = math.max
11
11
  local minN = math.min
12
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
+
13
35
  -- Public -----------------------------------------------------------------
14
36
 
15
37
  local Quantize = {}
16
38
 
17
39
  --[[
18
- Pick the narrowest u8/u16/u32 wire form for [rangeMin, rangeMax] at
40
+ Pick the narrowest u8/u16/u24/u32 wire form for [rangeMin, rangeMax] at
19
41
  `precision`. Returns (compBytes, wfn, rfn, scale, invScale, delta).
20
42
  `apiName` shows in error messages so callers see their public surface.
21
43
  ]]
@@ -32,8 +54,8 @@ function Quantize.setup(
32
54
  number,
33
55
  number
34
56
  )
35
- if rangeMin > rangeMax then
36
- Log.error(`{apiName}: min ({rangeMin}) must be <= max ({rangeMax})`)
57
+ if rangeMin >= rangeMax then
58
+ Log.error(`{apiName}: min ({rangeMin}) must be < max ({rangeMax})`)
37
59
  end
38
60
  if precision <= 0 then
39
61
  Log.error(`{apiName}: precision must be positive, got {precision}`)
@@ -50,6 +72,8 @@ function Quantize.setup(
50
72
  compBytes, wfn, rfn = 1, buffer.writeu8, buffer.readu8
51
73
  elseif maxInt <= 0xFFFF then
52
74
  compBytes, wfn, rfn = 2, buffer.writeu16, buffer.readu16
75
+ elseif maxInt <= 0xFFFFFF then
76
+ compBytes, wfn, rfn = 3, writeu24, readu24
53
77
  else
54
78
  compBytes, wfn, rfn = 4, buffer.writeu32, buffer.readu32
55
79
  end
@@ -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)