Browse Source

[web] add charset/palette loading

kvm
asie 6 months ago
parent
commit
31d59f7499
5 changed files with 179 additions and 25 deletions
  1. 27
    6
      web/res/README.md
  2. 1
    5
      web/res/zeta_loader.js
  3. 145
    8
      web/src/emulator.js
  4. 6
    4
      web/src/index.js
  5. 0
    2
      zeta_wasm.sh

web/res/readme.txt → web/res/README.md View File

@@ -1,22 +1,26 @@
Zeta HTML5 port: (very) rough instructions
# Zeta HTML5 port

## (Very) rough instructions

index.html provides an effective template of a Zeta instance which will span the whole browser frame.

Basic requirements:
## Basic requirements

* A canvas object with a width of or larger than 640x350, specified in options.render.canvas.
* Integer scaling will be applied automatically based on the canvas width/height - CSS styling only resizes the final texture!
* A presence of all the files herein other than "index.html" in a certain directory.

The entrypoint is "ZetaLoad(options);".
The entrypoint is "ZetaLoad(options, callback);". The callback is optional, and returns an instance of the emulator.

## Option keys

Mandatory option keys:
### Mandatory

* render.canvas: the element of the desired canvas.
* path: the (relative or absolute) path to the Zeta engine files.
* files: an array containing file entries to be loaded.

Optional option keys:
### Optional

