File size: 7,983 Bytes
0712d5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
-- 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)