Skip to content

Instantly share code, notes, and snippets.

@dphfox
Created July 27, 2022 19:12
Show Gist options
  • Save dphfox/69e1de145f48906554285cabd1dafa95 to your computer and use it in GitHub Desktop.
Save dphfox/69e1de145f48906554285cabd1dafa95 to your computer and use it in GitHub Desktop.
Serialisation/deserialisation of values to attribute-safe and storage-safe formats
--[[
Utilities for serialising and deserialising values for safe representation
in limited media, for example when saving to plugin storage or attributes.
(c) Elttob 2022 - Licensed under MIT
]]
local HttpService = game:GetService("HttpService")
local Serde = {}
local NO_TRANSFORM = {
serialise = function(value)
return value
end,
deserialise = function(value)
return value
end,
}
local TRANSFORMERS = {
["number"] = NO_TRANSFORM,
["string"] = NO_TRANSFORM,
["boolean"] = NO_TRANSFORM,
["nil"] = NO_TRANSFORM,
["Axes"] = {
serialise = function(axes)
-- FUTURE: we could pack this into a single ASCII character if we wanted to
local serialised = ""
for _, axisName in {"X", "Y", "Z"} do
serialised ..= if axes[axisName] then "1" else "0"
end
return serialised
end,
deserialise = function(serialised)
local axes = {}
for index, axisName in {"X", "Y", "Z"} do
axes[Enum.Axis[axisName]] = string.sub(serialised, index, index) == "1"
end
return Axes.new(unpack(axes))
end,
},
["BrickColor"] = {
serialise = function(brickcolour)
return brickcolour.Name
end,
deserialise = function(serialised)
return BrickColor.new(serialised)
end,
},
["CFrame"] = {
serialise = function(cframe)
local components = {cframe:GetComponents()}
local parts = {}
for index, component in components do
parts[index] = tostring(component)
end
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local components = {}
for index, part in parts do
components[index] = tonumber(part)
end
return CFrame.new(unpack(components))
end,
},
["Color3"] = {
serialise = function(colour)
return table.concat({tostring(colour.R), tostring(colour.G), tostring(colour.B)}, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Color3.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
end,
},
["ColorSequence"] = {
serialise = function(sequence)
local parts = {}
for _, keypoint in sequence.Keypoints do
table.insert(parts, tostring(keypoint.Time))
table.insert(parts, tostring(keypoint.Value.R))
table.insert(parts, tostring(keypoint.Value.G))
table.insert(parts, tostring(keypoint.Value.B))
end
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local keypoints = {}
for index=1, #parts, 4 do
local time = tonumber(parts[index])
local r = tonumber(parts[index + 1])
local g = tonumber(parts[index + 2])
local b = tonumber(parts[index + 3])
table.insert(keypoints, ColorSequenceKeypoint.new(time, Color3.new(r, g, b)))
end
return ColorSequence.new(keypoints)
end,
},
["ColorSequenceKeypoint"] = {
serialise = function(keypoint)
local parts = {}
table.insert(parts, tostring(keypoint.Time))
table.insert(parts, tostring(keypoint.Value.R))
table.insert(parts, tostring(keypoint.Value.G))
table.insert(parts, tostring(keypoint.Value.B))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local time = tonumber(parts[1])
local r = tonumber(parts[2])
local g = tonumber(parts[3])
local b = tonumber(parts[4])
return ColorSequenceKeypoint.new(time, Color3.new(r, g, b))
end,
},
["DateTime"] = {
serialise = function(date)
return date.UnixTimestampMillis
end,
deserialise = function(serialised)
return DateTime.fromUnixTimestampMillis(serialised)
end,
},
["EnumItem"] = {
serialise = function(enumitem)
local enumName, itemName = tostring(enumitem):match("^Enum%.(.+)%.(.+)$")
return enumName .. " " .. itemName
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Enum[parts[1]][parts[2]]
end,
},
["Faces"] = {
serialise = function(faces)
-- FUTURE: we could pack this into a single ASCII character if we wanted to
local serialised = ""
for _, faceName in {"Top", "Bottom", "Left", "Right", "Back", "Front"} do
serialised ..= if faces[faceName] then "1" else "0"
end
return serialised
end,
deserialise = function(serialised)
local faces = {}
for index, faceName in {"Top", "Bottom", "Left", "Right", "Back", "Front"} do
faces[Enum.NormalId[faceName]] = string.sub(serialised, index, index) == "1"
end
return Faces.new(unpack(faces))
end,
},
["Font"] = {
serialise = function(font)
return HttpService:JSONEncode({
family = font.Family,
weight = font.Weight.Name,
style = font.Style.Name
})
end,
deserialise = function(serialised)
local data = HttpService:JSONDecode(serialised)
return Font.new(data.family, Enum.FontWeight[data.weight], Enum.FontStyle[data.style])
end,
},
["Instance"] = {
needsInstanceIDs = true,
serialise = function(instance, instanceToIDCallback)
return instanceToIDCallback(instance)
end,
deserialise = function(serialised, instanceFromIDCallback)
return instanceFromIDCallback(serialised)
end,
},
["NumberRange"] = {
serialise = function(range)
return range.Min .. " " .. range.Max
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return NumberRange.new(tonumber(parts[1]) :: number, tonumber(parts[2]) :: number)
end,
},
["NumberSequence"] = {
serialise = function(sequence)
local parts = {}
for _, keypoint in sequence.Keypoints do
table.insert(parts, tostring(keypoint.Time))
table.insert(parts, tostring(keypoint.Value))
end
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local keypoints = {}
for index=1, #parts, 2 do
local time = tonumber(parts[index])
local value = tonumber(parts[index + 1])
table.insert(keypoints, NumberSequenceKeypoint.new(time, value))
end
return NumberSequence.new(keypoints)
end,
},
["NumberSequenceKeypoint"] = {
serialise = function(keypoint)
local parts = {}
table.insert(parts, tostring(keypoint.Time))
table.insert(parts, tostring(keypoint.Value))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local time = tonumber(parts[1])
local value = tonumber(parts[2])
return NumberSequenceKeypoint.new(time, value)
end,
},
["PhysicalProperties"] = {
serialise = function(properties)
local parts = {}
table.insert(parts, tostring(properties.Density))
table.insert(parts, tostring(properties.Friction))
table.insert(parts, tostring(properties.Elasticity))
table.insert(parts, tostring(properties.FrictionWeight))
table.insert(parts, tostring(properties.ElasticityWeight))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local density = tonumber(parts[1])
local friction = tonumber(parts[2])
local elasticity = tonumber(parts[3])
local frictionWeight = tonumber(parts[4])
local elasticityWeight = tonumber(parts[5])
return PhysicalProperties.new(density, friction, elasticity, frictionWeight, elasticityWeight)
end,
},
["Ray"] = {
serialise = function(ray)
local parts = {}
table.insert(parts, tostring(ray.Origin.X))
table.insert(parts, tostring(ray.Origin.Y))
table.insert(parts, tostring(ray.Origin.Z))
table.insert(parts, tostring(ray.Direction.X))
table.insert(parts, tostring(ray.Direction.Y))
table.insert(parts, tostring(ray.Direction.Z))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local origin = Vector3.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
local direction = Vector3.new(tonumber(parts[4]), tonumber(parts[5]), tonumber(parts[6]))
return Ray.new(origin, direction)
end,
},
["Rect"] = {
serialise = function(rect)
local parts = {}
table.insert(parts, tostring(rect.Min.X))
table.insert(parts, tostring(rect.Min.Y))
table.insert(parts, tostring(rect.Max.X))
table.insert(parts, tostring(rect.Max.Y))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local min = Vector2.new(tonumber(parts[1]), tonumber(parts[2]))
local max = Vector2.new(tonumber(parts[3]), tonumber(parts[4]))
return Rect.new(min, max)
end,
},
["Region3"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.Min.X))
table.insert(parts, tostring(region.Min.Y))
table.insert(parts, tostring(region.Min.Z))
table.insert(parts, tostring(region.Max.X))
table.insert(parts, tostring(region.Max.Y))
table.insert(parts, tostring(region.Max.Z))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local min = Vector3.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
local max = Vector3.new(tonumber(parts[4]), tonumber(parts[5]), tonumber(parts[6]))
return Region3.new(min, max)
end,
},
["Region3int16"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.Min.X))
table.insert(parts, tostring(region.Min.Y))
table.insert(parts, tostring(region.Min.Z))
table.insert(parts, tostring(region.Max.X))
table.insert(parts, tostring(region.Max.Y))
table.insert(parts, tostring(region.Max.Z))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local min = Vector3int16.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
local max = Vector3int16.new(tonumber(parts[4]), tonumber(parts[5]), tonumber(parts[6]))
return Region3int16.new(min, max)
end,
},
["UDim"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.Scale))
table.insert(parts, tostring(region.Offset))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return UDim.new(tonumber(parts[1]), tonumber(parts[2]))
end,
},
["UDim2"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.X.Scale))
table.insert(parts, tostring(region.X.Offset))
table.insert(parts, tostring(region.Y.Scale))
table.insert(parts, tostring(region.Y.Offset))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
local x = UDim.new(tonumber(parts[1]), tonumber(parts[2]))
local y = UDim.new(tonumber(parts[3]), tonumber(parts[4]))
return UDim2.new(x, y)
end,
},
["Vector2"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.X))
table.insert(parts, tostring(region.Y))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Vector2.new(tonumber(parts[1]), tonumber(parts[2]))
end,
},
["Vector2int16"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.X))
table.insert(parts, tostring(region.Y))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Vector2int16.new(tonumber(parts[1]), tonumber(parts[2]))
end,
},
["Vector3"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.X))
table.insert(parts, tostring(region.Y))
table.insert(parts, tostring(region.Z))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Vector3.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
end,
},
["Vector3int16"] = {
serialise = function(region)
local parts = {}
table.insert(parts, tostring(region.X))
table.insert(parts, tostring(region.Y))
table.insert(parts, tostring(region.Z))
return table.concat(parts, " ")
end,
deserialise = function(serialised)
local parts = string.split(serialised, " ")
return Vector3int16.new(tonumber(parts[1]), tonumber(parts[2]), tonumber(parts[3]))
end,
}
}
function Serde.serialise(value, instanceToIDCallback)
local valueType = typeof(value)
local transformer = TRANSFORMERS[valueType]
assert(transformer ~= nil, "'" .. valueType .. "' types are not serialisable")
if transformer.needsInstanceIDs then
assert(instanceToIDCallback ~= nil, "To serialise '" .. valueType .. "' types, a callback for retrieving instance IDs must be provided")
return {type = valueType, value = TRANSFORMERS[valueType].serialise(value, instanceToIDCallback)}
else
return {type = valueType, value = TRANSFORMERS[valueType].serialise(value)}
end
end
function Serde.deserialise(serialised, instanceFromIDCallback)
local valueType = serialised.type
local transformer = TRANSFORMERS[valueType]
assert(transformer ~= nil, "'" .. valueType .. "' types are not serialisable")
if transformer.needsInstanceIDs then
assert(instanceFromIDCallback ~= nil, "To deserialise '" .. valueType .. "' types, a callback for resolving instance IDs must be provided")
return TRANSFORMERS[valueType].serialise(serialised.value, instanceFromIDCallback)
else
return TRANSFORMERS[valueType].deserialise(serialised.value)
end
end
return Serde
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment