Data Serializer Legacy | Save ObjectValues and Attributes!

Hello everyone,

I have been working on Data Stores a lot lately, and have been improving as the times go by. Before I have been wondering how to save an ObjectValue, then I realized it was too complicated until recently I have found the answer.

Data Serializer Legacy: Marketplace | GitHub | API Documentation

This is a folder-based data store module that is capable of saving objects under a folder.

Notable Features

  • Capable of saving any Instance type. This includes Scripts, Models, MeshParts, ObjectValue, Parts, CFrameValue, Motor6D, along with the rest of the objects that are present in the “Insert Object” widget.
  • All attribute types are supported.
  • Save-fail retries.
  • Offline Studio support.

The structure is as follows:

DataSerializer \ -- ModuleScript
    LoadData | -- ModuleScript
    SaveData | -- ModuleScript

Why use it?

  • Unlike some other community modules, this one allows full control for the developer. You can choose when you want to save by using your prefered methods.

  • The data structure is organized so you can easily edit it using a plugin.

  • Capable of serializing all data objects, including ObjectValues.

How to Setup (and Limitations)

Initial Setup

  • A folder named “PresetPlayerData” must be present under the ServerStorage. This will be used as the default folder for each new player.

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.

> Changing Values

Changing Values

The Data Serializer Legacy version allows only Folders and ValueBase to be stored under PresetPlayerData, alongside their attributes. Two or more objects under the same Parent cannot share the same name.

Instances supported:

  • Folder
  • IntValue
  • NumberValue
  • BoolValue
  • CFrameValue
  • Vector3Value
  • Color3Value
  • BrickColorValue
  • RayValue
  • ObjectValue

Attribute data types supported:

  • All data types are supported.

Note

ObjectValue features unrestricted saving, meaning that storing any instance type is possible under this value object. This includes: Models, Beams, Full NPCs, Scripts, LocalScripts, MeshParts, Highlight, and everything else featured in the “Insert Object” menu.

> ObjectValue Limitations

ObjectValue Limitations

ObjectValue.Value itself has no limitations, but the objects within it do.
Trying to save a property that references an Instance can be risky if the path to instance is not unique.

Examples of objects with a property referencing an Instance:

  • SurfaceGui.Adornee
  • WeldConstraint.Part0
  • Motor6D.Part0
  • Beam.Attachment1

The path to an Instance can be brought up by calling Instance:GetFullName(). If the path is not unique enough, the objects within the path should have their names changed to be unique. For example, if the path to Beam.Attachment0 is, Workspace.Part.Part.Part.Attachment, it should be changed to Workspace.Part.Part2.Part3.Attachment.

> MeshParts and Scripts

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

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 (Practical Usage)

Post-Setup

Play test in studio, look in Players service to see if you have the PlayerData folder for your user. If it exists under the player, that means it’s working.

> If it's not working, here is what to do

If the PlayerData folder did not appear under the player, you first need a script that requires this module. DataSerializer:GetStore() will return a DataStore class object that you will need later on.

DataStore:Get() is the magic that copies that PresetPlayerData folder over to the Player with it renamed to “PlayerData”. It returns the PlayerData folder.

DataStore:CleanUpdate() is a function that can only be used when the player is removing because it will destroy ObjectValue.Values within the PlayerData folder. This is the magic that saves the PlayerData when you rejoin studio.

DataStore:Update() (optional) performs the same action as :CleanUpdate() except without the cleaning. This function will not destroy ObjectValue.Values, therefore it is safe to use when you need to auto-save. Just be aware of Roblox’s DataStore limitations.

This is how it would look like in code:

local Players = game:GetService("Players")

local ServerScriptService = game:GetService("ServerScriptService")
local dsMod = ServerScriptService:WaitForChild("DataSerializer")

local DataSerializer = require(dsMod)
local DataStore = DataSerializer:GetStore("DataStore_name")

Players.PlayerAdded:Connect(function(Player)
    local plrKey = "plr_" .. Player.UserId
    local PlayerData = DataStore:Get(Player, plrKey, {Player.UserId})
end)

Players.PlayerRemoving:Connect(function(Player)
    local plrKey = "plr_" .. Player.UserId
    DataStore:CleanUpdate(Player, plrKey)
end)

For more information on how to use these functions, see the api docs.

image

> Attributes info

