Browse Source

[web] add charset/palette loading

master
asie 1 month 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 @@
1
-Zeta HTML5 port: (very) rough instructions
1
+# Zeta HTML5 port
2
+
3
+## (Very) rough instructions
2 4
 
3 5
 index.html provides an effective template of a Zeta instance which will span the whole browser frame.
4 6
 
5
-Basic requirements:
7
+## Basic requirements
6 8
 
7 9
 * A canvas object with a width of or larger than 640x350, specified in options.render.canvas.
8 10
     * Integer scaling will be applied automatically based on the canvas width/height - CSS styling only resizes the final texture!
9 11
 * A presence of all the files herein other than "index.html" in a certain directory.
10 12
 
11
-The entrypoint is "ZetaLoad(options);".
13
+The entrypoint is "ZetaLoad(options, callback);". The callback is optional, and returns an instance of the emulator.
14
+
15
+## Option keys
12 16
 
13
-Mandatory option keys:
17
+### Mandatory
14 18
 
15 19
 * render.canvas: the element of the desired canvas.
16 20
 * path: the (relative or absolute) path to the Zeta engine files.
17 21
 * files: an array containing file entries to be loaded.
18 22
 
19
-Optional option keys:
23
+### Optional
20 24
 
21 25
 * arg: the argument string provided to ZZT's executable. Ignored if "commands" is present.
22 26
 * 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:
25 29
 * 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.
26 30
     * type: can be "auto" (preferred), "localstorage" or "indexeddb".
27 31
     * 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.
32
+* engine:
33
+    * charset: the character set (8x14 only!) to initially load, as one of:
34
+        * string - filename of a Zeta-supported format,
35
+        * array - of bytes as would be contained in such a .chr file.
36
+    * palette: the palette, to initially load, as one of:
37
+        * string - filename of a Zeta-supported format,
38
+        * object, with the following fields:
39
+            * min - minimum palette value in the array (f.e. 0),
40
+            * max - maximum palette value in the array (f.e. 255),
41
+            * colors - an array of sixteen colors, either:
42
+                * 3-component arrays of range min - max (inclusive),
43
+                * "#012345" or "#012"-format strings.
28 44
 * render:
29 45
     * type: the engine to use for video rendering; can be "auto" (preferred) or "canvas"
30 46
     * blink: true if video blinking should be enabled, false otherwise
31 47
     * blink_duration: the length of a full blink cycle, in milliseconds
32
-    * charset_override: the location of a PNG image file (16x16 chars) overriding the character set, if present
48
+    * charset_override: the location of a PNG image file (16x16 chars) overriding the engine's character set, if present
33 49
 * audio:
34 50
     * type: the engine to use for audio rendering; can be "auto" (preferred), "buffer" or "oscillator" (pre-beta15; deprecated)
35 51
     * 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
43 59
     * string - describes a subdirectory whose contents are loaded (paths are /-separated, like on Unix)
44 60
     * object - describes a mapping of ZIP filenames to target filesystem filenames; no other files are loaded
45 61
     * function - accepts a ZIP filename and returns a target filesystem filename; return "undefined" to not load a file
