Datastore for Models and Tools with custom Attributes?

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)


Don’t just paste 500+ Lines of code and call it a day.

Usually to save models spawnned by the player you just save the Name and Position only.
To load them when the player joins, search for the model in the folder where you store them clone it and set it to the saved position.

You don’t need to save everything about the model, only the properties that can change.

For attributes use GetAttributes()