35adf718cf
Rename engine.start() to startGame(), new start() takes string arguments handed directly to main(). Rename Engine.loadEngine() to load(). Add setLocale(), setResizeCanvasOnStart(), setExecutableName() and preloadFile().
365 lines
8.8 KiB
JavaScript
365 lines
8.8 KiB
JavaScript
return Module;
|
|
},
|
|
};
|
|
|
|
(function() {
|
|
var engine = Engine;
|
|
|
|
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
|
|
|
var basePath = null;
|
|
var engineLoadPromise = null;
|
|
|
|
var loadingFiles = {};
|
|
|
|
function getBasePath(path) {
|
|
|
|
if (path.endsWith('/'))
|
|
path = path.slice(0, -1);
|
|
if (path.lastIndexOf('.') > path.lastIndexOf('/'))
|
|
path = path.slice(0, path.lastIndexOf('.'));
|
|
return path;
|
|
}
|
|
|
|
function getBaseName(path) {
|
|
|
|
path = getBasePath(path);
|
|
return path.slice(path.lastIndexOf('/') + 1);
|
|
}
|
|
|
|
Engine = function Engine() {
|
|
|
|
this.rtenv = null;
|
|
|
|
var initPromise = null;
|
|
var unloadAfterInit = true;
|
|
|
|
var preloadedFiles = [];
|
|
|
|
var resizeCanvasOnStart = true;
|
|
var progressFunc = null;
|
|
var preloadProgressTracker = {};
|
|
var lastProgress = { loaded: 0, total: 0 };
|
|
|
|
var canvas = null;
|
|
var executableName = null;
|
|
var locale = null;
|
|
var stdout = null;
|
|
var stderr = null;
|
|
|
|
this.init = function(newBasePath) {
|
|
|
|
if (!initPromise) {
|
|
initPromise = Engine.load(newBasePath).then(
|
|
instantiate.bind(this)
|
|
);
|
|
requestAnimationFrame(animateProgress);
|
|
if (unloadAfterInit)
|
|
initPromise.then(Engine.unloadEngine);
|
|
}
|
|
return initPromise;
|
|
};
|
|
|
|
function instantiate(wasmBuf) {
|
|
|
|
var rtenvProps = {
|
|
engine: this,
|
|
ENV: {},
|
|
};
|
|
if (typeof stdout === 'function')
|
|
rtenvProps.print = stdout;
|
|
if (typeof stderr === 'function')
|
|
rtenvProps.printErr = stderr;
|
|
rtenvProps.instantiateWasm = function(imports, onSuccess) {
|
|
WebAssembly.instantiate(wasmBuf, imports).then(function(result) {
|
|
onSuccess(result.instance);
|
|
});
|
|
return {};
|
|
};
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
rtenvProps.onRuntimeInitialized = resolve;
|
|
rtenvProps.onAbort = reject;
|
|
rtenvProps.engine.rtenv = Engine.RuntimeEnvironment(rtenvProps);
|
|
});
|
|
}
|
|
|
|
this.preloadFile = function(pathOrBuffer, bufferFilename) {
|
|
|
|
if (pathOrBuffer instanceof ArrayBuffer) {
|
|
pathOrBuffer = new Uint8Array(pathOrBuffer);
|
|
} else if (ArrayBuffer.isView(pathOrBuffer)) {
|
|
pathOrBuffer = new Uint8Array(pathOrBuffer.buffer);
|
|
}
|
|
if (pathOrBuffer instanceof Uint8Array) {
|
|
preloadedFiles.push({
|
|
name: bufferFilename,
|
|
buffer: pathOrBuffer
|
|
});
|
|
return Promise.resolve();
|
|
} else if (typeof pathOrBuffer === 'string') {
|
|
return loadPromise(pathOrBuffer, preloadProgressTracker).then(function(xhr) {
|
|
preloadedFiles.push({
|
|
name: pathOrBuffer,
|
|
buffer: xhr.response
|
|
});
|
|
});
|
|
} else {
|
|
throw Promise.reject("Invalid object for preloading");
|
|
}
|
|
};
|
|
|
|
this.start = function() {
|
|
|
|
return this.init().then(
|
|
Function.prototype.apply.bind(synchronousStart, this, arguments)
|
|
);
|
|
};
|
|
|
|
this.startGame = function(mainPack) {
|
|
|
|
executableName = getBaseName(mainPack);
|
|
return Promise.all([this.init(getBasePath(mainPack)), this.preloadFile(mainPack)]).then(
|
|
Function.prototype.apply.bind(synchronousStart, this, [])
|
|
);
|
|
};
|
|
|
|
function synchronousStart() {
|
|
|
|
if (canvas instanceof HTMLCanvasElement) {
|
|
this.rtenv.canvas = canvas;
|
|
} else {
|
|
var firstCanvas = document.getElementsByTagName('canvas')[0];
|
|
if (firstCanvas instanceof HTMLCanvasElement) {
|
|
this.rtenv.canvas = firstCanvas;
|
|
} else {
|
|
throw new Error("No canvas found");
|
|
}
|
|
}
|
|
|
|
var actualCanvas = this.rtenv.canvas;
|
|
var context = false;
|
|
try {
|
|
context = actualCanvas.getContext('webgl2') || actualCanvas.getContext('experimental-webgl2');
|
|
} catch (e) {}
|
|
if (!context) {
|
|
throw new Error("WebGL 2 not available");
|
|
}
|
|
|
|
// canvas can grab focus on click
|
|
if (actualCanvas.tabIndex < 0) {
|
|
actualCanvas.tabIndex = 0;
|
|
}
|
|
// necessary to calculate cursor coordinates correctly
|
|
actualCanvas.style.padding = 0;
|
|
actualCanvas.style.borderWidth = 0;
|
|
actualCanvas.style.borderStyle = 'none';
|
|
// until context restoration is implemented
|
|
actualCanvas.addEventListener('webglcontextlost', function(ev) {
|
|
alert("WebGL context lost, please reload the page");
|
|
ev.preventDefault();
|
|
}, false);
|
|
|
|
if (locale) {
|
|
this.rtenv.locale = locale;
|
|
} else {
|
|
this.rtenv.locale = navigator.languages ? navigator.languages[0] : navigator.language;
|
|
}
|
|
this.rtenv.locale = this.rtenv.locale.split('.')[0];
|
|
this.rtenv.resizeCanvasOnStart = resizeCanvasOnStart;
|
|
|
|
this.rtenv.thisProgram = executableName || getBaseName(basePath);
|
|
|
|
preloadedFiles.forEach(function(file) {
|
|
this.rtenv.FS.createDataFile('/', file.name, new Uint8Array(file.buffer), true, true, true);
|
|
}, this);
|
|
|
|
preloadedFiles = null;
|
|
initPromise = null;
|
|
this.rtenv.callMain(arguments);
|
|
}
|
|
|
|
this.setProgressFunc = function(func) {
|
|
progressFunc = func;
|
|
};
|
|
|
|
this.setResizeCanvasOnStart = function(enabled) {
|
|
resizeCanvasOnStart = enabled;
|
|
};
|
|
|
|
function animateProgress() {
|
|
|
|
var loaded = 0;
|
|
var total = 0;
|
|
var totalIsValid = true;
|
|
var progressIsFinal = true;
|
|
|
|
[loadingFiles, preloadProgressTracker].forEach(function(tracker) {
|
|
Object.keys(tracker).forEach(function(file) {
|
|
if (!tracker[file].final)
|
|
progressIsFinal = false;
|
|
if (!totalIsValid || tracker[file].total === 0) {
|
|
totalIsValid = false;
|
|
total = 0;
|
|
} else {
|
|
total += tracker[file].total;
|
|
}
|
|
loaded += tracker[file].loaded;
|
|
});
|
|
});
|
|
if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
|
|
lastProgress.loaded = loaded;
|
|
lastProgress.total = total;
|
|
if (typeof progressFunc === 'function')
|
|
progressFunc(loaded, total);
|
|
}
|
|
if (!progressIsFinal)
|
|
requestAnimationFrame(animateProgress);
|
|
}
|
|
|
|
this.setCanvas = function(elem) {
|
|
canvas = elem;
|
|
};
|
|
|
|
this.setExecutableName = function(newName) {
|
|
|
|
executableName = newName;
|
|
};
|
|
|
|
this.setLocale = function(newLocale) {
|
|
|
|
locale = newLocale;
|
|
};
|
|
|
|
this.setUnloadAfterInit = function(enabled) {
|
|
|
|
if (enabled && !unloadAfterInit && initPromise) {
|
|
initPromise.then(Engine.unloadEngine);
|
|
}
|
|
unloadAfterInit = enabled;
|
|
};
|
|
|
|
this.setStdoutFunc = function(func) {
|
|
|
|
var print = function(text) {
|
|
if (arguments.length > 1) {
|
|
text = Array.prototype.slice.call(arguments).join(" ");
|
|
}
|
|
func(text);
|
|
};
|
|
if (this.rtenv)
|
|
this.rtenv.print = print;
|
|
stdout = print;
|
|
};
|
|
|
|
this.setStderrFunc = function(func) {
|
|
|
|
var printErr = function(text) {
|
|
if (arguments.length > 1)
|
|
text = Array.prototype.slice.call(arguments).join(" ");
|
|
func(text);
|
|
};
|
|
if (this.rtenv)
|
|
this.rtenv.printErr = printErr;
|
|
stderr = printErr;
|
|
};
|
|
|
|
|
|
}; // Engine()
|
|
|
|
Engine.RuntimeEnvironment = engine.RuntimeEnvironment;
|
|
|
|
Engine.load = function(newBasePath) {
|
|
|
|
if (newBasePath !== undefined) basePath = getBasePath(newBasePath);
|
|
if (engineLoadPromise === null) {
|
|
if (typeof WebAssembly !== 'object')
|
|
return Promise.reject(new Error("Browser doesn't support WebAssembly"));
|
|
// TODO cache/retrieve module to/from idb
|
|
engineLoadPromise = loadPromise(basePath + '.wasm').then(function(xhr) {
|
|
return xhr.response;
|
|
});
|
|
engineLoadPromise = engineLoadPromise.catch(function(err) {
|
|
engineLoadPromise = null;
|
|
throw err;
|
|
});
|
|
}
|
|
return engineLoadPromise;
|
|
};
|
|
|
|
Engine.unload = function() {
|
|
engineLoadPromise = null;
|
|
};
|
|
|
|
function loadPromise(file, tracker) {
|
|
if (tracker === undefined)
|
|
tracker = loadingFiles;
|
|
return new Promise(function(resolve, reject) {
|
|
loadXHR(resolve, reject, file, tracker);
|
|
});
|
|
}
|
|
|
|
function loadXHR(resolve, reject, file, tracker) {
|
|
|
|
var xhr = new XMLHttpRequest;
|
|
xhr.open('GET', file);
|
|
if (!file.endsWith('.js')) {
|
|
xhr.responseType = 'arraybuffer';
|
|
}
|
|
['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) {
|
|
xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
|
|
});
|
|
xhr.send();
|
|
}
|
|
|
|
function onXHREvent(resolve, reject, file, tracker, ev) {
|
|
|
|
if (this.status >= 400) {
|
|
|
|
if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
|
reject(new Error("Failed loading file '" + file + "': " + this.statusText));
|
|
this.abort();
|
|
return;
|
|
} else {
|
|
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
|
}
|
|
}
|
|
|
|
switch (ev.type) {
|
|
case 'loadstart':
|
|
if (tracker[file] === undefined) {
|
|
tracker[file] = {
|
|
total: ev.total,
|
|
loaded: ev.loaded,
|
|
attempts: 0,
|
|
final: false,
|
|
};
|
|
}
|
|
break;
|
|
|
|
case 'progress':
|
|
tracker[file].loaded = ev.loaded;
|
|
tracker[file].total = ev.total;
|
|
break;
|
|
|
|
case 'load':
|
|
tracker[file].final = true;
|
|
resolve(this);
|
|
break;
|
|
|
|
case 'error':
|
|
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
|
tracker[file].final = true;
|
|
reject(new Error("Failed loading file '" + file + "'"));
|
|
} else {
|
|
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
|
}
|
|
break;
|
|
|
|
case 'abort':
|
|
tracker[file].final = true;
|
|
reject(new Error("Loading file '" + file + "' was aborted."));
|
|
break;
|
|
}
|
|
}
|
|
})();
|