How would i save location of parts relative to a plots bounds

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:

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
1 Like

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

  • parts should 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()



these are screenshots of the files/slots

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 :confused:

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.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.