Files
diabloweb/src/api/game.worker.js
2019-08-15 23:03:53 +03:00

429 lines
12 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';
import websocket_open from './websocket';
const DiabloSize = 1466809;
const SpawnSize = 1337416;
/* 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;
let is_spawn = false;
let websocket = null;
const ChunkSize = 1 << 20;
class RemoteFile {
constructor(url) {
const request = new XMLHttpRequest();
request.open('HEAD', url, false);
request.send();
if (request.status < 200 || request.status >= 300) {
worker.postMessage({action: "error", error: `Failed to load remote file`});
}
this.byteLength = parseInt(request.getResponseHeader('Content-Length'));
this.url = url;
this.buffer = new Uint8Array(this.byteLength);
this.chunks = new Uint8Array(((this.byteLength + ChunkSize - 1) >> 20) | 0);
}
subarray(start, end) {
let chunk0 = (start / ChunkSize) | 0;
let chunk1 = ((end + ChunkSize - 1) / ChunkSize) | 0;
let missing0 = chunk1, missing1 = chunk0;
for (let i = chunk0; i < chunk1; ++i) {
if (!this.chunks[i]) {
missing0 = Math.min(missing0, i);
missing1 = Math.max(missing1, i);
}
}
if (missing0 <= missing1) {
const request = new XMLHttpRequest();
request.open('GET', this.url, false);
request.setRequestHeader('Range', `bytes=${missing0 * ChunkSize}-${Math.min(missing1 * ChunkSize + ChunkSize - 1, this.byteLength - 1)}`);
request.responseType = 'arraybuffer';
request.send();
if (request.status < 200 || request.status >= 300) {
worker.postMessage({action: "error", error: `Failed to load remote file`});
} else {
const header = request.getResponseHeader('Content-Range');
let m, start = 0;
if (header && (m = header.match(/bytes (\d+)-(\d+)\/(\d+)/))) {
start = parseInt(m[1]);
}
this.buffer.set(new Uint8Array(request.response), start);
chunk0 = ((start + ChunkSize - 1) / ChunkSize) | 0;
chunk1 = ((start + request.response.byteLength + ChunkSize - 1) / ChunkSize) | 0;
for (let i = chunk0; i < chunk1; ++i) {
this.chunks[i] = 1;
}
}
}
return this.buffer.subarray(start, end);
}
}
const DApi = {
exit_error(error) {
worker.postMessage({action: "error", error});
},
exit_game() {
worker.postMessage({action: "exit"});
},
current_save_id(id) {
worker.postMessage({action: "current_save", name: id >= 0 ? (is_spawn ? `spawn${id}.sv` : `single_${id}.sv`) : null});
},
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.byteLength));
}
},
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(...args) {
worker.postMessage({action: "keyboard", rect: [...args]});
},
close_keyboard() {
worker.postMessage({action: "keyboard", rect: null});
},
use_websocket(flag) {
if (flag) {
if (!websocket || websocket.readyState !== 1) {
const sock = websocket = websocket_open('wss://diablo.rivsoft.net/websocket', data => {
if (websocket === sock) {
try_api(() => {
const ptr = wasm._DApi_AllocPacket(data.byteLength);
wasm.HEAPU8.set(new Uint8Array(data), ptr);
});
}
}, code => {
if (typeof code !== "number") {
worker.postMessage({action: "error", error: code.toString(), stack: code.stack});
code = 3;
}
call_api("SNet_WebsocketStatus", code);
});
} else {
call_api("SNet_WebsocketStatus", 0);
}
} else {
if (websocket) {
websocket.close();
}
websocket = null;
}
},
websocket_closed() {
return websocket ? websocket.readyState !== 1 : 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_raw", "create_sound", "duplicate_sound"].forEach(func => {
DApi[func] = function(...params) {
if (audioBatch) {
maxBatchId = params[0] + 1;
audioBatch.push({func, params});
if (func !== "duplicate_sound") {
audioTransfer.push(params[1].buffer);
}
} else {
maxSoundId = params[0] + 1;
const transfer = [];
if (func !== "duplicate_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});
}
}
});
let packetBatch = null;
DApi.websocket_send = function(data) {
if (websocket) {
websocket.send(data);
} else if (packetBatch) {
packetBatch.push(data.slice().buffer);
} else {
worker.postMessage({action: "packet", buffer: data});
}
};
worker.DApi = DApi;
let wasm = null;
function try_api(func) {
try {
func();
} catch (e) {
worker.postMessage({action: "error", error: e.toString(), stack: e.stack});
}
}
function call_api(func, ...params) {
try_api(() => {
audioBatch = [];
audioTransfer = [];
packetBatch = [];
if (func !== "text") {
wasm["_" + func](...params);
} else {
const ptr = wasm._DApi_SyncTextPtr();
const text = params[0];
const length = Math.min(text.length, 255);
const heap = wasm.HEAPU8;
for (let i = 0; i < length; ++i) {
heap[ptr + i] = text.charCodeAt(i);
}
heap[ptr + length] = 0;
wasm._DApi_SyncText(params[1]);
}
if (audioBatch.length) {
maxSoundId = maxBatchId;
worker.postMessage({action: "audioBatch", batch: audioBatch}, audioTransfer);
}
if (packetBatch.length) {
worker.postMessage({action: "packetBatch", batch: packetBatch}, packetBatch);
}
audioBatch = null;
audioTransfer = null;
packetBatch = null;
});
}
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) {
is_spawn = spawn;
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);
}
if (!mpq) {
const name = (spawn ? 'spawn.mpq' : 'diabdat.mpq');
if (!files.has(name)) {
// This should never happen, but we do support remote loading
files.set(name, new RemoteFile(`${process.env.PUBLIC_URL}/${name}`));
}
}
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._SNet_InitWebsocket();
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.toString(), stack: e.stack}));
break;
case "event":
call_api(data.func, ...data.params);
break;
case "packet":
try_api(() => {
const ptr = wasm._DApi_AllocPacket(data.buffer.byteLength);
wasm.HEAPU8.set(new Uint8Array(data.buffer), ptr);
});
break;
case "packetBatch":
try_api(() => {
for (let packet of data.batch) {
const ptr = wasm._DApi_AllocPacket(packet.byteLength);
wasm.HEAPU8.set(new Uint8Array(packet), ptr);
}
});
break;
default:
}
});