|
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 |
|
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 = 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 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
local data = {} |
|
local err |
|
if not metadata then |
|
|
|
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 |
|
|
|
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 |