diff --git a/gulp/bundle-loader.js b/gulp/bundle-loader.js deleted file mode 100644 index 2ae10169..00000000 --- a/gulp/bundle-loader.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * ES6 Bundle Loader - * - * Attempts to load the game code, and if that fails tries with the transpiled - * version. Also handles errors during load. - */ - -(function () { - var loadTimeout = null; - var callbackDone = false; - - // Catch load errors - - function errorHandler(event, source, lineno, colno, error) { - console.error("👀 Init Error:", event, source, lineno, colno, error); - var element = document.createElement("div"); - element.style.position = "fixed"; - element.style.top = "0"; - element.style.right = "0"; - element.style.bottom = "0"; - element.style.left = "0"; - element.style.zIndex = "29999"; - element.style.backgroundColor = "#222429"; - element.style.display = "flex"; - element.style.justifyContent = "center"; - element.style.alignItems = "center"; - - var inner = document.createElement("div"); - inner.style.color = "#fff"; - inner.style.fontFamily = "GameFont, sans-serif"; - inner.style.fontSize = "15px"; - inner.style.padding = "30px"; - inner.style.textAlign = "center"; - element.appendChild(inner); - - var heading = document.createElement("h3"); - heading.style.color = "#ef5072"; - heading.innerText = "Error"; - heading.style.marginBottom = "40px"; - heading.style.fontSize = "45px"; - inner.appendChild(heading); - - var content = document.createElement("p"); - content.style.color = "#eee"; - content.innerText = error || (event && event.message) || event || "Unknown Error"; - inner.appendChild(content); - - if (source) { - var sourceElement = document.createElement("p"); - sourceElement.style.color = "#777"; - sourceElement.innerText = sourceElement + ":" + lineno + ":" + colno; - inner.appendChild(sourceElement); - } - - document.documentElement.appendChild(element); - } - - if (window.location.host.indexOf("localhost") < 0) { - window.addEventListener("error", errorHandler); - window.addEventListener("unhandledrejection", errorHandler); - } - - function makeJsTag(src, integrity) { - var script = document.createElement("script"); - script.src = src; - script.type = "text/javascript"; - script.charset = "utf-8"; - script.defer = true; - if (integrity) { - script.setAttribute("integrity", integrity); - } - return script; - } - - // function loadFallbackJs(error) { - // console.warn("👀 ES6 Script not supported, loading transpiled code."); - // console.warn("👀 Error was:", error); - // var scriptTransp = makeJsTag(bundleSrcTranspiled, bundleIntegrityTranspiled); - // scriptTransp.addEventListener("error", scriptFail); - // scriptTransp.addEventListener("load", onJsLoaded); - // document.head.appendChild(scriptTransp); - // } - - function scriptFail(error) { - console.error("👀 Failed to load bundle!"); - console.error("👀 Error was:", error); - throw new Error("Core load failed."); - } - - function expectJsParsed() { - if (!callbackDone) { - console.error("👀 Got no core callback"); - throw new Error("Core thread failed to respond within time."); - } - } - - function onJsLoaded() { - console.log("👀 Core loaded at", Math.floor(performance.now()), "ms"); - loadTimeout = setTimeout(expectJsParsed, 15000); - window.removeEventListener("error", errorHandler); - window.removeEventListener("unhandledrejection", errorHandler); - } - - window.coreThreadLoadedCb = function () { - console.log("👀 Core responded at", Math.floor(performance.now()), "ms"); - clearTimeout(loadTimeout); - loadTimeout = null; - callbackDone = true; - }; - - var scriptEs6 = makeJsTag(bundleSrc, bundleIntegrity); - // scriptEs6.addEventListener("error", loadFallbackJs); - scriptEs6.addEventListener("load", onJsLoaded); - document.head.appendChild(scriptEs6); -})(); diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index 8db31a48..0029a992 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -165,7 +165,7 @@ function serveHTML({ version = "web-dev" }) { // Watch .html files, those trigger a html rebuild gulp.watch("../src/**/*.html", gulp.series("html." + version + ".dev")); - gulp.watch("./preloader.css", gulp.series("html." + version + ".dev")); + gulp.watch("./preloader/*.*", gulp.series("html." + version + ".dev")); // Watch translations gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson")); diff --git a/gulp/html.js b/gulp/html.js index ced7f515..df80d2d4 100644 --- a/gulp/html.js +++ b/gulp/html.js @@ -110,7 +110,8 @@ function gulptasksHTML($, gulp, buildFolder) { } `; let loadingCss = - fontCss + fs.readFileSync(path.join(__dirname, "preloader.css")).toString(); + fontCss + + fs.readFileSync(path.join(__dirname, "preloader", "preloader.css")).toString(); const style = document.createElement("style"); style.setAttribute("type", "text/css"); @@ -152,35 +153,16 @@ function gulptasksHTML($, gulp, buildFolder) { scriptContent += "var bundleIntegrityTranspiled = null;\n"; } - scriptContent += fs.readFileSync("./bundle-loader.js").toString(); + scriptContent += fs + .readFileSync(path.join(__dirname, "preloader", "preloader.js")) + .toString(); loadJs.textContent = scriptContent; document.head.appendChild(loadJs); } - const bodyContent = ` -
_
-
- -
- -
${ - hasLocalFiles ? "Loading" : "Downloading" - } Game Files
-
- -
Downloading Bundle
-
-
- Page does not load? Try the Steam Version! -
-
-
- `; + const bodyContent = fs + .readFileSync(path.join(__dirname, "preloader", "preloader.html")) + .toString(); document.body.innerHTML = bodyContent; } diff --git a/gulp/preloader.css b/gulp/preloader/preloader.css similarity index 65% rename from gulp/preloader.css rename to gulp/preloader/preloader.css index e972238c..f6775f76 100644 --- a/gulp/preloader.css +++ b/gulp/preloader/preloader.css @@ -47,7 +47,7 @@ body { } #ll_fp { - font-family: GameFont; + font-family: "GameFont", Arial, sans-serif; font-size: 14px; position: fixed; z-index: -1; @@ -89,8 +89,9 @@ body { #ll_loader > .ll_text { text-align: center; color: #777a7f; - font-family: "GameFont", sans-serif; + font-family: "GameFont", Arial, sans-serif; font-size: 24px; + height: 30px; line-height: 1.2em; } @@ -98,60 +99,39 @@ body { width: 80vw; max-width: 800px; margin-top: 40px; - height: 14px; + height: 7px; border-radius: 20px; - background: rgba(0, 10, 40, 0.1); - border: 5px solid transparent; + background: rgba(0, 10, 20, 0.08); + + /* border: 5px solid transparent; */ display: flex; position: relative; align-items: flex-start; } @keyframes LL_LoadingAnimation { - 0% { - width: 0%; - background-color: #777a7f; - } - 19.99% { - width: 99.5%; - background-color: rgb(50, 197, 121); - } - 20% { - width: 0%; - background-color: #777a7f; - } - 49.99% { - width: 98%; - background-color: #4fbfce; - } 50% { - width: 0%; - background-color: #777a7f; - } - 74.99% { - width: 98%; - background-color: #74a8c0; - } - 75% { - width: 0%; - background-color: #777a7f; - } - 100% { - width: 98%; - background-color: #a186d4; + background-color: #34ae67; } } #ll_progressbar > span { border-radius: 20px; position: absolute; - height: 100%; - width: 98%; + height: 190%; + width: 5%; background: #fff; + transform: translateY(-50%); + top: 50%; display: inline-flex; - animation: LL_LoadingAnimation 90s ease-in-out infinite; + background-color: #269fba; + animation: LL_LoadingAnimation 4s ease-in-out infinite; position: relative; z-index: 10; + border: 4px solid #d5d8de; + /* box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); */ + transition: width 0.5s ease-in-out; + min-width: 4%; } #ll_progressbar > #ll_loadinglabel { @@ -181,10 +161,10 @@ body { #ll_standalone { text-align: center; color: #777a7f; - margin-top: 20px; + margin-top: 30px; display: block; font-size: 16px; - animation: ShowStandaloneBannerAfterDelay 30s linear; + animation: ShowStandaloneBannerAfterDelay 60s linear; } #ll_standalone a { @@ -212,13 +192,71 @@ body { #ll_preload_status { position: absolute; - top: 30px; + top: 40px; left: 50%; transform: translate(-50%, -50%); z-index: 100; - color: rgba(#000, 0.4); opacity: 1 !important; - font-size: 12px; + font-size: 18px; + color: rgba(0, 10, 20, 0.5); + + font-family: "GameFont", Arial, sans-serif; text-transform: uppercase; text-align: center; } + +#ll_preload_error { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 999999; + background: #d5d8de; + display: flex; + justify-content: center; + align-items: center; +} + +#ll_preload_error > .inner { + color: #fff; + font-family: Arial, "sans-serif"; + font-size: 15px; + padding: 0; + text-align: center; +} + +#ll_preload_error > .inner > .heading { + color: #ef5072; + margin-bottom: 40px; + font-size: 45px; +} + +#ll_preload_error > .inner > .content { + color: #55585f; + font-family: monospace; + text-align: left; + background-color: #fff; + padding: 20px; + border-radius: 10px; + + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1); +} + +#ll_preload_error > .inner .discordLink { + color: #333; + margin-top: 20px; + margin-bottom: 20px; + font-family: Arial; +} + +#ll_preload_error > .inner .discordLink a { + color: #39f; +} +#ll_preload_error > .inner .discordLink strong { + font-weight: 900 !important; +} + +#ll_preload_error > .inner .source { + color: #777; +} diff --git a/gulp/preloader/preloader.html b/gulp/preloader/preloader.html new file mode 100644 index 00000000..48d3579c --- /dev/null +++ b/gulp/preloader/preloader.html @@ -0,0 +1,21 @@ +
_
+
+ +
+ +
+
+ +
Downloading Game
+
+
+ Page does not load? Try the + Steam Version! +
+
+
diff --git a/gulp/preloader/preloader.js b/gulp/preloader/preloader.js new file mode 100644 index 00000000..808ea132 --- /dev/null +++ b/gulp/preloader/preloader.js @@ -0,0 +1,127 @@ +(function () { + var loadTimeout = null; + var callbackDone = false; + + // Catch load errors + + function errorHandler(event, source, lineno, colno, error) { + console.error("👀 Init Error:", event, source, lineno, colno, error); + var element = document.createElement("div"); + element.id = "ll_preload_error"; + + var inner = document.createElement("div"); + inner.classList.add("inner"); + element.appendChild(inner); + + var heading = document.createElement("h3"); + heading.classList.add("heading"); + heading.innerText = "Fatal Error"; + inner.appendChild(heading); + + var content = document.createElement("p"); + content.classList.add("content"); + content.innerText = error || (event && event.message) || event || "Unknown Error"; + inner.appendChild(content); + + var discordLink = document.createElement("p"); + discordLink.classList.add("discordLink"); + discordLink.innerHTML = + "Please report this error in the #bugs channel of the official discord!"; + + inner.appendChild(discordLink); + + if (source) { + var sourceElement = document.createElement("p"); + sourceElement.classList.add("source"); + sourceElement.innerText = source + ":" + lineno + ":" + colno; + inner.appendChild(sourceElement); + } + + document.documentElement.appendChild(element); + } + + window.addEventListener("error", errorHandler); + + function expectJsParsed() { + if (!callbackDone) { + console.error("👀 Got no core callback"); + throw new Error("Core thread failed to respond within time."); + } + } + + function onJsLoaded() { + console.log("👀 Core loaded at", Math.floor(performance.now()), "ms"); + loadTimeout = setTimeout(expectJsParsed, 120000); + window.removeEventListener("unhandledrejection", errorHandler); + } + + window.coreThreadLoadedCb = function () { + console.log("👀 Core responded at", Math.floor(performance.now()), "ms"); + clearTimeout(loadTimeout); + loadTimeout = null; + callbackDone = true; + }; + + function progressHandler(progress) { + var progressElement = document.querySelector("#ll_preload_status"); + if (progressElement) { + progressElement.innerText = "Downloading Bundle (" + Math.round(progress * 1200) + " / 1200 KB)"; + } + var barElement = document.querySelector("#ll_progressbar span"); + if (barElement) { + barElement.style.width = (5 + progress * 75.0).toFixed(2) + "%"; + } + } + + function startBundleDownload() { + var xhr = new XMLHttpRequest(); + var notifiedNotComputable = false; + + xhr.open("GET", bundleSrc, true); + xhr.responseType = "arraybuffer"; + xhr.onprogress = function (ev) { + if (ev.lengthComputable) { + progressHandler(ev.loaded / ev.total); + } else { + if (!notifiedNotComputable) { + notifiedNotComputable = true; + console.warn("Progress not computable:", ev); + progressHandler(0); + } + } + }; + + xhr.onloadend = function () { + if (!xhr.status.toString().match(/^2/)) { + throw new Error("Failed to load bundle: " + xhr.status + " " + xhr.statusText); + } else { + if (!notifiedNotComputable) { + progressHandler(1); + } + + var options = {}; + var headers = xhr.getAllResponseHeaders(); + var m = headers.match(/^Content-Type\:\s*(.*?)$/im); + + if (m && m[1]) { + options.type = m[1]; + } + + var blob = new Blob([this.response], options); + var script = document.createElement("script"); + script.addEventListener("load", onJsLoaded); + script.src = window.URL.createObjectURL(blob); + script.type = "text/javascript"; + script.charset = "utf-8"; + if (bundleIntegrity) { + script.setAttribute("integrity", bundleIntegrity); + } + document.head.appendChild(script); + } + }; + xhr.send(); + } + + console.log("Start bundle download ..."); + window.addEventListener("load", startBundleDownload); +})(); diff --git a/src/css/application_error.scss b/src/css/application_error.scss deleted file mode 100644 index b69a1cfe..00000000 --- a/src/css/application_error.scss +++ /dev/null @@ -1,67 +0,0 @@ -#applicationError { - z-index: 9999; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $mainBgColor; - color: #333; - display: flex; - flex-direction: column; - align-content: center; - align-items: center; - justify-content: center; - @include S(padding, 30px); - - @include Text; - text-align: center; - - h1 { - @include TextShadow3D(#ff0b40); - @include S(margin-top, 20px); - @include S(margin-bottom, 30px); - @include SuperHeading; - @include S(font-size, 35px); - } - - .desc { - // color: rgba(#fff, 0.6); - color: $themeColor; - text-align: left; - @include PlainText; - font-weight: bold; - - a { - cursor: pointer; - pointer-events: all; - font-weight: bold; - display: block; - @include TextShadow3D(#ff0b40); - @include S(margin-top, 10px); - } - - display: block; - @include S(max-width, 350px); - width: 100%; - } - - .details { - font-size: 11px; - line-height: 15px; - color: #888; - font-family: monospace; - text-align: left; - @include S(padding, 6px); - @include S(border-radius, $globalBorderRadius); - @include BoxShadow3D(#eee); - position: absolute; - @include S(bottom, 25px); - left: 50%; - transform: translateX(-50%); - max-width: calc(100vw - 40px); - box-sizing: border-box; - @include BreakText; - min-width: 300px; - } -} diff --git a/src/css/common.scss b/src/css/common.scss index f4946e9d..b3b3b103 100644 --- a/src/css/common.scss +++ b/src/css/common.scss @@ -427,6 +427,16 @@ canvas { } } +.prefab_LoadingProgressIndicator { + @include PlainText; + @include S(margin-top, 20px); + width: 100%; + color: #336c9f; + @include S(height, 20px); + text-transform: uppercase; + text-align: center; +} + .prefab_FeatureComingSoon { position: relative; &::after { diff --git a/src/css/main.scss b/src/css/main.scss index 77ee8cd0..6ac138f8 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -16,7 +16,6 @@ @import "common"; @import "animations"; @import "game_state"; -@import "application_error"; @import "textual_game_state"; @import "adinplay"; @import "changelog_skins"; diff --git a/src/html/index.html b/src/html/index.html index a4a0fff4..ab32169c 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -1,5 +1,5 @@ - + shapez Demo - Factory Automation Game diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index 99948850..8e8c8242 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -2,58 +2,36 @@ import { Application } from "../application"; /* typehints:end */ +import { initSpriteCache } from "../game/meta_building_registry"; +import { MUSIC, SOUNDS } from "../platform/sound"; +import { T } from "../translations"; +import { AtlasDefinition, atlasFiles } from "./atlas_definitions"; +import { cachebust } from "./cachebust"; import { Loader } from "./loader"; import { createLogger } from "./logging"; import { Signal } from "./signal"; -import { SOUNDS, MUSIC } from "../platform/sound"; -import { AtlasDefinition, atlasFiles } from "./atlas_definitions"; -import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry"; -import { cachebust } from "./cachebust"; +import { clamp, getLogoSprite, timeoutPromise } from "./utils"; const logger = createLogger("background_loader"); -export function getLogoSprite() { - if (G_WEGAME_VERSION) { - return "logo_wegame.png"; - } +const MAIN_MENU_ASSETS = { + sprites: [getLogoSprite()], + sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk], + atlas: [], + css: [], +}; - if (G_IS_STEAM_DEMO) { - return "logo_demo.png"; - } +const INGAME_ASSETS = { + sprites: [], + sounds: [ + ...Array.from(Object.values(MUSIC)), + ...Array.from(Object.values(SOUNDS)).filter(sound => !MAIN_MENU_ASSETS.sounds.includes(sound)), + ], + atlas: atlasFiles, + css: ["async-resources.css"], +}; - if (G_CHINA_VERSION) { - return "logo_cn.png"; - } - - if (G_IS_STANDALONE) { - return "logo.png"; - } - - if (G_IS_BROWSER) { - return "logo_demo.png"; - } - - return "logo.png"; -} - -const essentialMainMenuSprites = [getLogoSprite()]; - -const essentialMainMenuSounds = [ - SOUNDS.uiClick, - SOUNDS.uiError, - SOUNDS.dialogError, - SOUNDS.dialogOk, - SOUNDS.swishShow, - SOUNDS.swishHide, -]; - -const essentialBareGameAtlases = atlasFiles; -const essentialBareGameSprites = []; -const essentialBareGameSounds = [MUSIC.theme]; - -const additionalGameSprites = []; -// @ts-ignore -const additionalGameSounds = [...Object.values(SOUNDS), ...Object.values(MUSIC)]; +const LOADER_TIMEOUT_PER_RESOURCE = 180000; export class BackgroundResourcesLoader { /** @@ -63,193 +41,199 @@ export class BackgroundResourcesLoader { constructor(app) { this.app = app; - this.registerReady = false; - this.mainMenuReady = false; - this.bareGameReady = false; - this.additionalReady = false; + this.mainMenuPromise = null; + this.ingamePromise = null; - this.signalMainMenuLoaded = new Signal(); - this.signalBareGameLoaded = new Signal(); - this.signalAdditionalLoaded = new Signal(); - - this.numAssetsLoaded = 0; - this.numAssetsToLoadTotal = 0; - - // Avoid loading stuff twice - this.spritesLoaded = []; - this.soundsLoaded = []; - this.atlasesLoaded = []; - this.cssLoaded = []; + this.resourceStateChangedSignal = new Signal(); } - getNumAssetsLoaded() { - return this.numAssetsLoaded; - } - - getNumAssetsTotal() { - return this.numAssetsToLoadTotal; - } - - getPromiseForMainMenu() { - if (this.mainMenuReady) { - return Promise.resolve(); + getMainMenuPromise() { + if (this.mainMenuPromise) { + return this.mainMenuPromise; } - return new Promise(resolve => { - this.signalMainMenuLoaded.add(resolve); - }); + logger.warn("⏰ Loading main menu assets"); + return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS)); } - getPromiseForBareGame() { - if (this.bareGameReady) { - return Promise.resolve(); + getIngamePromise() { + if (this.ingamePromise) { + return this.ingamePromise; } - - return new Promise(resolve => { - this.signalBareGameLoaded.add(resolve); - }); - } - - startLoading() { - this.internalStartLoadingEssentialsForMainMenu(); - } - - internalStartLoadingEssentialsForMainMenu() { - logger.log("⏰ Start load: main menu"); - this.internalLoadSpritesAndSounds(essentialMainMenuSprites, essentialMainMenuSounds) - .catch(err => { - logger.warn("⏰ Failed to load essentials for main menu:", err); - }) - .then(() => { - logger.log("⏰ Finish load: main menu"); - this.mainMenuReady = true; - this.signalMainMenuLoaded.dispatch(); - }); - } - - internalStartLoadingEssentialsForBareGame() { - logger.log("⏰ Start load: bare game"); - this.internalLoadSpritesAndSounds( - essentialBareGameSprites, - essentialBareGameSounds, - essentialBareGameAtlases - ) - .then(() => this.internalPreloadCss("async-resources.scss")) - .catch(err => { - logger.warn("⏰ Failed to load essentials for bare game:", err); - }) - .then(() => { - logger.log("⏰ Finish load: bare game"); - this.bareGameReady = true; - initBuildingCodesAfterResourcesLoaded(); - this.signalBareGameLoaded.dispatch(); - this.internalStartLoadingAdditionalGameAssets(); - }); - } - - internalStartLoadingAdditionalGameAssets() { - const additionalAtlases = []; - logger.log("⏰ Start load: additional assets (", additionalAtlases.length, "images)"); - this.internalLoadSpritesAndSounds(additionalGameSprites, additionalGameSounds, additionalAtlases) - .catch(err => { - logger.warn("⏰ Failed to load additional assets:", err); - }) - .then(() => { - logger.log("⏰ Finish load: additional assets"); - this.additionalReady = true; - this.signalAdditionalLoaded.dispatch(); - }); - } - - internalPreloadCss(name) { - if (this.cssLoaded.includes(name)) { - return; - } - this.cssLoaded.push(name); - return new Promise((resolve, reject) => { - const link = document.createElement("link"); - - link.onload = resolve; - link.onerror = reject; - - link.setAttribute("rel", "stylesheet"); - link.setAttribute("media", "all"); - link.setAttribute("type", "text/css"); - link.setAttribute("href", cachebust("async-resources.css")); - document.head.appendChild(link); - }); + logger.warn("⏰ Loading ingame assets"); + const promise = this.loadAssets(INGAME_ASSETS).then(() => initSpriteCache()); + return (this.ingamePromise = promise); } /** - * @param {Array} sprites - * @param {Array} sounds - * @param {Array} atlases - * @returns {Promise} + * + * @param {object} param0 + * @param {string[]} param0.sprites + * @param {string[]} param0.sounds + * @param {AtlasDefinition[]} param0.atlas + * @param {string[]} param0.css */ - internalLoadSpritesAndSounds(sprites, sounds, atlases = []) { - this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length; - this.numAssetsLoaded = 0; + async loadAssets({ sprites, sounds, atlas, css }) { + /** + * @type {((progressHandler: (progress: number) => void) => Promise)[]} + */ + let promiseFunctions = []; - let promises = []; - - for (let i = 0; i < sounds.length; ++i) { - if (this.soundsLoaded.indexOf(sounds[i]) >= 0) { - // Already loaded - continue; - } - - this.soundsLoaded.push(sounds[i]); - promises.push( - this.app.sound - .loadSound(sounds[i]) - .catch(err => { - logger.warn("Failed to load sound:", sounds[i]); - }) - .then(() => { - this.numAssetsLoaded++; - }) + // CSS + for (let i = 0; i < css.length; ++i) { + promiseFunctions.push(progress => + timeoutPromise( + this.internalPreloadCss(cachebust(css[i]), progress), + LOADER_TIMEOUT_PER_RESOURCE + ).catch(err => { + logger.error("Failed to load css:", css[i], err); + throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err); + }) ); } + // ATLAS FILES + for (let i = 0; i < atlas.length; ++i) { + promiseFunctions.push(progress => + timeoutPromise(Loader.preloadAtlas(atlas[i], progress), LOADER_TIMEOUT_PER_RESOURCE).catch( + err => { + logger.error("Failed to load atlas:", atlas[i].sourceFileName, err); + throw new Error("Atlas " + atlas[i].sourceFileName + " failed to load: " + err); + } + ) + ); + } + + // HUD Sprites for (let i = 0; i < sprites.length; ++i) { - if (this.spritesLoaded.indexOf(sprites[i]) >= 0) { - // Already loaded - continue; - } - this.spritesLoaded.push(sprites[i]); - promises.push( - Loader.preloadCSSSprite(sprites[i]) - .catch(err => { - logger.warn("Failed to load css sprite:", sprites[i]); - }) - .then(() => { - this.numAssetsLoaded++; - }) + promiseFunctions.push(progress => + timeoutPromise( + Loader.preloadCSSSprite(sprites[i], progress), + LOADER_TIMEOUT_PER_RESOURCE + ).catch(err => { + logger.error("Failed to load css sprite:", sprites[i], err); + throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err); + }) ); } - for (let i = 0; i < atlases.length; ++i) { - const atlas = atlases[i]; - if (this.atlasesLoaded.includes(atlas)) { - // Already loaded - continue; - } - this.atlasesLoaded.push(atlas); - - promises.push( - Loader.preloadAtlas(atlas) - .catch(err => { - logger.warn("Failed to load atlas:", atlas.sourceFileName, err); - }) - .then(() => { - this.numAssetsLoaded++; - }) + // SFX & Music + for (let i = 0; i < sounds.length; ++i) { + promiseFunctions.push(progress => + timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch( + err => { + logger.warn("Failed to load sound, will not be available:", sounds[i], err); + } + ) ); } - return Promise.all(promises).then(() => { - this.numAssetsToLoadTotal = 0; - this.numAssetsLoaded = 0; + const originalAmount = promiseFunctions.length; + const start = performance.now(); + + logger.log("⏰ Preloading", originalAmount, "assets"); + + let progress = 0; + this.resourceStateChangedSignal.dispatch({ progress }); + let promises = []; + for (let i = 0; i < promiseFunctions.length; i++) { + let lastIndividualProgress = 0; + const progressHandler = individualProgress => { + const delta = clamp(individualProgress) - lastIndividualProgress; + lastIndividualProgress = individualProgress; + progress += delta / originalAmount; + this.resourceStateChangedSignal.dispatch({ progress }); + }; + promises.push( + promiseFunctions + .shift()(progressHandler) + .then(() => { + progressHandler(1); + }) + ); + } + await Promise.all(promises); + + logger.log("⏰ Preloaded assets in", Math.round((performance.now() - start) / 1000.0), "ms"); + } + + /** + * Shows an error when a resource failed to load and allows to reload the game + */ + showLoaderError(dialogs, err) { + if (G_IS_STANDALONE) { + dialogs + .showWarning( + T.dialogs.resourceLoadFailed.title, + T.dialogs.resourceLoadFailed.descSteamDemo + "
" + err, + ["retry"] + ) + .retry.add(() => window.location.reload()); + } else { + dialogs + .showWarning( + T.dialogs.resourceLoadFailed.title, + T.dialogs.resourceLoadFailed.descWeb.replace( + "", + `${T.dialogs.resourceLoadFailed.demoLinkText}` + ) + + "
" + + err, + ["retry"] + ) + .retry.add(() => window.location.reload()); + } + } + + preloadWithProgress(src, progressHandler) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + let notifiedNotComputable = false; + + xhr.open("GET", src, true); + xhr.responseType = "arraybuffer"; + xhr.onprogress = function (ev) { + if (ev.lengthComputable) { + progressHandler(ev.loaded / ev.total); + } else { + if (!notifiedNotComputable) { + notifiedNotComputable = true; + console.warn("Progress not computable:", ev); + progressHandler(0); + } + } + }; + + xhr.onloadend = function () { + if (!xhr.status.toString().match(/^2/)) { + reject(src + ": " + xhr.status + " " + xhr.statusText); + } else { + if (!notifiedNotComputable) { + progressHandler(1); + } + + const options = {}; + const headers = xhr.getAllResponseHeaders(); + const contentType = headers.match(/^Content-Type\:\s*(.*?)$/im); + if (contentType && contentType[1]) { + options.type = contentType[1].split(";")[0]; + } + const blob = new Blob([this.response], options); + resolve(window.URL.createObjectURL(blob)); + } + }; + xhr.send(); + }); + } + + internalPreloadCss(src, progressHandler) { + return this.preloadWithProgress(cachebust(src), progressHandler).then(blobSrc => { + var styleElement = document.createElement("link"); + styleElement.href = blobSrc; + styleElement.rel = "stylesheet"; + styleElement.setAttribute("media", "all"); + styleElement.type = "text/css"; + document.head.appendChild(styleElement); }); } } diff --git a/src/js/core/error_handler.js b/src/js/core/error_handler.js index 686e4e4e..61cd9251 100644 --- a/src/js/core/error_handler.js +++ b/src/js/core/error_handler.js @@ -1,7 +1,3 @@ -import { logSection } from "./logging"; -import { stringifyObjectContainingErrors } from "./logging"; -import { removeAllChildren } from "./utils"; - export let APPLICATION_ERROR_OCCURED = false; /** @@ -13,114 +9,8 @@ export let APPLICATION_ERROR_OCCURED = false; * @param {Error} source */ function catchErrors(message, source, lineno, colno, error) { - let fullPayload = JSON.parse( - stringifyObjectContainingErrors({ - message, - source, - lineno, - colno, - error, - }) - ); - - if (("" + message).indexOf("Script error.") >= 0) { - console.warn("Thirdparty script error:", message); - return; - } - - if (("" + message).indexOf("NS_ERROR_FAILURE") >= 0) { - console.warn("Firefox NS_ERROR_FAILURE error:", message); - return; - } - - if (("" + message).indexOf("Cannot read property 'postMessage' of null") >= 0) { - console.warn("Safari can not read post message error:", message); - return; - } - - if (!G_IS_DEV && G_IS_BROWSER && ("" + source).indexOf("shapez.io") < 0) { - console.warn("Thirdparty error:", message); - return; - } - - console.log("\n\n\n⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️\n\n\n"); - console.log(" APPLICATION CRASHED "); - console.log("\n\n⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️\n\n\n"); - - logSection("APPLICATION CRASH", "#e53935"); - console.error("Error:", message, "->", error); - console.log("Payload:", fullPayload); - - if (window.Sentry && !window.anyModLoaded) { - window.Sentry.withScope(scope => { - window.Sentry.setTag("message", message); - window.Sentry.setTag("source", source); - - window.Sentry.setExtra("message", message); - window.Sentry.setExtra("source", source); - window.Sentry.setExtra("lineno", lineno); - window.Sentry.setExtra("colno", colno); - window.Sentry.setExtra("error", error); - window.Sentry.setExtra("fullPayload", fullPayload); - - try { - const userName = window.localStorage.getItem("tracking_context") || null; - window.Sentry.setTag("username", userName); - } catch (ex) { - // ignore - } - - window.Sentry.captureException(error || source); - }); - } - - if (APPLICATION_ERROR_OCCURED) { - console.warn("ERROR: Only showing and submitting first error"); - return; - } - APPLICATION_ERROR_OCCURED = true; - const element = document.createElement("div"); - element.id = "applicationError"; - - const title = document.createElement("h1"); - title.innerText = "Whoops!"; - element.appendChild(title); - - const desc = document.createElement("div"); - desc.classList.add("desc"); - desc.innerHTML = ` - It seems the application crashed - I am sorry for that!

- An anonymized crash report has been sent, and I will have a look as soon as possible.

- If you have additional information how I can reproduce this error, please E-Mail me:  - bugs@shapez.io`; - element.appendChild(desc); - - const details = document.createElement("pre"); - details.classList.add("details"); - details.innerText = (error && error.stack) || message; - element.appendChild(details); - - const inject = function () { - if (!G_IS_DEV) { - removeAllChildren(document.body); - } - if (document.body.parentElement) { - document.body.parentElement.appendChild(element); - } else { - document.body.appendChild(element); - } - }; - - if (document.body) { - inject(); - } else { - setTimeout(() => { - inject(); - }, 200); - } - - return true; + console.error(message, source, lineno, colno, error); } -window.onerror = catchErrors; +window.addEventListener("error", catchErrors); diff --git a/src/js/core/loader.js b/src/js/core/loader.js index dc52185d..b134aca9 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -76,61 +76,36 @@ class LoaderImpl { /** * * @param {string} key + * @param {(progress: number) => void} progressHandler * @returns {Promise} */ - internalPreloadImage(key) { + internalPreloadImage(key, progressHandler) { const url = cachebust("res/" + key); const image = new Image(); - let triesSoFar = 0; - - return Promise.race([ - new Promise((resolve, reject) => { - setTimeout(() => reject("loader request timeout"), G_IS_DEV ? 500 : 30000); - }), - - new Promise(resolve => { - image.onload = () => { - image.onerror = null; - image.onload = null; - - if (typeof image.decode === "function") { - // SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail - // on that - // FIREFOX: Decode never returns if the image is in cache, so call it in background - image.decode().then( - () => null, - () => null - ); - } - resolve(image); - }; - - image.onerror = reason => { - logger.warn("Failed to load '" + url + "':", reason); - if (++triesSoFar < 4) { - logger.log("Retrying to load image from", url); - image.src = url + "?try=" + triesSoFar; - } else { - logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason); - image.onerror = null; - image.onload = null; - resolve(null); - } - }; - - image.src = url; - }), - ]); + return this.app.backgroundResourceLoader + .preloadWithProgress(url, progress => { + progressHandler(progress); + }) + .then(url => { + return new Promise((resolve, reject) => { + image.addEventListener("load", () => resolve(image)); + image.addEventListener("error", err => + reject("Failed to load sprite " + key + ": " + err) + ); + image.src = url; + }); + }); } /** * Preloads a sprite * @param {string} key + * @param {(progress: number) => void} progressHandler * @returns {Promise} */ - preloadCSSSprite(key) { - return this.internalPreloadImage(key).then(image => { + preloadCSSSprite(key, progressHandler) { + return this.internalPreloadImage(key, progressHandler).then(image => { if (key.indexOf("game_misc") >= 0) { // Allow access to regular sprites this.sprites.set(key, new RegularSprite(image, image.width, image.height)); @@ -142,10 +117,11 @@ class LoaderImpl { /** * Preloads an atlas * @param {AtlasDefinition} atlas + * @param {(progress: number) => void} progressHandler * @returns {Promise} */ - preloadAtlas(atlas) { - return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => { + preloadAtlas(atlas, progressHandler) { + return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then(image => { // @ts-ignore image.label = atlas.sourceFileName; return this.internalParseAtlas(atlas, image); diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 4092d861..e75789b9 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -747,3 +747,42 @@ export function getRomanNumber(number) { romanLiteralsCache[number] = formatted; return formatted; } + +/** + * Returns the appropriate logo sprite path + */ +export function getLogoSprite() { + if (G_WEGAME_VERSION) { + return "logo_wegame.png"; + } + + if (G_IS_STEAM_DEMO) { + return "logo_demo.png"; + } + + if (G_CHINA_VERSION) { + return "logo_cn.png"; + } + + if (G_IS_STANDALONE) { + return "logo.png"; + } + + if (G_IS_BROWSER) { + return "logo_demo.png"; + } + + return "logo.png"; +} + +/** + * Rejects a promise after X ms + */ +export function timeoutPromise(promise, timeout = 30000) { + return Promise.race([ + new Promise((resolve, reject) => { + setTimeout(() => reject("timeout of " + timeout + " ms exceeded"), timeout); + }), + promise, + ]); +} diff --git a/src/js/game/game_loading_overlay.js b/src/js/game/game_loading_overlay.js index d6bb79f0..94a07160 100644 --- a/src/js/game/game_loading_overlay.js +++ b/src/js/game/game_loading_overlay.js @@ -46,6 +46,7 @@ export class GameLoadingOverlay { this.parent.appendChild(this.element); this.internalAddSpinnerAndText(this.element); this.internalAddHint(this.element); + this.internalAddProgressIndicator(this.element); } /** @@ -68,4 +69,12 @@ export class GameLoadingOverlay { hint.classList.add("prefab_GameHint"); element.appendChild(hint); } + + internalAddProgressIndicator(element) { + const indicator = document.createElement("span"); + indicator.innerHTML = ""; + indicator.classList.add("prefab_LoadingProgressIndicator"); + element.appendChild(indicator); + this.loadingIndicator = indicator; + } } diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 55bc46d4..eba6fd61 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -113,7 +113,7 @@ export function initMetaBuildingRegistry() { /** * Once all sprites are loaded, propagates the cache */ -export function initBuildingCodesAfterResourcesLoaded() { +export function initSpriteCache() { logger.log("Propagating sprite cache"); for (const key in gBuildingVariants) { const variant = gBuildingVariants[key]; diff --git a/src/js/main.js b/src/js/main.js index 0f80f527..634ad4d2 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -64,4 +64,4 @@ function bootApp() { app.boot(); } -window.addEventListener("load", bootApp); +bootApp(); diff --git a/src/js/platform/browser/sound.js b/src/js/platform/browser/sound.js index a837c2e4..a7089665 100644 --- a/src/js/platform/browser/sound.js +++ b/src/js/platform/browser/sound.js @@ -21,33 +21,28 @@ class SoundSpritesContainer { if (this.loadingPromise) { return this.loadingPromise; } - return (this.loadingPromise = Promise.race([ - new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 500 : 5000); - }), - new Promise(resolve => { - this.howl = new Howl({ - src: cachebust("res/sounds/sfx.mp3"), - sprite: sprites.sprite, - autoplay: false, - loop: false, - volume: 0, - preload: true, - pool: 20, - onload: () => { - resolve(); - }, - onloaderror: (id, err) => { - logger.warn("SFX failed to load:", id, err); - this.howl = null; - resolve(); - }, - onplayerror: (id, err) => { - logger.warn("SFX failed to play:", id, err); - }, - }); - }), - ])); + return (this.loadingPromise = new Promise(resolve => { + this.howl = new Howl({ + src: cachebust("res/sounds/sfx.mp3"), + sprite: sprites.sprite, + autoplay: false, + loop: false, + volume: 0, + preload: true, + pool: 20, + onload: () => { + resolve(); + }, + onloaderror: (id, err) => { + logger.warn("SFX failed to load:", id, err); + this.howl = null; + resolve(); + }, + onplayerror: (id, err) => { + logger.warn("SFX failed to play:", id, err); + }, + }); + })); } play(volume, key) { @@ -98,41 +93,37 @@ class MusicInstance extends MusicInstanceInterface { this.playing = false; } load() { - return Promise.race([ - new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 500 : 5000); - }), - new Promise((resolve, reject) => { - this.howl = new Howl({ - src: cachebust("res/sounds/music/" + this.url + ".mp3"), - autoplay: false, - loop: true, - html5: true, - volume: 1, - preload: true, - pool: 2, + return new Promise((resolve, reject) => { + this.howl = new Howl({ + src: cachebust("res/sounds/music/" + this.url + ".mp3"), + autoplay: false, + loop: true, + html5: true, + volume: 1, + preload: true, + pool: 2, - onunlock: () => { - if (this.playing) { - logger.log("Playing music after manual unlock"); - this.play(); - } - }, + onunlock: () => { + if (this.playing) { + logger.log("Playing music after manual unlock"); + this.play(); + } + }, - onload: () => { - resolve(); - }, - onloaderror: (id, err) => { - logger.warn(this, "Music", this.url, "failed to load:", id, err); - this.howl = null; - resolve(); - }, - onplayerror: (id, err) => { - logger.warn(this, "Music", this.url, "failed to play:", id, err); - }, - }); - }), - ]); + onload: () => { + resolve(); + }, + onloaderror: (id, err) => { + logger.warn(this, "Music", this.url, "failed to load:", id, err); + this.howl = null; + resolve(); + }, + + onplayerror: (id, err) => { + logger.warn(this, "Music", this.url, "failed to play:", id, err); + }, + }); + }); } stop() { diff --git a/src/js/states/about.js b/src/js/states/about.js index 4380b02c..69f2e9b5 100644 --- a/src/js/states/about.js +++ b/src/js/states/about.js @@ -2,7 +2,7 @@ import { TextualGameState } from "../core/textual_game_state"; import { T } from "../translations"; import { THIRDPARTY_URLS } from "../core/config"; import { cachebust } from "../core/cachebust"; -import { getLogoSprite } from "../core/background_resources_loader"; +import { getLogoSprite } from "../core/utils"; export class AboutState extends TextualGameState { constructor() { diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index d950553c..21e29803 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -10,6 +10,8 @@ import { GameCore } from "../game/core"; import { MUSIC } from "../platform/sound"; import { enumGameModeIds } from "../game/game_mode"; import { MOD_SIGNALS } from "../mods/mod_signals"; +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { T } from "../translations"; const logger = createLogger("state/ingame"); @@ -231,19 +233,42 @@ export class InGameState extends GameState { if (this.switchStage(GAME_LOADING_STATES.s3_createCore)) { logger.log("Waiting for resources to load"); - this.app.backgroundResourceLoader.getPromiseForBareGame().then(() => { - logger.log("Creating new game core"); - this.core = new GameCore(this.app); - - this.core.initializeRoot(this, this.savegame, this.gameModeId); - - if (this.savegame.hasGameDump()) { - this.stage4bResumeGame(); - } else { - this.app.gameAnalytics.handleGameStarted(); - this.stage4aInitEmptyGame(); - } + this.app.backgroundResourceLoader.resourceStateChangedSignal.add(({ progress }) => { + this.loadingOverlay.loadingIndicator.innerText = T.global.loadingResources.replace( + "", + (progress * 100.0).toFixed(1) + ); }); + + this.app.backgroundResourceLoader.getIngamePromise().then( + () => { + this.loadingOverlay.loadingIndicator.innerText = ""; + this.app.backgroundResourceLoader.resourceStateChangedSignal.removeAll(); + + logger.log("Creating new game core"); + this.core = new GameCore(this.app); + + this.core.initializeRoot(this, this.savegame, this.gameModeId); + + if (this.savegame.hasGameDump()) { + this.stage4bResumeGame(); + } else { + this.app.gameAnalytics.handleGameStarted(); + this.stage4aInitEmptyGame(); + } + }, + err => { + logger.error("Failed to preload resources:", err); + const dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.createElement("div"); + dialogsElement.id = "ingame_HUD_ModalDialogs"; + dialogsElement.style.zIndex = "999999"; + document.body.appendChild(dialogsElement); + dialogs.initializeToElement(dialogsElement); + + this.app.backgroundResourceLoader.showLoaderError(dialogs, err); + } + ); } } diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index f8c90c33..ff013bc0 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -1,4 +1,3 @@ -import { getLogoSprite } from "../core/background_resources_loader"; import { cachebust } from "../core/cachebust"; import { globalConfig, openStandaloneLink, THIRDPARTY_URLS } from "../core/config"; import { GameState } from "../core/game_state"; @@ -8,7 +7,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy"; import { formatSecondsToTimeAgo, generateFileDownload, - isSupportedBrowser, + getLogoSprite, makeButton, makeDiv, makeDivElement, @@ -321,8 +320,9 @@ export class MainMenuState extends GameState { } onEnter(payload) { + // Start loading already const app = this.app; - setTimeout(() => app.backgroundResourceLoader.internalStartLoadingEssentialsForBareGame(), 10); + setTimeout(() => app.backgroundResourceLoader.getIngamePromise(), 10); this.dialogs = new HUDModalDialogs(null, this.app); const dialogsElement = document.body.querySelector(".modalDialogParent"); diff --git a/src/js/states/mobile_warning.js b/src/js/states/mobile_warning.js index 520b155a..24eda8e5 100644 --- a/src/js/states/mobile_warning.js +++ b/src/js/states/mobile_warning.js @@ -1,7 +1,6 @@ -import { GameState } from "../core/game_state"; import { cachebust } from "../core/cachebust"; -import { THIRDPARTY_URLS } from "../core/config"; -import { getLogoSprite } from "../core/background_resources_loader"; +import { GameState } from "../core/game_state"; +import { getLogoSprite } from "../core/utils"; export class MobileWarningState extends GameState { constructor() { diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 3d9c2370..3706555f 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -1,9 +1,9 @@ import { CHANGELOG } from "../changelog"; -import { getLogoSprite } from "../core/background_resources_loader"; import { cachebust } from "../core/cachebust"; import { globalConfig } from "../core/config"; import { GameState } from "../core/game_state"; import { createLogger } from "../core/logging"; +import { getLogoSprite } from "../core/utils"; import { getRandomHint } from "../game/hints"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; @@ -39,6 +39,7 @@ export class PreloadState extends GameState { this.nextHintDuration = 0; this.statusText = this.htmlElement.querySelector("#ll_preload_status"); + this.progressElement = this.htmlElement.querySelector("#ll_progressbar span"); this.startLoading(); } @@ -70,10 +71,10 @@ export class PreloadState extends GameState { startLoading() { this.setStatus("Booting") - .then(() => this.setStatus("Creating platform wrapper")) + .then(() => this.setStatus("Creating platform wrapper", 3)) .then(() => this.app.platformWrapper.initialize()) - .then(() => this.setStatus("Initializing local storage")) + .then(() => this.setStatus("Initializing local storage", 6)) .then(() => { const wrapper = this.app.platformWrapper; if (wrapper instanceof PlatformWrapperImplBrowser) { @@ -94,19 +95,19 @@ export class PreloadState extends GameState { } }) - .then(() => this.setStatus("Creating storage")) + .then(() => this.setStatus("Creating storage", 9)) .then(() => { return this.app.storage.initialize(); }) - .then(() => this.setStatus("Initializing libraries")) + .then(() => this.setStatus("Initializing libraries", 12)) .then(() => this.app.analytics.initialize()) .then(() => this.app.gameAnalytics.initialize()) - .then(() => this.setStatus("Connecting to api")) + .then(() => this.setStatus("Connecting to api", 15)) .then(() => this.fetchDiscounts()) - .then(() => this.setStatus("Initializing settings")) + .then(() => this.setStatus("Initializing settings", 20)) .then(() => { return this.app.settings.initialize(); }) @@ -118,7 +119,7 @@ export class PreloadState extends GameState { } }) - .then(() => this.setStatus("Initializing language")) + .then(() => this.setStatus("Initializing language", 25)) .then(() => { if (G_CHINA_VERSION || G_WEGAME_VERSION) { return this.app.settings.updateLanguage("zh-CN"); @@ -139,22 +140,17 @@ export class PreloadState extends GameState { updateApplicationLanguage(language); }) - .then(() => this.setStatus("Initializing sounds")) + .then(() => this.setStatus("Initializing sounds", 30)) .then(() => { - // Notice: We don't await the sounds loading itself return this.app.sound.initialize(); }) - .then(() => { - this.app.backgroundResourceLoader.startLoading(); - }) - - .then(() => this.setStatus("Initializing restrictions")) + .then(() => this.setStatus("Initializing restrictions", 34)) .then(() => { return this.app.restrictionMgr.initialize(); }) - .then(() => this.setStatus("Initializing savegame")) + .then(() => this.setStatus("Initializing savegames", 38)) .then(() => { return this.app.savegameMgr.initialize().catch(err => { logger.error("Failed to initialize savegames:", err); @@ -165,12 +161,25 @@ export class PreloadState extends GameState { }); }) - .then(() => this.setStatus("Downloading resources")) + .then(() => this.setStatus("Downloading resources", 40)) .then(() => { - return this.app.backgroundResourceLoader.getPromiseForMainMenu(); + this.app.backgroundResourceLoader.resourceStateChangedSignal.add(({ progress }) => { + this.setStatus( + "Downloading resources (" + (progress * 100.0).toFixed(1) + " %)", + 40 + progress * 50 + ); + }); + return this.app.backgroundResourceLoader.getMainMenuPromise().catch(err => { + logger.error("Failed to load resources:", err); + this.app.backgroundResourceLoader.showLoaderError(this.dialogs, err); + return new Promise(() => null); + }); + }) + .then(() => { + this.app.backgroundResourceLoader.resourceStateChangedSignal.removeAll(); }) - .then(() => this.setStatus("Checking changelog")) + .then(() => this.setStatus("Checking changelog", 95)) .then(() => { if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) { return; @@ -231,7 +240,7 @@ export class PreloadState extends GameState { }); }) - .then(() => this.setStatus("Launching")) + .then(() => this.setStatus("Launching", 99)) .then( () => { this.moveToState("MainMenuState"); @@ -274,7 +283,7 @@ export class PreloadState extends GameState { * * @param {string} text */ - setStatus(text) { + setStatus(text, progress) { logger.log("✅ " + text); if (G_CHINA_VERSION || G_WEGAME_VERSION) { @@ -282,6 +291,7 @@ export class PreloadState extends GameState { } this.currentStatus = text; this.statusText.innerText = text; + this.progressElement.style.width = 80 + (progress / 100) * 20 + "%"; return Promise.resolve(); } diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 6afe84f1..72e4457a 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -50,6 +50,8 @@ global: error: Error loggingIn: Logging in + loadingResources: Downloading additional resources ( %) + # How big numbers are rendered, e.g. "10,000" thousandsDivider: "," @@ -443,6 +445,22 @@ dialogs: missingMods: Missing Mods newMods: Newly installed Mods + resourceLoadFailed: + title: Resources failed to load + + demoLinkText: shapez demo on Steam + descWeb: >- + One ore more resources could not be loaded. Make sure you have a stable internet connection and try again. + If this still doesn't work, make sure to also disable any browser extensions (including adblockers).

+ As an alternative, you can also play the . +

+ Error Message: + + descSteamDemo: >- + One ore more resources could not be loaded. Try restarting the game - If that does not help, try reinstalling and verifying the game files via Steam. +

+ Error Message: + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation