@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.
- package/README.md +29 -19
- package/package.json +1 -1
- package/src/Types.luau +12 -6
- package/src/api/Packet.luau +95 -35
- package/src/codec/Base.luau +7 -0
- package/src/codec/composite/Array.luau +274 -15
- package/src/codec/composite/Map.luau +328 -39
- package/src/codec/composite/Optional.luau +4 -0
- package/src/codec/composite/Shared.luau +60 -81
- package/src/codec/composite/Struct.luau +13 -3
- package/src/codec/composite/Tagged.luau +8 -0
- package/src/codec/composite/Tuple.luau +12 -1
- package/src/codec/datatype/Buffer.luau +1 -3
- package/src/codec/datatype/CFrame.luau +6 -106
- package/src/codec/meta/DeltaScalar.luau +390 -0
- package/src/codec/primitive/Varint.luau +13 -7
- package/src/codec/primitive/Zint.luau +99 -0
- package/src/index.d.ts +46 -0
- package/src/init.luau +7 -0
- package/src/internal/Channel.luau +60 -25
- package/src/transport/Reader.luau +16 -5
- package/src/util/Buffer.luau +2 -4
- package/src/util/Quantize.luau +27 -3
- package/src/util/Quat.luau +124 -0
|
@@ -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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/util/Buffer.luau
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/util/Quantize.luau
CHANGED
|
@@ -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
|
|
36
|
-
Log.error(`{apiName}: min ({rangeMin}) must be
|
|
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)
|