When you inspect your player’s attributes, you will notice two named “DSLoaded” and “IsSaving”. DSLoaded fires when PlayerData is fully loaded, this attribute returns a string with the name of the folder (which in this case is “PlayerData”). IsSaving is true whenever this module is saving, and turns false when finished.
image
To retrieve the PlayerData folder in code, you would put:

local Players = game:GetService("Players")
local Player = Players.LocalPlayer

if not Player:GetAttribute("DSLoaded") then
    Player:GetAttributeChangedSignal("DSLoaded"):Wait()
end
local PlayerData = Player:FindFirstChild(Player:GetAttribute("DSLoaded"))

This method is better over local PlayerData = Players:WaitForChild("PlayerData") because it waits for data to fully load.

To change values, the test session needs to be in “Server” mode. It is possible by going to the “Home” tab (at the top bar), and clicking this button for it to say “Server” instead of “Client”. If you were to change data values from the Client side, it will not save because a server Script is handling data-stores. Feel free to learn more about Client-Server model.
image

Let’s say I want my player to buy something by spending coins. First you will need an IntValue named “Coins” somewhere in the PlayerData folder. If it’s not there, then you can simply make a new one.
image

Now change the value to whatever you prefer. I will also be adding an attribute named “TestColor” on the Coins value object to check if that will save when I test the game again.

Stop testing, then test it again. Repeat the steps to find the PlayerData folder under the player, and see if the “TestColor” attribute and the Coin’s value saved to how you changed it.

> How to change PlayerData in scripts

Client or LocalScripts cannot access data stores, instead you will need to use RemoteEvents to transfer data to the server and make the Script change the values.

Here is my PlayerData folder.
image

Now, I want to give the player additional Coins after clicking a button on a LocalScript. In order to do that, I will need to define the RemoteEvent and call RemoteEvent:FireServer() to tell the server to update my coins.

In this little tutorial, the requirements are:

  • “ServEvents” Folder needs to be in ReplicatedStorage
  • “ChangeData” RemoteEvent needs to be in ServEvents folder.
  • The LocalScript provided below, must be placed under a TextButton or ImageButton.
  • The Script provided below must be placed in ServerScriptService.
-- local script

-- replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServEvents = ReplicatedStorage:FindFirstChild("ServEvents")

-- defined remote event
local evChangeData = ServEvents:FindFirstChild("ChangeData")

local button = script.Parent

-- on mouse button click, give 100 coins to the player
button.MouseButton1Click:Connect(function()
    evChangeData:FireServer()
end)

Then, on a Script, evChangeData will get fired and change the Coins value to Coins.Value + 100. Basic requirements: PlayerData needs to be defined, then the coins. Just for fun, I will also add 1 Gems.

-- server script

-- replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServEvents = ReplicatedStorage:FindFirstChild("ServEvents")

-- defined remote event
local evChangeData = ServEvents:FindFirstChild("ChangeData")

evChangeData.OnServerEvent:Connect(function(plr: Player)
    -- check if Player loaded data yet.
    local loadedName = "DSLoaded"
    if not plr:GetAttribute(loadedName) then
        plr:GetAttributeChangedSignal(loadedName):Wait()
    end
    
    -- player data folder
    local PlayerData = plr:FindFirstChild(plr:GetAttribute(loadedName))
    
    -- wallet
    local WalletFile = PlayerData:FindFirstChild("Wallet")
    local charCoins = WalletFile:FindFirstChild("Coins")
    local charGems = WalletFile:FindFirstChild("Gems")

    -- give coins and gems
    charCoins.Value = charCoins.Value + 100
    charGems.Value = charGems.Value + 1
end)

Now, after you are done spamming the button that adds + 100 Coins and + 1 Gems per click, rejoin Studio to see if the data saved.

Prebuilt Script

This script and the DataSerializer module must 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_TestLV0-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_TestLV0-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]--
Solving the memory or RAM issue

The downside of having a folder-based datastore in your game is that it can take up memory space at least for the client because every player will have their PlayerData folder replicated to all players in the server. To only have the LocalPlayer’s data folder, the other player data folders must be deleted as well as their ObjectValues using a LocalScript.

ObjectValue.Value's are stored in a folder under ReplicatedStorage named “DataTempFile”. Imagine if your game that has 200 players in a server, and a feature where players can build custom houses, and they all get put under ReplicatedStorage, it may lag the client while causing their game to crash.

To assist in solving the memory issue, I have provided some code that detects objects that do not belong to the LocalPlayer’s data folder and deletes the object, as well as deleting the other PlayerData folders that are not a part of the Local Player.

