Example
Note: Roblox doesn’t yet support binary data to be saved to the DataStore, as discussed here, so you would have to encode the buffer as Base64 before saving it e.g. this repo
local LP_EPSILON = 1e-6
local I16_PRECISION = 32767
-- i.e flags for CFrame datatype, learn more:
-- - https://www.alanzucconi.com/2015/07/26/enum-flags-and-bitwise-operators/
-- - https://turnerj.com/blog/fun-with-flags-enums-and-bit-shifting
local sigFlags = {
-- ---------------------
-- | BINARY | Decimal |
-- ---------------------
None = 0, -- | 0000000 | 0 |
Coordinate = 1, -- | 0000001 | 1 |
Positional = bit32.lshift(1, 1), -- | 0000010 | 2 |
Rotational = bit32.lshift(1, 2), -- | 0000100 | 4 |
X = bit32.lshift(1, 3), -- | 0001000 | 8 |
Y = bit32.lshift(1, 4), -- | 0010000 | 16 |
Z = bit32.lshift(1, 5), -- | 0100000 | 32 |
Component = bit32.lshift(1, 6), -- | 1000000 | 64 |
-- ---------------------
}
-- i.e. maps a sigFlag to a quaternion index, i.e. 1 = x; 2 = y, 3 = z
local quatIndex = {
[sigFlags.X] = 1,
[sigFlags.Y] = 2,
[sigFlags.Z] = 3,
}
-- i.e. maps a quaternion index (1 = x) to a sigflag
local quatRelative = {
[1] = sigFlags.X,
[2] = sigFlags.Y,
[3] = sigFlags.Z,
}
-- e.g. some custom datatype you could define
local customDatatypes = {
-- e.g. some custom datatype(s)
GridAlignedData = 'table', -- blahblah
}
--[=[
Computes a normalised quaternion given a CFrame
@param cf CFrame -- a CFrame value
@return varargs<...> -- normalised components of a quaternion
]=]
local function computeNormalisedQuaternion(cf)
local axis, angle = cf:ToAxisAngle()
local x = axis.X
local y = axis.Y
local z = axis.Z
local d = x*x + y*y + z*z
if d > LP_EPSILON then
d = 1 / math.sqrt(d)
x *= d
y *= d
z *= d
else
x = 1
y = 0
z = 0
end
local ha = angle*0.5
local sha = math.sin(ha)
local w = math.cos(ha)
x = sha * x
y = sha * y
z = sha * z
d = x*x + y*y + z*z + w*w
if d > 0 then
d = 1 / math.sqrt(d)
return x*d, y*d, z*d, w*d
end
return 0, 0, 0, 1
end
--[=[
Compress a quaternion into { 1x u8, ... 3x i16 }
@param qx number -- x component of a quaternion
@param qy number -- y component of a quaternion
@param qz number -- z component of a quaternion
@param qw number -- w component of a quaternion
@param qw boolean -- (optional) boolean to det. whether we will
attempt to compress the quaternion such that
its qi component represents a quaternion's index
if its value is approx. 1
@return varargs<...> -- the compressed quaternion components
]=]
local function compressQuaternion(qx, qy, qz, qw, useMinimal)
local index = -1
local value = -math.huge
local element, v0, v1, v2
for i = 1, 4, 1 do
local val = select(i, qx, qy, qz, qw)
local abs = math.abs(val)
if abs > value then
index = i
value = abs
element = val
end
end
-- i.e. if 1x component is approx. 1 then we can reduce to single u8; since rotation (w, x, y, z) = -1*(w, x, y, z)
if useMinimal and math.abs(1 - value) < LP_EPSILON then
return index + 4
end
local sign = element >= 0 and 1 or -1
if index == 1 then
v0 = math.floor(qy * sign * I16_PRECISION + 0.5)
v1 = math.floor(qz * sign * I16_PRECISION + 0.5)
v2 = math.floor(qw * sign * I16_PRECISION + 0.5)
elseif index == 2 then
v0 = math.floor(qx * sign * I16_PRECISION + 0.5)
v1 = math.floor(qz * sign * I16_PRECISION + 0.5)
v2 = math.floor(qw * sign * I16_PRECISION + 0.5)
elseif index == 3 then
v0 = math.floor(qx * sign * I16_PRECISION + 0.5)
v1 = math.floor(qy * sign * I16_PRECISION + 0.5)
v2 = math.floor(qw * sign * I16_PRECISION + 0.5)
elseif index == 4 then
v0 = math.floor(qx * sign * I16_PRECISION + 0.5)
v1 = math.floor(qy * sign * I16_PRECISION + 0.5)
v2 = math.floor(qz * sign * I16_PRECISION + 0.5)
end
return index, v0, v1, v2
end
--[=[
Compress a quaternion into { 1x u8, ... 3x i16 }
@param cf CFrame -- a CFrame value
@param qw boolean -- (optional) boolean to det. whether we will
attempt to compress the quaternion such that
its qi component represents a quaternion's index
if its value is approx. 1
@return varargs<...> -- the compressed quaternion components
]=]
local function compressQuaternionFromCFrame(cframe, useMinimal)
local qx, qy, qz, qw = computeNormalisedQuaternion(cframe)
return compressQuaternion(qx, qy, qz, qw, useMinimal)
end
--[=[
Attempts to derive a valid quaternion from a compressed quaternion
as given by the `::compressQuaternion` and `::compressQuaternionFromCFrame` method
@param ... varargs<...> -- compress components
@return varargs<...> -- the x, y, z and w components of the inflated quaternion
]=]
local function decompressQuaternion(qi, q0, q1, q2)
-- decompress minimal where index describes one q component that's 1
if qi > 4 then
if qi == 5 then
return 1, 0, 0, 0
elseif qi == 6 then
return 0, 1, 0, 0
elseif qi == 7 then
return 0, 0, 1, 0
elseif qi == 8 then
return 0, 0, 0, 1
end
end
-- decompress components
q0 /= I16_PRECISION
q1 /= I16_PRECISION
q2 /= I16_PRECISION
local d = math.sqrt(1 - (q0*q0 + q1*q1 + q2*q2))
if qi == 1 then
return d, q0, q1, q2
elseif qi == 2 then
return q0, d, q1, q2
elseif qi == 3 then
return q0, q1, d, q2
end
return q0, q1, q2, d
end
----------------------------------------------
-- --
-- EXAMPLE --
-- --
----------------------------------------------
--[[
[!] Note:
The 'writeExampleBuffer' could do with some work here:
1. you should be iterating through the values and determining the len of your
buffer first before creating it
OR;
2. You should be dynamically resizing your buffer
The same can be said for the read example which could be expanded to
read a u8 that would describe the datatype; the reader would read that flag
before decoding the value
]]
------
--[=[
Attempts to write the given value to the buffer according to its datatype
@param datatype string -- the desired datatype
@param value any -- the value to write
@return (1) boolean -- reflects success state
(2) buffer | string -- either (a) the resulting buffer or (b) an error message
]=]
local function writeExampleBuffer(datatype, value)
local t = typeof(datatype)
if t ~= 'string' then
return false, string.format('Expected string for DataType but got %q', t)
end
local trueDataType = customDatatypes[datatype] or datatype
t = typeof(value)
if datatype ~= t then
return false, string.format(
'TypeMismatch: expected value as type %q but got %q',
datatype, t
)
end
local buf
if datatype == 'CFrame' then
local vector = value.Position
local qi, q0, q1, q2 = compressQuaternionFromCFrame(value, true)
local hasPositionalData = vector.Magnitude >= LP_EPSILON
local hasRotationalData = qi <= 4
local sg, sz
if hasPositionalData and hasRotationalData then
sg = sigFlags.Coordinate
sz = 19
elseif hasPositionalData then
sg = sigFlags.Positional
sz = 13
elseif hasRotationalData then
sg = sigFlags.Rotational
sz = 7
else
sg = sigFlags.None
sz = 1
end
local index
if qi > 4 then
index = quatRelative[qi - 4]
if index then
index = bit32.bor(index, sigFlags.Component)
end
else
index = quatRelative[qi]
end
if index then
sg = bit32.bor(sg, index)
end
local offset = 1
buf = buffer.create(sz)
buffer.writeu8(buf, 0, sg)
if hasPositionalData then
buffer.writef32(buf, offset + 0, value.X)
buffer.writef32(buf, offset + 4, value.Y)
buffer.writef32(buf, offset + 8, value.Z)
offset += 12
end
if hasRotationalData then
buffer.writei16(buf, offset + 0, q0)
buffer.writei16(buf, offset + 2, q1)
buffer.writei16(buf, offset + 4, q2)
offset += 6
end
else
-- e.g. some other datatype
end
if buf then
return true, buf
end
return false, string.format('NotImplementedError: unknown datatype %q', datatype)
end
--[=[
Attempts to read the given datatype from the given buffer at the given offset
@param buf buffer -- the buffer to read from
@param datatype string -- the desired datatype to read
@param offset number -- (optional) buffer offset index
@return (1) boolean -- reflects success state
(2) buffer | string -- either (a) the resulting value or (b) an error message
(3) number -- the offset after reading the value
]=]
local function readExampleBuffer(buf, datatype, offset)
local t = typeof(buf)
if t ~= 'buffer' then
return false, string.format('Expected BufferObject for buffer but got %q', t), offset
end
t = typeof(datatype)
if t ~= 'string' then
return false, string.format('Expected string for DataType but got %q', t), offset
end
t = typeof(offset)
if t == 'nil' then
offset = 0
elseif t ~= 'number' then
return false, string.format('Expected number|nil for offset but got %q', t), offset
end
if datatype == 'CFrame' then
local sig = buffer.readu8(buf, offset + 0)
offset += 1
local coordinate
local x, y, z
local qi, q0, q1, q2
local qx, qy, qz, qw
local isComponent = bit32.band(sig, sigFlags.Component) == sigFlags.Component
if isComponent then
sig = bit32.band(sig, bit32.bnot(sigFlags.Component))
end
if bit32.band(sig, sigFlags.Coordinate) == sigFlags.Coordinate then
-- some coordinate frame incl. position + rotation
sig = bit32.band(sig, bit32.bnot(sigFlags.Coordinate))
qi = sig > 0 and quatIndex[sig] or 4
qi = isComponent and qi + 4 or qi
x = buffer.readf32(buf, offset + 0)
y = buffer.readf32(buf, offset + 4)
z = buffer.readf32(buf, offset + 8)
q0 = buffer.readi16(buf, offset + 12)
q1 = buffer.readi16(buf, offset + 14)
q2 = buffer.readi16(buf, offset + 16)
qx, qy, qz, qw = decompressQuaternion(qi, q0, q1, q2)
coordinate = CFrame.new(x, y, z, qx, qy, qz, qw)
offset += 18
elseif bit32.band(sig, sigFlags.Positional) == sigFlags.Positional then
-- some positional coordinate +/- rotation
sig = bit32.band(sig, bit32.bnot(sigFlags.Positional))
qi = sig > 0 and quatIndex[sig] + 4 or 8
x = buffer.readf32(buf, offset + 0)
y = buffer.readf32(buf, offset + 4)
z = buffer.readf32(buf, offset + 8)
qx, qy, qz, qw = decompressQuaternion(qi)
coordinate = CFrame.new(x, y, z, qx, qy, qz, qw)
offset += 12
elseif bit32.band(sig, sig) ~= sigFlags.None then
-- some rotational coordinate
if bit32.band(sig, sigFlags.Rotational) == sigFlags.Rotational then
sig = bit32.band(sig, bit32.bnot(sigFlags.Rotational))
qi = sig > 0 and quatIndex[sig] or 4
qi = isComponent and qi + 4 or qi
else
qi = sig > 0 and quatIndex[sig] or 0
qi = isComponent and qi + 4 or qi
end
if qi <= 4 then
q0 = buffer.readi16(buf, offset + 0)
q1 = buffer.readi16(buf, offset + 2)
q2 = buffer.readi16(buf, offset + 4)
offset += 6
end
qx, qy, qz, qw = decompressQuaternion(qi, q0, q1, q2)
coordinate = CFrame.new(0, 0, 0, qx, qy, qz, qw)
else
-- identity
coordinate = CFrame.identity
end
return true, coordinate, offset
else
-- e.g. some other datatype
end
return false, string.format('NotImplementedError: unknown datatype %q', datatype), offset
end
----------------------------------------------
-- --
-- DEMO --
-- --
----------------------------------------------
-- e.g. some wrapper function around your read/write methods ...
local function someWrapperFunction(target)
local success, result = writeExampleBuffer('CFrame', target)
if success then
success, result = readExampleBuffer(result, 'CFrame', 0)
if success then
return true, { Target = target, Result = result }
end
end
return false, result or 'Unknown error occurred'
end
-- [?] Note:
-- The following is wrapped in a function because I tested it using a storybook,
-- just rip out the contents and use it in some script to test it
--
return function ()
local exampleCFrame = CFrame.new(0, 5, 0) * CFrame.Angles(math.pi*0.25, 0, -math.pi*0.25)
local success, result = someWrapperFunction(exampleCFrame)
if not success then
warn(result)
return
end
local part = Instance.new('Part')
part.Size = Vector3.one + Vector3.zAxis
part.Shape = Enum.PartType.Block
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Anchored = true
local parts = { }
for name, cframe in next, result do
local p = part:Clone()
p.Name = name
p.CFrame = cframe
p.BrickColor = BrickColor.Random()
p.Parent = workspace
table.insert(parts, p)
end
return function ()
for i, v in next, parts do
pcall(v.Destroy, v)
end
end
end