Refactor background resources loader - game should now load much faster and also reports progress while downloading resources

This commit is contained in:
tobspr 2022-06-18 14:43:26 +02:00
parent 7fe088a0c8
commit 34ed689875
24 changed files with 657 additions and 721 deletions

View File

@ -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);
})();

View File

@ -165,7 +165,7 @@ function serveHTML({ version = "web-dev" }) {
// Watch .html files, those trigger a html rebuild // Watch .html files, those trigger a html rebuild
gulp.watch("../src/**/*.html", gulp.series("html." + version + ".dev")); 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 // Watch translations
gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson")); gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson"));

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,7 @@ body {
} }
#ll_fp { #ll_fp {
font-family: GameFont; font-family: "GameFont", Arial, sans-serif;
font-size: 14px; font-size: 14px;
position: fixed; position: fixed;
z-index: -1; z-index: -1;
@ -89,8 +89,9 @@ body {
#ll_loader > .ll_text { #ll_loader > .ll_text {
text-align: center; text-align: center;
color: #777a7f; color: #777a7f;
font-family: "GameFont", sans-serif; font-family: "GameFont", Arial, sans-serif;
font-size: 24px; font-size: 24px;
height: 30px;
line-height: 1.2em; line-height: 1.2em;
} }
@ -98,60 +99,39 @@ body {
width: 80vw; width: 80vw;
max-width: 800px; max-width: 800px;
margin-top: 40px; margin-top: 40px;
height: 14px; height: 7px;
border-radius: 20px; border-radius: 20px;
background: rgba(0, 10, 40, 0.1); background: rgba(0, 10, 20, 0.08);
border: 5px solid transparent;
/* border: 5px solid transparent; */
display: flex; display: flex;
position: relative; position: relative;
align-items: flex-start; align-items: flex-start;
} }
@keyframes LL_LoadingAnimation { @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% { 50% {
width: 0%; background-color: #34ae67;
background-color: #777a7f;
}
74.99% {
width: 98%;
background-color: #74a8c0;
}
75% {
width: 0%;
background-color: #777a7f;
}
100% {
width: 98%;
background-color: #a186d4;
} }
} }
#ll_progressbar > span { #ll_progressbar > span {
border-radius: 20px; border-radius: 20px;
position: absolute; position: absolute;
height: 100%; height: 190%;
width: 98%; width: 5%;
background: #fff; background: #fff;
transform: translateY(-50%);
top: 50%;
display: inline-flex; display: inline-flex;
animation: LL_LoadingAnimation 90s ease-in-out infinite; background-color: #269fba;
animation: LL_LoadingAnimation 4s ease-in-out infinite;
position: relative; position: relative;
z-index: 10; 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 { #ll_progressbar > #ll_loadinglabel {
@ -181,10 +161,10 @@ body {
#ll_standalone { #ll_standalone {
text-align: center; text-align: center;
color: #777a7f; color: #777a7f;
margin-top: 20px; margin-top: 30px;
display: block; display: block;
font-size: 16px; font-size: 16px;
animation: ShowStandaloneBannerAfterDelay 30s linear; animation: ShowStandaloneBannerAfterDelay 60s linear;
} }
#ll_standalone a { #ll_standalone a {
@ -212,13 +192,71 @@ body {
#ll_preload_status { #ll_preload_status {
position: absolute; position: absolute;
top: 30px; top: 40px;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 100; z-index: 100;
color: rgba(#000, 0.4);
opacity: 1 !important; 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-transform: uppercase;
text-align: center; 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;
}

File diff suppressed because one or more lines are too long

127
gulp/preloader/preloader.js Normal file
View File

@ -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 <strong>#bugs</strong> channel of the <a href='https://discord.gg/rtuRRJDc7u' target='_blank'>official discord</a>!";
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);
})();

View File

@ -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;
}
}

View File