* arg: the argument string provided to ZZT's executable. Ignored if "commands" is present.
* commands: a list containing commands to be executed in order. This will override "arg" *and* ZZT execution - the final entry on the list is assumed to be ZZT! Allowed types:
@@ -25,11 +29,23 @@ Optional option keys:
* storage: define the settings for persistent storage. If not present, save files etc. will be stored in memory, and lost with as little as a page refresh.
* type: can be "auto" (preferred), "localstorage" or "indexeddb".
* database: for all of the above, a required database name. If you're hosting multiple games on the same domain, you may want to make this unique.
* engine:
* charset: the character set (8x14 only!) to initially load, as one of:
* string - filename of a Zeta-supported format,
* array - of bytes as would be contained in such a .chr file.
* palette: the palette, to initially load, as one of:
* string - filename of a Zeta-supported format,
* object, with the following fields:
* min - minimum palette value in the array (f.e. 0),
* max - maximum palette value in the array (f.e. 255),
* colors - an array of sixteen colors, either:
* 3-component arrays of range min - max (inclusive),
* "#012345" or "#012"-format strings.
* render:
* type: the engine to use for video rendering; can be "auto" (preferred) or "canvas"
* blink: true if video blinking should be enabled, false otherwise
* blink_duration: the length of a full blink cycle, in milliseconds
* charset_override: the location of a PNG image file (16x16 chars) overriding the character set, if present
* charset_override: the location of a PNG image file (16x16 chars) overriding the engine's character set, if present
* audio:
* type: the engine to use for audio rendering; can be "auto" (preferred), "buffer" or "oscillator" (pre-beta15; deprecated)
* bufferSize (buffer): the audio buffer size, in samples
@@ -43,3 +59,8 @@ File entries can be either a string (denoting the relative or absolute path to a
* string - describes a subdirectory whose contents are loaded (paths are /-separated, like on Unix)
* object - describes a mapping of ZIP filenames to target filesystem filenames; no other files are loaded
* function - accepts a ZIP filename and returns a target filesystem filename; return "undefined" to not load a file

## Public emulator methods

* emu.loadCharset(charset) - argument format as in options.emulator.charset. Returns true upon success.
* emu.loadPalette(palette) - argument format as in options.emulator.palette. Returns true upon success.

+ 1
- 5
web/res/zeta_loader.js View File

@@ -23,11 +23,7 @@ ZetaLoad = function(options, callback) {
var scripts_array = [];
var script_ldr = function() {
if (scripts_array.length == 0) {
if (callback) {
callback(ZetaInitialize(options));
} else {
ZetaInitialize(options);
}
ZetaInitialize(options, callback);
} else {
var scrSrc = scripts_array.shift();
var scr = document.createElement("script");

+ 145
- 8
web/src/emulator.js View File

@@ -29,6 +29,8 @@ import { initVfsWrapper, setWrappedEmu, setWrappedVfs } from "./vfs_wrapper.js";

const TIMER_DURATION = 1000 / 18.2;

const PLD_TO_PAL = [0, 1, 2, 3, 4, 5, 20, 7, 56, 57, 58, 59, 60, 61, 62, 63];

class Emulator {
constructor(element, emu, render, audio, vfs, options) {
this.element = element;
@@ -54,8 +56,8 @@ class Emulator {
if (id == 1 /* joy connected */) return true;
else if (id == 2 /* mouse connected */) return true;
else return false;
}
}
window.zetag_update_charset = function(width, height, char_ptr) {
const data = new Uint8Array(emu.HEAPU8.buffer, char_ptr, 256 * height);
render.setCharset(width, height, data);
@@ -132,7 +134,7 @@ class Emulator {

this.element.addEventListener("mousemove", function(e) {
if (emu == undefined) return;
const mx = e.movementX * self.mouseSensitivity;
const my = e.movementY * self.mouseSensitivity;
emu._zzt_mouse_axis(0, mx);
@@ -144,20 +146,131 @@ class Emulator {
if (mouseY < 0) mouseY = 0;
else if (mouseY >= 350) mouseY = 349; */
});
this.element.addEventListener("mousedown", function(e) {
element.requestPointerLock();
if (emu == undefined) return;
emu._zzt_mouse_set(e.button);
});
this.element.addEventListener("mouseup", function(e) {
if (emu == undefined) return;
emu._zzt_mouse_clear(e.button);
});
}

loadCharset(charset) {
const emu = this.emu;

if (typeof(charset) == "string") {
charset = this.vfs.get(charset);
}

if (typeof(charset) == "object") {
if ((charset.length & 0xFF) != 0) return false;

const width = 8;
const height = charset.length >> 8;

if (height != 14) return false;

let result = false;

this._u8array2buffer(charset, charset_buffer => {
result = emu._zzt_load_charset(width, height, charset_buffer) >= 0;
});

return result;
} else {
return false;
}
}

_pal_file_append(paletteArray, array, offset, max) {
const red = Math.floor(array[offset] * 255 / max);
const green = Math.floor(array[offset + 1] * 255 / max);
const blue = Math.floor(array[offset + 2] * 255 / max);
paletteArray.push(blue, green, red, 0);
}

loadPalette(palette) {
const emu = this.emu;
let paletteArray = [];

if (typeof(palette) == "string") {
let type = "pal";
if (palette.toLowerCase().endsWith(".pld")) type = "pld";

const palData = this.vfs.get(palette);
if (palData == null) {
return false;
}

if (type == "pld") {
if (palData.length < 192) return false;
for (var i = 0; i < 16; i++) {
this._pal_file_append(paletteArray, palData, PLD_TO_PAL[i] * 3, 63);
}
} else {
if (palData.length < 48) return false;
for (var i = 0; i < 16; i++) {
this._pal_file_append(paletteArray, palData, i * 3, 63);
}
}
} else if (typeof(palette) == "object") {
const min = palette.min || 0;
const max = palette.max || 255;
if (palette.colors == null || palette.colors.length < 16) {
console.warn("[zeta.loadPalette] missing or too small colors array");
return false;
}
for (var i = 0; i < 16; i++) {
const color = palette.colors[i];
if (typeof(color) == "string" && color.startsWith("#")) {
if (color.length == 7) {
const red = parseInt(color.substring(1, 3), 16);
const green = parseInt(color.substring(3, 5), 16);
const blue = parseInt(color.substring(5, 7), 16);

paletteArray.push(blue, green, red, 0);
} else if (color.length == 4) {
const red = parseInt(color.substring(1, 2), 16) * 0x11;
const green = parseInt(color.substring(2, 3), 16) * 0x11;
const blue = parseInt(color.substring(3, 4), 16) * 0x11;
paletteArray.push(blue, green, red, 0);
} else {
console.warn("[zeta.loadPalette] invalid color string length: " + color.length);
return false;
}
} else if (typeof(color) == "object" && color.length >= 3) {
const red = Math.floor((palette.colors[i][0] - min) * 255 / (max - min));
const green = Math.floor((palette.colors[i][1] - min) * 255 / (max - min));
const blue = Math.floor((palette.colors[i][2] - min) * 255 / (max - min));
paletteArray.push(blue, green, red, 0);
} else {
console.warn("[zeta.loadPalette] invalid color type @ " + i);
return false;
}
}
}

if (paletteArray.length == 64) {
let result = false;

this._u8array2buffer(paletteArray, paletteBuffer => {
result = emu._zzt_load_palette(paletteBuffer) >= 0;
});

return result;
} else {
return false;
}
}

_frame() {
this._pollGamepads();

@@ -233,6 +346,16 @@ class Emulator {
}
}

_u8array2buffer(arr, mth) {
const arg_buffer = this.emu._malloc(arr.length);
const arg_heap = new Uint8Array(this.emu.HEAPU8.buffer, arg_buffer, arr.length);
for (var i = 0; i < arr.length; i++) {
arg_heap[i] = arr[i];
}
mth(arg_buffer);
this.emu._free(arg_buffer);
}

_str2buffer(arg, mth) {
const arg_buffer = this.emu._malloc(arg.length + 1);
const arg_heap = new Uint8Array(this.emu.HEAPU8.buffer, arg_buffer, arg.length + 1);
@@ -251,7 +374,7 @@ class Emulator {
}

export function createEmulator(render, audio, vfs, options) {
return new Promise(resolve => {
return new Promise(resolve => {
ZetaNative().then(emu => {
setWrappedVfs(vfs);
setWrappedEmu(emu);
@@ -261,7 +384,7 @@ export function createEmulator(render, audio, vfs, options) {

emu._zzt_init();
emu._zzt_set_timer_offset(Date.now() % 86400000)
if (options && options.commands) {
const lastCommand = options.commands.length - 1;
for (var i = 0; i <= lastCommand; i++) {
@@ -295,6 +418,7 @@ export function createEmulator(render, audio, vfs, options) {
handle = vfsg_open(executable, 0);
extension = undefined;
}

if (handle < 0) {
throw "Could not find ZZT/Super ZZT executable!";
}
@@ -318,6 +442,19 @@ export function createEmulator(render, audio, vfs, options) {
console.log("executing " + executable + " " + vfs_arg);
}

if (options && options.engine) {
if (options.engine.charset) {
if (!emuObj.loadCharset(options.engine.charset)) {
console.error("Could not load charset from options!");
}
}
if (options.engine.palette) {
if (!emuObj.loadPalette(options.engine.palette)) {
console.error("Could not load palette from options!");
}
}
}

emuObj._resetLastTimerTime();
emuObj._tick();
resolve(emuObj);

+ 6
- 4
web/src/index.js View File

@@ -70,7 +70,7 @@ class LoadingScreen {
}
}

window.ZetaInitialize = function(options) {
window.ZetaInitialize = function(options, callback) {
console.log(" _ \n _______| |_ __ _ \n|_ / _ \\ __/ _` |\n / / __/ || (_| |\n/___\\___|\\__\\__,_|\n\n " + VERSION);

if (!options.render) throw "Missing option: render!";
@@ -104,7 +104,7 @@ window.ZetaInitialize = function(options) {
loadingScreen.progress(vfsProgresses.reduce((p, c) => p + c) / options.files.length);
}

if (Array.isArray(file)) {
if (Array.isArray(file)) {
var opts = file[1];
if (!opts.hasOwnProperty("readonly")) {
opts.readonly = true;
@@ -172,9 +172,11 @@ window.ZetaInitialize = function(options) {
}

const vfs = createCompositeStorage(vfsObjects);

return createEmulator(render, audio, vfs, options);
const emu = createEmulator(render, audio, vfs, options);
if (callback != null) callback(emu);
return emu;
}).then(_ => true).catch(reason => {
callback(undefined, reason);
drawErrorMessage(canvas, ctx, reason);
});
}

+ 0
- 2
zeta_wasm.sh View File

@@ -27,7 +27,5 @@ mv a.out.js build/zeta_native.js
mv a.out.wasm build/zeta_native.wasm
sed -i -e "s/a\.out/zeta_native/g" build/zeta_native.js

rm build/web/*
cp web/* build/web/
cp build/zeta_native.js build/web/
cp build/zeta_native.wasm build/web/

Loading…
Cancel
Save