diff --git a/cmake/platforms/emscripten.cmake b/cmake/platforms/emscripten.cmake index f168607..fafa04e 100644 --- a/cmake/platforms/emscripten.cmake +++ b/cmake/platforms/emscripten.cmake @@ -59,7 +59,11 @@ set(emcc_export_list _malloc _free # Main program, run at initialisation time - _main) + _main + # wasmga.me: new games, parameters and status for the page + _wg_new_game + _wg_params + _wg_status) list(TRANSFORM emcc_export_list PREPEND \") list(TRANSFORM emcc_export_list APPEND \") @@ -68,7 +72,7 @@ set(CMAKE_C_LINK_FLAGS "\ -s ALLOW_MEMORY_GROWTH=1 \ -s ENVIRONMENT=web \ -s EXPORTED_FUNCTIONS='[${emcc_export_string}]' \ --s EXPORTED_RUNTIME_METHODS='[cwrap]' \ +-s EXPORTED_RUNTIME_METHODS='[cwrap,ccall]' \ -s MIN_FIREFOX_VERSION=${MIN_FIREFOX_VERSION} \ -s MIN_SAFARI_VERSION=${MIN_SAFARI_VERSION} \ -s MIN_CHROME_VERSION=${MIN_CHROME_VERSION} \ diff --git a/emcc.c b/emcc.c index ae90f3c..81abc19 100644 --- a/emcc.c +++ b/emcc.c @@ -62,6 +62,7 @@ extern void js_set_colour(int colour_number, const char *colour_string); extern void js_get_date_64(unsigned *p); extern void js_update_permalinks(const char *desc, const char *seed); extern void js_enable_undo_redo(bool undo, bool redo); +extern void js_post_move(int status); extern void js_update_key_labels(const char *lsk, const char *csk); extern void js_activate_timer(void); extern void js_deactivate_timer(void); @@ -127,6 +128,9 @@ void dlg_return_ival(int index, int val); void resize_puzzle(int w, int h); void restore_puzzle_size(int w, int h); void rescale_puzzle(void); +void wg_new_game(const char *params); +char *wg_params(void); +int wg_status(void); /* * Internal forward references. @@ -309,6 +313,7 @@ static void post_move(void) js_enable_undo_redo(midend_can_undo(me), midend_can_redo(me)); js_update_key_labels(midend_current_key_label(me, CURSOR_SELECT2), midend_current_key_label(me, CURSOR_SELECT)); + js_post_move(midend_status(me)); } /* @@ -809,6 +814,35 @@ void command(int n) } } +/* + * wasmga.me: the page this frame sits in picks the game parameters + * (its setup screen does what the Type menu did) and reads them back + * after loading a saved game. + */ +void wg_new_game(const char *params) +{ + if (params[0] && midend_game_id(me, params)) + return; + midend_new_game(me); + resize(); + midend_redraw(me); + update_permalinks(); + post_move(); +} + +char *wg_params(void) +{ + game_params *params = midend_get_params(me); + char *encoded = thegame.encode_params(params, true); + thegame.free_params(params); + return encoded; +} + +int wg_status(void) +{ + return midend_status(me); +} + char *get_text_format(void) { return midend_text_format(me); diff --git a/emcclib.js b/emcclib.js index c03283d..f695cef 100644 --- a/emcclib.js +++ b/emcclib.js @@ -51,7 +51,13 @@ mergeInto(LibraryManager.library, { * in a configuration dialog). */ js_error_box: function(ptr) { - alert(UTF8ToString(ptr)); + // wasmga.me: no alert boxes. A fatal error goes to the page's + // error screen; others are kept for wg.restore(). + var message = UTF8ToString(ptr); + if (message.startsWith("puzzle fatal error")) + throw new Error(message); + console.warn(message); + wg_error = message; }, /* @@ -93,6 +99,7 @@ mergeInto(LibraryManager.library, { * clicked. */ js_add_preset: function(menuid, ptr, value) { + if (gametypelist === null) return; var name = UTF8ToString(ptr); var item = document.createElement("li"); var label = document.createElement("label"); @@ -121,6 +128,7 @@ mergeInto(LibraryManager.library, { * js_add_preset or this function. */ js_add_preset_submenu: function(menuid, ptr, value) { + if (gametypelist === null) return 0; var name = UTF8ToString(ptr); var item = document.createElement("li"); // We still create a transparent tick element, even though it @@ -148,6 +156,7 @@ mergeInto(LibraryManager.library, { * dropdown. */ js_get_selected_preset: function() { + if (menuform === null) return -1; return menuform.elements["preset"].value; }, @@ -159,7 +168,7 @@ mergeInto(LibraryManager.library, { * which turn out to exactly match a preset). */ js_select_preset: function(n) { - menuform.elements["preset"].value = n; + if (menuform !== null) menuform.elements["preset"].value = n; }, /* @@ -235,6 +244,15 @@ mergeInto(LibraryManager.library, { * Set the enabled/disabled states of the undo and redo buttons, * after a move. */ + /* + * void js_post_move(int status); + * + * wasmga.me: tells the page midend_status() after every move. + */ + js_post_move: function(status) { + if (wg.onstatus) wg.onstatus(status); + }, + js_enable_undo_redo: function(undo, redo) { disable_menu_item(undo_button, (undo == 0)); disable_menu_item(redo_button, (redo == 0)); @@ -823,11 +841,10 @@ mergeInto(LibraryManager.library, { js_save_prefs: function(buf) { var prefsdata = UTF8ToString(buf); try { - localStorage.setItem(location.pathname + " preferences", prefsdata); + localStorage.setItem(location.pathname + location.search + " preferences", prefsdata); } catch (error) { // Tell the user their preferences have not been saved. console.error(error); - alert("Saving of preferences failed: " + error.message); } }, @@ -856,7 +873,7 @@ mergeInto(LibraryManager.library, { // Load saved preferences if they exist. try { load_prefs_from_string( - localStorage.getItem(location.pathname + " preferences")); + localStorage.getItem(location.pathname + location.search + " preferences")); } catch (error) { // Log the error but otherwise pretend the settings were // absent. diff --git a/emccpre.js b/emccpre.js index dc3b367..6c6a43e 100644 --- a/emccpre.js +++ b/emccpre.js @@ -21,9 +21,9 @@ // Error handler to make any failures from here on visible to the // user, maybe. -window.addEventListener("error", function (e) { - alert(e.message); -}); +// +// wasmga.me: not here. The page this frame sits in shows errors on its +// own error screen. // To avoid flicker while doing complicated drawing, we use two // canvases, the same size. One is actually on the web page, and the @@ -100,6 +100,9 @@ var Module = { // Pass argv[1] as the fragment identifier (so that permalinks of // the form puzzle.html#game-id can launch the specified id). 'arguments': [decodeURIComponent(location.hash)], + // wasmga.me: the play page has downloaded the puzzle's .wasm already + // and hands it over here, so it isn't fetched again. + 'wasmBinary': window.puzzleWasm, 'noExitRuntime': true }; @@ -198,6 +201,58 @@ var containing_div = document.getElementById("puzzlecanvascontain"); // necessarily the same as window.devicePixelRatio. var fe_scale = 1; +// wasmga.me: what the page this frame sits in reads and drives. +// wg_paused stops input while the page's own screens are up; wg_error +// keeps the last message the puzzle would have shown in an alert box. +var wg_paused = false; +var wg_error = null; +var wg = { + // Called after every move with midend_status(): 1 solved, -1 lost. + onstatus: null, + // The game in progress as midend_serialise() writes it (text). + snapshot: function() { + var ptr = Module.ccall('get_save_file', 'number', [], []); + var text = UTF8ToString(ptr); + Module.ccall('free_save_file', 'void', ['number'], [ptr]); + return text; + }, + // Loads a game snapshot() wrote; false if the puzzle refused it. + restore: function(text) { + var bytes = new TextEncoder().encode(text), pos = 0; + wg_error = null; + savefile_read_callback = function(buf, len) { + if (pos + len > bytes.length) + return false; + writeArrayToMemory(bytes.subarray(pos, pos + len), buf); + pos += len; + return true; + }; + Module.ccall('load_game', 'void', [], []); + savefile_read_callback = null; + return wg_error === null; + }, + // A new game with these parameters (as a game ID gives them + // before its colon, such as "10x10"), or the current ones if empty. + newGame: function(params) { + Module.ccall('wg_new_game', 'void', ['string'], [params || ""]); + }, + // The current game's parameters, as newGame() takes them. + params: function() { + var ptr = Module.ccall('wg_params', 'number', [], []); + var params = UTF8ToString(ptr); + Module.ccall('free_save_file', 'void', ['number'], [ptr]); + return params; + }, + status: function() { + return Module.ccall('wg_status', 'number', [], []); + }, + restart: function() { command(6); }, + undo: function() { command(7); }, + redo: function() { command(8); }, + setPaused: function(on) { wg_paused = on; }, + canvas: function() { return onscreen_canvas; } +}; + // Helper function to find the absolute position of a given DOM // element on a page, by iterating upwards through the DOM finding // each element's offset from its parent, and thus calculating the @@ -251,7 +306,7 @@ function canvas_set_font(ctx, size, monospaced) { // Enable and disable items in the CSS menus. function disable_menu_item(item, disabledFlag) { - item.disabled = disabledFlag; + if (item) item.disabled = disabledFlag; } // Dialog-box functions called from both C and JS. @@ -352,9 +407,66 @@ function initPuzzle() { // that if we managed to receive a pointerdown event, // Element.setPointerCapture() is available. onscreen_canvas.setPointerCapture(event.pointerId); + if (event.pointerType == "touch") + touchdown(event); + } + // wasmga.me: touch screens send no mouse events while a finger + // drags, and have no right button. So a finger is a mouse here: + // a tap is a left click, a drag a left drag, and a finger held + // still for a moment presses the right button (and drags with it). + var touch = null; + function touchdown(event) { + event.preventDefault(); + if (touch !== null || wg_paused) + return; + var xy = canvas_mouse_coords(event, onscreen_canvas); + touch = {id: event.pointerId, x: xy.x, y: xy.y, + px: event.clientX, py: event.clientY, button: null}; + touch.timer = setTimeout(function() { + if (touch === null || touch.button !== null) + return; + touch.button = 2; + mousedown(touch.x, touch.y, 2); + if (navigator.vibrate) + navigator.vibrate(15); + }, 400); } + onscreen_canvas.addEventListener("pointermove", function(event) { + if (touch === null || event.pointerId !== touch.id) + return; + event.preventDefault(); + var xy = canvas_mouse_coords(event, onscreen_canvas); + if (touch.button === null) { + if (Math.hypot(event.clientX - touch.px, + event.clientY - touch.py) < 8) + return; + clearTimeout(touch.timer); + touch.button = 0; + mousedown(touch.x, touch.y, 0); + } + mousemove(xy.x, xy.y, 1 << touch.button); + }); + function touchup(event) { + if (touch === null || event.pointerId !== touch.id) + return; + event.preventDefault(); + clearTimeout(touch.timer); + var xy = canvas_mouse_coords(event, onscreen_canvas); + if (touch.button === null) { + touch.button = 0; + mousedown(touch.x, touch.y, 0); + } + mouseup(xy.x, xy.y, touch.button); + touch = null; + } + onscreen_canvas.addEventListener("pointerup", touchup); + onscreen_canvas.addEventListener("pointercancel", touchup); + // Mouse events a browser makes up from a tap would click twice. + onscreen_canvas.addEventListener("touchstart", function(event) { + if (event.cancelable) event.preventDefault(); + }, {passive: false}); onscreen_canvas.onmousedown = function(event) { - if (event.button >= 3) + if (event.button >= 3 || wg_paused) return; var xy = canvas_mouse_coords(event, onscreen_canvas); @@ -403,6 +515,9 @@ function initPuzzle() { var key = Module.cwrap('key', 'boolean', ['number', 'string', 'string', 'number', 'number', 'number']); onscreen_canvas.onkeydown = function(event) { + // wasmga.me: New game and Quit belong to the page. + if (wg_paused || (!event.ctrlKey && /^[nNqQ]$/.test(event.key))) + return; if (key(event.keyCode, event.key, event.char, event.location, event.shiftKey ? 1 : 0, event.ctrlKey ? 1 : 0)) event.preventDefault(); @@ -621,7 +736,7 @@ function initPuzzle() { event.preventDefault(); event.stopPropagation(); } - menuform.addEventListener("keydown", menukey); + if (menuform) menuform.addEventListener("keydown", menukey); // Open documentation links within the application in KaiOS. for (var elem of document.querySelectorAll("#gamemenu a[href]")) { @@ -677,7 +792,7 @@ function initPuzzle() { document.addEventListener("keydown", function(event) { // Key to open the menu on KaiOS. - if ((event.key == "SoftRight" || event.key == "F10") && + if ((event.key == "SoftRight" || event.key == "F10") && menuform && !menuform.contains(document.activeElement)) { menuform.querySelector("li div, li button").focus(); event.preventDefault();