Basically the title, i know other things show this as well but im also interested in how to save it and recall it in data store
Step 1: Store Parts Relative to Plot
Lets say each plot has a BasePart called "PlotBounds" (could be the baseplate of the plot)
To store a parts location relative to the plot:
local function getRelativeCFrame(part, plotBounds)
return plotBounds.CFrame:ToObjectSpace(part.CFrame)
end
To restore it later:
local function applyRelativeCFrame(part, relativeCFrame, plotBounds)
part.CFrame = plotBounds.CFrame:ToWorldSpace(relativeCFrame)
end
Step 2: Saving to DataStore
(Data stores | Documentation - Roblox Creator Hub)
To save the data, youll need to serialize the relative CFrame into a format supported by DataStore (tables with numbers, strings, etc)
local function serializeCFrame(cf)
return {
cf.X, cf.Y, cf.Z,
cf:ToEulerAnglesXYZ()
}
end
-- Or, store full components if you want more precision (12 values)
local function fullSerializeCFrame(cf)
local components = {cf:GetComponents()}
return components
end
Your full save structure might look like:
{
{
partType = "Wall",
relativeCFrame = serializeCFrame(relCF),
color = {R=1, G=0, B=0}
},
...
}
(Save that to the players data using DataStoreService)
Step 3: Loading Parts
When loading the data:
- List item Deserialize the CFrame
- Clone a part template
- Apply the CFrame in world space using
ToWorldSpace()(CFrame | Documentation - Roblox Creator Hub)
local function deserializeCFrame(data)
local x, y, z, rx, ry, rz = unpack(data)
return CFrame.new(x, y, z) * CFrame.Angles(rx, ry, rz)
end
local part = partTemplate:Clone()
part.CFrame = plotBounds.CFrame:ToWorldSpace(deserializeCFrame(saved.relativeCFrame))
part.Color = Color3.new(saved.color.R, saved.color.G, saved.color.B)
part.Parent = workspace
how would i loop through the players data and deserialise each CFrame
and how would i actually create the table
for _, savedPart in ipairs(savedData) do
local relCF = CFrame.new(unpack(savedPart.relativeCFrame)) -- assuming full 12-value CFrame
local worldCF = plotBounds.CFrame:ToWorldSpace(relCF)
local part = partTemplates[savedPart.partType]:Clone()
part.CFrame = worldCF
part.Color = Color3.new(savedPart.color.R, savedPart.color.G, savedPart.color.B)
part.Parent = workspace
end
Make sure savedPart.relativeCFrame is a table of 12 numbers ( from cf:GetComponents()) (CFrame | Documentation - Roblox Creator Hub)
and then to actually save it? cause i am so confused
You just loop through parts and serialize their CFrame relative to the plot
partsshould be a list of all the parts you want to save (e.g.,plotFolder:GetChildren())- this assumes you’re saving per-player using their
UserId
local function getRelativeCFrame(part, plotBounds)
return plotBounds.CFrame:ToObjectSpace(part.CFrame)
end
local function serializeCFrame(cf)
return {cf:GetComponents()} -- gives 12 values: X, Y, Z, rotation matrix
end
(Understanding CFrame with Matrices!)
local function savePlotParts(player, plotBounds, parts)
local dataToSave = {}
for _, part in ipairs(parts) do
table.insert(dataToSave, {
partType = part.Name, -- or a tag or type or somthing like that
relativeCFrame = serializeCFrame(getRelativeCFrame(part, plotBounds)),
color = {R = part.Color.R, G = part.Color.G, B = part.Color.B}
})
end
local DataStoreService = game:GetService("DataStoreService")
local ds = DataStoreService:GetDataStore("PlotData")
local success, err = pcall(function()
ds:SetAsync(player.UserId, dataToSave)
end)
if not success then
warn("Failed to save plot data:", err)
end
end
ok one last question, how would i retrieve the saved data
To retrieve Saved Plot Data
partTemplates should be a dictionary like { Wall = wallTemplate, Chair = chairTemplate }
local function loadPlotParts(player, plotBounds, partTemplates)
local DataStoreService = game:GetService("DataStoreService")
local ds = DataStoreService:GetDataStore("PlotData")
local success, savedData = pcall(function()
return ds:GetAsync(player.UserId)
end)
if not success or not savedData then
warn("No saved data or error:", savedData)
return
end
for _, saved in ipairs(savedData) do
local relCF = CFrame.new(unpack(saved.relativeCFrame))
local worldCF = plotBounds.CFrame:ToWorldSpace(relCF)
local part = partTemplates[saved.partType]:Clone()
part.CFrame = worldCF
part.Color = Color3.new(saved.color.R, saved.color.G, saved.color.B)
part.Parent = workspace
end
end
The function assumes the data was saved in the format from earlier…
Always wrap GetAsync in pcall to avoid runtime errors
(GlobalDataStore | Documentation - Roblox Creator Hub , Pcalls - When and how to use them)
What have i done wrong. It doesnt save or load at all, but no errors
local Sessions = {} -- our table
local DataStore = game:GetService("DataStoreService")
local playerData = DataStore:GetDataStore("PlayerData")
local event = game.ReplicatedStorage.eventies.plop
local start = 0
local partTemplates = game.ReplicatedStorage.Real
local total = 6
local players = game:GetService("Players")
local function getRelativeCFrame(part, plotBounds)
return plotBounds.CFrame:ToObjectSpace(part.PrimaryPart.CFrame)
end
local function loadPlotParts(player, plotBounds, partTemplates)
local DataStoreService = game:GetService("DataStoreService")
local ds = DataStoreService:GetDataStore("PlotData")
local success, savedData = pcall(function()
return ds:GetAsync(player.UserId)
end)
if not success or not savedData then
warn("No saved data or error:", savedData)
return
end
for _, saved in ipairs(savedData) do
local relCF = CFrame.new(unpack(saved.relativeCFrame))
local worldCF = plotBounds.CFrame:ToWorldSpace(relCF)
local part = partTemplates[saved.partType]:Clone()
part.PrimaryPart.CFrame = worldCF
part.Parent = workspace
end
end
local function serializeCFrame(cf)
return {cf:GetComponents()} -- gives 12 values: X, Y, Z, rotation matrix
end
players.PlayerAdded:Connect(function(player)
start += 1
if start <= total then
workspace.Slots[start].Owner.Value = player.Name
print(player.Name, start)
end
local plotBounds = workspace.Slots[start].Bounds
loadPlotParts(player,plotBounds,partTemplates)
end)
local function savePlotParts(player)
local plotBounds = workspace.Slots[start].Bounds
local parts = workspace.Slots[start].Folder:GetChildren()
local dataToSave = {}
for _, part in ipairs(parts) do
table.insert(dataToSave, {
partType = part.Name, -- or a tag or type or somthing like that
relativeCFrame = serializeCFrame(getRelativeCFrame(part, plotBounds)),
})
end
local DataStoreService = game:GetService("DataStoreService")
local ds = DataStoreService:GetDataStore("PlotData")
local success, err = pcall(function()
ds:SetAsync(player.UserId, dataToSave)
end)
if not success then
warn("Failed to save plot data:", err)
end
end
--
event.OnServerEvent:Connect(function(player, object, plopCFrame, child, slot)
child:Destroy()
local yes = game.ReplicatedStorage.Real:WaitForChild(object):Clone()
yes.Parent = slot.Folder
yes:WaitForChild(yes.Name):WaitForChild("Owner").Value = player.Name
yes:SetPrimaryPartCFrame(plopCFrame)
end)
repeat
wait()
until start > 0
players.PlayerRemoving:Connect(function(player)
savePlotParts(player)
end)
ok the problem is the cframe doesnt set, idk why
1. start is global and shared between all players
Your using a global start variable to determine which plot a player owns:
start += 1
if start <= total then
workspace.Slots[start].Owner.Value = player.Name
end
Problem: When the player leaves and savePlotParts(player) is called, it uses start, which now points to the most recently joined player, not the one whos leaving
Simple Fix:
Track each players plot using a dictionary:
local playerPlots = {} -- New dictionary
Then when a player joins:
players.PlayerAdded:Connect(function(player)
start += 1
if start <= total then
workspace.Slots[start].Owner.Value = player.Name
playerPlots[player] = start -- Save which plot this player owns
end
local plotBounds = workspace.Slots[playerPlots[player]].Bounds
loadPlotParts(player, plotBounds, partTemplates)
end)
And when they leave:
players.PlayerRemoving:Connect(function(player)
local index = playerPlots[player]
if index then
savePlotParts(player, index)
playerPlots[player] = nil
end
end)
Now update savePlotParts to take plotIndex:
local function savePlotParts(player, plotIndex)
local plotBounds = workspace.Slots[plotIndex].Bounds
local parts = workspace.Slots[plotIndex].Folder:GetChildren()
-- ... rest stays the same as you gave me
end
3. .PrimaryPart may not be set on your models
This line:
part.PrimaryPart.CFrame = worldCF
Only works if PrimaryPart is set correctly on all templates
Another Simple Fix:
Ensure you set the PrimaryPart in your template models in ReplicatedStorage or at runtime like:
part.PrimaryPart = part:FindFirstChild(part.Name) -- or however you name your main part
Or avoid using PrimaryPart entirely and just move the part manually if its a single part…
If you want you can send me the output of your console with a screenshot!
i did this to find the correct slot instead. still does not work. and i have set primary part on models due to it being used in plopping them down
local slots = nil
for i, v in workspace.Slots:GetChildren() do
if v:FindFirstChild("Owner") and v:FindFirstChild("Owner").Value == player.Name then
slots = v
end
end
print(slots.Name)
local plotBounds = slots.Bounds
local parts = slots.Folder:GetChildren()
Try to debug
For exmaple use print (print(“CFrame:”, worldCF.Position)):
local part = partTemplates[saved.partType]:Clone()
print("PrimaryPart is", part.PrimaryPart)
-- Force-set it again just in case
if not part.PrimaryPart then
part.PrimaryPart = part:FindFirstChild(part.Name)
end
print("Set PrimaryPart:", part.PrimaryPart and part.PrimaryPart.Name or "None")
part:SetPrimaryPartCFrame(worldCF)
part.Parent = workspace
...
Sometimes parenting to workspace first can cause physics or values to mess with placement… Try this:
local part = partTemplates[saved.partType]:Clone()
part.Parent = nil
part.PrimaryPart = part:FindFirstChild(part.Name)
part:SetPrimaryPartCFrame(worldCF)
part.Parent = workspace
it moves them slightly down whenever i load in ![]()
1. Your Setting CFrame Before Parenting
Sometimes, CFrame operations fail silently if the model isn’t in the world yet. Try reordering like this:
local part = template:Clone()
-- Set PrimaryPart again, just to be safe
part.PrimaryPart = part:FindFirstChild(part.Name) -- assuming "Part" = model name
-- Parent FIRST
part.Parent = workspace
-- THEN apply the CFrame
if part.PrimaryPart then
part:SetPrimaryPartCFrame(worldCF)
else
warn("PrimaryPart missing from", part.Name)
end
Try both before and after parenting, the behavior varies depending on how complex your models are (especially if they contain constraints or non-anchored parts)
2. Make Sure Your Saving the Full CFrame Components
You !must! serialize the full 12-value matrix like this when saving:
local function serializeCFrame(cf)
return {cf:GetComponents()}
end
Then when loading:
local relCF = CFrame.new(unpack(saved.relativeCFrame)) -- must be 12 numbers
Add a print to confirm (debug):
print("Saved CFrame Data:", saved.relativeCFrame)
If it prints something like {0, 0, 0} or fewer than 12 numbers → that’s your issue
3. Test with a Simple Manual CFrame
Try replacing worldCF with a test CFrame to test and eliminate the data as the issue:
part:SetPrimaryPartCFrame(CFrame.new(0, 20, 0)) -- Should definitely move it up
If this works your model is fine and the issue is with saved data
Final Debug
Heres proper debugging:
for _, saved in ipairs(savedData) do
local template = partTemplates:FindFirstChild(saved.partType)
if not template then
warn("Template missing:", saved.partType)
continue
end
local part = template:Clone()
part.PrimaryPart = part:FindFirstChild(part.Name)
if not part.PrimaryPart then
warn("PrimaryPart not set on:", part.Name)
continue
end
-- Test print saved CFrame data
print("Saved CFrame:", saved.relativeCFrame)
local relCF = CFrame.new(unpack(saved.relativeCFrame or {}))
local worldCF = plotBounds.CFrame:ToWorldSpace(relCF)
part.Parent = workspace
part:SetPrimaryPartCFrame(worldCF)
end
Root Cause Likely:
Your saving the part.PrimaryPart.CFrame relative to the plot’s center/origin, but in Roblox parts are often placed above the base (Y axis up)… So when reloaded theyre “under” or “sunken” depending on the plots shape or height.
Example:
Lets say your plot is a part at Y = 0 and your placed object is at Y = 3 when saved…
If you saved the relative CFrame as CFrame.new(0, 3, 0), but your plot base is 6 studs thick and centered on the origin (e.g., CFrame.new(0, 0, 0) with size 6) then when restored your part ends up inside or under the plot!
Option 1: Make sure your plot bases CFrame origin is the top surface not the center
You can adjust the plot base in studio by moving the part upward so its Position.Y is at the top or just account for it in code:
-- Offset the plotBounds CFrame upward by half its Y size
local boundsOffset = Vector3.new(0, plotBounds.Size.Y / 2, 0)
local adjustedPlotCFrame = plotBounds.CFrame + boundsOffset
local worldCF = adjustedPlotCFrame:ToWorldSpace(relCF)
Use this adjusted CFrame when reloading parts!!!
Option 2: Save relative to the plots top surface instead of its center
Instead of plotBounds.CFrame, you could create an invisible “Anchor” part positioned at the surface of the plot and use that as your plotBounds. (Cant recommend this Option thoe)
Fixed, turns out the script was adjusting the primary part to the wrong part. Im gonna test with multiple people now.
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.