@ -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 { .prefab_FeatureComingSoon {
position: relative; position: relative;
&::after { &::after {

View File

@ -16,7 +16,6 @@
@import "common"; @import "common";
@import "animations"; @import "animations";
@import "game_state"; @import "game_state";
@import "application_error";
@import "textual_game_state"; @import "textual_game_state";
@import "adinplay"; @import "adinplay";
@import "changelog_skins"; @import "changelog_skins";

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" style="--ui-scale: 1.33;">
<head> <head>
<title>shapez Demo - Factory Automation Game</title> <title>shapez Demo - Factory Automation Game</title>

View File

@ -2,58 +2,36 @@
import { Application } from "../application"; import { Application } from "../application";
/* typehints:end */ /* 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 { Loader } from "./loader";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Signal } from "./signal"; import { Signal } from "./signal";
import { SOUNDS, MUSIC } from "../platform/sound"; import { clamp, getLogoSprite, timeoutPromise } from "./utils";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
import { cachebust } from "./cachebust";
const logger = createLogger("background_loader"); const logger = createLogger("background_loader");
export function getLogoSprite() { const MAIN_MENU_ASSETS = {
if (G_WEGAME_VERSION) { sprites: [getLogoSprite()],
return "logo_wegame.png"; sounds: [SOUNDS.uiClick, SOUNDS.uiError, SOUNDS.dialogError, SOUNDS.dialogOk],
} atlas: [],
css: [],
};
if (G_IS_STEAM_DEMO) { const INGAME_ASSETS = {
return "logo_demo.png"; 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) { const LOADER_TIMEOUT_PER_RESOURCE = 180000;
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)];
export class BackgroundResourcesLoader { export class BackgroundResourcesLoader {
/** /**
@ -63,193 +41,199 @@ export class BackgroundResourcesLoader {
constructor(app) { constructor(app) {
this.app = app; this.app = app;
this.registerReady = false; this.mainMenuPromise = null;
this.mainMenuReady = false; this.ingamePromise = null;
this.bareGameReady = false;
this.additionalReady = false;
this.signalMainMenuLoaded = new Signal(); this.resourceStateChangedSignal = 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 = [];
} }
getNumAssetsLoaded() { getMainMenuPromise() {
return this.numAssetsLoaded; if (this.mainMenuPromise) {
} return this.mainMenuPromise;
getNumAssetsTotal() {
return this.numAssetsToLoadTotal;
}
getPromiseForMainMenu() {
if (this.mainMenuReady) {
return Promise.resolve();
} }
return new Promise(resolve => { logger.warn("⏰ Loading main menu assets");
this.signalMainMenuLoaded.add(resolve); return (this.mainMenuPromise = this.loadAssets(MAIN_MENU_ASSETS));
});
} }
getPromiseForBareGame() { getIngamePromise() {
if (this.bareGameReady) { if (this.ingamePromise) {
return Promise.resolve(); return this.ingamePromise;
} }
logger.warn("⏰ Loading ingame assets");
return new Promise(resolve => { const promise = this.loadAssets(INGAME_ASSETS).then(() => initSpriteCache());
this.signalBareGameLoaded.add(resolve); return (this.ingamePromise = promise);
});
}
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);
});
} }
/** /**
* @param {Array<string>} sprites *
* @param {Array<string>} sounds * @param {object} param0
* @param {Array<AtlasDefinition>} atlases * @param {string[]} param0.sprites
* @returns {Promise<void>} * @param {string[]} param0.sounds
* @param {AtlasDefinition[]} param0.atlas
* @param {string[]} param0.css
*/ */
internalLoadSpritesAndSounds(sprites, sounds, atlases = []) { async loadAssets({ sprites, sounds, atlas, css }) {
this.numAssetsToLoadTotal = sprites.length + sounds.length + atlases.length; /**
this.numAssetsLoaded = 0; * @type {((progressHandler: (progress: number) => void) => Promise<void>)[]}
*/
let promiseFunctions = [];
let promises = []; // CSS
for (let i = 0; i < css.length; ++i) {
for (let i = 0; i < sounds.length; ++i) { promiseFunctions.push(progress =>
if (this.soundsLoaded.indexOf(sounds[i]) >= 0) { timeoutPromise(
// Already loaded this.internalPreloadCss(cachebust(css[i]), progress),
continue; LOADER_TIMEOUT_PER_RESOURCE
} ).catch(err => {
logger.error("Failed to load css:", css[i], err);
this.soundsLoaded.push(sounds[i]); throw new Error("HUD Stylesheet " + css[i] + " failed to load: " + err);
promises.push( })
this.app.sound
.loadSound(sounds[i])
.catch(err => {
logger.warn("Failed to load sound:", sounds[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
); );
} }
// 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) { for (let i = 0; i < sprites.length; ++i) {
if (this.spritesLoaded.indexOf(sprites[i]) >= 0) { promiseFunctions.push(progress =>
// Already loaded timeoutPromise(
continue; Loader.preloadCSSSprite(sprites[i], progress),
} LOADER_TIMEOUT_PER_RESOURCE
this.spritesLoaded.push(sprites[i]); ).catch(err => {
promises.push( logger.error("Failed to load css sprite:", sprites[i], err);
Loader.preloadCSSSprite(sprites[i]) throw new Error("HUD Sprite " + sprites[i] + " failed to load: " + err);
.catch(err => { })
logger.warn("Failed to load css sprite:", sprites[i]);
})
.then(() => {
this.numAssetsLoaded++;
})
); );
} }
for (let i = 0; i < atlases.length; ++i) { // SFX & Music
const atlas = atlases[i]; for (let i = 0; i < sounds.length; ++i) {
if (this.atlasesLoaded.includes(atlas)) { promiseFunctions.push(progress =>
// Already loaded timeoutPromise(this.app.sound.loadSound(sounds[i]), LOADER_TIMEOUT_PER_RESOURCE).catch(
continue; err => {
} logger.warn("Failed to load sound, will not be available:", sounds[i], err);
this.atlasesLoaded.push(atlas); }
)
promises.push(
Loader.preloadAtlas(atlas)
.catch(err => {
logger.warn("Failed to load atlas:", atlas.sourceFileName, err);
})
.then(() => {
this.numAssetsLoaded++;
})
); );
} }
return Promise.all(promises).then(() => { const originalAmount = promiseFunctions.length;
this.numAssetsToLoadTotal = 0; const start = performance.now();
this.numAssetsLoaded = 0;
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 + "<br>" + err,
["retry"]
)
.retry.add(() => window.location.reload());
} else {
dialogs
.showWarning(
T.dialogs.resourceLoadFailed.title,
T.dialogs.resourceLoadFailed.descWeb.replace(
"<demoOnSteamLinkText>",
`<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`
) +
"<br>" +
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);
}); });
} }
} }

