Hey everyone,
I’ve got a folder in the workspace where players can place objects. These objects are either Tools or Models, and they come with different attributes and scripts. Players can change their properties, and they also have custom meshes made in external software.
I’ve set up serialization and deserialization in the Leaderstats script, and it works great with regular parts. However, I’m having some trouble with the Tools and Models.
How can I make sure that when players place down these models and leave the game, they come back with all their stats and attributes just like they were before?
I previously tried GnomeCode’s solution for models, but it doesn’t work well for Multiplayer nor does it fit for having the attributes of the Tools/Models being modified, as that method required Cloning the object from ReplicatedStorage which would’ve reset the attributes.
Any help would be awesome! Thanks!
Here’s the Leaderstats script:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local DataStoreService = game:GetService("DataStoreService")
local database = DataStoreService:GetDataStore("DataGnome")
local sessionData = {}
local function DeserializePart(data) -- For "Building" data - remains unchanged
local part = Instance.new("Part")
part.Anchored = true
part.Position = Vector3.new(data[1], data[2], data[3])
part.Size = Vector3.new(data[4], data[5], data[6])
part.Color = Color3.new(data[7], data[8], data[9])
part.Orientation = Vector3.new(data[10], data[11], data[12])
part.Shape = Enum.PartType[data[13]]
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.LeftSurface = Enum.SurfaceType.Smooth
part.RightSurface = Enum.SurfaceType.Smooth
part.FrontSurface = Enum.SurfaceType.Smooth
part.BackSurface = Enum.SurfaceType.Smooth
part.Parent = workspace.CustomBuild
end
-- New recursive serialization function for plot items and their children
local function SerializeInstanceRecursive(instance)
if instance.ClassName == "TouchTransmitter" then -- Exclude TouchTransmitter
return nil
end
if not instance.Archivable then
return nil
end
local data = {
Name = instance.Name,
ClassName = instance.ClassName,
Archivable = instance.Archivable
}
-- Type-specific properties
if instance:IsA("BasePart") then
data.Anchored = instance.Anchored
data.Size = {instance.Size.X, instance.Size.Y, instance.Size.Z}
data.Color = {instance.Color.R, instance.Color.G, instance.Color.B}
-- Store CFrame components for better precision with position and orientation
data.CFrame = {instance.CFrame:GetComponents()}
data.Transparency = instance.Transparency
data.Material = instance.Material.Name
data.CanCollide = instance.CanCollide
data.CastShadow = instance.CastShadow
if instance:IsA("Part") then
data.Shape = instance.Shape.Name
elseif instance:IsA("MeshPart") then
data.MeshId = instance.MeshId
data.TextureID = instance.TextureID
end
elseif instance:IsA("Model") then
-- Models are primarily defined by their children
elseif instance:IsA("Script") or instance:IsA("LocalScript") or instance:IsA("ModuleScript") then
-- data.Source = instance.Source -- Cannot read Source property from server scripts
data.Disabled = instance.Disabled
elseif instance:IsA("Constraint") then
data.Enabled = instance.Enabled
-- Note: Serializing Part0/Part1 references is complex and omitted here.
-- The constraint instance and its basic properties will be saved.
elseif instance:IsA("Attachment") then
data.CFrame = {instance.CFrame:GetComponents()} -- CFrame relative to parent
elseif instance:IsA("Tool") then
data.Enabled = instance.Enabled
data.ToolTip = instance.ToolTip
data.RequiresHandle = instance.RequiresHandle
data.CanBeDropped = instance.CanBeDropped
data.Grip = {instance.Grip:GetComponents()}
-- Add other types as needed (e.g., Decal, SurfaceGui, BillboardGui, Sound)
elseif instance:IsA("Decal") or instance:IsA("Texture") then
data.Texture = instance.Texture
data.Color3 = {instance.Color3.R, instance.Color3.G, instance.Color3.B}
data.Transparency = instance.Transparency
data.Face = instance.Face.Name
elseif instance:IsA("SurfaceGui") or instance:IsA("BillboardGui") then
data.Enabled = instance.Enabled
data.Adornee = if instance.Adornee then instance.Adornee.Name else nil -- Save by name, resolve on load if needed (complex)
data.AlwaysOnTop = instance.AlwaysOnTop
if instance:IsA("BillboardGui") then
data.Size = {instance.Size.X.Scale, instance.Size.X.Offset, instance.Size.Y.Scale, instance.Size.Y.Offset}
data.StudsOffset = {instance.StudsOffset.X, instance.StudsOffset.Y, instance.StudsOffset.Z}
end
elseif instance:IsA("Sound") then
data.SoundId = instance.SoundId
data.Volume = instance.Volume
data.Looped = instance.Looped
data.Pitch = instance.Pitch
data.TimePosition = instance.TimePosition -- Might be relevant if saving mid-play
data.Playing = instance.Playing -- Save if it should resume playing
end
-- Serialize children
local childrenData = {}
for _, child in instance:GetChildren() do
local childData = SerializeInstanceRecursive(child)
if childData then
table.insert(childrenData, childData)
end
end
if #childrenData > 0 then
data.Children = childrenData
end
return data
end
-- New recursive deserialization function for plot items and their children
local function DeserializeInstanceRecursive(itemData, parentInstance)
if not itemData or not itemData.ClassName then
warn("Invalid itemData for Deserialization: Missing ClassName. Data:", itemData)
return nil
end
local newItem
local success, err = pcall(function()
newItem = Instance.new(itemData.ClassName)
end)
if not success or not newItem then
warn("Failed to create instance of ClassName:", itemData.ClassName, "Error:", err)
return nil
end
newItem.Name = itemData.Name
if itemData.Archivable ~= nil then
newItem.Archivable = itemData.Archivable
end
-- Apply type-specific properties
if newItem:IsA("BasePart") then
if itemData.Anchored ~= nil then newItem.Anchored = itemData.Anchored end
if itemData.Size then newItem.Size = Vector3.new(unpack(itemData.Size)) end
if itemData.Color then newItem.Color = Color3.new(unpack(itemData.Color)) end
if itemData.CFrame then newItem.CFrame = CFrame.new(unpack(itemData.CFrame)) end
if itemData.Transparency ~= nil then newItem.Transparency = itemData.Transparency end
if itemData.Material then
local matEnum = Enum.Material[itemData.Material]
if matEnum then newItem.Material = matEnum else warn("Invalid material:", itemData.Material, "for item:", newItem.Name) end
end
if itemData.CanCollide ~= nil then newItem.CanCollide = itemData.CanCollide end
if itemData.CastShadow ~= nil then newItem.CastShadow = itemData.CastShadow end
if newItem:IsA("Part") and itemData.Shape then
local shapeEnum = Enum.PartType[itemData.Shape]
if shapeEnum then newItem.Shape = shapeEnum else warn("Invalid shape:", itemData.Shape, "for part:", newItem.Name) end
elseif newItem:IsA("MeshPart") then
-- MeshId
if itemData.MeshId and itemData.MeshId ~= "" then
local fullId = tostring(itemData.MeshId)
if string.sub(fullId, 1, 13) == "rbxassetid://" then
local idNumberPart = string.sub(fullId, 14)
if idNumberPart ~= "" then
local num = tonumber(idNumberPart)
if num and num > 0 and math.floor(num) == num then -- Positive integer check
local suc, errorMsg = pcall(function()
newItem.MeshId = fullId
end)
if not suc then
warn("pcall failed to set MeshId '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Error: " .. tostring(errorMsg))
end
else
warn("Invalid numeric content in MeshId: '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Empty ID number in MeshId: '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Invalid MeshId format (missing or incorrect prefix): '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://...'")
end
end
-- TextureID
if itemData.TextureID and itemData.TextureID ~= "" then
local fullId = tostring(itemData.TextureID)
if string.sub(fullId, 1, 13) == "rbxassetid://" then
local idNumberPart = string.sub(fullId, 14)
if idNumberPart ~= "" then
local num = tonumber(idNumberPart)
if num and num > 0 and math.floor(num) == num then -- Positive integer check
local suc, errorMsg = pcall(function()
newItem.TextureID = fullId
end)
if not suc then
warn("pcall failed to set TextureID '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Error: " .. tostring(errorMsg))
end
else
warn("Invalid numeric content in TextureID: '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Empty ID number in TextureID: '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Invalid TextureID format (missing or incorrect prefix): '" .. fullId .. "' for MeshPart: " .. newItem.Name .. ". Expected 'rbxassetid://...'")
end
end
end
elseif newItem:IsA("Model") then
-- Properties applied, children will be handled next
elseif (newItem:IsA("Script") or newItem:IsA("LocalScript") or newItem:IsA("ModuleScript")) then
-- if itemData.Source then newItem.Source = itemData.Source end -- Cannot set Source property from server scripts
if itemData.Disabled ~= nil then newItem.Disabled = itemData.Disabled end
elseif newItem:IsA("Constraint") then
if itemData.Enabled ~= nil then newItem.Enabled = itemData.Enabled end
-- Note: Restoring Part0/Part1 would require a more complex system to find parts by path/ID post-deserialization.
elseif newItem:IsA("Attachment") then
if itemData.CFrame then newItem.CFrame = CFrame.new(unpack(itemData.CFrame)) end
elseif newItem:IsA("Tool") then
if itemData.Enabled ~= nil then newItem.Enabled = itemData.Enabled end
if itemData.ToolTip then newItem.ToolTip = itemData.ToolTip end
if itemData.RequiresHandle ~= nil then newItem.RequiresHandle = itemData.RequiresHandle end
if itemData.CanBeDropped ~= nil then newItem.CanBeDropped = itemData.CanBeDropped end
if itemData.Grip then newItem.Grip = CFrame.new(unpack(itemData.Grip)) end
elseif newItem:IsA("Decal") or newItem:IsA("Texture") then
-- Texture Property
if itemData.Texture and itemData.Texture ~= "" then
local fullId = tostring(itemData.Texture)
if string.sub(fullId, 1, 13) == "rbxassetid://" then
local idNumberPart = string.sub(fullId, 14)
if idNumberPart ~= "" then
local num = tonumber(idNumberPart)
if num and num > 0 and math.floor(num) == num then -- Positive integer check
local suc, errorMsg = pcall(function()
newItem.Texture = fullId
end)
if not suc then
warn("pcall failed to set Texture '" .. fullId .. "' for " .. newItem.ClassName .. ": " .. newItem.Name .. ". Error: " .. tostring(errorMsg))
end
else
warn("Invalid numeric content in Texture property: '" .. fullId .. "' for " .. newItem.ClassName .. ": " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Empty ID number in Texture property: '" .. fullId .. "' for " .. newItem.ClassName .. ": " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Invalid Texture property format (missing or incorrect prefix): '" .. fullId .. "' for " .. newItem.ClassName .. ": " .. newItem.Name .. ". Expected 'rbxassetid://...'")
end
end
if itemData.Color3 then newItem.Color3 = Color3.new(unpack(itemData.Color3)) end
if itemData.Transparency ~= nil then newItem.Transparency = itemData.Transparency end
if itemData.Face then
local faceEnum = Enum.NormalId[itemData.Face]
if faceEnum then newItem.Face = faceEnum else warn("Invalid face:", itemData.Face, "for item:", newItem.Name) end
end
elseif newItem:IsA("SurfaceGui") or newItem:IsA("BillboardGui") then
if itemData.Enabled ~= nil then newItem.Enabled = itemData.Enabled end
-- Adornee restoration is complex if it's not a child. For now, assume Adornee is handled if it's part of the structure or set manually.
if itemData.AlwaysOnTop ~= nil then newItem.AlwaysOnTop = itemData.AlwaysOnTop end
if newItem:IsA("BillboardGui") then
if itemData.Size then newItem.Size = UDim2.new(itemData.Size[1], itemData.Size[2], itemData.Size[3], itemData.Size[4]) end
if itemData.StudsOffset then newItem.StudsOffset = Vector3.new(unpack(itemData.StudsOffset)) end
end
elseif newItem:IsA("Sound") then
-- SoundId
if itemData.SoundId and itemData.SoundId ~= "" then
local fullId = tostring(itemData.SoundId)
if string.sub(fullId, 1, 13) == "rbxassetid://" then
local idNumberPart = string.sub(fullId, 14)
if idNumberPart ~= "" then
local num = tonumber(idNumberPart)
if num and num > 0 and math.floor(num) == num then -- Positive integer check
local suc, errorMsg = pcall(function()
newItem.SoundId = fullId
end)
if not suc then
warn("pcall failed to set SoundId '" .. fullId .. "' for Sound: " .. newItem.Name .. ". Error: " .. tostring(errorMsg))
end
else
warn("Invalid numeric content in SoundId: '" .. fullId .. "' for Sound: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Empty ID number in SoundId: '" .. fullId .. "' for Sound: " .. newItem.Name .. ". Expected 'rbxassetid://<positive_integer>'")
end
else
warn("Invalid SoundId format (missing or incorrect prefix): '" .. fullId .. "' for Sound: " .. newItem.Name .. ". Expected 'rbxassetid://...'")
end
end
if itemData.Volume ~= nil then newItem.Volume = itemData.Volume end
if itemData.Looped ~= nil then newItem.Looped = itemData.Looped end
if itemData.Pitch ~= nil then newItem.Pitch = itemData.Pitch end
if itemData.TimePosition ~= nil then newItem.TimePosition = itemData.TimePosition end
if itemData.Playing ~= nil and itemData.Playing then
-- Check if SoundId was successfully set before trying to play
local currentSoundId = ""
local soundIdSuccess, soundIdValue = pcall(function() currentSoundId = newItem.SoundId end)
if soundIdSuccess and currentSoundId and currentSoundId ~= "" then
task.wait() -- Ensure sound is ready
newItem:Play()
elseif itemData.SoundId and itemData.SoundId ~= "" then
warn("Sound '" .. newItem.Name .. "' was marked as Playing, but its SoundId ('" .. tostring(itemData.SoundId) .. "') was invalid or failed to set. Will not play.")
end
end
end
-- Deserialize and parent children (before parenting newItem itself, so child properties are relative to newItem)
if itemData.Children then
for _, childData in itemData.Children do
DeserializeInstanceRecursive(childData, newItem) -- Children parented to newItem
end
end
if parentInstance then
newItem.Parent = parentInstance
end
return newItem
end
local function SavePlotItems(player)
if not sessionData[player.UserId] then return end
local plotName = player:FindFirstChild("ClaimedPlotBase") and player.ClaimedPlotBase.Value
if not plotName or plotName == "" then
sessionData[player.UserId].PlotItems = {}
return
end
local plotFolder = workspace:FindFirstChild(plotName)
if not plotFolder then
warn("SavePlotItems: Plot folder not found: " .. plotName)
sessionData[player.UserId].PlotItems = {}
return
end
local baseFolder = plotFolder:FindFirstChild("base")
if not baseFolder then
warn("SavePlotItems: Base folder not found in plot: " .. plotName)
sessionData[player.UserId].PlotItems = {}
return
end
local itemHolder = baseFolder:FindFirstChild("itemHolder")
if not itemHolder then
warn("SavePlotItems: itemHolder folder not found in: " .. plotName .. ".base")
sessionData[player.UserId].PlotItems = {}
return
end
local itemsToSave = {}
for _, item in itemHolder:GetChildren() do
local serializedItem = SerializeInstanceRecursive(item) -- Use new recursive function
if serializedItem then
table.insert(itemsToSave, serializedItem)
end
end
sessionData[player.UserId].PlotItems = itemsToSave
print("Saved", #itemsToSave, "items from", plotName, "itemHolder for player", player.Name)
end
local function LoadPlotItems(player)
if not sessionData[player.UserId] or not sessionData[player.UserId].PlotItems then
return
end
local plotName = player:FindFirstChild("ClaimedPlotBase") and player.ClaimedPlotBase.Value
if not plotName or plotName == "" then
return
end
local plotFolder = workspace:FindFirstChild(plotName)
if not plotFolder then
warn("LoadPlotItems: Plot folder not found: " .. plotName)
return
end
local baseFolder = plotFolder:FindFirstChild("base")
if not baseFolder then
warn("LoadPlotItems: Base folder not found in plot: " .. plotName)
return
end
local itemHolder = baseFolder:FindFirstChild("itemHolder")
if not itemHolder then
-- Try to create itemHolder if it doesn't exist
itemHolder = Instance.new("Folder")
itemHolder.Name = "itemHolder"
itemHolder.Parent = baseFolder
warn("LoadPlotItems: itemHolder folder not found in: " .. plotName .. ".base. Created one.")
-- return -- Original script returned, but let's try to load into a new one.
end
itemHolder:ClearAllChildren()
local plotItemsData = sessionData[player.UserId].PlotItems
if plotItemsData and #plotItemsData > 0 then
print("Loading", #plotItemsData, "items into", plotName, "itemHolder for player", player.Name)
for _, itemData in plotItemsData do
DeserializeInstanceRecursive(itemData, itemHolder) -- Use new recursive function
end
end
end
function PlayerAdded(player)
local leaderstats = Instance.new("Folder")
leaderstats.Parent = player
leaderstats.Name = "leaderstats"
local cash = Instance.new("IntValue")
cash.Parent = leaderstats
cash.Name = "Cash"
cash.Value = 5000
local landValue = Instance.new("ObjectValue")
landValue.Name = "ClaimLand"
landValue.Parent = player
local claimedPlotBase = Instance.new("StringValue")
claimedPlotBase.Name = "ClaimedPlotBase"
claimedPlotBase.Parent = player
claimedPlotBase.Value = "Plot1"
local success = nil
local playerData = nil
local attempt = 1
repeat
success, playerData = pcall(function()
return database:GetAsync(player.UserId)
end)
attempt += 1
if not success then
warn(playerData)
task.wait(3)
end
until success or attempt == 5
if success then
print("Connected to database for", player.Name)
if not playerData then
print("Assigning default data for", player.Name)
playerData = {
["Cash"] = 5000,
["Building"] = {},
["PlotItems"] = {},
["ClaimedPlotBaseName"] = "Plot1"
}
end
sessionData[player.UserId] = playerData
sessionData[player.UserId].Building = sessionData[player.UserId].Building or {}
sessionData[player.UserId].PlotItems = sessionData[player.UserId].PlotItems or {}
sessionData[player.UserId].Cash = sessionData[player.UserId].Cash or 5000
if sessionData[player.UserId].ClaimedPlotBaseName then
claimedPlotBase.Value = sessionData[player.UserId].ClaimedPlotBaseName
else
sessionData[player.UserId].ClaimedPlotBaseName = claimedPlotBase.Value
end
for _, partData in sessionData[player.UserId].Building do
DeserializePart(partData)
end
LoadPlotItems(player)
else
warn("Unable to get data for", player.UserId)
player:Kick("Unable to load your data. Try again later.")
return
end
cash.Value = sessionData[player.UserId].Cash
cash.Changed:Connect(function()
if sessionData[player.UserId] then
sessionData[player.UserId].Cash = cash.Value
end
end)
claimedPlotBase.Changed:Connect(function(newPlotName)
if sessionData[player.UserId] then
sessionData[player.UserId].ClaimedPlotBaseName = newPlotName
end
end)
end
Players.PlayerAdded:Connect(PlayerAdded)
local function SerializePart(part) -- For "Building" data - remains unchanged
return {
part.Position.X,
part.Position.Y,
part.Position.Z,
part.Size.X,
part.Size.Y,
part.Size.Z,
part.Color.R,
part.Color.G,
part.Color.B,
part.Orientation.X,
part.Orientation.Y,
part.Orientation.Z,
part.Shape.Name,
}
end
function PlayerLeaving(player)
print(player.Name, "is leaving")
if sessionData[player.UserId] then
SavePlotItems(player)
local myParts = {}
if workspace:FindFirstChild("CustomBuild") then
for _, part in workspace.CustomBuild:GetChildren() do
table.insert(myParts, SerializePart(part))
end
end
sessionData[player.UserId]["Building"] = myParts
local claimedPlotBaseInstance = player:FindFirstChild("ClaimedPlotBase")
if claimedPlotBaseInstance then
sessionData[player.UserId].ClaimedPlotBaseName = claimedPlotBaseInstance.Value
end
local success = nil
local errorMsg = nil
local attempt = 1
repeat
success, errorMsg = pcall(function()
database:SetAsync(player.UserId, sessionData[player.UserId])
end)
attempt += 1
if not success then
warn("Save attempt " .. attempt-1 .. " failed for " .. player.Name .. ": " .. errorMsg)
task.wait(3)
end
until success or attempt == 5
if success then
print("Data saved for", player.Name)
else
warn("Unable to save data for", player.Name, "after multiple attempts.")
end
sessionData[player.UserId] = nil
end
local claimedLandValue = player:FindFirstChild("ClaimLand") and player.ClaimLand.Value
if claimedLandValue and claimedLandValue:IsA("Model") and claimedLandValue:FindFirstChild("SignBoard") then
local signBoard = claimedLandValue.SignBoard
if signBoard:FindFirstChild("SurfaceGui") then
local surfaceGui = signBoard.SurfaceGui
if surfaceGui:FindFirstChild("OwnershipLabel") then
surfaceGui.OwnershipLabel.Text = "Unclaimed Land"
end
if surfaceGui:FindFirstChild("OwnershipProfile") then
surfaceGui.OwnershipProfile.Image = ""
end
end
if signBoard:FindFirstChild("ProximityPrompt") then
signBoard.ProximityPrompt.Enabled = true
end
end
if player:FindFirstChild("ClaimLand") then player.ClaimLand.Value = nil end
if player:FindFirstChild("ClaimedPlotBase") then player.ClaimedPlotBase.Value = "" end
end
Players.PlayerRemoving:Connect(PlayerLeaving)
game.ReplicatedStorage.LeaveLand.OnServerEvent:Connect(function(player, plotName)
local claimedLandValue = player:FindFirstChild("ClaimLand") and player.ClaimLand.Value
if claimedLandValue and claimedLandValue:IsA("Model") and claimedLandValue:FindFirstChild("SignBoard") then
local signBoard = claimedLandValue.SignBoard
if signBoard:FindFirstChild("SurfaceGui") then
local surfaceGui = signBoard.SurfaceGui
if surfaceGui:FindFirstChild("OwnershipLabel") then
surfaceGui.OwnershipLabel.Text = "Unclaimed Land"
end
if surfaceGui:FindFirstChild("OwnershipProfile") then
surfaceGui.OwnershipProfile.Image = ""
end
end
if signBoard:FindFirstChild("ProximityPrompt") then
signBoard.ProximityPrompt.Enabled = true
end
end
if player:FindFirstChild("ClaimLand") then player.ClaimLand.Value = nil end
if player:FindFirstChild("ClaimedPlotBase") then player.ClaimedPlotBase.Value = "" end
local resetEvent = game:GetService("ReplicatedStorage"):FindFirstChild("remotes"):FindFirstChild("ResetClaimedPlot")
if resetEvent then
resetEvent:FireClient(player)
else
warn("ResetClaimedPlot RemoteEvent not found in ReplicatedStorage.remotes")
end
end)
function ServerShutdown()
if RunService:IsStudio() then
return
end
print("Server shutting down..")
local activePlayers = Players:GetPlayers()
for _, player in activePlayers do
task.spawn(function()
PlayerLeaving(player)
end)
end
if #activePlayers > 0 then
task.wait(5)
end
end
game:BindToClose(ServerShutdown)