File size: 12,849 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
if FirstLoad then
	g_SaveGameObj = false
	g_SaveLoadThread = false
	
	g_CurrentSaveGameItemId = false
	g_SaveGameDescrThread = false
end

DefineClass.SaveLoadObject = {
	__parents = { "PropertyObject" },
	items = false,
	initialized = false,
}

function SaveLoadObject:ListSavegames()
	return Savegame.ListForTag("savegame")
end

function SaveLoadObject:DoSavegame(name)
	return SaveGame(name, { save_as_last  = true })
end

function SaveLoadObject:DoLoadgame(name)
	return LoadGame(name, { save_as_last = true })
end

function SaveLoadObject:WaitGetSaveItems()
	local items = {}
	local err, list = self:ListSavegames()
	if not err then
		for _, v in ipairs(list) do
			local id = #items + 1
			items[id] = {
				text = v.displayname,
				id = id,
				savename = v.savename,
				metadata = v,
			}
		end
	end
	self.items = items
	if not self.initialized then
		self.initialized = true
	end
end

function SaveLoadObject:RemoveItem(id)
	local items = self.items or empty_table
	for i = #items, 1, -1 do
		local item_id = items[i].id
		if item_id == id then
			table.remove(items, i)
		elseif item_id > id then
			items[i].id = item_id - 1
		end
	end
end

function SaveLoadObject:CalcDefaultSaveName()
	local default_text = _InternalTranslate(T(278399852865, "Savegame"))
	local items = self.items
	local max_num = 0
	for k, v in ipairs(items) do
		local text = v.text
		if string.match(text, "^" .. default_text) then
			local number = (text == default_text) and 1 or tonumber(string.match(text, "^" .. default_text .. "%s%((%d+)%)$") or 0)
			max_num = Max(max_num, number)
		end
	end
	if max_num > 0 then
		return default_text .. " (" .. max_num + 1 .. ")"
	end
	return default_text:trim_spaces()
end

function SaveLoadObject:ShowNewSavegameNamePopup(host, item)
	if not host:IsThreadRunning("rename") then
		host:CreateThread("rename", function(item)
			local caption = _InternalTranslate(T(808375213123, "Enter name:"))
			local savename = config.DefaultOverwriteSavegameAnswer and item and item.text or
				WaitInputText(nil, caption, item and item.text or self:CalcDefaultSaveName(), 32,
					function (name)
						if not name:match("%w") then
							return T(528136022504, "The save name must contain at least one letter or digit")
						end
				end)
			if savename then
				self:Save(item, savename)
			end
		end, item)
	end
end

function SaveLoadObject:Save(item, name)
	name = name:trim_spaces()
	if name and name ~= "" then
		g_SaveLoadThread = IsValidThread(g_SaveLoadThread) and g_SaveLoadThread or CreateRealTimeThread(function(name, item)
			local parent = GetPreGameMainMenu() or GetInGameMainMenu()
			local err, savename
			if item then
				if config.DefaultOverwriteSavegameAnswer or WaitQuestion(parent,
					T(824112417429, "Warning"),
					T{883071764117, "Are you sure you want to overwrite <savename>?", savename = '"' .. Untranslated(item.text) .. '"'},
					T(689884995409, "Yes"),
					T(782927325160, "No")) == "ok" then
					err = DeleteGame(item.savename)
				else
					return
				end
			end
			if not err or err == "File Not Found" then
				err, savename = self:DoSavegame(name)
			end
			if not err then
				CloseMenuDialogs()
			else
				CreateErrorMessageBox(err, "savegame", nil, parent, {savename = T{129666099950, '"<name>"', name = Untranslated(name)}, error_code = Untranslated(err)})
			end
		end, name, item)
	end
end

function SaveLoadObject:Load(dlg, item, skipAreYouSure)
	if item then
		local savename = item.savename
		g_SaveLoadThread = IsValidThread(g_SaveLoadThread) and g_SaveLoadThread or CreateRealTimeThread(function(dlg, savename)
			local metadata = item.metadata
			local err
			local parent = GetPreGameMainMenu() or GetInGameMainMenu() or (dlg and dlg.parent) or terminal.desktop
			if metadata and not metadata.corrupt and not metadata.incompatible then
				local in_game = GameState.gameplay -- this might change during loading
				local res = config.DefaultLoadAnywayAnswer or (in_game and not skipAreYouSure) and
					WaitQuestion(parent, T(824112417429, "Warning"),
						T(927104451536, "Are you sure you want to load this savegame? Any unsaved progress will be lost."),
						T(689884995409, "Yes"), T(782927325160, "No"))
					or "ok"
				if res == "ok" then
					err = self:DoLoadgame(savename, metadata)
					if not err then
						CloseMenuDialogs()
					else
						ProjectSpecificLoadGameFailed(dlg)
					end
				end
			else
				err = metadata and metadata.incompatible and "incompatible" or "corrupt"
			end
			if err then
				-- parent might have been destroyed
				parent = GetPreGameMainMenu() or GetInGameMainMenu() or (dlg and dlg.parent) or terminal.desktop
				CreateErrorMessageBox(err, "loadgame", nil, parent, {name = '"' .. Untranslated(item.text) .. '"'})
			end
		end, dlg, savename)
	end
