chiark / gitweb /
Forbid undo of new-game if it would change the params.
[sgt-puzzles.git] / emcc.c
diff --git a/emcc.c b/emcc.c
index 2a5f788e403486d883ae2a96a1ce4b4b733db931..23ab333f5db0cb58b7579b10b9f974d4e45f9c95 100644 (file)
--- a/emcc.c
+++ b/emcc.c
@@ -3,7 +3,10 @@
  * end for Puzzles.
  *
  * The Javascript parts of this system live in emcclib.js and
- * emccpre.js.
+ * emccpre.js. It also depends on being run in the context of a web
+ * page containing an appropriate collection of bits and pieces (a
+ * canvas, some buttons and links etc), which is generated for each
+ * puzzle by the script html/jspage.pl.
  */
 
 /*
  *    by using the DOM File API to ask the user to select a file and
  *    permit us to see its contents.
  *
- *  - it ought to be possible to make the puzzle canvases resizable,
- *    by superimposing some kind of draggable resize handle. Also I
- *    quite like the idea of having a few buttons for standard sizes:
- *    reset to default size, maximise to the browser window dimensions
- *    (if we can find those out), and perhaps even go full-screen.
- *
  *  - I should think about whether these webified puzzles can support
  *    touchscreen-based tablet browsers (assuming there are any that
  *    can cope with the reasonably modern JS and run it fast enough to
  *    that using whatever they normally use to print PDFs!)
  */
 
+#include <assert.h>
+#include <stdio.h>
 #include <string.h>
+#include <stdarg.h>
 
 #include "puzzles.h"
 
@@ -61,7 +61,8 @@ extern void js_debug(const char *);
 extern void js_error_box(const char *message);
 extern void js_remove_type_dropdown(void);
 extern void js_remove_solve_button(void);
-extern void js_add_preset(const char *name);
+extern void js_add_preset(int menuid, const char *name, int value);
+extern int js_add_preset_submenu(int menuid, const char *name);
 extern int js_get_selected_preset(void);
 extern void js_select_preset(int n);
 extern void js_get_date_64(unsigned *p);
@@ -135,6 +136,27 @@ void fatal(char *fmt, ...)
     js_error_box(buf);
 }
 
+void debug_printf(char *fmt, ...)
+{
+    char buf[512];
+    va_list ap;
+    va_start(ap, fmt);
+    vsnprintf(buf, sizeof(buf), fmt, ap);
+    va_end(ap);
+    js_debug(buf);
+}
+
+/*
+ * Helper function that makes it easy to test strings that might be
+ * NULL.
+ */
+int strnullcmp(const char *a, const char *b)
+{
+    if (a == NULL || b == NULL)
+        return a != NULL ? +1 : b != NULL ? -1 : 0;
+    return strcmp(a, b);
+}
+
 /*
  * HTMLish names for the colours allocated by the puzzle.
  */
@@ -169,10 +191,12 @@ void timer_callback(double tplus)
 }
 
 /* ----------------------------------------------------------------------
- * Helper function to resize the canvas, and variables to remember its
- * size for other functions (e.g. trimming blitter rectangles).
+ * Helper functions to resize the canvas, and variables to remember
+ * its size for other functions (e.g. trimming blitter rectangles).
  */
 static int canvas_w, canvas_h;
+
+/* Called when we resize as a result of changing puzzle settings */
 static void resize(void)
 {
     int w, h;
@@ -183,6 +207,26 @@ static void resize(void)
     canvas_h = h;
 }
 
+/* Called from JS when the user uses the resize handle */
+void resize_puzzle(int w, int h)
+{
+    midend_size(me, &w, &h, TRUE);
+    if (canvas_w != w || canvas_h != h) { 
+        js_canvas_set_size(w, h);
+        canvas_w = w;
+        canvas_h = h;
+        midend_force_redraw(me);
+    }
+}
+
+/* Called from JS when the user uses the restore button */
+void restore_puzzle_size(int w, int h)
+{
+    midend_reset_tilesize(me);
+    resize();
+    midend_force_redraw(me);
+}
+
 /*
  * HTML doesn't give us a default frontend colour of its own, so we
  * just make up a lightish grey ourselves.
@@ -232,67 +276,68 @@ void mousemove(int x, int y, int buttons)
 /*
  * Keyboard handler called from JS.
  */
-void key(int keycode, int charcode, int shift, int ctrl)
+void key(int keycode, int charcode, const char *key, const char *chr,
+         int shift, int ctrl)
 {
     int keyevent = -1;
-    if (charcode != 0) {
-        keyevent = charcode & (ctrl ? 0x1F : 0xFF);
-    } else {
-        switch (keycode) {
-          case 8:
-            keyevent = '\177';         /* backspace */
-            break;
-          case 13:
-            keyevent = 13;             /* return */
-            break;
-          case 37:
-            keyevent = CURSOR_LEFT;
-            break;
-          case 38:
-            keyevent = CURSOR_UP;
-            break;
-          case 39:
-            keyevent = CURSOR_RIGHT;
-            break;
-          case 40:
-            keyevent = CURSOR_DOWN;
-            break;
-            /*
-             * We interpret Home, End, PgUp and PgDn as numeric keypad
-             * controls regardless of whether they're the ones on the
-             * numeric keypad (since we can't tell). The effect of
-             * this should only be that the non-numeric-pad versions
-             * of those keys generate directions in 8-way movement
-             * puzzles like Cube and Inertia.
-             */
-          case 35:                     /* End */
-            keyevent = MOD_NUM_KEYPAD | '1';
-            break;
-          case 34:                     /* PgDn */
-            keyevent = MOD_NUM_KEYPAD | '3';
-            break;
-          case 36:                     /* Home */
-            keyevent = MOD_NUM_KEYPAD | '7';
-            break;
-          case 33:                     /* PgUp */
-            keyevent = MOD_NUM_KEYPAD | '9';
-            break;
-          case 96: case 97: case 98: case 99: case 100:
-          case 101: case 102: case 103: case 104: case 105:
-            keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
-            break;
-          default:
-            /* not a key we care about */
-            return;
-        }
+
+    if (!strnullcmp(key, "Backspace") || !strnullcmp(key, "Del") ||
+        keycode == 8 || keycode == 46) {
+        keyevent = 127;                /* Backspace / Delete */
+    } else if (!strnullcmp(key, "Enter") || keycode == 13) {
+        keyevent = 13;             /* return */
+    } else if (!strnullcmp(key, "Left") || keycode == 37) {
+        keyevent = CURSOR_LEFT;
+    } else if (!strnullcmp(key, "Up") || keycode == 38) {
+        keyevent = CURSOR_UP;
+    } else if (!strnullcmp(key, "Right") || keycode == 39) {
+        keyevent = CURSOR_RIGHT;
+    } else if (!strnullcmp(key, "Down") || keycode == 40) {
+        keyevent = CURSOR_DOWN;
+    } else if (!strnullcmp(key, "End") || keycode == 35) {
+        /*
+         * We interpret Home, End, PgUp and PgDn as numeric keypad
+         * controls regardless of whether they're the ones on the
+         * numeric keypad (since we can't tell). The effect of
+         * this should only be that the non-numeric-pad versions
+         * of those keys generate directions in 8-way movement
+         * puzzles like Cube and Inertia.
+         */
+        keyevent = MOD_NUM_KEYPAD | '1';
+    } else if (!strnullcmp(key, "PageDown") || keycode==34) {
+        keyevent = MOD_NUM_KEYPAD | '3';
+    } else if (!strnullcmp(key, "Home") || keycode==36) {
+        keyevent = MOD_NUM_KEYPAD | '7';
+    } else if (!strnullcmp(key, "PageUp") || keycode==33) {
+        keyevent = MOD_NUM_KEYPAD | '9';
+    } else if (shift && ctrl && (keycode & 0x1F) == 26) {
+        keyevent = UI_REDO;
+    } else if (chr && chr[0] && !chr[1]) {
+        keyevent = chr[0] & 0xFF;
+    } else if (keycode >= 96 && keycode < 106) {
+        keyevent = MOD_NUM_KEYPAD | ('0' + keycode - 96);
+    } else if (keycode >= 65 && keycode <= 90) {
+        keyevent = keycode + (shift ? 0 : 32);
+    } else if (keycode >= 48 && keycode <= 57) {
+        keyevent = keycode;
+    } else if (keycode == 32) {        /* space / CURSOR_SELECT2 */
+        keyevent = keycode;
     }
-    if (shift && keyevent >= 0x100)
-        keyevent |= MOD_SHFT;
-    if (ctrl && keyevent >= 0x100)
-        keyevent |= MOD_CTRL;
 
-    midend_process_key(me, 0, 0, keyevent);
-    update_undo_redo();
+    if (keyevent >= 0) {
+        if (shift && (keyevent >= 0x100 && !IS_UI_FAKE_KEY(keyevent)))
+            keyevent |= MOD_SHFT;
+
+        if (ctrl && !IS_UI_FAKE_KEY(keyevent)) {
+            if (keyevent >= 0x100)
+                keyevent |= MOD_CTRL;
+            else
+                keyevent &= 0x1F;
+        }
+
+        midend_process_key(me, 0, 0, keyevent);
+        update_undo_redo();
+    }
 }
 
 /*
@@ -310,10 +355,10 @@ static void update_permalinks(void)
 }
 
 /*
- * Callback from the midend if Mines supersedes its game description,
- * so we can update the permalinks.
+ * Callback from the midend when the game ids change, so we can update
+ * the permalinks.
  */
-static void desc_changed(void *ignored)
+static void ids_changed(void *ignored)
 {
     update_permalinks();
 }
@@ -416,27 +461,30 @@ static void js_blitter_free(void *handle, blitter *bl)
 
 static void trim_rect(int *x, int *y, int *w, int *h)
 {
+    int x0, x1, y0, y1;
+
     /*
      * Reduce the size of the copied rectangle to stop it going
      * outside the bounds of the canvas.
      */
-    if (*x < 0) {
-        *w += *x;
-        *x = 0;
-    }
-    if (*y < 0) {
-        *h += *y;
-        *y = 0;
-    }
-    if (*w > canvas_w - *x)
-        *w = canvas_w - *x;
-    if (*h > canvas_h - *y)
-        *h = canvas_h - *y;
 
-    if (*w < 0)
-        *w = 0;
-    if (*h < 0)
-        *h = 0;
+    /* Transform from x,y,w,h form into coordinates of all edges */
+    x0 = *x;
+    y0 = *y;
+    x1 = *x + *w;
+    y1 = *y + *h;
+
+    /* Clip each coordinate at both extremes of the canvas */
+    x0 = (x0 < 0 ? 0 : x0 > canvas_w ? canvas_w : x0);
+    x1 = (x1 < 0 ? 0 : x1 > canvas_w ? canvas_w : x1);
+    y0 = (y0 < 0 ? 0 : y0 > canvas_h ? canvas_h : y0);
+    y1 = (y1 < 0 ? 0 : y1 > canvas_h ? canvas_h : y1); 
+
+    /* Transform back into x,y,w,h to return */
+    *x = x0;
+    *y = y0;
+    *w = x1 - x0;
+    *h = y1 - y0;
 }
 
 static void js_blitter_save(void *handle, blitter *bl, int x, int y)
@@ -458,7 +506,8 @@ static void js_blitter_load(void *handle, blitter *bl, int x, int y)
 static void js_draw_update(void *handle, int x, int y, int w, int h)
 {
     trim_rect(&x, &y, &w, &h);
-    js_canvas_draw_update(x, y, w, h);
+    if (w > 0 && h > 0)
+        js_canvas_draw_update(x, y, w, h);
 }
 
 static void js_end_draw(void *handle)
@@ -503,7 +552,31 @@ const struct drawing_api js_drawing = {
  * Presets and game-configuration dialog support.
  */
 static game_params **presets;
-static int custom_preset;
+static int npresets;
+int have_presets_dropdown;
+
+void populate_js_preset_menu(int menuid, struct preset_menu *menu)
+{
+    int i;
+    for (i = 0; i < menu->n_entries; i++) {
+        struct preset_menu_entry *entry = &menu->entries[i];
+        if (entry->params) {
+            presets[entry->id] = entry->params;
+            js_add_preset(menuid, entry->title, entry->id);
+        } else {
+            int js_submenu = js_add_preset_submenu(menuid, entry->title);
+            populate_js_preset_menu(js_submenu, entry->submenu);
+        }
+    }
+}
+
+void select_appropriate_preset(void)
+{
+    if (have_presets_dropdown) {
+        int preset = midend_which_preset(me);
+        js_select_preset(preset < 0 ? -1 : preset);
+    }
+}
 
 static config_item *cfg = NULL;
 static int cfg_which;
@@ -578,13 +651,10 @@ static void cfg_end(int use_results)
              * New settings are fine; start a new game and close the
              * dialog.
              */
-            int preset = midend_which_preset(me);
-            js_select_preset(preset < 0 ? custom_preset : preset);
-
+            select_appropriate_preset();
             midend_new_game(me);
             resize();
             midend_redraw(me);
-            update_permalinks();
             free_cfg(cfg);
             js_dialog_cleanup();
         }
@@ -602,8 +672,7 @@ static void cfg_end(int use_results)
          * js_add_preset in emcclib.js - so you won't even be able to
          * select Custom without a faffy workaround.)
          */
