pvbemu/app/_boot.js

325 lines
10 KiB
JavaScript

// Produce an async function from a source string
if (!globalThis.AsyncFunction)
globalThis.AsyncFunction =
Object.getPrototypeOf(async function(){}).constructor;
// Read scripts from files on disk to aid with debugging
let debug = location.hash == "#debug";
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Resource asset manager
globalThis.Bundle = class BundledFile {
///////////////////////////// Static Methods //////////////////////////////
// Adds a bundled file from loaded file data
static add(name, data) {
return Bundle.files[name] = new Bundle(name, data);
}
// Retrieve the file given its filename
static get(name) {
return Bundle.files[name];
}
// Run a file as a JavaScript source file
static async run(name) {
await Bundle.files[name].run();
}
// Resolve a URL for a source file
static source(name) {
return debug ? name : Bundle.files[name].toDataURL();
}
///////////////////////// Initialization Methods //////////////////////////
// Object constructor
constructor(name, data) {
// Configure instance fields
this.data = data;
this.name = name;
// Detect the MIME type
this.mime =
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm" ) ? "application/wasm" :
name.endsWith(".woff2") ? "font/woff2" :
"application/octet-stream"
;
}
///////////////////////////// Public Methods //////////////////////////////
// Execute the file as a JavaScript source file
async run() {
// Not running in debug mode
if (!debug) {
await new AsyncFunction(this.toString())();
return;
}
// Running in debug mode
await new Promise((resolve,reject)=>{
let script = document.createElement("script");
document.head.appendChild(script);
script.addEventListener("load", ()=>resolve());
script.src = this.name;
});
}
// Register the file as a CSS stylesheet
style(enabled) {
let link = document.createElement("link");
link.href = debug ? this.name : this.toDataURL();
link.rel = "stylesheet";
link.type = "text/css";
link.setEnabled = enabled=>{
if (enabled)
link.removeAttribute("disabled");
else link.setAttribute("disabled", null);
};
link.setEnabled(enabled === undefined || !!enabled);
document.head.appendChild(link);
return link;
}
// Produce a blob from the file data
toBlob() {
return new Blob(this.data, { type: this.mime });
}
// Produce a blob URL for the file data
toBlobURL() {
return URL.createObjectURL(this.toBlob());
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," + btoa(this.toString());
}
// Decode the file data as a UTF-8 string
toString() {
return new TextDecoder().decode(this.data);
}
};
Bundle.files = [];
///////////////////////////////////////////////////////////////////////////////
// ZIP Bundler //
///////////////////////////////////////////////////////////////////////////////
// Data buffer utility processor
class Bin {
// Object constructor
constructor() {
this.data = [];
this.offset = 0;
}
///////////////////////////// Public Methods //////////////////////////////
// Convert the data contents to a byte array
toByteArray() {
return Uint8Array.from(this.data);
}
// Encode a byte array
writeBytes(data) {
this.data = this.data.concat(Array.from(data));
}
// Encode a sized integer
writeInt(length, value) {
for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8)
this.data.push(value & 0xFF);
}
// Encode a string as UTF-8 with prepended length
writeString(value) {
this.writeBytes(new TextEncoder().encode(value));
}
}
// Generate the CRC32 lookup table
let crcLookup = new Uint32Array(256);
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
crcLookup[x] = l;
}
// Calculate the CRC32 checksum for a byte array
function crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
// Produce a .ZIP header from a bundled file
function toZipHeader(file, crc32, offset) {
let central = offset || offset === 0;
let ret = new Bin();
if (central) {
ret.writeInt (4, 0x02014B50); // Signature
ret.writeInt (2, 20); // Version created by
} else
ret.writeInt (4, 0x04034B50); // Signature
ret.writeInt (2, 20); // Version required
ret.writeInt (2, 0); // Bit flags
ret.writeInt (2, 0); // Compression method
ret.writeInt (2, 0); // Modified time
ret.writeInt (2, 0); // Modified date
ret.writeInt (4, crc32); // Checksum
ret.writeInt (4, file.data.length); // Compressed size
ret.writeInt (4, file.data.length); // Uncompressed size
ret.writeInt (2, file.name.length); // Filename length
ret.writeInt (2, 0); // Extra field length
if (central) {
ret.writeInt (2, 0); // File comment length
ret.writeInt (2, 0); // Disk number start
ret.writeInt (2, 0); // Internal attributes
ret.writeInt (4, 0); // External attributes
ret.writeInt (4, offset); // Relative offset
}
ret.writeString (file.name, true); // Filename
if (!central)
ret.writeBytes(file.data); // File data
return ret.toByteArray();
}
// Package all bundled files into a .zip file for download
Bundle.save = function() {
let centrals = new Array(manifest.length);
let locals = new Array(manifest.length);
let offset = 0;
let size = 0;
// Encode file and directory entries
let keys = Object.keys(Bundle.files);
for (let x = 0; x < keys.length; x++) {
let file = Bundle.get(keys[x]);
let sum = crc32(file.data);
locals [x] = toZipHeader(file, sum);
centrals[x] = toZipHeader(file, sum, offset);
offset += locals [x].length;
size += centrals[x].length;
}
// Encode end of central directory
let end = new Bin();
end.writeInt(4, 0x06054B50); // Signature
end.writeInt(2, 0); // Disk number
end.writeInt(2, 0); // Central dir start disk
end.writeInt(2, centrals.length); // # central dir this disk
end.writeInt(2, centrals.length); // # central dir total
end.writeInt(4, size); // Size of central dir
end.writeInt(4, offset); // Offset of central dir
end.writeInt(2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = bundleName + ".zip";
a.href = URL.createObjectURL(new Blob(
locals.concat(centrals).concat([end.toByteArray()]),
{ type: "application/zip" }
));
a.click();
}
///////////////////////////////////////////////////////////////////////////////
// Boot Loader //
///////////////////////////////////////////////////////////////////////////////
/*
The Bundle.java utility prepends a manifest object to _boot.js that is an
array of file infos, each of which has a name and size property.
*/
// Remove the bundle image element from the document
arguments[0].remove();
// Process all files from the bundle blob
let blob = arguments[1];
let offset = arguments[2] - manifest[0].size * 4;
for (let file of manifest) {
let data = new Uint8Array(file.size);
for (let x = 0; x < file.size; x++, offset += 4)
data[x] = blob[offset];
if (file == manifest[0])
offset += 4;
Bundle.add(file.name, data);
}
// Program startup
let run = async function() {
// Fonts
for (let file of Object.values(Bundle.files)) {
if (!file.name.endsWith(".woff2"))
continue;
let family = "/" + file.name;
family = family.substring(family.lastIndexOf("/")+1, family.length-6);
let font = new FontFace(family, file.data);
await font.load();
document.fonts.add(font);
}
// Scripts
await Bundle.run("app/App.js");
await Bundle.run("app/Debugger.js");
await Bundle.run("app/toolkit/Toolkit.js");
await Bundle.run("app/toolkit/Component.js");
await Bundle.run("app/toolkit/Panel.js");
await Bundle.run("app/toolkit/Application.js");
await Bundle.run("app/toolkit/Button.js");
await Bundle.run("app/toolkit/ButtonGroup.js");
await Bundle.run("app/toolkit/CheckBox.js");
await Bundle.run("app/toolkit/Label.js");
await Bundle.run("app/toolkit/MenuBar.js");
await Bundle.run("app/toolkit/MenuItem.js");
await Bundle.run("app/toolkit/Menu.js");
await Bundle.run("app/toolkit/RadioButton.js");
await Bundle.run("app/toolkit/Splitter.js");
await Bundle.run("app/toolkit/TextBox.js");
await Bundle.run("app/toolkit/Window.js");
await Bundle.run("app/windows/CPUWindow.js");
await Bundle.run("app/windows/Disassembler.js");
await Bundle.run("app/windows/Register.js");
await Bundle.run("app/windows/MemoryWindow.js");
await App.create();
};
run();