-- src/ServerScriptService/DatastoreManager.server.lua local DataStoreService = game:GetService("DataStoreService") local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local PlotDataStore local datastoreAvailable = false local success, err = pcall(function() PlotDataStore = DataStoreService:GetDataStore("Timberbound_PlayerData_v2") end) if success then datastoreAvailable = true else warn("DataStore not available (place may not be published): " .. tostring(err)) end local AUTO_SAVE_INTERVAL = 120 -- seconds local MAX_RETRIES = 3 local function serializePlotItems(plotPart) local dataToSave = {} for _, item in pairs(plotPart:GetChildren()) do if not (item:IsA("Model") or item:IsA("BasePart")) then continue end local itemId = item:GetAttribute("ItemId") or item.Name local relativeCFrame = plotPart.CFrame:ToObjectSpace(item:GetPivot()) local px, py, pz, r00, r01, r02, r10, r11, r12, r20, r21, r22 = relativeCFrame:GetComponents() local stateData = {} if item:GetAttribute("FillLevel") then stateData.FillLevel = item:GetAttribute("FillLevel") end if item:GetAttribute("WoodFilled") then stateData.WoodFilled = item:GetAttribute("WoodFilled") end if item:GetAttribute("ProcessState") then stateData.ProcessState = item:GetAttribute("ProcessState") end table.insert(dataToSave, { id = itemId, cframe = {px, py, pz, r00, r01, r02, r10, r11, r12, r20, r21, r22}, state = stateData, }) end return dataToSave end local function buildSaveData(player, plotPart) local data = _G.GetPlayerData(player) local leaderstats = player:FindFirstChild("leaderstats") local saveData = { version = 2, cash = 0, woodChopped = 0, inventory = {}, questProgress = {}, achievements = {}, biomesVisited = {}, totalEarned = 0, totalWoodSold = 0, totalBuilt = 0, plotItems = {}, } -- Cash and stats from leaderstats if leaderstats then local cash = leaderstats:FindFirstChild("Cash") if cash then saveData.cash = cash.Value end local wood = leaderstats:FindFirstChild("WoodChopped") if wood then saveData.woodChopped = wood.Value end end -- Player data if data then saveData.inventory = data.Inventory or {} saveData.questProgress = data.QuestProgress or {} saveData.biomesVisited = data.BiomesVisited or {} saveData.totalEarned = data.TotalEarned or 0 saveData.totalWoodSold = data.TotalWoodSold or 0 saveData.totalBuilt = data.TotalBuilt or 0 end -- Quest data local quests = _G.GetPlayerQuests and _G.GetPlayerQuests(player) if quests then saveData.questProgress = quests end -- Achievement data local achievements = _G.GetPlayerAchievements and _G.GetPlayerAchievements(player) if achievements then saveData.achievements = achievements end -- Plot items if plotPart then saveData.plotItems = serializePlotItems(plotPart) end return saveData end -- Save with retry logic local function saveWithRetry(userId, saveData) if not datastoreAvailable then return false end for attempt = 1, MAX_RETRIES do local success, err = pcall(function() local jsonString = HttpService:JSONEncode(saveData) PlotDataStore:SetAsync(tostring(userId), jsonString) end) if success then return true else warn("Save attempt " .. attempt .. " failed for userId " .. userId .. ": " .. tostring(err)) if attempt < MAX_RETRIES then task.wait(2 ^ attempt) -- Exponential backoff end end end return false end -- Save Player Data (globally accessible) _G.SavePlayerData = function(player, plotPart) if not datastoreAvailable then print("DataStore not available, skipping save for " .. player.Name) return false end local saveData = buildSaveData(player, plotPart) local success = saveWithRetry(player.UserId, saveData) if success then print("Successfully saved data for " .. player.Name) else warn("Failed to save data for " .. player.Name .. " after " .. MAX_RETRIES .. " retries") end return success end -- Load Player Data _G.LoadPlayerData = function(player, plotPart) if not datastoreAvailable then print("DataStore not available, skipping load for " .. player.Name) return end local success, result = pcall(function() return PlotDataStore:GetAsync(tostring(player.UserId)) end) if not success then warn("Failed to load data for " .. player.Name .. ": " .. tostring(result)) return end if not result then print("No saved data found for " .. player.Name .. " (new player)") return end local savedData = HttpService:JSONDecode(result) -- Restore cash local leaderstats = player:FindFirstChild("leaderstats") if leaderstats and savedData.cash then local cash = leaderstats:FindFirstChild("Cash") if cash then cash.Value = savedData.cash end local wood = leaderstats:FindFirstChild("WoodChopped") if wood and savedData.woodChopped then wood.Value = savedData.woodChopped end end -- Restore player data cache local data = _G.GetPlayerData(player) if data then if savedData.inventory then data.Inventory = savedData.inventory end if savedData.biomesVisited then data.BiomesVisited = savedData.biomesVisited end if savedData.totalEarned then data.TotalEarned = savedData.totalEarned end if savedData.totalWoodSold then data.TotalWoodSold = savedData.totalWoodSold end if savedData.totalBuilt then data.TotalBuilt = savedData.totalBuilt end -- Restore equipped axe if savedData.inventory and savedData.inventory.Tools and #savedData.inventory.Tools > 0 then local lastTool = savedData.inventory.Tools[#savedData.inventory.Tools] data.EquippedAxe = lastTool player:SetAttribute("EquippedAxe", lastTool) end end -- Restore plot items if plotPart and savedData.plotItems then for _, itemData in pairs(savedData.plotItems) do -- Create parts from saved data local newPart = Instance.new("Part") newPart.Name = itemData.id newPart:SetAttribute("ItemId", itemData.id) -- Reconstruct CFrame if itemData.cframe and #itemData.cframe >= 12 then local cf = CFrame.new(unpack(itemData.cframe)) newPart:PivotTo(plotPart.CFrame * cf) end -- Restore state if itemData.state then for key, value in pairs(itemData.state) do newPart:SetAttribute(key, value) end end newPart.Anchored = true newPart.Size = Vector3.new(4, 4, 4) newPart.Material = Enum.Material.WoodPlanks newPart.Parent = plotPart end end print("Successfully loaded data for " .. player.Name) end -- Auto-save loop task.spawn(function() while true do task.wait(AUTO_SAVE_INTERVAL) for _, player in ipairs(Players:GetPlayers()) do task.spawn(function() pcall(function() -- Find their plot local plotPart = nil local plotsFolder = workspace:FindFirstChild("WorldStructures") if plotsFolder then local plots = plotsFolder:FindFirstChild("Plots") if plots then for _, plot in pairs(plots:GetChildren()) do if plot:GetAttribute("OwnerId") == player.UserId then plotPart = plot break end end end end _G.SavePlayerData(player, plotPart) end) end) end end end) -- BindToClose for server shutdown game:BindToClose(function() for _, player in ipairs(Players:GetPlayers()) do pcall(function() local plotPart = nil local plotsFolder = workspace:FindFirstChild("WorldStructures") if plotsFolder then local plots = plotsFolder:FindFirstChild("Plots") if plots then for _, plot in pairs(plots:GetChildren()) do if plot:GetAttribute("OwnerId") == player.UserId then plotPart = plot break end end end end _G.SavePlayerData(player, plotPart) end) end end)