62
+
63
+## Public emulator methods
64
+
65
+* emu.loadCharset(charset) - argument format as in options.emulator.charset. Returns true upon success.
66
+* 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) {
23 23
 	var scripts_array = [];
24 24
 	var script_ldr = function() {
25 25
 		if (scripts_array.length == 0) {
26
-			if (callback) {
27
-				callback(ZetaInitialize(options));
28
-			} else {
29
-				ZetaInitialize(options);
30
-			}
26
+			ZetaInitialize(options, callback);
31 27
 		} else {
32 28
 			var scrSrc = scripts_array.shift();
33 29
 			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";
29 29
 
30 30
 const TIMER_DURATION = 1000 / 18.2;
31 31
 
32
+const PLD_TO_PAL = [0, 1, 2, 3, 4, 5, 20, 7, 56, 57, 58, 59, 60, 61, 62, 63];
33
+
32 34
 class Emulator {
33 35
     constructor(element, emu, render, audio, vfs, options) {
34 36
         this.element = element;
@@ -54,8 +56,8 @@ class Emulator {
54 56
             if (id == 1 /* joy connected */) return true;
55 57
             else if (id == 2 /* mouse connected */) return true;
56 58
             else return false;
57
-        }        
58
-        
59
+        }
60
+
59 61
         window.zetag_update_charset = function(width, height, char_ptr) {
60 62
 	        const data = new Uint8Array(emu.HEAPU8.buffer, char_ptr, 256 * height);
61 63
 	        render.setCharset(width, height, data);
@@ -132,7 +134,7 @@ class Emulator {
132 134
 
133 135
         this.element.addEventListener("mousemove", function(e) {
134 136
             if (emu == undefined) return;
135
-    
137
+
136 138
             const mx = e.movementX * self.mouseSensitivity;
137 139
             const my = e.movementY * self.mouseSensitivity;
138 140
             emu._zzt_mouse_axis(0, mx);
@@ -144,20 +146,131 @@ class Emulator {
144 146
             if (mouseY < 0) mouseY = 0;
145 147
             else if (mouseY >= 350) mouseY = 349; */
146 148
         });
147
-    
149
+
148 150
         this.element.addEventListener("mousedown", function(e) {
149 151
             element.requestPointerLock();
150
-    
152
+
151 153
             if (emu == undefined) return;
152 154
             emu._zzt_mouse_set(e.button);
153 155
         });
154
-    
156
+
155 157
         this.element.addEventListener("mouseup", function(e) {
156 158
             if (emu == undefined) return;
157 159
             emu._zzt_mouse_clear(e.button);
158 160
         });
159 161
     }
160 162
 
163
+    loadCharset(charset) {
164
+        const emu = this.emu; 
165
+
166
+        if (typeof(charset) == "string") {
167
+            charset = this.vfs.get(charset);
168
+        }
169
+
170
+        if (typeof(charset) == "object") {
171
+            if ((charset.length & 0xFF) != 0) return false;
172
+
173
+            const width = 8;
174
+            const height = charset.length >> 8;
175
+
176
+            if (height != 14) return false;
177
+
178
+            let result = false;
179
+
180
+            this._u8array2buffer(charset, charset_buffer => {
181
+                result = emu._zzt_load_charset(width, height, charset_buffer) >= 0;
182
+            });
183
+
184
+            return result;
185
+        } else {
186
+            return false;
187
+        }
188
+    }
189
+
190
+    _pal_file_append(paletteArray, array, offset, max) {
191
+        const red = Math.floor(array[offset] * 255 / max);
192
+        const green = Math.floor(array[offset + 1] * 255 / max);
193
+        const blue = Math.floor(array[offset + 2] * 255 / max);
194
+        
195
+        paletteArray.push(blue, green, red, 0);
196
+    }
197
+
198
+    loadPalette(palette) {
199
+        const emu = this.emu; 
200
+        let paletteArray = [];
201
+
202
+        if (typeof(palette) == "string") {
203
+            let type = "pal";
204
+            if (palette.toLowerCase().endsWith(".pld")) type = "pld";
205
+
206
+            const palData = this.vfs.get(palette);
207
+            if (palData == null) {
208
+                return false;
209
+            }
210
+
211
+            if (type == "pld") {
212
+                if (palData.length < 192) return false;
213
+                for (var i = 0; i < 16; i++) {
214
+                    this._pal_file_append(paletteArray, palData, PLD_TO_PAL[i] * 3, 63);
215
+                }
216
+            } else {
217
+                if (palData.length < 48) return false;
218
+                for (var i = 0; i < 16; i++) {
219
+                    this._pal_file_append(paletteArray, palData, i * 3, 63);
220
+                }
221
+            }
222
+        } else if (typeof(palette) == "object") {
223
+            const min = palette.min || 0;
224
+            const max = palette.max || 255;
225
+            if (palette.colors == null || palette.colors.length < 16) {
226
+                console.warn("[zeta.loadPalette] missing or too small colors array");
227
+                return false;
228
+            }
229
+            for (var i = 0; i < 16; i++) {
230
+                const color = palette.colors[i];
231
+                if (typeof(color) == "string" && color.startsWith("#")) {
232
+                    if (color.length == 7) {
233
+                        const red = parseInt(color.substring(1, 3), 16);
234
+                        const green = parseInt(color.substring(3, 5), 16);
235
+                        const blue = parseInt(color.substring(5, 7), 16);
236
+
237
+                        paletteArray.push(blue, green, red, 0);
238
+                    } else if (color.length == 4) {
239
+                        const red = parseInt(color.substring(1, 2), 16) * 0x11;
240
+                        const green = parseInt(color.substring(2, 3), 16) * 0x11;
241
+                        const blue = parseInt(color.substring(3, 4), 16) * 0x11;
242
+    
243
+                        paletteArray.push(blue, green, red, 0);
244
+                    } else {
245
+                        console.warn("[zeta.loadPalette] invalid color string length: " + color.length);
246
+                        return false;
247
+                    }
248
+                } else if (typeof(color) == "object" && color.length >= 3) {
249
+                    const red = Math.floor((palette.colors[i][0] - min) * 255 / (max - min));
250
+                    const green = Math.floor((palette.colors[i][1] - min) * 255 / (max - min));
251
+                    const blue = Math.floor((palette.colors[i][2] - min) * 255 / (max - min));
252
+            
253
+                    paletteArray.push(blue, green, red, 0);
254
+                } else {
255
+                    console.warn("[zeta.loadPalette] invalid color type @ " + i);
256
+                    return false;
257
+                }
258
+            }
259
+        }
260
+
261
+        if (paletteArray.length == 64) {
262
+            let result = false;
263
+
264
+            this._u8array2buffer(paletteArray, paletteBuffer => {
265
+                result = emu._zzt_load_palette(paletteBuffer) >= 0;
266
+            });
267
+
268
+            return result;
269
+        } else {
270
+            return false;
271
+        }
272
+    }
273
+
161 274
     _frame() {
162 275
         this._pollGamepads();
163 276
 
@@ -233,6 +346,16 @@ class Emulator {
233 346
         }
234 347
     }
235 348
 
349
+    _u8array2buffer(arr, mth) {
350
+        const arg_buffer = this.emu._malloc(arr.length);
351
+        const arg_heap = new Uint8Array(this.emu.HEAPU8.buffer, arg_buffer, arr.length);
352
+        for (var i = 0; i < arr.length; i++) {
353
+            arg_heap[i] = arr[i];
354
+        }
355
+        mth(arg_buffer);
356
+        this.emu._free(arg_buffer);
357
+    }
358
+
236 359
     _str2buffer(arg, mth) {
237 360
         const arg_buffer = this.emu._malloc(arg.length + 1);
238 361
         const arg_heap = new Uint8Array(this.emu.HEAPU8.buffer, arg_buffer, arg.length + 1);
@@ -251,7 +374,7 @@ class Emulator {
251 374
 }
252 375
 
253 376
 export function createEmulator(render, audio, vfs, options) {
254
-	return new Promise(resolve => {
377
+    return new Promise(resolve => {
255 378
         ZetaNative().then(emu => {
256 379
             setWrappedVfs(vfs);
257 380
             setWrappedEmu(emu);
@@ -261,7 +384,7 @@ export function createEmulator(render, audio, vfs, options) {
261 384
 
262 385
             emu._zzt_init();
263 386
             emu._zzt_set_timer_offset(Date.now() % 86400000)
264
-            
387
+
265 388
             if (options && options.commands) {
266 389
                 const lastCommand = options.commands.length - 1;
267 390
                 for (var i = 0; i <= lastCommand; i++) {
@@ -295,6 +418,7 @@ export function createEmulator(render, audio, vfs, options) {
295 418
                     handle = vfsg_open(executable, 0);
296 419
                     extension = undefined;
297 420
                 }
421
+
298 422
                 if (handle < 0) {
299 423
                     throw "Could not find ZZT/Super ZZT executable!";
300 424
                 }
@@ -318,6 +442,19 @@ export function createEmulator(render, audio, vfs, options) {
318 442
                 console.log("executing " + executable + " " + vfs_arg);
319 443
             }
320 444
 
445
+            if (options && options.engine) {
446
+                if (options.engine.charset) {
447
+                    if (!emuObj.loadCharset(options.engine.charset)) {
448
+                        console.error("Could not load charset from options!");
449
+                    }
450
+                }
451
+                if (options.engine.palette) {
452
+                    if (!emuObj.loadPalette(options.engine.palette)) {
453
+                        console.error("Could not load palette from options!");
454
+                    }
455
+                }
456
+            }
457
+
321 458
             emuObj._resetLastTimerTime();
322 459
             emuObj._tick();
323 460
             resolve(emuObj);

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

@@ -70,7 +70,7 @@ class LoadingScreen {
70 70
     }
71 71
 }
72 72
 
73
-window.ZetaInitialize = function(options) {
73
+window.ZetaInitialize = function(options, callback) {
74 74
     console.log("         _        \n _______| |_ __ _ \n|_  / _ \\ __/ _` |\n / /  __/ || (_| |\n/___\\___|\\__\\__,_|\n\n " + VERSION);
75 75
 
76 76
     if (!options.render) throw "Missing option: render!";
@@ -104,7 +104,7 @@ window.ZetaInitialize = function(options) {
104 104
             loadingScreen.progress(vfsProgresses.reduce((p, c) => p + c) / options.files.length);            
105 105
         }
106 106
 
107
-		if (Array.isArray(file)) {
107
+        if (Array.isArray(file)) {
108 108
             var opts = file[1];
109 109
             if (!opts.hasOwnProperty("readonly")) {
110 110
                 opts.readonly = true;
@@ -172,9 +172,11 @@ window.ZetaInitialize = function(options) {
172 172
         }
173 173
 
174 174
         const vfs = createCompositeStorage(vfsObjects);
175
-
176
-        return createEmulator(render, audio, vfs, options);
175
+        const emu = createEmulator(render, audio, vfs, options);
176
+        if (callback != null) callback(emu);
177
+        return emu;
177 178
     }).then(_ => true).catch(reason => {
179
+        callback(undefined, reason);
178 180
         drawErrorMessage(canvas, ctx, reason);
179 181
     });
180 182
 }

+ 0
- 2
zeta_wasm.sh View File

@@ -27,7 +27,5 @@ mv a.out.js build/zeta_native.js
27 27
 mv a.out.wasm build/zeta_native.wasm
28 28
 sed -i -e "s/a\.out/zeta_native/g" build/zeta_native.js
29 29
 
30
-rm build/web/*
31
-cp web/* build/web/
32 30
 cp build/zeta_native.js build/web/
33 31
 cp build/zeta_native.wasm build/web/

Loading…
Cancel
Save