--[Made by Jozeni00]--
--Put this LocalScript into "game.StarterPlayer.StarterPlayerScripts"
--It destroys the PlayerData of other players in exchange for lighter memory usage.
--Detects other objects that do not belong to the player and deletes it.

--plr
local Players = game:GetService("Players")
local Player = Players.LocalPlayer or script.Parent.Parent

--replicated
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataTempFile = ReplicatedStorage:WaitForChild("DataTempFile")

--data folder
local loader = string.char(68, 83, 76, 111, 97, 100, 101, 100) -- "DSLoader"
if not Player:GetAttribute(loader) then
	Player:GetAttributeChangedSignal(loader):Wait()
end

--check temp file
function tempObjAdded()
	task.wait(0.1)
	local PlayerData = Player:FindFirstChild(Player:GetAttribute(loader))
	if not PlayerData then
		return nil
	end
	
	--scan the temp file
	for i, v in pairs(DataTempFile:GetChildren()) do
		local foundObj = false
		
		--find matching obj
		for _, data in pairs(PlayerData:GetDescendants()) do
			if data:IsA("ObjectValue") then
				if data.Value then
					if data.Value == v then
						foundObj = true
						break
					end
				end
			end
		end
		
		if not foundObj then
			v:Destroy()
		end
	end
	
end

--player added function
function onPlayerEntered(plr)
	if plr == Player then
		return nil
	end
	
	--get plr data
	if not plr:GetAttribute(loader) then
		plr:GetAttributeChangedSignal(loader):Wait()
	end
	local PlrData = plr:FindFirstChild(plr:GetAttribute(loader))
	
	--scan for object values and destroy it
	for i, v in pairs(PlrData:GetDescendants()) do
		if v:IsA("ObjectValue") then
			if v.Value then
				v.Value:Destroy()
			end
		end
	end
	PlrData:Destroy()
end

--check existing players
for i, v in pairs(Players:GetPlayers()) do
	if v:IsA("Player") then
		local thread = coroutine.wrap(function()
			onPlayerEntered(v)
		end)
		thread()
	end
end

--check existing objects
local checkObj = coroutine.wrap(function()
	tempObjAdded()
end)
checkObj()

--events
Players.PlayerAdded:Connect(onPlayerEntered)
DataTempFile.ChildAdded:Connect(tempObjAdded)
--[Made by Jozeni00]--
What if this module receives an update?

Do I need to update this script?

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

The place files should include:

  • DataSerializer module
  • DSHandler, a script that uses this module.
  • ClearDebris, a local script that optimizes performance for the client.
  • MarketService, a script that handles the process receipt, edited for this module.
  • PresetPlayerData folder

Place Link:

– or –

RBXL File:
DS_Legacy_Place.rbxl (83.9 KB)


Update Log

Update 1.3

  • Fixed PresetPlayerData folder getting renamed to “PlayerData” on require. It now stays as “PresetPlayerData” on require.
  • Added type-checking in main module.
  • Font attribute can be saved now.

Update 1.2

  • Reorganized the SaveData script’s code to make it neater.

Update 1.1

  • Changed from Script to ModuleScript.

This module will be available on the marketplace and maintained side by side with the GitHub repository.

Feedback

  • What are your thoughts on this post?
  • Is this module helpful to you?
  • What are some areas you think I should look to improve on?
16 Likes

Wanted to make a MMORPG and I actually wanted to use IntValue for most stats
Thanks to this, I got an easier way of saving well… mostly everything.

just a quick question, exploiters can’t spoof those value right?
I mean, they can of course but the datastore will only look for the server-side value not client?

1 Like

You’re welcome!

Good question, the datastore will only look for changes that happen on the serverside because a server Script is handling it. Server cannot see data changes from the client.
image

Literally had fun implementing this into a RPG kit just to have better control over things & easier access to data.

I was just wondering tho, how would you save tools exactly ?

1 Like

To save a tool, for example an “Iron Sword”, there are two ways I can think of.

1. Using an IntValue

Insert an IntValue and name it “Iron Sword”. The value of IntValue object would be treated as the amount of Iron Swords. When the value reaches 0, the IntValue will destroy itself because it means the player has no more Iron Swords in their inventory.
image

Maybe you want players to upgrade their weapons. An Iron Sword upgraded 1 time would be renamed to “Iron Sword[2]”, [2] means the weapon is now at level 2. But now the name is ruined, add a string attribute called “Name” with a value “Iron Sword”.
image

