@axpecter/lync 2.3.1 → 2.3.3
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 +93 -35
- package/src/api/Query.luau +2 -2
- package/src/api/Signal.luau +6 -1
- package/src/codec/Base.luau +8 -1
- package/src/codec/composite/Array.luau +293 -17
- package/src/codec/composite/Map.luau +345 -40
- package/src/codec/composite/Optional.luau +4 -0
- package/src/codec/composite/Shared.luau +101 -102
- package/src/codec/composite/Struct.luau +20 -10
- 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/Bitfield.luau +14 -1
- 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 +187 -58
- package/src/internal/Pool.luau +4 -3
- package/src/transport/Reader.luau +19 -8
- package/src/transport/Server.luau +35 -13
- package/src/util/Buffer.luau +2 -4
- package/src/util/Quantize.luau +27 -3
- package/src/util/Quat.luau +124 -0
|
@@ -117,11 +117,24 @@ function Bitfield.define(schema: { [string]: FieldSpec }): Types.InternalCodec<a
|
|
|
117
117
|
local packed = 0
|
|
118
118
|
for i = 1, fieldCount do
|
|
119
119
|
local v = value[fieldKeys[i]]
|
|
120
|
-
|
|
120
|
+
local ft = fieldTypes[i]
|
|
121
|
+
if ft == TYPE_BOOL then
|
|
121
122
|
if v then
|
|
122
123
|
packed = bor(packed, lshift(1, fieldOffsets[i]))
|
|
123
124
|
end
|
|
125
|
+
elseif ft == TYPE_UINT then
|
|
126
|
+
local mask = fieldMasks[i]
|
|
127
|
+
if v < 0 or v > mask or v % 1 ~= 0 then
|
|
128
|
+
Log.error(`field "{fieldKeys[i]}" value {v} out of uint range [0, {mask}]`)
|
|
129
|
+
end
|
|
130
|
+
packed = bor(packed, lshift(v, fieldOffsets[i]))
|
|
124
131
|
else
|
|
132
|
+
local signBit = fieldSignBit[i]
|
|
133
|
+
if v < -signBit or v >= signBit or v % 1 ~= 0 then
|
|
134
|
+
Log.error(
|
|
135
|
+
`field "{fieldKeys[i]}" value {v} out of int range [{-signBit}, {signBit - 1}]`
|
|
136
|
+
)
|
|
137
|
+
end
|
|
125
138
|
packed = bor(packed, lshift(band(v, fieldMasks[i]), fieldOffsets[i]))
|
|
126
139
|
end
|
|
127
140
|
end
|
|
@@ -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)
|
|
@@ -35,7 +35,7 @@ local U32_MAX = Constants.U32_MAX
|
|
|
35
35
|
|
|
36
36
|
-- Private ----------------------------------------------------------------
|
|
37
37
|
|
|
38
|
-
local
|
|
38
|
+
local ensure = Base.ensure
|
|
39
39
|
local band = bit32.band
|
|
40
40
|
local bor = bit32.bor
|
|
41
41
|
local rshift = bit32.rshift
|
|
@@ -46,18 +46,24 @@ local readu8 = buffer.readu8
|
|
|
46
46
|
local readu16 = buffer.readu16
|
|
47
47
|
local readu32 = buffer.readu32
|
|
48
48
|
|
|
49
|
-
local function ensure(ch: Types.ChannelState, n: number): ()
|
|
50
|
-
if ch.cursor + n > ch.size then
|
|
51
|
-
alloc(ch, n)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
49
|
-- Public -----------------------------------------------------------------
|
|
56
50
|
|
|
57
51
|
local Varint = {}
|
|
58
52
|
|
|
59
53
|
Varint.INLINE_MAX = INLINE_MAX
|
|
60
54
|
|
|
55
|
+
-- Wire byte count for a value, without encoding it. Used by cost heuristics.
|
|
56
|
+
function Varint.length(value: number): number
|
|
57
|
+
if value <= INLINE_MAX then
|
|
58
|
+
return 1
|
|
59
|
+
elseif value <= TWO_MAX then
|
|
60
|
+
return 2
|
|
61
|
+
elseif value <= THREE_MAX then
|
|
62
|
+
return 3
|
|
63
|
+
end
|
|
64
|
+
return 5
|
|
65
|
+
end
|
|
66
|
+
|
|
61
67
|
function Varint.write(ch: Types.ChannelState, value: number): ()
|
|
62
68
|
if value < 0 or value > U32_MAX or value % 1 ~= 0 then
|
|
63
69
|
Log.error(`value must be an integer in [0, {U32_MAX}], got {value}`)
|
|
@@ -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
|