-        int preset = midend_which_preset(me);
-        js_select_preset(preset < 0 ? custom_preset : preset);
+        select_appropriate_preset();
 
         free_cfg(cfg);
         js_dialog_cleanup();
@@ -626,7 +695,7 @@ void command(int n)
       case 2:                          /* game parameter dropdown changed */
         {
             int i = js_get_selected_preset();
-            if (i == custom_preset) {
+            if (i < 0) {
                 /*
                  * The user selected 'Custom', so launch the config
                  * box.
@@ -638,13 +707,14 @@ void command(int n)
                  * The user selected a preset, so just switch straight
                  * to that.
                  */
+                assert(i < npresets);
                 midend_set_params(me, presets[i]);
                 midend_new_game(me);
-                update_permalinks();
                 resize();
                 midend_redraw(me);
                 update_undo_redo();
                 js_focus_canvas();
+                select_appropriate_preset();
             }
         }
         break;
@@ -657,7 +727,7 @@ void command(int n)
         update_undo_redo();
         break;
       case 5:                          /* New Game */
-        midend_process_key(me, 0, 0, 'n');
+        midend_process_key(me, 0, 0, UI_NEWGAME);
         update_undo_redo();
         js_focus_canvas();
         break;
@@ -667,12 +737,12 @@ void command(int n)
         js_focus_canvas();
         break;
       case 7:                          /* Undo */
-        midend_process_key(me, 0, 0, 'u');
+        midend_process_key(me, 0, 0, UI_UNDO);
         update_undo_redo();
         js_focus_canvas();
         break;
       case 8:                          /* Redo */
-        midend_process_key(me, 0, 0, 'r');
+        midend_process_key(me, 0, 0, UI_REDO);
         update_undo_redo();
         js_focus_canvas();
         break;
@@ -688,6 +758,83 @@ void command(int n)
     }
 }
 
+/* ----------------------------------------------------------------------
+ * Called from JS to prepare a save-game file, and free one after it's
+ * been used.
+ */
+
+struct savefile_write_ctx {
+    char *buffer;
+    size_t pos;
+};
+
+static void savefile_write(void *vctx, void *buf, int len)
+{
+    struct savefile_write_ctx *ctx = (struct savefile_write_ctx *)vctx;
+    if (ctx->buffer)
+        memcpy(ctx->buffer + ctx->pos, buf, len);
+    ctx->pos += len;
+}
+
+char *get_save_file(void)
+{
+    struct savefile_write_ctx ctx;
+    size_t size;
+
+    /* First pass, to count up the size */
+    ctx.buffer = NULL;
+    ctx.pos = 0;
+    midend_serialise(me, savefile_write, &ctx);
+    size = ctx.pos;
+
+    /* Second pass, to actually write out the data */
+    ctx.buffer = snewn(size, char);
+    ctx.pos = 0;
+    midend_serialise(me, savefile_write, &ctx);
+    assert(ctx.pos == size);
+
+    return ctx.buffer;
+}
+
+void free_save_file(char *buffer)
+{
+    sfree(buffer);
+}
+
+struct savefile_read_ctx {
+    const char *buffer;
+    int len_remaining;
+};
+
+static int savefile_read(void *vctx, void *buf, int len)
+{
+    struct savefile_read_ctx *ctx = (struct savefile_read_ctx *)vctx;
+    if (ctx->len_remaining < len)
+        return FALSE;
+    memcpy(buf, ctx->buffer, len);
+    ctx->len_remaining -= len;
+    ctx->buffer += len;
+    return TRUE;
+}
+
+void load_game(const char *buffer, int len)
+{
+    struct savefile_read_ctx ctx;
+    const char *err;
+
+    ctx.buffer = buffer;
+    ctx.len_remaining = len;
+    err = midend_deserialise(me, savefile_read, &ctx);
+
+    if (err) {
+        js_error_box(err);
+    } else {
+        select_appropriate_preset();
+        resize();
+        midend_redraw(me);
+    }
+}
+
 /* ----------------------------------------------------------------------
  * Setup function called at page load time. It's called main() because
  * that's the most convenient thing in Emscripten, but it's not main()
@@ -733,21 +880,28 @@ int main(int argc, char **argv)
 
     /*
      * Set up the game-type dropdown with presets and/or the Custom
-     * option. We remember the index of the Custom option (as
-     * custom_preset) so that we can easily treat it specially when
-     * it's selected.
+     * option.
      */
-    custom_preset = midend_num_presets(me);
-    presets = snewn(custom_preset, game_params *);
-    for (i = 0; i < custom_preset; i++) {
-        char *name;
-        midend_fetch_preset(me, i, &name, &presets[i]);
-        js_add_preset(name);
+    {
+        struct preset_menu *menu = midend_get_presets(me, &npresets);
+        presets = snewn(npresets, game_params *);
+        for (i = 0; i < npresets; i++)
+            presets[i] = NULL;
+
+        populate_js_preset_menu(0, menu);
+
+        if (thegame.can_configure)
+            js_add_preset(0, "Custom", -1);
+
+        have_presets_dropdown = TRUE;
+
+        /*
+         * Now ensure the appropriate element of the presets menu
+         * starts off selected, in case it isn't the first one in the
+         * list (e.g. Slant).
+         */
+        select_appropriate_preset();
     }
-    if (thegame.can_configure)
-        js_add_preset(NULL);           /* the 'Custom' entry in the dropdown */
-    else if (custom_preset == 0)
-        js_remove_type_dropdown();
 
     /*
      * Remove the Solve button if the game doesn't support it.
@@ -771,11 +925,11 @@ int main(int argc, char **argv)
     }
 
     /*
-     * Request notification if a puzzle (hopefully only ever Mines)
-     * supersedes its game description, so that we can proactively
-     * update the permalink.
+     * Request notification when the game ids change (e.g. if the user
+     * presses 'n', and also when Mines supersedes its game
+     * description), so that we can proactively update the permalink.
      */
-    midend_request_desc_changes(me, desc_changed, NULL);
+    midend_request_id_changes(me, ids_changed, NULL);
 
     /*
      * Draw the puzzle's initial state, and set up the permalinks and