Hello everyone! It took me hours upon researching how to store unique object identifiers (if Instance == Instance then
, but as strings
) into a Data Store, until I realized it was impossible, unfortunately. My goal was to be able to recreate the same object that was serialized and sent to a DataStore. The only workaround and alternative was to utilize GUIDs.
This is a module that serializes a folder with its descendants into the form of a table to be sent to a Data Store.
Data Serializer 2.0: Marketplace | GitHub | API-Documentation
If you don’t want to manipulate GUID’s and prefer a simpler folder-based data store, I recommend using the Legacy version of this module instead.
Why use this?
-
It has minimal control over data! Unlike some other community modules, this one will not manage when to save, update, or remove requests to Data Stores on its own.
-
Capable of saving Models, MeshParts, ObjectValues, Scripts, Attributes and more!
-
The serialization structure is organized and easy to manipulate with plugins!
How to use (Setup and Limitations)
Module Structure:
DataSerializer \ -- ModuleScript
LoadData | -- ModuleScript
SaveData | -- ModuleScript
Initial Setup
- A folder named “PresetPlayerData” must be present under the
ServerStorage
. This will be used as the default folder for each new player. - If you want to customize PresetPlayerData and it has GUID attributes, remove all GUIDs with the command bar in Studio using this code below.
Remove GUIDs:
local HttpService = game:GetService("HttpService")
local ServerStorage = game:GetService("ServerStorage")
local PresetPlayerData = ServerStorage:FindFirstChild("PresetPlayerData")
if not PresetPlayerData then
PresetPlayerData = Instance.new("Folder")
PresetPlayerData.Name = "PresetPlayerData"
PresetPlayerData.Parent = ServerStorage
end
local function setUniqueId(object)
local aName = "GUID"
if object:GetAttribute(aName) then
object:SetAttribute(aName, nil)
end
end
setUniqueId(PresetPlayerData)
for i, v in pairs(PresetPlayerData:GetDescendants()) do
setUniqueId(v)
end
This is the part where you want to start customizing PresetPlayerData, it can be achieved by adding Value objects and folders within the preset folder. More information about setting up PresetPlayerData can be found here.
Changing Values
The Data Serializer 2.0 version allows any Instance to be stored under PresetPlayerData, alongside their attributes.
This script features unrestricted saving, meaning that storing any instance type is possible. This includes: Models, Beams, Full NPCs, Scripts, LocalScripts, MeshParts, Highlight, and everything else featured in the “Insert Object” menu. However, it is recommended PresetPlayerData
should be treated the same as the Data Serializer Legacy version.
Instances supported:
- Everything
Attribute data types supported:
- All data types are supported.
Limitations
If an object’s property is referencing an Instance that will not be saved, it may be referenced to a cloned
version of the object instead.
Examples of objects with a property referencing an Instance:
SurfaceGui.Adornee
WeldConstraint.Part0
Motor6D.Part0
Beam.Attachment1
For example, if SurfaceGui
and SurfaceGui.Adornee
are not within the same Model
, SurfaceGui.Adornee
will be cloned to be set as the new Adornee
for the next time the Player joins the experience. This practice of setting the Adronee
property is not good because it is referencing to an object of the game rather than the player. To make the Adornee
officially an object of the Player, Adornee
should be cloned, then set it as the new Adornee
.
-- example of making a new adornee.
local SurfaceGui
local newAdornee = SurfaceGui.Adornee:Clone()
newAdornee.Parent = SurfaceGui.Adornee.Parent
SurfaceGui.Adornee = newAdornee
MeshParts and Scripts
MeshParts, Scripts, and SurfaceAppearance can persist, but they need to be present within the ServerStorage
service.
- For scripts to save, the name must match with the one in ServerStorage.
- MeshParts and SurfaceAppearances must have matching copies containing the same Properties as the one in ServerStorage. Name will not matter.
Deprecated Objects
Deprecated objects are objects that are no longer being maintained by Roblox engineers. These objects are supported in this script, but certain objects may not fully persist, meanining that there are certain properties of deprecated objects that may not save.
Post Setup
Now that PresetPlayerData has been fully customized, it will need the GUID attributes once again.
Using the Command Bar
PresetPlayerData will be cloned to the Player, however the script is going to think the whole folder is new everytime, therefore it will create a number of PresetPlayerData
folders within itself depending on the number of times the player rejoins the game. To prevent this, an attribute named “GUID” must be set on PresetPlayerData
and its descendants. This is possible using the command bar.
Setting GUIDs during a live game will not be necessary because using DataStore:Update()
or DataStore:CleanUpdate()
will set the GUIDs on objects automatically. A live game is when the client is playing the game.
Set GUIDs:
local serverStorage = game:GetService("ServerStorage")
local HttpService = game:GetService("HttpService")
local preset = serverStorage:FindFirstChild("PresetPlayerData")
local function checkMatchId(folder)
local function checkObj(scannedObj, ParaObject)
if scannedObj ~= ParaObject then
local guid = scannedObj:GetAttribute("GUID")
if guid then
if guid == ParaObject:GetAttribute("GUID") then
ParaObject:SetAttribute("GUID", HttpService:GenerateGUID(false))
checkMatchId(ParaObject)
end
end
end
end
checkObj(preset, folder)
for i, v in pairs(preset:GetDescendants()) do
checkObj(v, folder)
end
end
local function setGuid(folder)
if not folder:GetAttribute("GUID") then
folder:SetAttribute("GUID", HttpService:GenerateGUID(false))
checkMatchId(folder)
end
end
setGuid(preset)
for i, v in pairs(preset:GetDescendants()) do
setGuid(v)
end
Other uses
Other Uses
Retrieving Data Folder
After DataStore:Get() is called, it will set an attribute named “DSLoaded” to the Player
containing the name of the Folder
that is parented to the Player. This example shows how to retrieve the PlayerData folder from the Player:
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(Player)
--check if data store loaded
if not Player:GetAttribute("DSLoaded") then
Player:GetAttributeChangedSignal("DSLoaded"):Wait()
end
--find Player Data folder
local PlayerData = Player:FindFirstChild(Player:GetAttribute("DSLoaded"))
end)
Making a Save Icon
While the Player’s data is saving, Player:GetAttribute("IsSaving")
will be set to true
, then false
when it finishes saving data on the Player.
How to use this in code:
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
Player:GetAttributeChangedSignal("IsSaving"):Connect(function()
local isSaving = Player:GetAttribute("IsSaving")
if isSaving then
-- do something with icon
else
-- stop icon
end
end)
Prebuilt Script
This script and the DataSerializer module must both be parented to ServerScriptService
.
--[Made by Jozeni00]--
--settings
local DataSettings = {
--{DATA}--
--Any changes made below are susceptible to a clean data wipe, or revert data to its previous.
["Name"] = "DS_Test2V0-0-0"; --DataStore name for the entire game.
["Key"] = "Plr_"; --prefix for key. Example: "Player_" is used for "Player_123456".
--{FEATURES}--
["AutoSave"] = true; --set to true to enable auto saving.
["SaveTime"] = 1; --time (in minutes) how often it should automatically save.
["UseStudioScope"] = true; --set to true to use a different Scope for Studio only.
["DevName"] = "DEV/DS_Test2V0-0-0"; --Name of the Data Store for Studio if UseStudioScope is true.
["DevKey"] = "Dev_"; --Key of the Data Store for Studio, if UseStudioScope is true.
}
--scripts
local ServerScriptService = game:GetService("ServerScriptService")
local dataModule = ServerScriptService:FindFirstChild("DataSerializer") -- DataSerializer Module Script.
local DataSerializer = require(dataModule)
--players
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
--set scope
if DataSettings.UseStudioScope then
if RunService:IsStudio() then
DataSettings.Name = DataSettings.DevName
DataSettings.Key = DataSettings.DevKey
end
end
local DataStore = DataSerializer:GetStore(DataSettings.Name)
--on entered
function onPlayerEntered(Player)
local key = DataSettings.Key .. Player.UserId
--player data
local PlayerData = DataStore:Get(Player, key, {Player.UserId})
if DataStore and DataSettings.AutoSave then
local isGame = true
local plrRemove = nil
if DataSettings.SaveTime < 1 then
DataSettings.SaveTime = 1
end
local saveTimer = DataSettings.SaveTime * 60
plrRemove = Players.PlayerRemoving:Connect(function(plr)
if plr == Player then
isGame = false
end
end)
while Player and isGame do
task.wait(saveTimer)
--update
DataStore:Update(Player, key)
end
if plrRemove and plrRemove.Connected then
plrRemove:Disconnect()
end
end
end
--on removing
function onPlayerRemoving(Player)
local key = DataSettings.Key .. Player.UserId
DataStore:CleanUpdate(Player, key)
end
for i, v in pairs(Players:GetPlayers()) do
if v:IsA("Player") then
local onEnter = coroutine.wrap(function()
onPlayerEntered(v)
end)
onEnter()
end
end
--events
Players.PlayerAdded:Connect(onPlayerEntered)
Players.PlayerRemoving:Connect(onPlayerRemoving)
game:BindToClose(function()
print("Closing...")
for i, v in pairs(Players:GetPlayers()) do
if v:IsA("Player") then
v:Kick()
end
end
task.wait(3)
print("Name:", DataSettings.Name)
end)
--[Made by Jozeni00]--
What if this module receives an update?
Do I need to update this module?
If you only care about the basic needs, then no. Although, this repository will still be maintained to support newer object classes Roblox may add in the near future.
Showcase:
-or-
Place FILE:
DS_2-0_Place.rbxl (81.1 KB)
Update Log
Update 1.1
Changed the way Enums are serialized to a more simplified manner. This new update should only be used for newer games because the saving structure was slightly changed.
Feedback is appreciated, thank you!