end

function SaveLoadObject:Delete(dlg, list)
	local list = list or dlg:ResolveId("idList")
	if not list or not list.focused_item then return end
	local ctrl = list[list.focused_item]
	if not ctrl then return end
	local item = ctrl and ctrl.context
	if item then
		local savename = item.savename
		CreateRealTimeThread(function(dlg, item, savename)
			if WaitQuestion(dlg.parent, T(824112417429, "Warning"), T{912614823850, "Are you sure you want to delete the savegame <savename>?", savename = '"' .. Untranslated(item.text) .. '"'}, T(689884995409, "Yes"), T(782927325160, "No")) == "ok" then
				LoadingScreenOpen("idDeleteScreen", "delete savegame")
				local err = DeleteGame(savename)
				if not err then
					if g_CurrentSaveGameItemId == item.id then
						g_CurrentSaveGameItemId = false
						DeleteThread(g_SaveGameDescrThread)
						dlg.idDescription:SetVisible(false)
					end
					self:RemoveItem(item.id)
					list:Clear()
					ObjModified(self)
					list:DeleteThread("SetInitialSelection")
					list:SetSelection(Min(item.id, #list))
					LoadingScreenClose("idDeleteScreen", "delete savegame")
				else
					LoadingScreenClose("idDeleteScreen", "delete savegame")
					CreateErrorMessageBox("", "deletegame", nil, dlg.parent, {name = '"' .. item.text .. '"'})
				end
			end
		end, dlg, item, savename)
	end
end

function SaveLoadObjectCreateAndLoad()
	g_SaveGameObj = SaveLoadObject:new()
	return g_SaveGameObj
end

function OnMsg.SavegameDeleted(name)
	ObjModified(g_SaveGameObj)
end

-- savegame description text

function SetSavegameDescriptionTexts(dialog, data, missing_dlcs, mods_string, mods_missing)
	local playtime = T(77, "Unknown")
	if data.playtime then
		local h, m, s = FormatElapsedTime(data.playtime, "hms")
		local hours = Untranslated(string.format("%02d", h))
		local minutes = Untranslated(string.format("%02d", m))
		playtime = T{7549, "<hours>:<minutes>", hours = hours, minutes = minutes}
	end
	if not dialog or dialog.window_state == "destroying" then return end
	dialog.idSavegameTitle:SetText(Untranslated(data.displayname))
	dialog.idPlaytime:SetText(T{614724487683, "Playtime <playtime>", playtime = playtime})
	
	if dialog.idTimestamp then
		dialog.idTimestamp:SetText(T(827551891632, "Saved At: ") .. Untranslated(os.date("%Y-%m-%d %H:%M", data.timestamp)))
	end
	
	if rawget(dialog, "idRevision") then 
		dialog.idRevision:SetText(T{220802271589, "Revision <lua_revision> - <assets_revision>", lua_revision = data.lua_revision, assets_revision = data.assets_revision or ""})
	end
	if rawget(dialog, "idMap") then 
		dialog.idMap:SetText(T{316316205743, "Map <map>", map = Untranslated(data.map)})
	end
	
	local problem_text = ""
	if data and data.corrupt then
		problem_text = T(384520518199, "Save file is corrupted!")
	elseif data and data.incompatible then
		problem_text = T(117116727535, "Please update the game to the latest version to load this savegame.")
	elseif missing_dlcs and missing_dlcs ~= "" then
		problem_text = T{309852317927, "Missing downloadable content: <dlcs>", dlcs = Untranslated(missing_dlcs)}
	elseif mods_missing then
		problem_text = T(196062882816, "There are missing mods!")
	elseif data.required_lua_revision and LuaRevision < data.required_lua_revision then
		problem_text = T(329542364773, "Unknown save file format!")
	elseif data.lua_revision < config.SupportedSavegameLuaRevision then
		problem_text = T(936146497756, "Deprecated save file format!")
	end
	dialog.idProblem:SetText(problem_text)
	
	if mods_string and mods_string ~= "" then
		dialog.idActiveMods:SetText(T{560410899617, "Active mods <value>",value = Untranslated(mods_string)})
	else
		dialog.idActiveMods:SetText("")
	end
	
	if GetUIStyleGamepad() then
		dialog.idDelInfo:SetVisible(false)
	else
		local del_hint = not data.new_save and T(173045065615, "DEL to delete. ") or T("")
		dialog.idDelInfo:SetText(del_hint)
	end
end

-- implement in project specific file
function ProjectSpecificLoadGameFailed(dialog)
end

function ShowSavegameDescription(item, dialog)
	if not item then return end
	if g_CurrentSaveGameItemId ~= item.id then
		g_CurrentSaveGameItemId = false
		DeleteThread(g_SaveGameDescrThread)
		g_SaveGameDescrThread = CreateRealTimeThread(function(item, dialog)
			Savegame.CancelLoad()
			
			local metadata = item.metadata
			
			if dialog.window_state == "destroying" then return end
			
			local description = dialog:ResolveId("idDescription")
			if description then
				description:SetVisible(false)
			end
			
			if config.SaveGameScreenshot then
				if IsValidThread(g_SaveScreenShotThread) then
					WaitMsg("SaveScreenShotEnd")
				end
				Sleep(210)
			end
			
			if dialog.window_state == "destroying" then return end
			g_CurrentSaveGameItemId = item.id
			
			-- we need to reload the meta from the disk in order to have the screenshot!
			local data = {}
			local err
			if not metadata then
				-- new save
				data.displayname = T(4182, "<<< New Savegame >>>")
				data.timestamp = os.time()
				data.playtime = GetCurrentPlaytime()
				data.new_save = true
				data.lua_revision = config.SupportedSavegameLuaRevision
				data.game_difficulty = GetGameDifficulty()
			else
				err = GetFullMetadata(metadata, "reload")
				if metadata.corrupt then
					data.corrupt = true
					data.displayname = T(6907, "Damaged savegame")
				elseif metadata.incompatible then
					data.incompatible = true
					data.displayname = T(8648, "Incompatible savegame")
				else
					data = table.copy(metadata)
					data.displayname = Untranslated(data.displayname)
					if Platform.developer then
						local savename = metadata.savename:match("(.*)%.savegame%.sav$")
						savename = savename:gsub("%+", " ")
						savename = savename:gsub("%%(%d%d)", function(hex_code)
							return string.char(tonumber("0x" .. hex_code))
						end)
						if savename ~= metadata.displayname then
							data.displayname = Untranslated(metadata.displayname .. " - " .. savename)
						end
						data.displayname = Untranslated(data.displayname)
					end
				end
			end
			
			local mods_list, mods_string, mods_missing
			local max_mods, more = 30
			if data.active_mods and #data.active_mods > 0 then
				mods_list = {}
				for _, mod in ipairs(data.active_mods) do
					--mod is a table, containing id, title, version and lua_revision or is just the id in older saves
					local local_mod = table.find_value(ModsLoaded, "id", mod.id or mod) or Mods[mod.id or mod]
					if #mods_list >= max_mods then
						more = true
						break
					end
					table.insert(mods_list, mod.title or (local_mod and local_mod.title))
					local is_blacklisted = GetModBlacklistedReason(mod.id)
					local is_deprecated = is_blacklisted and is_blacklisted == "deprecate"
					if not is_deprecated and (not local_mod or not table.find(AccountStorage.LoadMods, mod.id or mod)) then
						mods_missing = true
					end
				end
				mods_string = TList(mods_list, ", ")
				if more then
					mods_string = mods_string .. "<nbsp>..."
				end
			end
			
			local dlcs_list = {}
			for _, dlc in ipairs(data.dlcs or empty_table) do
				if not IsDlcAvailable(dlc.id) then
					dlcs_list[#dlcs_list + 1] = dlc.name
				end
			end
			
			SetSavegameDescriptionTexts(dialog, data, TList(dlcs_list), mods_string, mods_missing)
			
			if config.SaveGameScreenshot then
				local image = ""
				local forced_path = not metadata and g_TempScreenshotFilePath or false
				if not forced_path and Savegame._MountPoint then
					local images = io.listfiles(Savegame._MountPoint, "screenshot*.jpg", "non recursive")
					if #(images or "") > 0 then
						image = images[1]
					end
				elseif forced_path and io.exists(forced_path) then
					image = forced_path
				end
				
				local image_elem = dialog:ResolveId("idImage")
				if image_elem then
					if image ~= "" and not err then
						image_elem:SetImage(image)
					else
						image_elem:SetImage("UI/Common/placeholder.tga")
					end
				end
			end
			
			local description = dialog:ResolveId("idDescription")
			if description then
				description:SetVisible(true)
			end
		end, item, dialog)
	end
end