Files
diabloweb/src/api/game.worker.js
2019-08-02 03:11:35 +03:00

275 lines
7.5 KiB
JavaScript

import DiabloBinary from './Diablo.wasm';
import DiabloModule from './Diablo.jscc';
import SpawnBinary from './DiabloSpawn.wasm';
import SpawnModule from './DiabloSpawn.jscc';
import axios from 'axios';
const DiabloSize = 1288845;
const SpawnSize = 1160682;
/* eslint-disable-next-line no-restricted-globals */
const worker = self;
let canvas = null, context = null;
let imageData = null;
let files = null;
let renderBatch = null;
let drawBelt = null;
const DApi = {
exit_error(error) {
worker.postMessage({action: "error", error});
},
get_file_size(path) {
const data = files.get(path.toLowerCase());
return data ? data.byteLength : 0;
},
get_file_contents(path, array, offset) {
const data = files.get(path.toLowerCase());
if (data) {
array.set(data.subarray(offset, offset + array.length));
}
},
put_file_contents(path, array) {
path = path.toLowerCase();
// if (!path.match(/^(spawn\d+\.sv|single_\d+\.sv|config\.ini)$/i)) {
// alert(`Bad file name: ${path}`);
// }
files.set(path, array);
worker.postMessage({action: "fs", func: "update", params: [path, array]});
},
remove_file(path) {
path = path.toLowerCase();
files.delete(path);
worker.postMessage({action: "fs", func: "delete", params: [path]});
},
set_cursor(x, y) {
worker.postMessage({action: "cursor", x, y});
},
open_keyboard() {
worker.postMessage({action: "keyboard", open: true});
},
close_keyboard() {
worker.postMessage({action: "keyboard", open: false});
},
};
let frameTime = 0, lastTime = 0;
function getFPS() {
const time = performance.now();
if (!lastTime) {
lastTime = time;
}
frameTime = 0.9 * frameTime + 0.1 * (time - lastTime);
lastTime = time;
return frameTime ? 1000.0 / frameTime : 0.0;
}
const DApi_renderLegacy = {
draw_begin() {
renderBatch = {
images: [],
text: [],
clip: null,
belt: drawBelt,
};
drawBelt = null;
},
draw_blit(x, y, w, h, data) {
renderBatch.images.push({x, y, w, h, data: data.slice()});
},
draw_clip_text(x0, y0, x1, y1) {
renderBatch.clip = {x0, y0, x1, y1};
},
draw_text(x, y, text, color) {
renderBatch.text.push({x, y, text, color});
},
draw_end() {
//DApi.draw_text(10, 10, `FPS: ${getFPS().toFixed(1)} (Transfer)`, 0xFFCC00);
const transfer = renderBatch.images.map(({data}) => data.buffer);
if (renderBatch.belt) {
transfer.push(renderBatch.belt.buffer);
}
worker.postMessage({action: "render", batch: renderBatch}, transfer);
renderBatch = null;
},
draw_belt(items) {
drawBelt = items.slice();
},
};
const DApi_renderOffscreen = {
draw_begin() {
context.save();
context.font = 'bold 13px Times New Roman';
},
draw_blit(x, y, w, h, data) {
imageData.data.set(data);
context.putImageData(imageData, x, y);
},
draw_clip_text(x0, y0, x1, y1) {
context.beginPath();
context.rect(x0, y0, x1 - x0, y1 - y0);
context.clip();
},
draw_text(x, y, text, color) {
const r = ((color >> 16) & 0xFF);
const g = ((color >> 8) & 0xFF);
const b = (color & 0xFF);
context.fillStyle = `rgb(${r}, ${g}, ${b})`;
context.fillText(text, x, y + 22);
},
draw_end() {
//DApi.draw_text(10, 10, `FPS: ${getFPS().toFixed(1)} (Offscreen)`, 0xFFCC00);
context.restore();
const bitmap = canvas.transferToImageBitmap();
const transfer = [bitmap];
if (drawBelt) {
transfer.push(drawBelt.buffer);
}
worker.postMessage({action: "render", batch: {bitmap, belt: drawBelt}}, transfer);
drawBelt = null;
},
draw_belt(items) {
drawBelt = items.slice();
},
};
let audioBatch = null, audioTransfer = null;
let maxSoundId = 0, maxBatchId = 0;
["create_sound", "duplicate_sound"].forEach(func => {
DApi[func] = function(...params) {
if (audioBatch) {
maxBatchId = params[0] + 1;
audioBatch.push({func, params});
if (func === "create_sound") {
audioTransfer.push(params[1].buffer);
}
} else {
maxSoundId = params[0] + 1;
const transfer = [];
if (func === "create_sound") {
transfer.push(params[1].buffer);
}
worker.postMessage({action: "audio", func, params}, transfer);
}
};
});
["play_sound", "set_volume", "stop_sound", "delete_sound"].forEach(func => {
DApi[func] = function(...params) {
if (audioBatch && params[0] >= maxSoundId) {
audioBatch.push({func, params});
} else {
worker.postMessage({action: "audio", func, params});
}
}
});
worker.DApi = DApi;
let wasm = null;
function call_api(func, ...params) {
try {
audioBatch = [];
audioTransfer = [];
wasm["_" + func](...params);
if (audioBatch.length) {
maxSoundId = maxBatchId;
worker.postMessage({action: "audioBatch", batch: audioBatch}, audioTransfer);
audioBatch = null;
audioTransfer = null;
}
} catch (e) {
worker.postMessage({action: "error", error: e.message, stack: e.stack});
}
}
function progress(text, loaded, total) {
worker.postMessage({action: "progress", text, loaded, total});
}
const readFile = (file, progress) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (progress) {
progress({loaded: file.size});
}
resolve(reader.result);
};
reader.onerror = () => reject(reader.error);
reader.onabort = () => reject();
if (progress) {
reader.addEventListener("progress", progress);
}
reader.readAsArrayBuffer(file);
});
async function initWasm(spawn, progress) {
const binary = await axios.request({
url: spawn ? SpawnBinary : DiabloBinary,
responseType: 'arraybuffer',
onDownloadProgress: progress,
});
const result = await (spawn ? SpawnModule : DiabloModule)({wasmBinary: binary.data}).ready;
progress({loaded: 2000000});
return result;
}
async function init_game(mpq, spawn, offscreen) {
if (offscreen) {
canvas = new OffscreenCanvas(640, 480);
context = canvas.getContext("2d");
imageData = context.createImageData(640, 480);
Object.assign(DApi, DApi_renderOffscreen);
} else {
Object.assign(DApi, DApi_renderLegacy);
}
progress("Loading...");
let mpqLoaded = 0, mpqTotal = (mpq ? mpq.size : 0), wasmLoaded = 0, wasmTotal = (spawn ? SpawnSize : DiabloSize);
const wasmWeight = 5;
function updateProgress() {
progress("Loading...", mpqLoaded + wasmLoaded * wasmWeight, mpqTotal + wasmTotal * wasmWeight);
}
const loadWasm = initWasm(spawn, e => {
wasmLoaded = Math.min(e.loaded, wasmTotal);
updateProgress();
});
let loadMpq = mpq ? readFile(mpq, e => {
mpqLoaded = e.loaded;
updateProgress();
}) : Promise.resolve(null);
[wasm, mpq] = await Promise.all([loadWasm, loadMpq]);
if (mpq) {
files.set(spawn ? 'spawn.mpq' : 'diabdat.mpq', new Uint8Array(mpq));
}
progress("Initializing...");
const vers = process.env.VERSION.match(/(\d+)\.(\d+)\.(\d+)/);
wasm._DApi_Init(Math.floor(performance.now()), offscreen ? 1 : 0, parseInt(vers[1]), parseInt(vers[2]), parseInt(vers[3]));
setInterval(() => {
call_api("DApi_Render", Math.floor(performance.now()));
}, 50);
}
worker.addEventListener("message", ({data}) => {
switch (data.action) {
case "init":
files = data.files;
init_game(data.mpq, data.spawn, data.offscreen).then(
() => worker.postMessage({action: "loaded"}),
e => worker.postMessage({action: "failed", error: e.message || e.name, stack: e.stack}));
break;
case "event":
call_api(data.func, ...data.params);
break;
}
});