View File

@ -1,7 +1,3 @@
import { logSection } from "./logging";
import { stringifyObjectContainingErrors } from "./logging";
import { removeAllChildren } from "./utils";
export let APPLICATION_ERROR_OCCURED = false; export let APPLICATION_ERROR_OCCURED = false;
/** /**
@ -13,114 +9,8 @@ export let APPLICATION_ERROR_OCCURED = false;
* @param {Error} source * @param {Error} source
*/ */
function catchErrors(message, source, lineno, colno, error) { 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; APPLICATION_ERROR_OCCURED = true;
const element = document.createElement("div"); console.error(message, source, lineno, colno, error);
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!<br /><br />
An anonymized crash report has been sent, and I will have a look as soon as possible.<br /><br />
If you have additional information how I can reproduce this error, please E-Mail me:&nbsp;
<a href="mailto:bugs@shapez.io?title=Application+Crash">bugs@shapez.io</a>`;
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;
} }
window.onerror = catchErrors; window.addEventListener("error", catchErrors);

View File

@ -76,61 +76,36 @@ class LoaderImpl {
/** /**
* *
* @param {string} key * @param {string} key
* @param {(progress: number) => void} progressHandler
* @returns {Promise<HTMLImageElement|null>} * @returns {Promise<HTMLImageElement|null>}
*/ */
internalPreloadImage(key) { internalPreloadImage(key, progressHandler) {
const url = cachebust("res/" + key); const url = cachebust("res/" + key);
const image = new Image(); const image = new Image();
let triesSoFar = 0; return this.app.backgroundResourceLoader
.preloadWithProgress(url, progress => {
return Promise.race([ progressHandler(progress);
new Promise((resolve, reject) => { })
setTimeout(() => reject("loader request timeout"), G_IS_DEV ? 500 : 30000); .then(url => {
}), return new Promise((resolve, reject) => {
image.addEventListener("load", () => resolve(image));
new Promise(resolve => { image.addEventListener("error", err =>
image.onload = () => { reject("Failed to load sprite " + key + ": " + err)
image.onerror = null; );
image.onload = null; image.src = url;
});
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;
}),
]);
} }
/** /**
* Preloads a sprite * Preloads a sprite
* @param {string} key * @param {string} key
* @param {(progress: number) => void} progressHandler
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
preloadCSSSprite(key) { preloadCSSSprite(key, progressHandler) {
return this.internalPreloadImage(key).then(image => { return this.internalPreloadImage(key, progressHandler).then(image => {
if (key.indexOf("game_misc") >= 0) { if (key.indexOf("game_misc") >= 0) {
// Allow access to regular sprites // Allow access to regular sprites
this.sprites.set(key, new RegularSprite(image, image.width, image.height)); this.sprites.set(key, new RegularSprite(image, image.width, image.height));
@ -142,10 +117,11 @@ class LoaderImpl {
/** /**
* Preloads an atlas * Preloads an atlas
* @param {AtlasDefinition} atlas * @param {AtlasDefinition} atlas
* @param {(progress: number) => void} progressHandler
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
preloadAtlas(atlas) { preloadAtlas(atlas, progressHandler) {
return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => { return this.internalPreloadImage(atlas.getFullSourcePath(), progressHandler).then(image => {
// @ts-ignore // @ts-ignore
image.label = atlas.sourceFileName; image.label = atlas.sourceFileName;
return this.internalParseAtlas(atlas, image); return this.internalParseAtlas(atlas, image);

View File

@ -747,3 +747,42 @@ export function getRomanNumber(number) {
romanLiteralsCache[number] = formatted; romanLiteralsCache[number] = formatted;
return 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,
]);
}

View File

@ -46,6 +46,7 @@ export class GameLoadingOverlay {
this.parent.appendChild(this.element); this.parent.appendChild(this.element);
this.internalAddSpinnerAndText(this.element); this.internalAddSpinnerAndText(this.element);
this.internalAddHint(this.element); this.internalAddHint(this.element);
this.internalAddProgressIndicator(this.element);
} }
/** /**
@ -68,4 +69,12 @@ export class GameLoadingOverlay {
hint.classList.add("prefab_GameHint"); hint.classList.add("prefab_GameHint");
element.appendChild(hint); element.appendChild(hint);
} }
internalAddProgressIndicator(element) {
const indicator = document.createElement("span");
indicator.innerHTML = "";
indicator.classList.add("prefab_LoadingProgressIndicator");
element.appendChild(indicator);
this.loadingIndicator = indicator;
}
} }

View File

@ -113,7 +113,7 @@ export function initMetaBuildingRegistry() {
/** /**
* Once all sprites are loaded, propagates the cache * Once all sprites are loaded, propagates the cache
*/ */
export function initBuildingCodesAfterResourcesLoaded() { export function initSpriteCache() {
logger.log("Propagating sprite cache"); logger.log("Propagating sprite cache");
for (const key in gBuildingVariants) { for (const key in gBuildingVariants) {
const variant = gBuildingVariants[key]; const variant = gBuildingVariants[key];

View File

@ -64,4 +64,4 @@ function bootApp() {
app.boot(); app.boot();
} }
window.addEventListener("load", bootApp); bootApp();

View File

@ -21,33 +21,28 @@ class SoundSpritesContainer {
if (this.loadingPromise) { if (this.loadingPromise) {
return this.loadingPromise; return this.loadingPromise;
} }
return (this.loadingPromise = Promise.race([ return (this.loadingPromise = new Promise(resolve => {
new Promise((resolve, reject) => { this.howl = new Howl({
setTimeout(reject, G_IS_DEV ? 500 : 5000); src: cachebust("res/sounds/sfx.mp3"),
}), sprite: sprites.sprite,
new Promise(resolve => { autoplay: false,
this.howl = new Howl({ loop: false,
src: cachebust("res/sounds/sfx.mp3"), volume: 0,
sprite: sprites.sprite, preload: true,
autoplay: false, pool: 20,
loop: false, onload: () => {
volume: 0, resolve();
preload: true, },
pool: 20, onloaderror: (id, err) => {
onload: () => { logger.warn("SFX failed to load:", id, err);
resolve(); this.howl = null;
}, resolve();
onloaderror: (id, err) => { },
logger.warn("SFX failed to load:", id, err); onplayerror: (id, err) => {
this.howl = null; logger.warn("SFX failed to play:", id, err);
resolve(); },
}, });
onplayerror: (id, err) => { }));
logger.warn("SFX failed to play:", id, err);
},
});
}),
]));
} }
play(volume, key) { play(volume, key) {
@ -98,41 +93,37 @@ class MusicInstance extends MusicInstanceInterface {
this.playing = false; this.playing = false;
} }
load() { load() {
return Promise.race([ return new Promise((resolve, reject) => {
new Promise((resolve, reject) => { this.howl = new Howl({
setTimeout(reject, G_IS_DEV ? 500 : 5000); src: cachebust("res/sounds/music/" + this.url + ".mp3"),
}), autoplay: false,
new Promise((resolve, reject) => { loop: true,
this.howl = new Howl({ html5: true,
src: cachebust("res/sounds/music/" + this.url + ".mp3"), volume: 1,
autoplay: false, preload: true,
loop: true, pool: 2,
html5: true,
volume: 1,
preload: true,
pool: 2,
onunlock: () => { onunlock: () => {
if (this.playing) { if (this.playing) {
logger.log("Playing music after manual unlock"); logger.log("Playing music after manual unlock");
this.play(); this.play();
} }
}, },
onload: () => { onload: () => {
resolve(); resolve();
}, },
onloaderror: (id, err) => { onloaderror: (id, err) => {
logger.warn(this, "Music", this.url, "failed to load:", id, err); logger.warn(this, "Music", this.url, "failed to load:", id, err);
this.howl = null; this.howl = null;
resolve(); resolve();
}, },
onplayerror: (id, err) => {
logger.warn(this, "Music", this.url, "failed to play:", id, err); onplayerror: (id, err) => {
}, logger.warn(this, "Music", this.url, "failed to play:", id, err);
}); },
}), });
]); });
} }
stop() { stop() {

View File

@ -2,7 +2,7 @@ import { TextualGameState } from "../core/textual_game_state";
import { T } from "../translations"; import { T } from "../translations";
import { THIRDPARTY_URLS } from "../core/config"; import { THIRDPARTY_URLS } from "../core/config";
import { cachebust } from "../core/cachebust"; import { cachebust } from "../core/cachebust";
import { getLogoSprite } from "../core/background_resources_loader"; import { getLogoSprite } from "../core/utils";
export class AboutState extends TextualGameState { export class AboutState extends TextualGameState {
constructor() { constructor() {

View File

@ -10,6 +10,8 @@ import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound"; import { MUSIC } from "../platform/sound";
import { enumGameModeIds } from "../game/game_mode"; import { enumGameModeIds } from "../game/game_mode";
import { MOD_SIGNALS } from "../mods/mod_signals"; import { MOD_SIGNALS } from "../mods/mod_signals";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { T } from "../translations";
const logger = createLogger("state/ingame"); const logger = createLogger("state/ingame");
@ -231,19 +233,42 @@ export class InGameState extends GameState {
if (this.switchStage(GAME_LOADING_STATES.s3_createCore)) { if (this.switchStage(GAME_LOADING_STATES.s3_createCore)) {
logger.log("Waiting for resources to load"); logger.log("Waiting for resources to load");
this.app.backgroundResourceLoader.getPromiseForBareGame().then(() => { this.app.backgroundResourceLoader.resourceStateChangedSignal.add(({ progress }) => {
logger.log("Creating new game core"); this.loadingOverlay.loadingIndicator.innerText = T.global.loadingResources.replace(
this.core = new GameCore(this.app); "<percentage>",
(progress * 100.0).toFixed(1)
this.core.initializeRoot(this, this.savegame, this.gameModeId); );
if (this.savegame.hasGameDump()) {
this.stage4bResumeGame();
} else {
this.app.gameAnalytics.handleGameStarted();
this.stage4aInitEmptyGame();
}
}); });
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);
}
);
} }
} }

View File

@ -1,4 +1,3 @@
import { getLogoSprite } from "../core/background_resources_loader";
import { cachebust } from "../core/cachebust"; import { cachebust } from "../core/cachebust";
import { globalConfig, openStandaloneLink, THIRDPARTY_URLS } from "../core/config"; import { globalConfig, openStandaloneLink, THIRDPARTY_URLS } from "../core/config";
import { GameState } from "../core/game_state"; import { GameState } from "../core/game_state";
@ -8,7 +7,7 @@ import { ReadWriteProxy } from "../core/read_write_proxy";
import { import {
formatSecondsToTimeAgo, formatSecondsToTimeAgo,
generateFileDownload, generateFileDownload,
isSupportedBrowser, getLogoSprite,
makeButton, makeButton,
makeDiv, makeDiv,
makeDivElement, makeDivElement,
@ -321,8 +320,9 @@ export class MainMenuState extends GameState {
} }
onEnter(payload) { onEnter(payload) {
// Start loading already
const app = this.app; const app = this.app;
setTimeout(() => app.backgroundResourceLoader.internalStartLoadingEssentialsForBareGame(), 10); setTimeout(() => app.backgroundResourceLoader.getIngamePromise(), 10);
this.dialogs = new HUDModalDialogs(null, this.app); this.dialogs = new HUDModalDialogs(null, this.app);
const dialogsElement = document.body.querySelector(".modalDialogParent"); const dialogsElement = document.body.querySelector(".modalDialogParent");

View File

@ -1,7 +1,6 @@
import { GameState } from "../core/game_state";
import { cachebust } from "../core/cachebust"; import { cachebust } from "../core/cachebust";
import { THIRDPARTY_URLS } from "../core/config"; import { GameState } from "../core/game_state";
import { getLogoSprite } from "../core/background_resources_loader"; import { getLogoSprite } from "../core/utils";
export class MobileWarningState extends GameState { export class MobileWarningState extends GameState {
constructor() { constructor() {

View File

@ -1,9 +1,9 @@
import { CHANGELOG } from "../changelog"; import { CHANGELOG } from "../changelog";
import { getLogoSprite } from "../core/background_resources_loader";
import { cachebust } from "../core/cachebust"; import { cachebust } from "../core/cachebust";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { GameState } from "../core/game_state"; import { GameState } from "../core/game_state";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { getLogoSprite } from "../core/utils";
import { getRandomHint } from "../game/hints"; import { getRandomHint } from "../game/hints";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
@ -39,6 +39,7 @@ export class PreloadState extends GameState {
this.nextHintDuration = 0; this.nextHintDuration = 0;
this.statusText = this.htmlElement.querySelector("#ll_preload_status"); this.statusText = this.htmlElement.querySelector("#ll_preload_status");
this.progressElement = this.htmlElement.querySelector("#ll_progressbar span");
this.startLoading(); this.startLoading();
} }
@ -70,10 +71,10 @@ export class PreloadState extends GameState {
startLoading() { startLoading() {
this.setStatus("Booting") this.setStatus("Booting")
.then(() => this.setStatus("Creating platform wrapper")) .then(() => this.setStatus("Creating platform wrapper", 3))
.then(() => this.app.platformWrapper.initialize()) .then(() => this.app.platformWrapper.initialize())
.then(() => this.setStatus("Initializing local storage")) .then(() => this.setStatus("Initializing local storage", 6))
.then(() => { .then(() => {
const wrapper = this.app.platformWrapper; const wrapper = this.app.platformWrapper;
if (wrapper instanceof PlatformWrapperImplBrowser) { 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(() => { .then(() => {
return this.app.storage.initialize(); return this.app.storage.initialize();
}) })
.then(() => this.setStatus("Initializing libraries")) .then(() => this.setStatus("Initializing libraries", 12))
.then(() => this.app.analytics.initialize()) .then(() => this.app.analytics.initialize())
.then(() => this.app.gameAnalytics.initialize()) .then(() => this.app.gameAnalytics.initialize())
.then(() => this.setStatus("Connecting to api")) .then(() => this.setStatus("Connecting to api", 15))
.then(() => this.fetchDiscounts()) .then(() => this.fetchDiscounts())
.then(() => this.setStatus("Initializing settings")) .then(() => this.setStatus("Initializing settings", 20))
.then(() => { .then(() => {
return this.app.settings.initialize(); 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(() => { .then(() => {
if (G_CHINA_VERSION || G_WEGAME_VERSION) { if (G_CHINA_VERSION || G_WEGAME_VERSION) {
return this.app.settings.updateLanguage("zh-CN"); return this.app.settings.updateLanguage("zh-CN");
@ -139,22 +140,17 @@ export class PreloadState extends GameState {
updateApplicationLanguage(language); updateApplicationLanguage(language);
}) })
.then(() => this.setStatus("Initializing sounds")) .then(() => this.setStatus("Initializing sounds", 30))
.then(() => { .then(() => {
// Notice: We don't await the sounds loading itself
return this.app.sound.initialize(); return this.app.sound.initialize();
}) })
.then(() => { .then(() => this.setStatus("Initializing restrictions", 34))
this.app.backgroundResourceLoader.startLoading();
})
.then(() => this.setStatus("Initializing restrictions"))
.then(() => { .then(() => {
return this.app.restrictionMgr.initialize(); return this.app.restrictionMgr.initialize();
}) })
.then(() => this.setStatus("Initializing savegame")) .then(() => this.setStatus("Initializing savegames", 38))
.then(() => { .then(() => {
return this.app.savegameMgr.initialize().catch(err => { return this.app.savegameMgr.initialize().catch(err => {
logger.error("Failed to initialize savegames:", 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(() => { .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(() => { .then(() => {
if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) { if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) {
return; return;
@ -231,7 +240,7 @@ export class PreloadState extends GameState {
}); });
}) })
.then(() => this.setStatus("Launching")) .then(() => this.setStatus("Launching", 99))
.then( .then(
() => { () => {
this.moveToState("MainMenuState"); this.moveToState("MainMenuState");
@ -274,7 +283,7 @@ export class PreloadState extends GameState {
* *
* @param {string} text * @param {string} text
*/ */
setStatus(text) { setStatus(text, progress) {
logger.log("✅ " + text); logger.log("✅ " + text);
if (G_CHINA_VERSION || G_WEGAME_VERSION) { if (G_CHINA_VERSION || G_WEGAME_VERSION) {
@ -282,6 +291,7 @@ export class PreloadState extends GameState {
} }
this.currentStatus = text; this.currentStatus = text;
this.statusText.innerText = text; this.statusText.innerText = text;
this.progressElement.style.width = 80 + (progress / 100) * 20 + "%";
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -50,6 +50,8 @@ global:
error: Error error: Error
loggingIn: Logging in loggingIn: Logging in
loadingResources: Downloading additional resources (<percentage> %)
# How big numbers are rendered, e.g. "10,000" # How big numbers are rendered, e.g. "10,000"
thousandsDivider: "," thousandsDivider: ","
@ -443,6 +445,22 @@ dialogs:
missingMods: Missing Mods missingMods: Missing Mods
newMods: Newly installed 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).<br><br>
As an alternative, you can also play the <demoOnSteamLinkText>.
<br><br>
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.
<br><br>
Error Message:
ingame: ingame:
# This is shown in the top left corner and displays useful keybindings in # This is shown in the top left corner and displays useful keybindings in
# every situation # every situation