diff --git a/LuaMenu/widgets/api_download_handler.lua b/LuaMenu/widgets/api_download_handler.lua index 6d2f54b60..48363e851 100644 --- a/LuaMenu/widgets/api_download_handler.lua +++ b/LuaMenu/widgets/api_download_handler.lua @@ -31,10 +31,14 @@ local USE_WRAPPER_DOWNLOAD = true local typeMap = { game = "RAPID", map = "MAP", + engine = "ENGINE", + resource = "RESOURCE", } local reverseTypeMap = { RAPID = "game", MAP = "map", + ENGINE = "engine", + RESOURCE = "resource", } -------------------------------------------------------------------------------- @@ -132,7 +136,7 @@ local function DownloadQueueUpdate() local front = downloadQueue[1] if not front.active then if USE_WRAPPER_DOWNLOAD and WG.WrapperLoopback and WG.WrapperLoopback.DownloadFile then - WG.WrapperLoopback.DownloadFile(front.name, typeMap[front.fileType]) + WG.WrapperLoopback.DownloadFile(front.name, typeMap[front.fileType], front.resource) CallListeners("DownloadStarted", front.id, front.name, front.fileType) else VFS.DownloadArchive(front.name, front.fileType) @@ -239,7 +243,7 @@ end -------------------------------------------------------------------------------- -- Externals Functions -function externalFunctions.QueueDownload(name, fileType, priority, retryCount) +function externalFunctions.QueueDownload(name, fileType, priority, retryCount, resource) priority = priority or 1 if priority == -1 then priority = topPriority + 1 @@ -266,9 +270,10 @@ function externalFunctions.QueueDownload(name, fileType, priority, retryCount) priority = priority, id = downloadCount, retryCount = retryCount or 0, + resource = resource, } requestUpdate = true - CallListeners("DownloadQueued", downloadCount, name, fileType) + CallListeners("DownloadQueued", downloadCount, name, fileType, resource) end function externalFunctions.SetDownloadTopPriority(name, fileType) @@ -283,20 +288,13 @@ function externalFunctions.SetDownloadTopPriority(name, fileType) return true end -function externalFunctions.CancelDownload(name, fileType) +function externalFunctions.CancelDownload(name, fileType, success) local index = GetDownloadIndex(downloadQueue, name, fileType) if not index then return false end - if downloadQueue[index].active then - if USE_WRAPPER_DOWNLOAD and WG.WrapperLoopback and WG.WrapperLoopback.AbortDownload then - WG.WrapperLoopback.AbortDownload(name, typeMap[fileType]) - end - return - end - - downloadQueue[index].removalType = "cancel" + downloadQueue[index].removalType = (success == "fail") and "fail" or "cancel" removedDownloads[#removedDownloads + 1] = downloadQueue[index] downloadQueue[index] = downloadQueue[#downloadQueue] @@ -329,9 +327,22 @@ function externalFunctions.RemoveRemovedDownload(name, fileType) return true end -function externalFunctions.MaybeDownloadArchive(name, archiveType, priority) - if not VFS.HasArchive(name) then +local function haveEngineDir(path) + local springExecutable = Platform.osFamily == "Windows" and "spring.exe" or "spring" + return VFS.FileExists(path .. "//" .. springExecutable) +end + +function externalFunctions.MaybeDownloadArchive(name, archiveType, priority, resource) + if archiveType == "resource" then + local haveEngine = haveEngineDir(resource.destination) + if not haveEngine then + externalFunctions.QueueDownload(name, archiveType, priority, _, resource) + end + return + + elseif not VFS.HasArchive(name) then externalFunctions.QueueDownload(name, archiveType, priority) + return end end @@ -356,10 +367,12 @@ function wrapperFunctions.DownloadFinished(name, fileType, success, aborted) RemoveDownload(name, fileType, true, (aborted and "cancel") or (success and "success") or "fail") end - --Chotify:Post({ - -- title = "Download " .. ((success and "Finished") or "Failed"), - -- body = (name or "???") .. " of type " .. (fileType or "???"), - --}) + if not success then + Chotify:Post({ + title = i18n("download_failed"), + body = (name or "???") .. " of type " .. (fileType or "???"), + }) + end end function wrapperFunctions.DownloadFileProgress(name, progress, totalLength) @@ -369,7 +382,7 @@ function wrapperFunctions.DownloadFileProgress(name, progress, totalLength) end totalLength = (tonumber(totalLength or 0) or 0)/1023^2 - CallListeners("DownloadProgress", downloadQueue[index].id, totalLength*math.min(1, (tonumber(progress or 0) or 0)/100), totalLength, name) + CallListeners("DownloadProgress", downloadQueue[index].id, totalLength*math.min(1, (tonumber(progress or 0) or 0)/100), totalLength, downloadQueue[index].name) end function wrapperFunctions.ImageDownloadFinished(requestToken, imageUrl, imagePath) diff --git a/LuaMenu/widgets/chobby/components/configuration.lua b/LuaMenu/widgets/chobby/components/configuration.lua index 55eccaf80..c9d7e07f4 100644 --- a/LuaMenu/widgets/chobby/components/configuration.lua +++ b/LuaMenu/widgets/chobby/components/configuration.lua @@ -379,6 +379,14 @@ function Configuration:init() self.saneCharacters[saneCharacterList[i]] = true end + local engineSaneCharacterList = { + "-", ".", " ", + } + self.engineSaneCharacters = {} + for i = 1, #engineSaneCharacterList do + self.engineSaneCharacters[engineSaneCharacterList[i]] = true + end + self.barMngSettings = { autoBalance = true, teamSize = true, @@ -1051,6 +1059,25 @@ function Configuration:IsValidEngineVersion(engineVersion) return validengine end +function Configuration:SanitizeEngineVersion(engineVersion) + local ret = "" + local length = string.len(engineVersion) + for i = 1, length do + local c = string.sub(engineVersion, i, i) + if self.saneCharacters[c] or self.engineSaneCharacters[c] then + ret = ret .. c + end + end + + local format = "(%d+)%.(%d+)%.(%d+)%-(%d+)%-g([%x][%x][%x][%x][%x][%x][%x])%s" + if not ret:match(format) then + Spring.Echo("Invalid engine version format: " .. engineVersion) + ret = "" + end + + return ret +end + function Configuration:SanitizeName(name, usedNames) local ret = "" local length = string.len(name) diff --git a/LuaMenu/widgets/chobby/components/downloader.lua b/LuaMenu/widgets/chobby/components/downloader.lua index 03f232dcf..472290dcb 100644 --- a/LuaMenu/widgets/chobby/components/downloader.lua +++ b/LuaMenu/widgets/chobby/components/downloader.lua @@ -292,7 +292,11 @@ function Downloader:DownloadFailed(listener, downloadID, errorID) self:UpdateQueue() end -function Downloader:DownloadQueued(listener, downloadID, archiveName, archiveType) - self.downloads[downloadID] = { archiveName = archiveName, archiveType = archiveType, startTime = os.clock() } +function Downloader:DownloadQueued(listener, downloadID, archiveName, archiveType, resource) + self.downloads[downloadID] = { + archiveName = archiveName, + archiveType = archiveType, + resource = resource, + startTime = os.clock() } self:UpdateQueue() end diff --git a/LuaMenu/widgets/chobby/i18n/chililobby.lua b/LuaMenu/widgets/chobby/i18n/chililobby.lua index 9d1e7bf62..e751cba2b 100644 --- a/LuaMenu/widgets/chobby/i18n/chililobby.lua +++ b/LuaMenu/widgets/chobby/i18n/chililobby.lua @@ -76,7 +76,7 @@ return { delete_confirm = "Are you sure you want to delete this saved game?", load_confirm = "Loading will lose any unsaved progress. Are you sure?", replay_not_found = "Replay file not found, refresh the list!", - replay_different_version = "This replay requires a different engine version (FIXME download automatically if necessary)", + replay_different_version = "This replay requires a different engine version (will be downloaded automatically if necessary)", -- start_download = 'Start download', download_noun = 'Download', @@ -204,6 +204,7 @@ return { other = "%{count} items left to download.", }, downloads_completed = "All downloads completed.", + download_failed = "Download Failed", wip_challenges = "WiP Challenges", ["scenarios"] = "Scenarios", -- Settings diff --git a/LuaMenu/widgets/gui_download_window.lua b/LuaMenu/widgets/gui_download_window.lua index ac889c5d8..4754f3f9e 100644 --- a/LuaMenu/widgets/gui_download_window.lua +++ b/LuaMenu/widgets/gui_download_window.lua @@ -361,6 +361,8 @@ local function InitializeControls(window) local function DownloadProgress(_, _, sizeCurrent, sizeTotal, name) if downloads[name] then downloads[name].SetProgress(sizeCurrent, sizeTotal) + else + Spring.Log(LOG_SECTION, LOG.ERROR, "DownloadWindow:DownloadProgressListener not found") end end diff --git a/LuaMenu/widgets/gui_steam_coop_handler.lua b/LuaMenu/widgets/gui_steam_coop_handler.lua index 662ed45f0..715fbe6c9 100644 --- a/LuaMenu/widgets/gui_steam_coop_handler.lua +++ b/LuaMenu/widgets/gui_steam_coop_handler.lua @@ -61,7 +61,7 @@ local function MakeExclusivePopup(text, buttonText, ClickFunc, buttonClass, heig if replacablePopup then replacablePopup:Close() end - replacablePopup = WG.Chobby.InformationPopup(text, {caption = buttonText, closeFunc = ClickFunc, buttonClass = buttonClass, height = height, width = 500}) + replacablePopup = WG.Chobby.InformationPopup(text, {caption = buttonText, closeFunc = ClickFunc, buttonClass = buttonClass, height = height, width = 540}) end local function CloseExclusivePopup() @@ -159,7 +159,31 @@ end -------------------------------------------------------------------------------- -- Downloading -local function CheckDownloads(gameName, mapName, DoneFunc, gameList) +-- outcome example: "105.1.1-1354-g72b2d55 BAR105" -> "engine/105.1.1-1354-g72b2d55 bar" +local function GetEnginePath(engineVersion) + return ("engine/" .. engineVersion:gsub(" BAR105", " bar")):lower() -- maybe there are more special cases to take in mind here for very old demos or future ones! +end + +local function haveEngineVersion(engineVersion) + local springExecutable = Platform.osFamily == "Windows" and "spring.exe" or "spring" + return VFS.FileExists(GetEnginePath(engineVersion) .. "//" .. springExecutable) +end + +-- outcome example: https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-1354-g72b2d55/spring_bar_.BAR105.105.1.1-1354-g72b2d55_windows-64-minimal-portable.7z +local function GetEngineDownloadUrl(engineVersion) + local sanitizedEngineVersion = WG.Chobby.Configuration:SanitizeEngineVersion(engineVersion) + local branch = sanitizedEngineVersion:match("%s([%w-.]*)") or "" + local pureVersion = sanitizedEngineVersion:gsub(" " .. branch, "") + local baseUrl = "https://github.com/beyond-all-reason/spring/releases/download/" + local versionDir = "spring_bar_%7B" .. branch .. "%7D" .. pureVersion .. "/" + local platform64 = Platform.osFamily:lower() .. "-64" + local fileName = "spring_bar_." .. branch .. "." .. pureVersion .. "_" .. platform64 .. "-minimal-portable.7z" + return baseUrl .. versionDir .. fileName +end + +-- gameList = nil +-- local oneTimeResourceDl = false +local function CheckDownloads(gameName, mapName, DoneFunc, gameList, engineVersion) local haveGame = (not gameName) or WG.Package.ArchiveExists(gameName) if not haveGame then WG.DownloadHandler.MaybeDownloadArchive(gameName, "game", -1) @@ -179,12 +203,21 @@ local function CheckDownloads(gameName, mapName, DoneFunc, gameList) end end - if haveGame and haveMap then + local haveEngine = not engineVersion or haveEngineVersion(engineVersion) + if not haveEngine then + WG.DownloadHandler.MaybeDownloadArchive(engineVersion, "resource", -1,{ -- FB 2023-05-14: Use resource download until engine-download is supported by launcher + url = GetEngineDownloadUrl(engineVersion), + destination = GetEnginePath(engineVersion), + extract = true, + }) + end + + if haveGame and haveMap and haveEngine then return true end local function Update() - if ((not gameName) or WG.Package.ArchiveExists(gameName)) and ((not mapName) or VFS.HasArchive(mapName)) then + if ((not gameName) or WG.Package.ArchiveExists(gameName)) and ((not mapName) or VFS.HasArchive(mapName)) and ((not engineVersion) or haveEngineVersion(engineVersion)) then if gameList then for i = 1, #gameList do if not WG.Package.ArchiveExists(gameList[i]) then @@ -230,8 +263,14 @@ local function CheckDownloads(gameName, mapName, DoneFunc, gameList) downloading.downloads[mapName] = #downloading.progress end + if not haveEngine then + dlString = dlString .. ("\n - " .. engineVersion .. ": %d%%") + downloading.progress[#downloading.progress + 1] = 0 + downloading.downloads[engineVersion] = #downloading.progress + end + downloading.dlString = dlString - MakeExclusivePopup(string.format(dlString, unpack(downloading.progress)), "Cancel", CancelFunc, "negative_button", (gameList and (180 + (#gameList)*40))) + MakeExclusivePopup(string.format(dlString, unpack(downloading.progress)), "Cancel", CancelFunc, "negative_button", (gameList and (220 + (#gameList)*40))) end @@ -345,7 +384,7 @@ end -- External functions: Widget <-> Widget function SteamCoopHandler.AttemptGameStart(gameType, gameName, mapName, scriptTable, newFriendsReplaceAI, newReplayFile, newEngineVersion) - if coopClient then + if coopClient then -- ZK only, always false local statusAndInvitesPanel = WG.Chobby.interfaceRoot.GetStatusAndInvitesPanel() if statusAndInvitesPanel and statusAndInvitesPanel.GetChildByName("coopPanel") then WG.Chobby.InformationPopup("Only the host of the coop party can launch games.") @@ -394,7 +433,6 @@ function SteamCoopHandler.AttemptGameStart(gameType, gameName, mapName, scriptTa MakeExclusivePopup("Wrapper is required to watch replays with old engine versions.") return end - local engine = string.gsub(string.gsub(startEngineVersion, " maintenance", ""), " develop", "") local engine = string.gsub(startEngineVersion, "BAR105", "bar") -- because this is the path we use local params = { StartDemoName = startReplayFile, -- dont remove the 'demos/' string from it now @@ -462,7 +500,7 @@ function SteamCoopHandler.AttemptGameStart(gameType, gameName, mapName, scriptTa WG.WrapperLoopback.SteamHostGameRequest(args) end - if CheckDownloads(gameName, mapName, DownloadsComplete) then + if CheckDownloads(gameName, mapName, DownloadsComplete, _, newEngineVersion) then DownloadsComplete() end end diff --git a/LuaMenu/widgets/sl_loopback.lua b/LuaMenu/widgets/sl_loopback.lua index d424f1821..806400a60 100644 --- a/LuaMenu/widgets/sl_loopback.lua +++ b/LuaMenu/widgets/sl_loopback.lua @@ -54,19 +54,34 @@ function WrapperLoopback.ParseMiniMap(mapPath, destination, miniMapSize) }) end -local downloads = {} +local downloads = {} -- index table --- downloads a file, type can be any of RAPID, MAP, MISSION, DEMO, ENGINE, NOTKNOWN -function WrapperLoopback.DownloadFile(name, type) - downloads[name] = type +-- FB 2023-05-14 downloads a file, type can be any of game, map, resource +-- engine not supported yet by launcher, though using resource here for engine downloads with workarounds! +function WrapperLoopback.DownloadFile(name, type, resource) + LOG_SECTION = "downloader" - type = type:lower() - if type == "rapid" then - type = "game" + if type:lower() == "resource" and not resource then + + Spring.Log(LOG_SECTION, LOG.ERROR, "DownloadFile called with type resource, but no resource infos given") + + WG.DownloadHandler.CancelDownload(name, type:lower(), "fail") + + return false end + + table.insert(downloads, { + nameSent = type:lower() == "resource" and resource.destination or name, -- FB 2023-05-14: Workaround for now: With "resource" DownloadProgress & DownloadFinished return destination as name, so we just use destination as nameSent here to be forward compatible + name = name, + type = type, + typeSent = type:lower() == "rapid" and "game" or type:lower(), + resource = resource -- {url, destination, extract} + }) + WG.Connector.Send("Download", { - name = name, - type = type + name = downloads[#downloads].nameSent, + type = downloads[#downloads].typeSent, + resource = downloads[#downloads].resource, }) end @@ -75,24 +90,78 @@ function WrapperLoopback.StartNewSpring(args) WG.Connector.Send("StartNewSpring", args) end -function WrapperLoopback.AbortDownload(name, type) - WG.Connector.Send("AbortDownload", { - name = name, - type = type - }) +local function GetDownloadByName(name) + for i, download in ipairs(downloads) do + if download.name == name then + return download, i + end + end + return false, nil +end + +local function GetDownloadByNameSent(nameSent) + for i, download in ipairs(downloads) do + if download.nameSent == nameSent then + return download, i + end + end + return false, nil end +-- Replace all minutes (-) by (%-) so that it's not used by string.find as special char +-- example: "engine/105.1.1-1354-g72b2d55 bar" -> "engine/105.1.1%-1354%-g72b2d55 bar" +local function EscapeMinusPattern(text) + local txt = text:gsub("([%-])", "%%%1") + return txt +end +local function FindNameReceivedInDownloads(nameReceived) + for i, download in ipairs(downloads) do + if nameReceived:gsub("\\", "/"):find(EscapeMinusPattern(download.nameSent)) then -- replace backslashes(windows) with slashes, because that's how we generated it in CoopHandler:local GetEnginePath() + return download, i + end + end + return false, nil +end + +local function startsWith(targetstring, pattern) + if string.len(pattern) <= string.len(targetstring) and pattern == string.sub(targetstring,1, string.len(pattern)) then + return true, string.sub(targetstring, string.len(pattern) + 1) + else + return false + end +end + +local SkippingFile_PREFIX = "Skipping " +local SkippingFile_SUFFIX = ": already exists." +local download, dlIndex -- reports that download has ended/was aborted local function DownloadFinished(command) - local type = downloads[command.name] - WG.DownloadWrapperInterface.DownloadFinished(command.name, type, command.isSuccess, command.isAborted) - downloads[command.name] = nil + if not command.name then + return false + end + + download, dlIndex = FindNameReceivedInDownloads(command.name) + if not download then + Spring.Log(LOG_SECTION, LOG.ERROR, "Received command.name couldn't be matched to any known download:", command.name) + return false + end + + WG.DownloadWrapperInterface.DownloadFinished(download.name, download.type, command.isSuccess, command.isAborted) + table.remove(downloads, dlIndex) end --- reports download progress. 100 might not indicate complation, wait for downloadfiledone +-- reports download progress. 100 might not indicate completion, wait for DownloadFinished local function DownloadProgress(command) - WG.DownloadWrapperInterface.DownloadFileProgress(command.name, command.progress * 100, command.total) + if not command.name then + return false + end + + local download, i = GetDownloadByNameSent(command.name) + if not download then + return false + end + WG.DownloadWrapperInterface.DownloadFileProgress(download.name, command.progress * 100, command.total) end local function ParseMiniMapFinished(command)