Overview
When browsing the DevForum, I frequently come across posts asking how to save ObjectValues, CFrames, Instances, etc. to DataStores. I went ahead and made a public module to help streamline the serialization process since it can be a little bit troubling for beginners who are trying to figure out how to save things to DataStores but only end up with errors when they try to save incompatible datatypes. This can be a great tool for productive developers as well since it has many different use-cases, ranging from a dedicated data management utility all the way to something like Studio plugins.
There are a couple other existing modules on the Marketplace that achieve converting instance data for DataStores. However, I found that they had a mixture of missing features, ran into compatibility errors, and/or didn’t offer much in terms of customization. I also wanted to make something tailored for the Insert Object+ plugin as well as something that is fully compatible with the ModuleScripts from the Instance to ModuleScript Converter plugin.
Creator Marketplace Link
Converter Module: Instance ↔ Table
https://www.roblox.com/library/9768577563/Converter-Module
Features
- Supports the DataStore/DataStore2 format.
- Supports object variables†.
- Supports Workspace and Lighting properties.
- Can be used without HTTP Service.
- Many customizable settings available.
- Complete backwards compatibility.
- Full attribute support.
- Supports ALL data types (CFrame, String, Vector3, UDim2, Ray, Faces, etc.)
- Compatible with the ModuleScripts created from the Instance to ModuleScript Converter plugin.
Use Good DataStore Practices
You should only store objects in DataStores if they are unique objects. I do NOT recommend storing more than what is absolutely necessary in DataStores.
If your object is not unique, instead of saving an object to a DataStore, save a string (the name of the object) and find that model/object in your preferred storage location using that string when loading the data.
It should be OK to save individual or small groups of instances like accessories, clothing, values, and parts since they take up little save data. But for objects with many different instances inside of them like models, you should only save them to DataStores if the player is using something like a building sandbox where they have the ability to drastically modify the model.
How To’s
How to Convert a Table/Instance
Converting to Table:
local ConverterModule = require(9768577563) --Define and require the Module.
local InstanceToConvert = game.Workspace.Baseplate --Define an instance.
local ConvertedTable = ConverterModule:Convert(InstanceToConvert) --Create a variable for the converted data.
print(ConvertedTable) --Do stuff with the converted data.
Converting to Instance:
local ConverterModule = require(9768577563) --Define and require the Module.
local TableToConvert = {a = "hello", b = "world", c = 123} --Define a table.
local ConvertedInstance = ConverterModule:Convert(TableToConvert) --Create a variable for the converted data.
if ConvertedInstance then
ConvertedInstance.Parent = game.Workspace --Do stuff with the instance.
end
How to Save an Instance to a DataStore
local Converter = require(9768577563) --Require the module using the assetId and set it to the variable Converter
function SaveInstance(Player) --We'll make a function with an empty parameter, which we will recieve as the Player.
if not Player then return end --We need to make sure the Player exists. If the player doesn't we return.
local DataStoreService = game:GetService("DataStoreService") --Define the DSS
local StoreKey = DataStoreService:GetDataStore("SavedInstances") --Define a key within the DataStore. In this example, we'll call it SavedInstances.
local UserId = tostring(Player.UserId) --We'll fetch the Player's UserId but we need it in string so we will use tostring().
if UserId == "nil" then return end --We need to make sure the UserId exists. If it doesn't, we return.
local InstanceToSave = game:GetService("Workspace"):FindFirstChild("Baseplate") --Define an instance, which we will convert then save.
local ConvertedData = Converter:ConvertToTable(InstanceToSave) --We'll call a function of the module. Since we want a table, we'll use the :ConvertToTable() function and supply the instance we want to convert.
if not typeof(ConvertedData) == "table" then warn("Error with converted data.") return end --We need to make sure the data is a table, since DataStores save tables of information. If the Converter fails, it will return nil, which is not a table.
local SuccessfulSave, ErrorMessage = pcall(function() --We need to wrap a function with pcall to make sure the script doesn't crash if there is an error setting the data.
StoreKey:SetAsync(UserId, ConvertedData) --We'll set the user's data to the instance data.
--[[ This is how the hierarchy would look like:
• DataStoreService
• SavedInstances
• Player's UserId
• User's Data
]]
end)
if SuccessfulSave == false then --If there was an error,
warn(tostring(ErrorMessage)) --print the error.
else --If there were no issues,
print(UserId .."'s saved data:") --print the saved data.
print(ConvertedData)
end
end
game:GetService("Players").PlayerRemoving:Connect(SaveInstance) --Down here, we bind the SaveInstance function to the leaving player whenever a the player is removed from the Players service (when they leave, are kicked, or disconnect from the server).
How to Use Custom Settings
In order to use custom settings, we need to create a dictionary.
local CustomSettings = {}
To change the conversion settings to be different than the default settings, we just need to define them in the dictionary. [Note: If there’s a default setting that we are ok with, we don’t have to specify them in the dictionary.]
CustomSettings.IncludeProperties = false
CustomSettings.IncludeObjectVariables = false
CustomSettings.PrintCleanupAmount = true
Now we just need to supply the dictionary as the second parameter to the conversion function.
ConverterModule:Convert(InstanceOrTable, CustomSettings)
Use Case Examples
Save and Load Instances
Saving models to DataStores has never been easier! In the example above, with just the click of a button I can easily load or save all of the properties of my in-game furniture! You can visit the place here (place is also uncopylocked).
Click to view the DataStore code from the place above
local Converter = require(9768577563)
local DSS = game:GetService("DataStoreService")
local Housing = DSS:GetDataStore("Furniture")
function DoDataStuff(Player, Method)
if not Player then return false end
local PlayerID = Player.UserId
local HouseVal = Player:FindFirstChild("ClaimedHouse")
if not HouseVal or typeof(PlayerID) ~= "number" then return false end
if not HouseVal.Value then return false, "You don't own a house!" end
local House = HouseVal.Value
local FurnitureHolder = House:FindFirstChild("FurnitureHolder")
local AlignmentPart = House.PrimaryPart
if not AlignmentPart then return false, "An error occurred. House has no alignment part." end
if not FurnitureHolder then return false, "An error occurred. House has no furniture model." end
if Method == "Save" then
local ConvertedTable = Converter:Convert(FurnitureHolder)
if typeof(ConvertedTable) == "table" then
local Success,Fail = pcall(function()
Housing:SetAsync(tostring(PlayerID), ConvertedTable)
end)
return Success, Fail
else
return false, "An error occured saving instance data."
end
elseif Method == "Load" then
local InstanceTable
local Success,Fail = pcall(function()
InstanceTable = Housing:GetAsync(tostring(PlayerID))
end)
if Success then
if typeof(InstanceTable) == "table" then
if GetDictionaryLength(InstanceTable) > 0 then
local ConvertedInstance = Converter:Convert(InstanceTable)
if typeof(ConvertedInstance) == "Instance" then
FurnitureHolder:ClearAllChildren()
local NewAlignmentPart = ConvertedInstance:FindFirstChild("AlignmentPart")
for i,v in pairs(ConvertedInstance:GetChildren()) do
v.Parent = FurnitureHolder
end
if AlignmentPart and NewAlignmentPart then
FurnitureHolder.PrimaryPart = NewAlignmentPart
FurnitureHolder:PivotTo(AlignmentPart.CFrame)
else
warn("An error occurred. Either the house or the save data has no alignment part. Furniture was cleared.")
FurnitureHolder:ClearAllChildren()
return false, "An error occurred."
end
return true
else
warn("An error occured converting " ..tostring(PlayerID).."'s instance data.")
return false
end
end
end
else
warn("An error occured loading data for " ..tostring(PlayerID).. ": " ..tostring(Fail))
end
end
end
game.ReplicatedStorage.LoadMyData.OnServerEvent:Connect(function(Player) return DoDataStuff(Player,"Load") end)
game.ReplicatedStorage.SaveMyData.OnServerInvoke = function(Player) return DoDataStuff(Player,"Save") end
This is only a snippet of the code. To view the rest of the code, you can edit the uncopylocked place.
Easy Player Data Management
My personal favorite use of this module is to navigate and manage Player Data using folders. I manage everything in folders and attributes since attributes have built in editors and is a lot easier to use than navigating huge tables of data in the script editor. I just need to convert the folder into a table while loading the player’s data. It also leaves no room for user error if you have multiple people in Team Create handing the data. Typos or mistakes that would otherwise break the script are no longer present since the converter translates it directly into a table format.
Click to view conversion code
local Converter = require(9768577563)
local PlayerDataFolder = game:GetService("ServerStorage"):WaitForChild("DefaultPlayerData")
local DefaultPlayerData = Converter:Convert(PlayerDataFolder, {IncludeProperties = false})
Here’s the .rbxm file containing the Player Data folders shown in the example:
DefaultPlayerData.rbxm (1.2 KB)
Click to view the converted DefaultPlayerData table (Ready for DataStores!)
local DefaultPlayerData = {
["AscensionCharacterInfo"] = {
["Hat"] = "GreenEarsElf",
["Hat2"] = "MagicBeanTwirl",
["Hat3"] = "RAPTORHELM",
["SkinColor"] = {
["1"] = {
["B"] = 1,
["G"] = 0.0470588244497776,
["R"] = 0.0470588244497776,
["Time"] = 0
},
["2"] = {
["B"] = 0.01568627543747425,
["G"] = 0,
["R"] = 1,
["Time"] = 0.1231281161308289
},
["3"] = {
["B"] = 1,
["G"] = 0.3333333432674408,
["R"] = 0,
["Time"] = 0.4342761933803558
},
["4"] = {
["B"] = 0.3333333432674408,
["G"] = 1,
["R"] = 0.5333333611488342,
["Time"] = 0.5690515637397766
},
["5"] = {
["B"] = 0.01568627543747425,
["G"] = 0,
["R"] = 1,
["Time"] = 0.7537437677383423
},
["6"] = {
["B"] = 0.8666666746139526,
["G"] = 0,
["R"] = 1,
["Time"] = 0.9750415682792664
},
["7"] = {
["B"] = 0.3725490272045135,
["G"] = 0.3294117748737335,
["R"] = 1,
["Time"] = 1
}
},
["Walkspeed"] = {
["Max"] = 75,
["Min"] = 40
}
},
["CharacterOptions"] = {
["HairColor"] = {
["1"] = {
["B"] = 255,
["G"] = 81,
["R"] = 0,
["Scale"] = 255
},
["2"] = {
["B"] = 113,
["G"] = 37,
["R"] = 0,
["Scale"] = 255
},
["3"] = {
["B"] = 127,
["G"] = 0,
["R"] = 0,
["Scale"] = 255
}
},
["Hats"] = {
["1"] = "BlueElfEars",
["2"] = "RedElfEars",
["3"] = "LittleEars"
}
},
["CharacterStats"] = {
["Faction"] = "Neutral",
["LostArmDuringCutscene"] = false,
["MagicClass"] = "Arcane",
["Nickname"] = "",
["SkinColor"] = ""
},
["PlayerStats"] = {
["Deaths"] = {
["CreatureDeaths"] = {
["Birds"] = 0,
["Deer"] = 0,
["Ghosts"] = 0
},
["PlayerDeaths"] = 0
},
["Kills"] = {
["CreatureKills"] = {
["Birds"] = 0,
["Deer"] = 0,
["Ghosts"] = 0
},
["PlayerKills"] = 0
},
["Level"] = {
["CurrentLevel"] = 1,
["MaxLevels"] = {
["Max"] = 100,
["Min"] = 1
},
["XP"] = {
["CreatureHunt"] = {
["XPLevel"] = 0
},
["PvP"] = {
["XPLevel"] = 0
},
["StoryMode"] = {
["XPLevel"] = 0
},
["TotalXPLevel"] = 0
}
},
["Money"] = 0,
["Playtime"] = 0,
["PurchasedDLCPass"] = false,
["UserId"] = ""
},
["PurchasedHats"] = {
}
}
Serialize Any Instance
By printing the converted table into the output, you can easily copy & paste the table into a script or any other text editor. The above example uses the InCommand plugin.
Click to view the plugin command
for i,v in pairs (game.Selection:Get()) do
if v then
local ConvertedTable = require(9768577563):ConvertToTable(v, {['ShowHTTPWarning'] = false, ["PrintCompletionTime"] = false})
print(tostring(v.Name).. ":")
print(ConvertedTable)
end
end
Documentation
Function Parameters
Both conversion functions allow up to two parameters to be supplied.
The first parameter is the data (instance or table).
The second optional parameter is the list of override settings (dictionary).
ConverterModule:Convert( Data : Instance or Table, Settings : Dictionary )
Returning Variables
All functions will return a variable (the converted instance/table or nil).
Functions that convert an Instance to a Table will return a second bool variable if the table size is within the DataStore size limit.
Click to view example.
local ConverterModule = require(9768577563)
local InstanceToConvert = game.Workspace.Baseplate
local ConvertedTable, TableIsWithinLimits = ConverterModule:Convert(InstanceToConvert)
if TableIsWithinLimits == true then
print("Converted table meets DataStore requirements. :D")
elseif TableIsWithinLimits == false then
print("Converted table does not meet DataStore requirements. :(")
end
Available Functions
For convenience, I included multiple functions with different names that achieve the same thing.
Click to view a list of all available functions.
Available Settings
This is a list of all available settings and their defaults. If you want to use the converter with settings different than the default, simply provide your own dictionary as the second parameter when calling the function!
local DefaultSettings = {
--Property Settings--
IncludeProperties = true,
OnlySaveUniqueProperties = true, --This setting greatly reduces table size. When enabled, the converted table will only include properties if they are different than the default.
OnlySaveWritableProperties = true, --If Studio has a property marked as "Read Only", it will not be included when converting to table. Greatly reduces table size. If you are missing important properties, try setting this to false.
ExcludedProperties = {"ExcludedProperty1","ExcludedProperty2","ExcludedProperty3","etc."},
SpecificExcludedProperties = {
--FORMAT: ["ClassName"] = {"ExcludedProperty1","ExcludedProperty2","ExcludedProperty3","etc."},
--EXAMPLE: ["Part"] = {"LeftSurfaceInput", "TopSurfaceInput","BottomSurfaceInput", "RightSurfaceInput", "FrontSurfaceInput", "BackSurfaceInput"},
},
IgnoreAttachmentWorldProperties = true, --99% of the time, you won't need the attachment World properties over CFrame. Enabling this option greatly helps with accurate attachment creation.
FetchLatestPropertiesOnline = false, --If set to true, HTTPService.HttpEnabled needs to be enabled. If set to true and HTTPService.HttpEnabled is not enabled, it will revert to false. Using this setting requires a response from the setup.rbxcdn.com servers, so any response time will also add to your conversion time. (Typically 0-3 seconds but varies by server.)
ShowHTTPWarning = true, --If true, a warning will appear in the output if you try to fetch the latest properties while not having HTTP Service enabled.
IncludeAttributes = true, --Includes the attributes during conversion. [NOTE: When converting into an instance, if a property can't be found, it will automatically be applied as an attribute so I recommend keeping this setting enabled.]
IncludeTags = true, --Includes any CollectionService tags during conversion.
ExcludedTags = {"ExcludedTag1", "ExcludedTag2", "ExcludedTag3", "etc."}, --(Case Sensitive)
IncludeObjectVariables = true, --Objects are located using FindFirstChild so make sure your items within the same directory have UNIQUE names! [NOTE: Object variables with DataStoreFriendly DISABLED will reference the ORIGINAL variable so welds and other attachments will not work.]
Color3Scale = 255, --99% of the time, you should leave this setting alone. I don't suggest changing this setting unless you are using a custom instance table that has a different scale. 255 = Color3.fromRGB | 1 = Color3.new. This NEEDS to match the same scale as the instance table's formatted scale for accurate color reproduction.
EnumType = "Enum", --"Enum", "String", or "Int". If set to String, only the EnumItem.Name will be saved. I recommend keeping this on Enum.
--DataStore Compatability Settings--
DataStoreFriendly = true, --You can disable this if you don't need the table for DataStores. (The table size will be slightly smaller.)
PrintDataStoreApproximateSize = false, --When converted with DataStoreFriendly enabled, the module will print the approximate size of the table in KB/MB.
--InstanceSettings-- (These settings only apply when converting from Table to Instance)
DefaultInstanceType = "Folder", --If ClassName is not specified in the table, it will convert the table into this type of instance.
CarryOverSurfaceAppearances = false, --When converting to INSTANCE, this will carry over ALL POSSIBLE SurfaceAppearance variations for that specific MeshPart if it's found in-game. Command bar/plugins can create SurfaceAppearances from scratch, so keep as false if using a plugin. This option is only needed during runtime scripts. This will increase the cost on performance (loops through every single item in game!), so only enable if you need it!
--NOTE: CarryOverSurfaceAppearances will currently NOT function until a Roblox bug gets resolved.
IncludeSurfaceAppearancesOverride = false, --When converting to TABLE, include the SurfaceAppearance if the current security level cannot write the properties for SurfaceAppearance objects. (If you aren't using a plugin, do you still want the SurfaceAppearance data, even though SurfaceAppearances can't be modified during runtime scripts?)
AutoConvertMeshParts = true, --Automatically converts any MeshParts into a normal Part with a SpecialMesh object inside ONLY IF the original mesh can't be found in-game. Scripts cannot make MeshParts due to Roblox not providing support for runtime collision rendering. Hopefully Roblox will address this issue in the future so I can remove the need for this setting. If you use this converter outside of runtime (like with a plugin), set this property to false since MeshParts can still be created in edit mode.
AutoSmoothParts = true, --This automatically makes all part surfaces smooth if there isn't a SurfaceInput specified in the instance table (removes the inlets/stud textures).
--Output Settings--
PrintCompatibilityErrors = true, --If there are any issues with a specific datatype or an instance class, it will print a list of those errors.
PrintObjectVariableErrors = true, --If an error occurs when setting an object property, a warning will be put in the output. (Typically this will happen if the object is nil or if there are similar names under the same directory.)
PrintCompletionTime = false, --Prints the amount of time the conversion takes to complete. This does not take into account any server hangups (For example: when converting into a massive directory, it doesn't take into account the delayed time it takes the server to create the instances from Instance.new).
PrintCleanupAmount = false, --When converting a table into an instance, the source table needs to be cloned. The cleanup function will clean up the copy after using it for conversion. If this setting is enabled, it will print how much background memory in MB was cleaned up (approximate).
}
Note: Dictionary keys and all string values are case-sensitive.
Considerations
Click to view a script that removes duplicate names.
local AlwaysActive = false --false = only on server start up | true = always running. Setting this to true could be very intensive on performance.
local Directories = {game.Workspace, game.ServerStorage, game.ReplicatedStorage}
function RemoveDupeNames(Par)
local DupeNames = {}
if #Par:GetChildren() > 1 then
local CurrentNames = {}
local Children = Par:GetChildren()
for i = 1, #Children do
if table.find(CurrentNames, Children[i].Name) and Children[i].Name then
table.insert(DupeNames,Children[i].Name)
end
table.insert(CurrentNames, Children[i].Name)
end
end
local Count = {}
for i,v in pairs(Par:GetChildren()) do
if typeof(v.Name) ~= "string" then return end
if table.find(DupeNames, v.Name) then
local OldName = v.Name
if not Count[OldName] then
Count[OldName] = 1
end
v.Name = OldName.. tostring(Count[v.Name])
Count[OldName] += 1
end
end
end
function DoForEveryParent(Par)
if not typeof(Par) == "Instance" then return end
RemoveDupeNames(Par)
for i,v in pairs(Par:GetChildren()) do
if AlwaysActive == true then
v:GetPropertyChangedSignal("Name"):connect(function() RemoveDupeNames(Par) end)
end
DoForEveryParent(v)
end
if AlwaysActive == true then
Par.ChildAdded:connect(function() RemoveDupeNames(Par) end)
end
end
for _,Directory in pairs(Directories) do
DoForEveryParent(Directory)
end
- This module runs many nested loops so it can be stressful on performance (depends on the directory size). I would not recommend using the converter multiple times inside of a big loop.
- Massive directories (20,000+ Instances) consistently took at least 3 seconds.
- Medium directories (1,000 Instances) typically took less than 600 milliseconds.
- Smaller directories (200 Instances) typically took less than 70 milliseconds.
Note: I’ve done some trial runs to stress test the server with varying amounts of descendants. Keep the following in mind when trying to balance server performance. According to a few trial runs:
- Converting to instances use
Instance.new()
. Creating, reparenting, and destroying multiple instances will always affect server performance. The more instances you create during the conversion, the more it will affect performance. If you want to maintain good server performance, you should use the To Instance conversion on large tables as infrequent as possible. - DataStores still have a limit of 4 MB per key. Customized models with thousands of descendants or many unique properties can easily reach this limit so I suggest separating large directories into multiple groups before conversion.
- You should never allow this module to be accessed by the client. Always keep it in a secure location such as ServerStorage or ServerScriptService. Never store this module in ReplicatedStorage, Workspace, or any other place the client can access. If the client is allowed access to this module, they could theoretically use it with an external program to copy all of your building data. (This is already possible but if you give them access to this module, it makes it easier to accomplish.)
- MeshParts are not fully supported until Roblox allows computation of mesh collisions during runtime. When converting from Table to Instance, if a MeshPart cannot be found in the place and
AutoConvertMeshParts
setting is enabled, any MeshParts will be converted into a normal Part with a SpecialMesh. This means the collisions will reflect the Part collisions instead of the Mesh collisions. To avoid this, ensure you have that specific MeshPart located somewhere in your published/saved place. - Unions are NOT supported. Unions don’t carry any readable part data. It would be possible to convert unions if there was access to the :Seperate() function. Unfortunately, that function is currently only accessible from plugins.
Disclaimers
- If something goes wrong during the conversion, the conversion will return nil so make sure you are checking for a conversion result. I’ve tried to compile as many warnings as possible so that you’d be able to know when something goes wrong and how to fix it.
- This is a public module which will receive occasional updates. Bugfixes and future feature patches may take additional time depending on the scope since I need to verify the integrity of the changes before updating the module.
Click to view changelog
Changelog History
June 3rd, 2022 | v1.0.0
- DevForum Launch & Public Release
June 6th, 2022 | v1.1.0
- Added Enum.Value support. To use, set
EnumType
to “Int”.- Reworked MeshPart support. MeshParts are now supported if there is an existing mesh with the same MeshId in game. If there isn’t one, it will follow the rules of AutoConvertMeshParts. Special thanks to @Manachron for the ingenious workaround.
- Added support for a few different Services. (Workspace, Lighting, StarterPlayer, TestService, SoundService, Chat)
June 9th, 2022 | v1.1.1
- The warning message for a delayed HTTP response now includes the (rounded) time in seconds.
- Fixed a
HTTP 403 Forbidden
error by changing the domain path. (Previous address to verifyHTTPEnabled
had an incompatible path.)- During each session, the module now saves property fetches between conversions. Previous behavior was that it would fetch a new set of properties during every single conversion, which didn’t help performance when calling the convert function multiple times. Also when fetching from the online dataset, it sent way too many requests. It will now only refetch properties if the OverrideSettings contain a new list of
ExcludedProperties
.August 31st, 2022 | v1.1.2
- Added property and attribute support for the new Font DataType.
- Added additional failsafes for unsupported attribute types. (ObjectValues, etc.)
- When converting to an object, unsupported attributes will now be converted to a seperate object (
DefaultInstanceType
) with its attributes in a table format. The new object will be inside the object that could not have its attributes set.- If you don’t want objects made for unsupported attributes, you can revert to v1.1.1 (listed in the VersionIDList script).
November 10th, 2022 | v1.1.3
- Fixed a CustomPhysicalProperties issue when creating instances.
- Updated the OfflineAPI to the latest Studio API. (Previous OfflineAPI was from March 27th.)
November 11th, 2022 | v1.1.4
- Fixed an issue when creating new MeshParts where properties were copied from the source part instead of the data table.
- Added a cache for MeshParts.
November 12th, 2022 | v1.1.5
- Added an
IgnoreAttachmentWorldProperties
conversion option. (Default is set to true.)- Critical Fix: Even though the module did not have proper support for Union conversions, after a recent Studio update, attempting to convert a UnionOperation or a model with a UnionOperation caused a critical error causing full-on crashes. Unions are now automatically converted as Parts instead.
November 16th, 2022 | v1.1.6
- Added an
OnlySaveWritableProperties
conversion setting. (Default: true)
Note: If you end up with certain properties missing from your tables, try setting this to false.- Added
CarryOverSurfaceAppearances
andIncludeSurfaceAppearancesOverride
conversion settings for MeshParts. (Default: false)
Note:CarryOverSurfaceAppearances
will currently not function until a Roblox bug gets resolved.- SurfaceAppearance objects are now automatically converted into
DefaultInstanceType
until Roblox provides an update that allows SurfaceAppearances to be changed during runtime.- Fixed an issue where properties and attributes weren’t being reset for MeshParts prior to applying properties from the table.
December 4th, 2022 | v1.1.7
- Added Tag support. Use the
IncludeTags
setting (default: true) with theExcludedTags
setting (array) to specify tags you don’t want to include during conversion.- Fixed an issue where tags weren’t being reset for MeshParts prior to applying properties from the table.
December 14th, 2022 | v1.1.8
- Bugfix for FontFace properties not being correctly set with
DataStoreFriendly
enabled.- Fixed an issue with
OnlySaveWritableProperties
where write-only property fetches were replacing precached read+write fetches.April 6th, 2023 | v1.1.9
- Small fix for an infinite loop timeout error when trying to convert and encode an instance/object property that has been parented to nil. Objects parented to nil should now return as a blank string.