TimberWoods / src /ServerScriptService /DatastoreManager.server.lua
algorembrant's picture
Upload 88 files
0712d5f verified
-- 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)