import { dbg_assert } from "./log.js"; // pad string with spaces on the right export function pads(str, len) { str = (str || str === 0) ? str + "" : ""; return str.padEnd(len, " "); } // pad string with zeros on the left export function pad0(str, len) { str = (str || str === 0) ? str + "" : ""; return str.padStart(len, "0"); } export var view = function(constructor, memory, offset, length) { dbg_assert(offset >= 0); return new Proxy({}, { get: function(target, property, receiver) { const b = new constructor(memory.buffer, offset, length); const x = b[property]; if(typeof x === "function") { return x.bind(b); } dbg_assert(/^\d+$/.test(property) || property === "buffer" || property === "length" || property === "BYTES_PER_ELEMENT" || property === "byteOffset"); return x; }, set: function(target, property, value, receiver) { dbg_assert(/^\d+$/.test(property)); new constructor(memory.buffer, offset, length)[property] = value; return true; }, }); }; /** * number to hex * @param {number} n * @param {number=} len * @return {string} */ export function h(n, len) { if(!n) { var str = ""; } else { var str = n.toString(16); } return "0x" + pad0(str.toUpperCase(), len || 1); } export function hex_dump(buffer) { function hex(n, len) { return pad0(n.toString(16).toUpperCase(), len); } const result = []; let offset = 0; for(; offset + 15 < buffer.length; offset += 16) { let line = hex(offset, 5) + " "; for(let j = 0; j < 0x10; j++) { line += hex(buffer[offset + j], 2) + " "; } line += " "; for(let j = 0; j < 0x10; j++) { const x = buffer[offset + j]; line += (x >= 33 && x !== 34 && x !== 92 && x <= 126) ? String.fromCharCode(x) : "."; } result.push(line); } let line = hex(offset, 5) + " "; for(; offset < buffer.length; offset++) { line += hex(buffer[offset], 2) + " "; } const remainder = offset & 0xF; line += " ".repeat(0x10 - remainder); line += " "; for(let j = 0; j < remainder; j++) { const x = buffer[offset + j]; line += (x >= 33 && x !== 34 && x !== 92 && x <= 126) ? String.fromCharCode(x) : "."; } result.push(line); return "\n" + result.join("\n") + "\n"; } /* global require */ export var get_rand_int; if(typeof crypto !== "undefined" && crypto.getRandomValues) { const rand_data = new Int32Array(1); get_rand_int = function() { crypto.getRandomValues(rand_data); return rand_data[0]; }; } else if(typeof require !== "undefined") { /** @type {{ randomBytes: Function }} */ const crypto = require("crypto"); get_rand_int = function() { return crypto.randomBytes(4)["readInt32LE"](0); }; } else if(typeof process !== "undefined") { import("node:" + "crypto").then(crypto => { get_rand_int = function() { return crypto["randomBytes"](4)["readInt32LE"](0); }; }); } else { dbg_assert(false, "Unsupported platform: No cryptographic random values"); } export var int_log2; if(typeof Math.clz32 === "function" && Math.clz32(0) === 32 && Math.clz32(0x12345) === 15 && Math.clz32(-1) === 0) { /** * calculate the integer logarithm base 2 * @param {number} x * @return {number} */ int_log2 = function(x) { dbg_assert(x > 0); return 31 - Math.clz32(x); }; } else { var int_log2_table = new Int8Array(256); for(var i = 0, b = -2; i < 256; i++) { if(!(i & i - 1)) b++; int_log2_table[i] = b; } /** * calculate the integer logarithm base 2 * @param {number} x * @return {number} */ int_log2 = function(x) { x >>>= 0; dbg_assert(x > 0); // http://jsperf.com/integer-log2/6 var tt = x >>> 16; if(tt) { var t = tt >>> 8; if(t) { return 24 + int_log2_table[t]; } else { return 16 + int_log2_table[tt]; } } else { var t = x >>> 8; if(t) { return 8 + int_log2_table[t]; } else { return int_log2_table[x]; } } }; } export const round_up_to_next_power_of_2 = function(x) { dbg_assert(x >= 0); return x <= 1 ? 1 : 1 << 1 + int_log2(x - 1); }; if(typeof DEBUG !== "undefined" && DEBUG) { dbg_assert(int_log2(1) === 0); dbg_assert(int_log2(2) === 1); dbg_assert(int_log2(7) === 2); dbg_assert(int_log2(8) === 3); dbg_assert(int_log2(123456789) === 26); dbg_assert(round_up_to_next_power_of_2(0) === 1); dbg_assert(round_up_to_next_power_of_2(1) === 1); dbg_assert(round_up_to_next_power_of_2(2) === 2); dbg_assert(round_up_to_next_power_of_2(7) === 8); dbg_assert(round_up_to_next_power_of_2(8) === 8); dbg_assert(round_up_to_next_power_of_2(123456789) === 134217728); } /** * @constructor * * Queue wrapper around Uint8Array * Used by devices such as the PS2 controller */ export function ByteQueue(size) { var data = new Uint8Array(size), start, end; dbg_assert((size & size - 1) === 0); this.length = 0; this.push = function(item) { if(this.length === size) { // intentional overwrite } else { this.length++; } data[end] = item; end = end + 1 & size - 1; }; this.shift = function() { if(!this.length) { return -1; } else { var item = data[start]; start = start + 1 & size - 1; this.length--; return item; } }; this.peek = function() { if(!this.length) { return -1; } else { return data[start]; } }; this.clear = function() { start = 0; end = 0; this.length = 0; }; this.clear(); } /** * @constructor * * Queue wrapper around Float32Array * Used by devices such as the sound blaster sound card */ export function FloatQueue(size) { this.size = size; this.data = new Float32Array(size); this.start = 0; this.end = 0; this.length = 0; dbg_assert((size & size - 1) === 0); } FloatQueue.prototype.push = function(item) { if(this.length === this.size) { // intentional overwrite this.start = this.start + 1 & this.size - 1; } else { this.length++; } this.data[this.end] = item; this.end = this.end + 1 & this.size - 1; }; FloatQueue.prototype.shift = function() { if(!this.length) { return undefined; } else { var item = this.data[this.start]; this.start = this.start + 1 & this.size - 1; this.length--; return item; } }; FloatQueue.prototype.shift_block = function(count) { var slice = new Float32Array(count); if(count > this.length) { count = this.length; } var slice_end = this.start + count; var partial = this.data.subarray(this.start, slice_end); slice.set(partial); if(slice_end >= this.size) { slice_end -= this.size; slice.set(this.data.subarray(0, slice_end), partial.length); } this.start = slice_end; this.length -= count; return slice; }; FloatQueue.prototype.peek = function() { if(!this.length) { return undefined; } else { return this.data[this.start]; } }; FloatQueue.prototype.clear = function() { this.start = 0; this.end = 0; this.length = 0; }; /** * Simple circular queue for logs * * @param {number} size * @constructor */ function CircularQueue(size) { this.data = []; this.index = 0; this.size = size; } CircularQueue.prototype.add = function(item) { this.data[this.index] = item; this.index = (this.index + 1) % this.size; }; CircularQueue.prototype.toArray = function() { return [].slice.call(this.data, this.index).concat([].slice.call(this.data, 0, this.index)); }; CircularQueue.prototype.clear = function() { this.data = []; this.index = 0; }; /** * @param {Array} new_data */ CircularQueue.prototype.set = function(new_data) { this.data = new_data; this.index = 0; }; export function dump_file(ab, name) { if(!Array.isArray(ab)) { ab = [ab]; } var blob = new Blob(ab); download(blob, name); } export function download(file_or_blob, name) { var a = document.createElement("a"); a["download"] = name; a.href = window.URL.createObjectURL(file_or_blob); a.dataset["downloadurl"] = ["application/octet-stream", a["download"], a.href].join(":"); if(document.createEvent) { var ev = document.createEvent("MouseEvent"); ev.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); a.dispatchEvent(ev); } else { a.click(); } window.URL.revokeObjectURL(a.href); } /** * A simple 1d bitmap * @constructor */ export var Bitmap = function(length_or_buffer) { if(typeof length_or_buffer === "number") { this.view = new Uint8Array(length_or_buffer + 7 >> 3); } else if(length_or_buffer instanceof ArrayBuffer) { this.view = new Uint8Array(length_or_buffer); } else { dbg_assert(false, "Bitmap: Invalid argument"); } }; Bitmap.prototype.set = function(index, value) { const bit_index = index & 7; const byte_index = index >> 3; const bit_mask = 1 << bit_index; this.view[byte_index] = value ? this.view[byte_index] | bit_mask : this.view[byte_index] & ~bit_mask; }; Bitmap.prototype.get = function(index) { const bit_index = index & 7; const byte_index = index >> 3; return this.view[byte_index] >> bit_index & 1; }; Bitmap.prototype.get_buffer = function() { return this.view.buffer; }; export var load_file; export var get_file_size; if(typeof XMLHttpRequest === "undefined" || typeof process !== "undefined" && process.versions && process.versions.node) { let fs; /** * @param {string} filename * @param {Object} options * @param {number=} n_tries */ load_file = async function(filename, options, n_tries) { if(!fs) { // string concat to work around closure compiler 'Invalid module path "node:fs/promises" for resolution mode' fs = await import("node:" + "fs/promises"); } if(options.range) { dbg_assert(!options.as_json); const fd = await fs["open"](filename, "r"); const length = options.range.length; const buffer = Buffer.allocUnsafe(length); try { /** @type {{ bytesRead: Number }} */ const result = await fd["read"]({ buffer, position: options.range.start }); dbg_assert(result.bytesRead === length); } finally { await fd["close"](); } options.done && options.done(new Uint8Array(buffer)); } else { const o = { encoding: options.as_json ? "utf-8" : null, }; const data = await fs["readFile"](filename, o); const result = options.as_json ? JSON.parse(data) : new Uint8Array(data).buffer; options.done(result); } }; get_file_size = async function(path) { if(!fs) { // string concat to work around closure compiler 'Invalid module path "node:fs/promises" for resolution mode' fs = await import("node:" + "fs/promises"); } const stat = await fs["stat"](path); return stat.size; }; } else { /** * @param {string} filename * @param {Object} options * @param {number=} n_tries */ load_file = async function(filename, options, n_tries) { var http = new XMLHttpRequest(); http.open(options.method || "get", filename, true); if(options.as_json) { http.responseType = "json"; } else { http.responseType = "arraybuffer"; } if(options.headers) { var header_names = Object.keys(options.headers); for(var i = 0; i < header_names.length; i++) { var name = header_names[i]; http.setRequestHeader(name, options.headers[name]); } } if(options.range) { const start = options.range.start; const end = start + options.range.length - 1; http.setRequestHeader("Range", "bytes=" + start + "-" + end); http.setRequestHeader("X-Accept-Encoding", "identity"); // Abort if server responds with complete file in response to range // request, to prevent downloading large files from broken http servers http.onreadystatechange = function() { if(http.status === 200) { console.error("Server sent full file in response to ranged request, aborting", { filename }); http.abort(); } }; } http.onload = function(e) { if(http.readyState === 4) { if(http.status !== 200 && http.status !== 206) { console.error("Loading the image " + filename + " failed (status %d)", http.status); if(http.status >= 500 && http.status < 600) { retry(); } } else if(http.response) { if(options.range) { const enc = http.getResponseHeader("Content-Encoding"); if(enc && enc !== "identity") { console.error("Server sent Content-Encoding in response to ranged request", {filename, enc}); } } options.done && options.done(http.response, http); } } }; http.onerror = function(e) { console.error("Loading the image " + filename + " failed", e); retry(); }; if(options.progress) { http.onprogress = function(e) { options.progress(e); }; } http.send(null); function retry() { const number_of_tries = n_tries || 0; const timeout = [1, 1, 2, 3, 5, 8, 13, 21][number_of_tries] || 34; setTimeout(() => { load_file(filename, options, number_of_tries + 1); }, 1000 * timeout); } }; get_file_size = async function(url) { return new Promise((resolve, reject) => { load_file(url, { done: (buffer, http) => { var header = http.getResponseHeader("Content-Range") || ""; var match = header.match(/\/(\d+)\s*$/); if(match) { resolve(+match[1]); } else { const error = new Error("`Range: bytes=...` header not supported (Got `" + header + "`)"); reject(error); } }, headers: { Range: "bytes=0-0", "X-Accept-Encoding": "identity" } }); }); }; } // Reads len characters at offset from Memory object mem as a JS string export function read_sized_string_from_mem(mem, offset, len) { offset >>>= 0; len >>>= 0; return String.fromCharCode(...new Uint8Array(mem.buffer, offset, len)); } /** * Unicode mappings of supported 8-bit code pages. * Each mapping is a string of 256 Unicode symbols used as a lookup table for 8-bit character codes. * * Supported mappings and their encoding labels: * - "cp437": CP437 (MS-DOS Latin US), default * - "cp858": CP858 (Western Europe), the lower 128 bytes are identical to CP437 * - "ascii": ASCII (7-Bit), same as CP437 with lower 32 and upper 128 bytes mapped to "." * * @type {Object} */ const CHARMAPS = { cp437: " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ", cp858: "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈ€ÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ " }; CHARMAPS.cp858 = CHARMAPS.cp437.slice(0, 128) + CHARMAPS.cp858; CHARMAPS.ascii = CHARMAPS.cp437.split("").map((c, i) => i > 31 && i < 128 ? c : ".").join(""); /** * Return charmap for given encoding, default to CP437 if encoding is falsey or not defined in CHARMAPS. * * @param {string} encoding * @return {!string} */ export function get_charmap(encoding) { return encoding && CHARMAPS[encoding] ? CHARMAPS[encoding] : CHARMAPS.cp437; }