To grab the Iron Sword tool from a storage to the player’s character. I would see if “Iron Sword[2]” is in the storage, if true, then it gets cloned to the player’s character.

This method does not require an IntValue because it can be done with StringValue too, I just used it as an example for simplicity’s sake.

2. Using an ObjectValue

Maybe your game allows players to create and sell custom swords to other players in-game. In this case, swords are required to be unique.

To achieve this, insert an ObjectValue to an inventory folder, and name it “1”, the number represents the placement of an inventory.
image

Think of a table in Lua, where the number is the index of a table. This is what the numbers mean when thinking about the inventory.

local Inventory = {
    [1] = {
        ["Name"] = "Iron Sword";
    };
    [2] = {
        ["Name"] = "Custom Iron Sword";
    };
    [3] = {
        ["Name"] = "Custom Iron Sword";
    };
}

I would set the ObjectValue’s value to the Iron Sword model (or tool if you prefer it).

I would have a StringValue named “Equipped” that tells what weapon the player currently equipped, and set its value to “1” (the Iron Sword).
image

So now when I rejoin the game in test mode, check if Inventory:FindFirstChild(Equipped.Value) is not nil, then I can clone() the Iron Sword from the ObjectValue and set its parent to a tool when a player’s character joins in game.

local Character -- player's character model
local Inventory -- inventory folder containing the weapons
local Equipped -- string value object, what weapon the player has currently equipped

local equippedWep = Inventory:FindFirstChild(Equipped.Value) -- ObjectValue referencing an Iron Sword model
if equippedWep and equippedWep.Value then
    local newWeapon = equippedWep.Value:Clone()
    newWeapon.Parent = Character
end

For any extra info about the Iron Sword, I would set them as attributes on the ObjectValue.

Dual wielding

But, maybe you also want dual-wielding swords (the left and right hand each hold a sword). I would add a number attribute named “Left” to the “Equipped” value object for the left hand.

And the code above would be rewritten like this:

local Character -- player's character model
local Inventory -- inventory folder containing the weapons
local Equipped -- string value object, what weapon the player has currently equipped

local rightWep = Inventory:FindFirstChild(Equipped.Value) -- (right hand) ObjectValue referencing an Iron Sword model
local leftWep = Inventory:FindFirstChild(Equipped:GetAttribute("Left")) -- (left hand) ObjectValue referencing an Iron Sword model

if rightWep and rightWep.Value then
    local newWeapon = rightWep.Value:Clone()
    newWeapon.Parent = Character
end
if leftWep and leftWep.Value then
    local newWeapon = leftWep.Value:Clone()
    newWeapon.Parent = Character
end

Hello, I’m making a game and I’m using your datastore module to save settings

Eg : FOV(NumberValue) | FPS Booster(BoolValue)

When I change my FOV to 120 and exit studio, the value doesn’t save itself. Could you help.

Hi @vecuo , did you forget to use DataStore:CleanUpdate(plr: Player, key: string) in Players.PlayerRemoving:Connect()?

You will need to use :Get() on player entered, and :CleanUpdate() on player removing for it to save when you rejoin in studio after changing values.

This is how I would do it:

local ServerScriptService = game:GetService("ServerScriptService")
local dataModule = ServerScriptService:FindFirstChild("DataSerializer") -- DataSerializer Module Script.
local DataSerializer = require(dataModule)

local Players = game:GetService("Players")
local DataStore = DataSerializer:GetStore("TestFolderStore")

--on entered
local function onPlayerEntered(Player: Player)
	local key = DataSettings.Key .. Player.UserId

	--player data
	local PlayerData = DataStore:Get(Player, key, {Player.UserId})

end

--on removing
local function onPlayerRemoving(Player: Player)
	local key = DataSettings.Key .. Player.UserId
	DataStore:CleanUpdate(Player, key)
end

Players.PlayerAdded:Connect(onPlayerEntered)
Players.PlayerRemoving:Connect(onPlayerRemoving)

Just clarifying here, were you in Server mode when you did it? Values cannot save when changed on the Client side, it must be done on the Server.
image

If you need a full script to ensure that it works, put this Script in ServerScriptService.

Pre-built script
-- Put this script and DataSerializer in ServerScriptService.

--settings
local DataSettings = {
	--{DATA}--
	--Any changes made below are susceptible to a clean data wipe, or revert data to its previous.
	["Name"] = "DS_TestLV0-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_TestLV0-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)

If this is not enough, there is also a place file provided in this post that you can use for testing.