758 lines
24 KiB
JavaScript
758 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
// These are relative paths
|
|
const RELEASE_DIR = '%__RELEASE_UUID__%'; // set by build_www.sh
|
|
const DEFAULT_PACKS_DIR = RELEASE_DIR + '/packs';
|
|
|
|
const rtCSS = `
|
|
body {
|
|
font-family: arial;
|
|
margin: 0;
|
|
padding: none;
|
|
background-color: black;
|
|
}
|
|
|
|
.emscripten {
|
|
color: #aaaaaa;
|
|
padding-right: 0;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
display: block;
|
|
}
|
|
|
|
div.emscripten {
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
|
canvas.emscripten {
|
|
border: 0px none;
|
|
background-color: black;
|
|
}
|
|
|
|
#controls {
|
|
display: inline-block;
|
|
vertical-align: top;
|
|
height: 25px;
|
|
}
|
|
|
|
.console {
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
margin-top: 0px;
|
|
border-left: 0px;
|
|
border-right: 0px;
|
|
padding-left: 0px;
|
|
padding-right: 0px;
|
|
display: block;
|
|
background-color: black;
|
|
color: white;
|
|
font-family: 'Lucida Console', Monaco, monospace;
|
|
outline: none;
|
|
}
|
|
`;
|
|
|
|
const rtHTML = `
|
|
<div id="header">
|
|
|
|
<div class="emscripten">
|
|
<span id="controls">
|
|
<span>
|
|
<select id="resolution" onchange="fixGeometry()">
|
|
<option value="high">High Res</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="low">Low Res</option>
|
|
</select>
|
|
</span>
|
|
<span>
|
|
<select id="aspectRatio" onchange="fixGeometry()">
|
|
<option value="any">Fit Screen</option>
|
|
<option value="4:3">4:3</option>
|
|
<option value="16:9">16:9</option>
|
|
<option value="5:4">5:4</option>
|
|
<option value="21:9">21:9</option>
|
|
<option value="32:9">32:9</option>
|
|
<option value="1:1">1:1</option>
|
|
</select>
|
|
</span>
|
|
<span><input id="console_button" type="button" value="Show Console" onclick="consoleToggle()"></span>
|
|
<span>(full screen: try F11 or Command+Shift+F)</span>
|
|
</span>
|
|
<div id="progressbar_div" style="display: none">
|
|
<progress id="progressbar" value="0" max="100">0%</progress>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="emscripten" id="canvas_container">
|
|
</div>
|
|
|
|
<div id="footer">
|
|
<textarea id="console_output" class="console" rows="8" style="display: none; height: 200px"></textarea>
|
|
</div>
|
|
`;
|
|
|
|
// The canvas needs to be created before the wasm module is loaded.
|
|
// It is not attached to the document until activateBody()
|
|
const mtCanvas = document.createElement('canvas');
|
|
mtCanvas.className = "emscripten";
|
|
mtCanvas.id = "canvas";
|
|
mtCanvas.oncontextmenu = (event) => {
|
|
event.preventDefault();
|
|
};
|
|
mtCanvas.tabIndex = "-1";
|
|
mtCanvas.width = 1024;
|
|
mtCanvas.height = 600;
|
|
|
|
var consoleButton;
|
|
var consoleOutput;
|
|
var progressBar;
|
|
var progressBarDiv;
|
|
|
|
function activateBody() {
|
|
const extraCSS = document.createElement("style");
|
|
extraCSS.innerText = rtCSS;
|
|
document.head.appendChild(extraCSS);
|
|
|
|
// Replace the entire body
|
|
document.body.style = '';
|
|
document.body.className = '';
|
|
document.body.innerHTML = '';
|
|
|
|
const mtContainer = document.createElement('div');
|
|
mtContainer.innerHTML = rtHTML;
|
|
document.body.appendChild(mtContainer);
|
|
|
|
const canvasContainer = document.getElementById('canvas_container');
|
|
canvasContainer.appendChild(mtCanvas);
|
|
|
|
setupResizeHandlers();
|
|
|
|
consoleButton = document.getElementById('console_button');
|
|
consoleOutput = document.getElementById('console_output');
|
|
// Triggers the first and all future updates
|
|
consoleUpdate();
|
|
|
|
progressBar = document.getElementById('progressbar');
|
|
progressBarDiv = document.getElementById('progressbar_div');
|
|
updateProgressBar(0, 0);
|
|
}
|
|
|
|
var PB_bytes_downloaded = 0;
|
|
var PB_bytes_needed = 0;
|
|
function updateProgressBar(doneBytes, neededBytes) {
|
|
PB_bytes_downloaded += doneBytes;
|
|
PB_bytes_needed += neededBytes;
|
|
if (progressBar) {
|
|
progressBarDiv.style.display = (PB_bytes_downloaded == PB_bytes_needed) ? "none" : "block";
|
|
const pct = PB_bytes_needed ? Math.round(100 * PB_bytes_downloaded / PB_bytes_needed) : 0;
|
|
progressBar.value = `${pct}`;
|
|
progressBar.innerText = `${pct}%`;
|
|
}
|
|
}
|
|
|
|
// Singleton
|
|
var mtLauncher = null;
|
|
|
|
class LaunchScheduler {
|
|
constructor() {
|
|
this.conditions = new Map();
|
|
window.requestAnimationFrame(this.invokeCallbacks.bind(this));
|
|
}
|
|
|
|
isSet(name) {
|
|
return this.conditions.get(name)[0];
|
|
}
|
|
|
|
addCondition(name, startCallback = null, deps = []) {
|
|
this.conditions.set(name, [false, new Set(), startCallback]);
|
|
for (const depname of deps) {
|
|
this.addDep(name, depname);
|
|
}
|
|
}
|
|
|
|
addDep(name, depname) {
|
|
if (!this.isSet(depname)) {
|
|
this.conditions.get(name)[1].add(depname);
|
|
}
|
|
}
|
|
|
|
setCondition(name) {
|
|
if (this.isSet(name)) {
|
|
throw new Error('Scheduler condition set twice');
|
|
}
|
|
this.conditions.get(name)[0] = true;
|
|
this.conditions.forEach(v => {
|
|
v[1].delete(name);
|
|
});
|
|
window.requestAnimationFrame(this.invokeCallbacks.bind(this));
|
|
}
|
|
|
|
clearCondition(name, newCallback = null, deps = []) {
|
|
if (!this.isSet(name)) {
|
|
throw new Error('clearCondition called on unset condition');
|
|
}
|
|
const arr = this.conditions.get(name);
|
|
arr[0] = false;
|
|
arr[1] = new Set(deps);
|
|
arr[2] = newCallback;
|
|
}
|
|
|
|
invokeCallbacks() {
|
|
const callbacks = [];
|
|
this.conditions.forEach(v => {
|
|
if (!v[0] && v[1].size == 0 && v[2] !== null) {
|
|
callbacks.push(v[2]);
|
|
v[2] = null;
|
|
}
|
|
});
|
|
callbacks.forEach(cb => cb());
|
|
}
|
|
}
|
|
const mtScheduler = new LaunchScheduler();
|
|
|
|
function loadWasm() {
|
|
// Start loading the wasm module
|
|
// The module will call emloop_ready when it is loaded
|
|
// and waiting for main() arguments.
|
|
const mtModuleScript = document.createElement("script");
|
|
mtModuleScript.type = "text/javascript";
|
|
mtModuleScript.src = RELEASE_DIR + "/minetest.js";
|
|
mtModuleScript.async = true;
|
|
document.head.appendChild(mtModuleScript);
|
|
}
|
|
|
|
function callMain() {
|
|
const fullargs = [ './minetest', ...mtLauncher.args.toArray() ];
|
|
const [argc, argv] = makeArgv(fullargs);
|
|
emloop_invoke_main(argc, argv);
|
|
// Pausing and unpausing here gives the browser time to redraw the DOM
|
|
// before Minetest freezes the main thread generating the world. If this
|
|
// is not done, the page will stay frozen for several seconds
|
|
emloop_request_animation_frame();
|
|
mtScheduler.setCondition("main_called");
|
|
}
|
|
|
|
var emloop_pause;
|
|
var emloop_unpause;
|
|
var emloop_init_sound;
|
|
var emloop_invoke_main;
|
|
var emloop_install_pack;
|
|
var emloop_set_minetest_conf;
|
|
var irrlicht_want_pointerlock;
|
|
var irrlicht_force_pointerlock;
|
|
var irrlicht_resize;
|
|
var emsocket_init;
|
|
var emsocket_set_proxy;
|
|
var emsocket_set_vpn;
|
|
|
|
// Called when the wasm module is ready
|
|
function emloop_ready() {
|
|
emloop_pause = cwrap("emloop_pause", null, []);
|
|
emloop_unpause = cwrap("emloop_unpause", null, []);
|
|
emloop_init_sound = cwrap("emloop_init_sound", null, []);
|
|
emloop_invoke_main = cwrap("emloop_invoke_main", null, ["number", "number"]);
|
|
emloop_install_pack = cwrap("emloop_install_pack", null, ["number", "number", "number"]);
|
|
emloop_set_minetest_conf = cwrap("emloop_set_minetest_conf", null, ["number"]);
|
|
irrlicht_want_pointerlock = cwrap("irrlicht_want_pointerlock", "number");
|
|
irrlicht_force_pointerlock = cwrap("irrlicht_force_pointerlock", null);
|
|
irrlicht_resize = cwrap("irrlicht_resize", null, ["number", "number"]);
|
|
emsocket_init = cwrap("emsocket_init", null, []);
|
|
emsocket_set_proxy = cwrap("emsocket_set_proxy", null, ["number"]);
|
|
emsocket_set_vpn = cwrap("emsocket_set_vpn", null, ["number"]);
|
|
mtScheduler.setCondition("wasmReady");
|
|
}
|
|
|
|
// Called when the wasm module wants to force redraw before next frame
|
|
function emloop_request_animation_frame() {
|
|
emloop_pause();
|
|
window.requestAnimationFrame(() => { emloop_unpause(); });
|
|
}
|
|
|
|
function makeArgv(args) {
|
|
// Assuming 4-byte pointers
|
|
const argv = _malloc((args.length + 1) * 4);
|
|
let i;
|
|
for (i = 0; i < args.length; i++) {
|
|
HEAPU32[(argv >>> 2) + i] = allocateUTF8(args[i]);
|
|
}
|
|
HEAPU32[(argv >>> 2) + i] = 0; // argv[argc] == NULL
|
|
return [i, argv];
|
|
}
|
|
|
|
var consoleText = [];
|
|
var consoleLengthMax = 1000;
|
|
var consoleTextLast = 0;
|
|
var consoleDirty = false;
|
|
function consoleUpdate() {
|
|
if (consoleDirty) {
|
|
if (consoleText.length > consoleLengthMax) {
|
|
consoleText = consoleText.slice(-consoleLengthMax);
|
|
}
|
|
consoleOutput.value = consoleText.join('');
|
|
consoleOutput.scrollTop = consoleOutput.scrollHeight; // focus on bottom
|
|
consoleDirty = false;
|
|
}
|
|
window.requestAnimationFrame(consoleUpdate);
|
|
}
|
|
|
|
function consoleToggle() {
|
|
consoleOutput.style.display = (consoleOutput.style.display == 'block') ? 'none' : 'block';
|
|
consoleButton.value = (consoleOutput.style.display == 'none') ? 'Show Console' : 'Hide Console';
|
|
fixGeometry();
|
|
}
|
|
|
|
var enableTracing = false;
|
|
function consolePrint(text) {
|
|
if (enableTracing) {
|
|
console.trace(text);
|
|
}
|
|
consoleText.push(text + "\n");
|
|
consoleDirty = true;
|
|
if (mtLauncher && mtLauncher.onprint) {
|
|
mtLauncher.onprint(text);
|
|
}
|
|
}
|
|
|
|
var Module = {
|
|
preRun: [],
|
|
postRun: [],
|
|
print: consolePrint,
|
|
canvas: (function() {
|
|
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
|
|
// application robust, you may want to override this behavior before shipping!
|
|
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
|
|
mtCanvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
|
|
|
|
return mtCanvas;
|
|
})(),
|
|
setStatus: function(text) {
|
|
if (text) Module.print('[wasm module status] ' + text);
|
|
},
|
|
totalDependencies: 0,
|
|
monitorRunDependencies: function(left) {
|
|
this.totalDependencies = Math.max(this.totalDependencies, left);
|
|
if (!mtLauncher || !mtLauncher.onprogress) return;
|
|
mtLauncher.onprogress('wasm_module', (this.totalDependencies-left) / this.totalDependencies);
|
|
}
|
|
};
|
|
|
|
Module['printErr'] = Module['print'];
|
|
|
|
// This is injected into workers so that out/err are sent to the main thread.
|
|
// This probably should be the default behavior, but doesn't seem to be for WasmFS.
|
|
const workerInject = `
|
|
Module['print'] = (text) => {
|
|
postMessage({cmd: 'print', text: text, threadId: Module['_pthread_self']()});
|
|
};
|
|
Module['printErr'] = (text) => {
|
|
postMessage({cmd: 'printErr', text: text, threadId: Module['_pthread_self']()});
|
|
};
|
|
importScripts('minetest.js');
|
|
`;
|
|
Module['mainScriptUrlOrBlob'] = new Blob([workerInject], { type: "text/javascript" });
|
|
|
|
|
|
|
|
Module['onFullScreen'] = () => { fixGeometry(); };
|
|
window.onerror = function(event) {
|
|
consolePrint('Exception thrown, see JavaScript console');
|
|
};
|
|
|
|
function resizeCanvas(width, height) {
|
|
const canvas = mtCanvas;
|
|
if (canvas.width != width || canvas.height != height) {
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
canvas.widthNative = width;
|
|
canvas.heightNative = height;
|
|
}
|
|
// Trigger SDL window resize.
|
|
// This should happen automatically, not sure why it doesn't.
|
|
irrlicht_resize(width, height);
|
|
}
|
|
|
|
function now() {
|
|
return (new Date()).getTime();
|
|
}
|
|
|
|
// Only allow fixGeometry to be called every 250ms
|
|
// Firefox calls this way too often, causing flicker.
|
|
var fixGeometryPause = 0;
|
|
function fixGeometry(override) {
|
|
if (!override && now() < fixGeometryPause) {
|
|
return;
|
|
}
|
|
const resolutionSelect = document.getElementById('resolution');
|
|
const aspectRatioSelect = document.getElementById('aspectRatio');
|
|
var canvas = mtCanvas;
|
|
var resolution = resolutionSelect.value;
|
|
var aspectRatio = aspectRatioSelect.value;
|
|
var screenX;
|
|
var screenY;
|
|
|
|
// Prevent the controls from getting focus
|
|
canvas.focus();
|
|
|
|
var isFullScreen = document.fullscreenElement ? true : false;
|
|
if (isFullScreen) {
|
|
screenX = screen.width;
|
|
screenY = screen.height;
|
|
} else {
|
|
// F11-style full screen
|
|
var controls = document.getElementById('controls');
|
|
var maximized = !window.screenTop && !window.screenY;
|
|
controls.style = maximized ? 'display: none' : '';
|
|
|
|
var headerHeight = document.getElementById('header').offsetHeight;
|
|
var footerHeight = document.getElementById('footer').offsetHeight;
|
|
screenX = document.documentElement.clientWidth - 6;
|
|
screenY = document.documentElement.clientHeight - headerHeight - footerHeight - 6;
|
|
}
|
|
|
|
// Size of the viewport (after scaling)
|
|
var realX;
|
|
var realY;
|
|
if (aspectRatio == 'any') {
|
|
realX = screenX;
|
|
realY = screenY;
|
|
} else {
|
|
var ar = aspectRatio.split(':');
|
|
var innerRatio = parseInt(ar[0]) / parseInt(ar[1]);
|
|
var outerRatio = screenX / screenY;
|
|
if (innerRatio <= outerRatio) {
|
|
realX = Math.floor(innerRatio * screenY);
|
|
realY = screenY;
|
|
} else {
|
|
realX = screenX;
|
|
realY = Math.floor(screenX / innerRatio);
|
|
}
|
|
}
|
|
|
|
// Native canvas resolution
|
|
var resX;
|
|
var resY;
|
|
var scale = false;
|
|
if (resolution == 'high') {
|
|
resX = realX;
|
|
resY = realY;
|
|
} else if (resolution == 'medium') {
|
|
resX = Math.floor(realX / 1.5);
|
|
resY = Math.floor(realY / 1.5);
|
|
scale = true;
|
|
} else {
|
|
resX = Math.floor(realX / 2.0);
|
|
resY = Math.floor(realY / 2.0);
|
|
scale = true;
|
|
}
|
|
resizeCanvas(resX, resY);
|
|
|
|
if (scale) {
|
|
var styleWidth = realX + "px";
|
|
var styleHeight = realY + "px";
|
|
canvas.style.setProperty("width", styleWidth, "important");
|
|
canvas.style.setProperty("height", styleHeight, "important");
|
|
} else {
|
|
canvas.style.removeProperty("width");
|
|
canvas.style.removeProperty("height");
|
|
}
|
|
}
|
|
|
|
function setupResizeHandlers() {
|
|
window.addEventListener('resize', () => { fixGeometry(); });
|
|
|
|
// Needed to prevent special keys from triggering browser actions, like
|
|
// F5 causing page reload.
|
|
document.addEventListener('keydown', (e) => {
|
|
// Allow F11 to go full screen
|
|
if (e.code == "F11") {
|
|
// On Firefox, F11 is animated. The window smoothly grows to
|
|
// full screen over several seconds. During this transition, the 'resize'
|
|
// event is triggered hundreds of times. To prevent flickering, have
|
|
// fixGeometry ignore repeated calls, and instead resize every 500ms
|
|
// for 2.5 seconds. By then it should be finished.
|
|
fixGeometryPause = now() + 2000;
|
|
for (var delay = 100; delay <= 2600; delay += 500) {
|
|
setTimeout(() => { fixGeometry(true); }, delay);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class MinetestArgs {
|
|
constructor() {
|
|
this.go = false;
|
|
this.server = false;
|
|
this.name = '';
|
|
this.password = '';
|
|
this.gameid = '';
|
|
this.address = '';
|
|
this.port = '';
|
|
this.packs = [];
|
|
this.extra = [];
|
|
}
|
|
|
|
toArray() {
|
|
const args = [];
|
|
if (this.go) args.push('--go');
|
|
if (this.server) args.push('--server');
|
|
if (this.name) args.push('--name', this.name);
|
|
if (this.password) args.push('--password', this.password);
|
|
if (this.gameid) args.push('--gameid', this.gameid);
|
|
if (this.address) args.push('--address', this.address);
|
|
if (this.port) args.push('--port', this.port.toString());
|
|
args.push(...this.extra);
|
|
return args;
|
|
}
|
|
|
|
toQueryString() {
|
|
const params = new URLSearchParams();
|
|
if (this.go) params.append('go', '');
|
|
if (this.server) params.append('server', '');
|
|
if (this.name) params.append('name', this.name);
|
|
if (this.password) params.append('password', this.password);
|
|
if (this.gameid) params.append('gameid', this.gameid);
|
|
if (this.address) params.append('address', this.address);
|
|
if (this.port) params.append('port', this.port.toString());
|
|
const extra_packs = [];
|
|
this.packs.forEach(v => {
|
|
if (v != 'base' && v != 'minetest_game' && v != 'devtest' && v != this.gameid) {
|
|
extra_packs.push(v);
|
|
}
|
|
});
|
|
if (extra_packs.length) {
|
|
params.append('packs', extra_packs.join(','));
|
|
}
|
|
if (this.extra.length) {
|
|
params.append('extra', this.extra.join(','));
|
|
}
|
|
return params.toString();
|
|
}
|
|
|
|
static fromQueryString(qs) {
|
|
const r = new MinetestArgs();
|
|
const params = new URLSearchParams(qs);
|
|
if (params.has('go')) r.go = true;
|
|
if (params.has('server')) r.server = true;
|
|
if (params.has('name')) r.name = params.get('name');
|
|
if (params.has('password')) r.password = params.get('password');
|
|
if (params.has('gameid')) r.gameid = params.get('gameid');
|
|
if (params.has('address')) r.address = params.get('address');
|
|
if (params.has('port')) r.port = parseInt(params.get('port'));
|
|
if (r.gameid && r.gameid != 'minetest_game' && r.gameid != 'devtest' && r.gameid != 'base') {
|
|
r.packs.push(r.gameid);
|
|
}
|
|
if (params.has('packs')) {
|
|
params.get('packs').split(',').forEach(p => {
|
|
if (!r.packs.includes(p)) {
|
|
r.packs.push(p);
|
|
}
|
|
});
|
|
}
|
|
if (params.has('extra')) {
|
|
r.extra = params.get('extra').split(',');
|
|
}
|
|
return r;
|
|
}
|
|
}
|
|
|
|
class MinetestLauncher {
|
|
constructor() {
|
|
if (mtLauncher !== null) {
|
|
throw new Error("There can be only one launcher");
|
|
}
|
|
mtLauncher = this;
|
|
this.args = null;
|
|
this.onprogress = null; // function(name, percent done)
|
|
this.onready = null; // function()
|
|
this.onerror = null; // function(message)
|
|
this.onprint = null; // function(text)
|
|
this.addedPacks = new Set();
|
|
this.vpn = null;
|
|
this.serverCode = null;
|
|
this.clientCode = null;
|
|
this.proxyUrl = "wss://minetest.dustlabs.io/proxy";
|
|
this.packsDir = DEFAULT_PACKS_DIR;
|
|
this.packsDirIsCors = false;
|
|
this.minetestConf = null;
|
|
|
|
mtScheduler.addCondition("wasmReady", loadWasm);
|
|
mtScheduler.addCondition("launch_called");
|
|
mtScheduler.addCondition("ready", this.#notifyReady.bind(this), ['wasmReady']);
|
|
mtScheduler.addCondition("main_called", callMain, ['ready', 'launch_called']);
|
|
this.addPack('base');
|
|
}
|
|
|
|
setProxy(url) {
|
|
this.proxyUrl = url;
|
|
}
|
|
|
|
/*
|
|
* Set the url for the pack files directory
|
|
* This can be relative or absolute.
|
|
*/
|
|
setPacksDir(url, is_cors) {
|
|
this.packsDir = url;
|
|
this.packsDirIsCors = is_cors;
|
|
}
|
|
|
|
#notifyReady() {
|
|
mtScheduler.setCondition("ready");
|
|
if (this.onready) this.onready();
|
|
}
|
|
|
|
isReady() {
|
|
return mtScheduler.isSet("ready");
|
|
}
|
|
|
|
// Must be set before launch()
|
|
setVPN(serverCode, clientCode) {
|
|
this.serverCode = serverCode;
|
|
this.clientCode = clientCode;
|
|
this.vpn = serverCode ? serverCode : clientCode;
|
|
}
|
|
|
|
setMinetestConf(contents) {
|
|
this.minetestConf = contents;
|
|
}
|
|
|
|
// Returns pack status:
|
|
// 0 - pack has not been added
|
|
// 1 - pack is downloading
|
|
// 2 - pack has been installed
|
|
checkPack(name) {
|
|
if (!this.addedPacks.has(name)) {
|
|
return 0;
|
|
}
|
|
if (mtScheduler.isSet("installed:" + name)) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
addPacks(packs) {
|
|
for (const pack of packs) {
|
|
this.addPack(pack);
|
|
}
|
|
}
|
|
|
|
async addPack(name) {
|
|
if (mtScheduler.isSet("launch_called")) {
|
|
throw new Error("Cannot add packs after launch");
|
|
}
|
|
if (name == 'minetest_game' || name == 'devtest' || this.addedPacks.has(name))
|
|
return;
|
|
this.addedPacks.add(name);
|
|
|
|
const fetchedCond = "fetched:" + name;
|
|
const installedCond = "installed:" + name;
|
|
|
|
let chunks = [];
|
|
let received = 0;
|
|
// This is done here instead of at the bottom, because it needs to
|
|
// be delayed until after the 'wasmReady' condition.
|
|
// TODO: Add the ability to `await` a condition instead.
|
|
const installPack = () => {
|
|
// Install
|
|
const data = _malloc(received);
|
|
let offset = 0;
|
|
for (const arr of chunks) {
|
|
HEAPU8.set(arr, data + offset);
|
|
offset += arr.byteLength;
|
|
}
|
|
emloop_install_pack(allocateUTF8(name), data, received);
|
|
_free(data);
|
|
mtScheduler.setCondition(installedCond);
|
|
if (this.onprogress) {
|
|
this.onprogress(`download:${name}`, 1.0);
|
|
this.onprogress(`install:${name}`, 1.0);
|
|
}
|
|
};
|
|
mtScheduler.addCondition(fetchedCond, null);
|
|
mtScheduler.addCondition(installedCond, installPack, ["wasmReady", fetchedCond]);
|
|
mtScheduler.addDep("main_called", installedCond);
|
|
|
|
const packUrl = this.packsDir + '/' + name + '.pack';
|
|
let resp;
|
|
try {
|
|
resp = await fetch(packUrl, this.packsDirIsCors ? { credentials: 'omit' } : {});
|
|
} catch (err) {
|
|
if (this.onerror) {
|
|
this.onerror(`${err}`);
|
|
} else {
|
|
alert(`Error while loading ${packUrl}. Please refresh page`);
|
|
}
|
|
throw new Error(`${err}`);
|
|
}
|
|
// This could be null if the header is missing
|
|
var contentLength = resp.headers.get('Content-Length');
|
|
if (contentLength) {
|
|
contentLength = parseInt(contentLength);
|
|
updateProgressBar(0, contentLength);
|
|
}
|
|
let reader = resp.body.getReader();
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
chunks.push(value);
|
|
received += value.byteLength;
|
|
if (contentLength) {
|
|
updateProgressBar(value.byteLength, 0);
|
|
if (this.onprogress) {
|
|
this.onprogress(`download:${name}`, received / contentLength);
|
|
}
|
|
}
|
|
}
|
|
mtScheduler.setCondition(fetchedCond);
|
|
}
|
|
|
|
// Launch minetest.exe <args>
|
|
//
|
|
// This must be called from a keyboard or mouse event handler,
|
|
// after the 'onready' event has fired. (For this reason, it cannot
|
|
// be called from the `onready` handler)
|
|
launch(args) {
|
|
if (!this.isReady()) {
|
|
throw new Error("launch called before onready");
|
|
}
|
|
if (!(args instanceof MinetestArgs)) {
|
|
throw new Error("launch called without MinetestArgs");
|
|
}
|
|
if (mtScheduler.isSet("launch_called")) {
|
|
throw new Error("launch called twice");
|
|
}
|
|
this.args = args;
|
|
if (this.args.gameid) {
|
|
this.addPack(this.args.gameid);
|
|
}
|
|
this.addPacks(this.args.packs);
|
|
activateBody();
|
|
fixGeometry();
|
|
if (this.minetestConf) {
|
|
const confBuf = allocateUTF8(this.minetestConf)
|
|
emloop_set_minetest_conf(confBuf);
|
|
_free(confBuf);
|
|
}
|
|
emloop_init_sound();
|
|
// Setup emsocket
|
|
// TODO: emsocket should export the helpers for this
|
|
emsocket_init();
|
|
const proxyBuf = allocateUTF8(this.proxyUrl);
|
|
emsocket_set_proxy(proxyBuf);
|
|
_free(proxyBuf);
|
|
if (this.vpn) {
|
|
const vpnBuf = allocateUTF8(this.vpn);
|
|
emsocket_set_vpn(vpnBuf);
|
|
_free(vpnBuf);
|
|
}
|
|
if (args.go) {
|
|
irrlicht_force_pointerlock();
|
|
}
|
|
mtScheduler.setCondition("launch_called");
|
|
}
|
|
}
|