mirror of
https://github.com/d07RiV/diabloweb.git
synced 2026-06-03 21:41:38 +00:00
275 lines
7.5 KiB
JavaScript
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;
|
|
}
|
|
});
|