Created
July 27, 2022 19:12
-
-
Save dphfox/69e1de145f48906554285cabd1dafa95 to your computer and use it in GitHub Desktop.
Serialisation/deserialisation of values to attribute-safe and storage-safe formats
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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