X-Git-Url: https://git.distorted.org.uk/~mdw/sgt/puzzles/blobdiff_plain/44f0599fe04cb578fb64bf17e54a7c19611f40c4..HEAD:/midend.c diff --git a/midend.c b/midend.c index 691c643..53dca21 100644 --- a/midend.c +++ b/midend.c @@ -25,13 +25,13 @@ struct midend_state_entry { int movetype; }; -struct midend_data { +struct midend { frontend *frontend; random_state *random; const game *ourgame; game_params **presets; - char **preset_names; + char **preset_names, **preset_encodings; int npresets, presetsize; /* @@ -76,9 +76,11 @@ struct midend_data { float elapsed; char *laststatus; + drawing *drawing; + int pressed_mouse_button; - int tilesize, winwidth, winheight; + int preferred_tilesize, tilesize, winwidth, winheight; }; #define ensure(me) do { \ @@ -89,9 +91,10 @@ struct midend_data { } \ } while (0) -midend_data *midend_new(frontend *fe, const game *ourgame) +midend *midend_new(frontend *fe, const game *ourgame, + const drawing_api *drapi, void *drhandle) { - midend_data *me = snew(midend_data); + midend *me = snew(midend); void *randseed; int randseedsize; @@ -99,10 +102,26 @@ midend_data *midend_new(frontend *fe, const game *ourgame) me->frontend = fe; me->ourgame = ourgame; - me->random = random_init(randseed, randseedsize); + me->random = random_new(randseed, randseedsize); me->nstates = me->statesize = me->statepos = 0; me->states = NULL; me->params = ourgame->default_params(); + /* + * Allow environment-based changing of the default settings by + * defining a variable along the lines of `NET_DEFAULT=25x25w' + * in which the value is an encoded parameter string. + */ + { + char buf[80], *e; + int j, k; + sprintf(buf, "%s_DEFAULT", me->ourgame->name); + for (j = k = 0; buf[j]; j++) + if (!isspace((unsigned char)buf[j])) + buf[k++] = toupper((unsigned char)buf[j]); + buf[k] = '\0'; + if ((e = getenv(buf)) != NULL) + me->ourgame->decode_params(me->params, e); + } me->curparams = NULL; me->desc = me->privdesc = NULL; me->seedstr = NULL; @@ -112,6 +131,7 @@ midend_data *midend_new(frontend *fe, const game *ourgame) me->oldstate = NULL; me->presets = NULL; me->preset_names = NULL; + me->preset_encodings = NULL; me->npresets = me->presetsize = 0; me->anim_time = me->anim_pos = 0.0F; me->flash_time = me->flash_pos = 0.0F; @@ -122,13 +142,51 @@ midend_data *midend_new(frontend *fe, const game *ourgame) me->timing = FALSE; me->elapsed = 0.0F; me->tilesize = me->winwidth = me->winheight = 0; + if (drapi) + me->drawing = drawing_new(drapi, me, drhandle); + else + me->drawing = NULL; + + me->preferred_tilesize = ourgame->preferred_tilesize; + { + /* + * Allow an environment-based override for the default tile + * size by defining a variable along the lines of + * `NET_TILESIZE=15'. + */ + + char buf[80], *e; + int j, k, ts; + + sprintf(buf, "%s_TILESIZE", me->ourgame->name); + for (j = k = 0; buf[j]; j++) + if (!isspace((unsigned char)buf[j])) + buf[k++] = toupper((unsigned char)buf[j]); + buf[k] = '\0'; + if ((e = getenv(buf)) != NULL && sscanf(e, "%d", &ts) == 1 && ts > 0) + me->preferred_tilesize = ts; + } sfree(randseed); return me; } -static void midend_free_game(midend_data *me) +const game *midend_which_game(midend *me) +{ + return me->ourgame; +} + +static void midend_purge_states(midend *me) +{ + while (me->nstates > me->statepos) { + me->ourgame->free_game(me->states[--me->nstates].state); + if (me->states[me->nstates].movestr) + sfree(me->states[me->nstates].movestr); + } +} + +static void midend_free_game(midend *me) { while (me->nstates > 0) { me->nstates--; @@ -137,15 +195,17 @@ static void midend_free_game(midend_data *me) } if (me->drawstate) - me->ourgame->free_drawstate(me->drawstate); + me->ourgame->free_drawstate(me->drawing, me->drawstate); } -void midend_free(midend_data *me) +void midend_free(midend *me) { int i; midend_free_game(me); + if (me->drawing) + drawing_free(me->drawing); random_free(me->random); sfree(me->states); sfree(me->desc); @@ -157,9 +217,11 @@ void midend_free(midend_data *me) for (i = 0; i < me->npresets; i++) { sfree(me->presets[i]); sfree(me->preset_names[i]); + sfree(me->preset_encodings[i]); } sfree(me->presets); sfree(me->preset_names); + sfree(me->preset_encodings); } if (me->ui) me->ourgame->free_ui(me->ui); @@ -169,7 +231,7 @@ void midend_free(midend_data *me) sfree(me); } -static void midend_size_new_drawstate(midend_data *me) +static void midend_size_new_drawstate(midend *me) { /* * Don't even bother, if we haven't worked out our tile size @@ -178,29 +240,44 @@ static void midend_size_new_drawstate(midend_data *me) if (me->tilesize > 0) { me->ourgame->compute_size(me->params, me->tilesize, &me->winwidth, &me->winheight); - me->ourgame->set_size(me->drawstate, me->params, me->tilesize); + me->ourgame->set_size(me->drawing, me->drawstate, + me->params, me->tilesize); } } -void midend_size(midend_data *me, int *x, int *y, int expand) +void midend_size(midend *me, int *x, int *y, int user_size) { int min, max; int rx, ry; /* + * We can't set the size on the same drawstate twice. So if + * we've already sized one drawstate, we must throw it away and + * create a new one. + */ + if (me->drawstate && me->tilesize > 0) { + me->ourgame->free_drawstate(me->drawing, me->drawstate); + me->drawstate = me->ourgame->new_drawstate(me->drawing, + me->states[0].state); + } + + /* * Find the tile size that best fits within the given space. If - * `expand' is TRUE, we must actually find the _largest_ such - * tile size; otherwise, we bound above at the game's preferred - * tile size. + * `user_size' is TRUE, we must actually find the _largest_ such + * tile size, in order to get as close to the user's explicit + * request as possible; otherwise, we bound above at the game's + * preferred tile size, so that the game gets what it wants + * provided that this doesn't break the constraint from the + * front-end (which is likely to be a screen size or similar). */ - if (expand) { + if (user_size) { max = 1; do { max *= 2; me->ourgame->compute_size(me->params, max, &rx, &ry); } while (rx <= *x && ry <= *y); } else - max = me->ourgame->preferred_tilesize + 1; + max = me->preferred_tilesize + 1; min = 1; /* @@ -223,37 +300,49 @@ void midend_size(midend_data *me, int *x, int *y, int expand) */ me->tilesize = min; + if (user_size) + /* If the user requested a change in size, make it permanent. */ + me->preferred_tilesize = me->tilesize; midend_size_new_drawstate(me); *x = me->winwidth; *y = me->winheight; } -void midend_set_params(midend_data *me, game_params *params) +int midend_tilesize(midend *me) { return me->tilesize; } + +void midend_set_params(midend *me, game_params *params) { me->ourgame->free_params(me->params); me->params = me->ourgame->dup_params(params); } -static void midend_set_timer(midend_data *me) +game_params *midend_get_params(midend *me) +{ + return me->ourgame->dup_params(me->params); +} + +static void midend_set_timer(midend *me) { me->timing = (me->ourgame->is_timed && - me->ourgame->timing_state(me->states[me->statepos-1].state)); + me->ourgame->timing_state(me->states[me->statepos-1].state, + me->ui)); if (me->timing || me->flash_time || me->anim_time) activate_timer(me->frontend); else deactivate_timer(me->frontend); } -void midend_force_redraw(midend_data *me) +void midend_force_redraw(midend *me) { if (me->drawstate) - me->ourgame->free_drawstate(me->drawstate); - me->drawstate = me->ourgame->new_drawstate(me->states[0].state); + me->ourgame->free_drawstate(me->drawing, me->drawstate); + me->drawstate = me->ourgame->new_drawstate(me->drawing, + me->states[0].state); midend_size_new_drawstate(me); midend_redraw(me); } -void midend_new_game(midend_data *me) +void midend_new_game(midend *me) { midend_free_game(me); @@ -278,9 +367,9 @@ void midend_new_game(midend_data *me) char newseed[16]; int i; newseed[15] = '\0'; - newseed[0] = '1' + random_upto(me->random, 9); + newseed[0] = '1' + (char)random_upto(me->random, 9); for (i = 1; i < 15; i++) - newseed[i] = '0' + random_upto(me->random, 10); + newseed[i] = '0' + (char)random_upto(me->random, 10); sfree(me->seedstr); me->seedstr = dupstr(newseed); @@ -294,31 +383,132 @@ void midend_new_game(midend_data *me) sfree(me->aux_info); me->aux_info = NULL; - rs = random_init(me->seedstr, strlen(me->seedstr)); + rs = random_new(me->seedstr, strlen(me->seedstr)); + /* + * If this midend has been instantiated without providing a + * drawing API, it is non-interactive. This means that it's + * being used for bulk game generation, and hence we should + * pass the non-interactive flag to new_desc. + */ me->desc = me->ourgame->new_desc(me->curparams, rs, - &me->aux_info, TRUE); + &me->aux_info, (me->drawing != NULL)); me->privdesc = NULL; random_free(rs); } ensure(me); + + /* + * It might seem a bit odd that we're using me->params to + * create the initial game state, rather than me->curparams + * which is better tailored to this specific game and which we + * always know. + * + * It's supposed to be an invariant in the midend that + * me->params and me->curparams differ in no aspect that is + * important after generation (i.e. after new_desc()). By + * deliberately passing the _less_ specific of these two + * parameter sets, we provoke play-time misbehaviour in the + * case where a game has failed to encode a play-time parameter + * in the non-full version of encode_params(). + */ me->states[me->nstates].state = me->ourgame->new_game(me, me->params, me->desc); + + /* + * As part of our commitment to self-testing, test the aux + * string to make sure nothing ghastly went wrong. + */ + if (me->ourgame->can_solve && me->aux_info) { + game_state *s; + char *msg, *movestr; + + msg = NULL; + movestr = me->ourgame->solve(me->states[0].state, + me->states[0].state, + me->aux_info, &msg); + assert(movestr && !msg); + s = me->ourgame->execute_move(me->states[0].state, movestr); + assert(s); + me->ourgame->free_game(s); + sfree(movestr); + } + + /* + * Soak test, enabled by setting _TESTSOLVE in the + * environment. This causes an immediate attempt to re-solve the + * game without benefit of aux_info. The effect is that (at least + * on Unix) you can run 'FOO_TESTSOLVE=1 foo --generate 10000 + * #12345' and it will generate a lot of game ids and + * instantly pass each one back to the solver. + * + * (It's worth putting in an explicit seed in any such test, so + * you can repeat it to diagnose a problem if one comes up!) + */ + { + char buf[80]; + int j, k; + static int doing_test_solve = -1; + if (doing_test_solve < 0) { + sprintf(buf, "%s_TESTSOLVE", me->ourgame->name); + for (j = k = 0; buf[j]; j++) + if (!isspace((unsigned char)buf[j])) + buf[k++] = toupper((unsigned char)buf[j]); + buf[k] = '\0'; + if (getenv(buf)) { + /* + * Since this is used for correctness testing, it's + * helpful to have a visual acknowledgment that the + * user hasn't mistyped the environment variable name. + */ + fprintf(stderr, "Running solver soak tests\n"); + doing_test_solve = TRUE; + } else { + doing_test_solve = FALSE; + } + } + if (doing_test_solve) { + game_state *s; + char *msg, *movestr; + + msg = NULL; + movestr = me->ourgame->solve(me->states[0].state, + me->states[0].state, + NULL, &msg); + assert(movestr && !msg); + s = me->ourgame->execute_move(me->states[0].state, movestr); + assert(s); + me->ourgame->free_game(s); + sfree(movestr); + } + } + me->states[me->nstates].movestr = NULL; me->states[me->nstates].movetype = NEWGAME; me->nstates++; me->statepos = 1; - me->drawstate = me->ourgame->new_drawstate(me->states[0].state); + me->drawstate = me->ourgame->new_drawstate(me->drawing, + me->states[0].state); midend_size_new_drawstate(me); me->elapsed = 0.0F; - midend_set_timer(me); if (me->ui) me->ourgame->free_ui(me->ui); me->ui = me->ourgame->new_ui(me->states[0].state); + midend_set_timer(me); me->pressed_mouse_button = 0; } -static int midend_undo(midend_data *me) +int midend_can_undo(midend *me) +{ + return (me->statepos > 1); +} + +int midend_can_redo(midend *me) +{ + return (me->statepos < me->nstates); +} + +static int midend_undo(midend *me) { if (me->statepos > 1) { if (me->ui) @@ -332,7 +522,7 @@ static int midend_undo(midend_data *me) return 0; } -static int midend_redo(midend_data *me) +static int midend_redo(midend *me) { if (me->statepos < me->nstates) { if (me->ui) @@ -346,7 +536,7 @@ static int midend_redo(midend_data *me) return 0; } -static void midend_finish_move(midend_data *me) +static void midend_finish_move(midend *me) { float flashtime; @@ -379,7 +569,7 @@ static void midend_finish_move(midend_data *me) midend_set_timer(me); } -void midend_stop_anim(midend_data *me) +void midend_stop_anim(midend *me) { if (me->oldstate || me->anim_time != 0) { midend_finish_move(me); @@ -387,7 +577,7 @@ void midend_stop_anim(midend_data *me) } } -void midend_restart_game(midend_data *me) +void midend_restart_game(midend *me) { game_state *s; @@ -410,8 +600,7 @@ void midend_restart_game(midend_data *me) * Now enter the restarted state as the next move. */ midend_stop_anim(me); - while (me->nstates > me->statepos) - me->ourgame->free_game(me->states[--me->nstates].state); + midend_purge_states(me); ensure(me); me->states[me->nstates].state = s; me->states[me->nstates].movestr = dupstr(me->desc); @@ -427,11 +616,11 @@ void midend_restart_game(midend_data *me) midend_set_timer(me); } -static int midend_really_process_key(midend_data *me, int x, int y, int button) +static int midend_really_process_key(midend *me, int x, int y, int button) { game_state *oldstate = me->ourgame->dup_game(me->states[me->statepos - 1].state); - int special = FALSE, gotspecial = FALSE, ret = 1; + int type = MOVE, gottype = FALSE, ret = 1; float anim_time; game_state *s; char *movestr; @@ -449,8 +638,8 @@ static int midend_really_process_key(midend_data *me, int x, int y, int button) } else if (button == 'u' || button == 'u' || button == '\x1A' || button == '\x1F') { midend_stop_anim(me); - special = special(me->states[me->statepos-1].movetype); - gotspecial = TRUE; + type = me->states[me->statepos-1].movetype; + gottype = TRUE; if (!midend_undo(me)) goto done; } else if (button == 'r' || button == 'R' || @@ -458,6 +647,9 @@ static int midend_really_process_key(midend_data *me, int x, int y, int button) midend_stop_anim(me); if (!midend_redo(me)) goto done; + } else if (button == '\x13' && me->ourgame->can_solve) { + if (midend_solve(me)) + goto done; } else if (button == 'q' || button == 'Q' || button == '\x11') { ret = 0; goto done; @@ -479,11 +671,11 @@ static int midend_really_process_key(midend_data *me, int x, int y, int button) * state has been updated and a redraw is called for. */ midend_redraw(me); + midend_set_timer(me); goto done; } else if (s) { midend_stop_anim(me); - while (me->nstates > me->statepos) - me->ourgame->free_game(me->states[--me->nstates].state); + midend_purge_states(me); ensure(me); assert(movestr != NULL); me->states[me->nstates].state = s; @@ -491,18 +683,23 @@ static int midend_really_process_key(midend_data *me, int x, int y, int button) me->states[me->nstates].movetype = MOVE; me->statepos = ++me->nstates; me->dir = +1; + if (me->ui) + me->ourgame->changed_state(me->ui, + me->states[me->statepos-2].state, + me->states[me->statepos-1].state); } else { goto done; } } - if (!gotspecial) - special = special(me->states[me->statepos-1].movetype); + if (!gottype) + type = me->states[me->statepos-1].movetype; /* * See if this move requires an animation. */ - if (special) { + if (special(type) && !(type == SOLVE && + (me->ourgame->flags & SOLVE_ANIMATES))) { anim_time = 0; } else { anim_time = me->ourgame->anim_length(oldstate, @@ -528,7 +725,7 @@ static int midend_really_process_key(midend_data *me, int x, int y, int button) return ret; } -int midend_process_key(midend_data *me, int x, int y, int button) +int midend_process_key(midend *me, int x, int y, int button) { int ret = 1; @@ -597,6 +794,14 @@ int midend_process_key(midend_data *me, int x, int y, int button) * like a left click for the benefit of users of other * implementations. So the last of the above points is modified * in the presence of an (optional) button priority order. + * + * A further addition: we translate certain keyboard presses to + * cursor key 'select' buttons, so that a) frontends don't have + * to translate these themselves (like they do for CURSOR_UP etc), + * and b) individual games don't have to hard-code button presses + * of '\n' etc for keyboard-based cursors. The choice of buttons + * here could eventually be controlled by a runtime configuration + * option. */ if (IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) { if (me->pressed_mouse_button) { @@ -614,7 +819,7 @@ int midend_process_key(midend_data *me, int x, int y, int button) * If the new button has lower priority than the old one, * don't bother doing this. */ - if (me->ourgame->mouse_priorities & + if (me->ourgame->flags & BUTTON_BEATS(me->pressed_mouse_button, button)) return ret; /* just ignore it */ @@ -627,6 +832,23 @@ int midend_process_key(midend_data *me, int x, int y, int button) } /* + * Translate keyboard presses to cursor selection. + */ + if (button == '\n' || button == '\r') + button = CURSOR_SELECT; + if (button == ' ') + button = CURSOR_SELECT2; + + /* + * Normalise both backspace characters (8 and 127) to \b. Easier + * to do this once, here, than to require all front ends to + * carefully generate the same one - now each front end can + * generate whichever is easiest. + */ + if (button == '\177') + button = '\b'; + + /* * Now send on the event we originally received. */ ret = ret && midend_really_process_key(me, x, y, button); @@ -642,27 +864,42 @@ int midend_process_key(midend_data *me, int x, int y, int button) return ret; } -void midend_redraw(midend_data *me) +void midend_redraw(midend *me) { + assert(me->drawing); + if (me->statepos > 0 && me->drawstate) { - start_draw(me->frontend); + start_draw(me->drawing); if (me->oldstate && me->anim_time > 0 && me->anim_pos < me->anim_time) { assert(me->dir != 0); - me->ourgame->redraw(me->frontend, me->drawstate, me->oldstate, + me->ourgame->redraw(me->drawing, me->drawstate, me->oldstate, me->states[me->statepos-1].state, me->dir, me->ui, me->anim_pos, me->flash_pos); } else { - me->ourgame->redraw(me->frontend, me->drawstate, NULL, + me->ourgame->redraw(me->drawing, me->drawstate, NULL, me->states[me->statepos-1].state, +1 /*shrug*/, me->ui, 0.0, me->flash_pos); } - end_draw(me->frontend); + end_draw(me->drawing); } } -void midend_timer(midend_data *me, float tplus) +/* + * Nasty hacky function used to implement the --redo option in + * gtk.c. Only used for generating the puzzles' icons. + */ +void midend_freeze_timer(midend *me, float tprop) { + me->anim_pos = me->anim_time * tprop; + midend_redraw(me); + deactivate_timer(me->frontend); +} + +void midend_timer(midend *me, float tplus) +{ + int need_redraw = (me->anim_time > 0 || me->flash_time > 0); + me->anim_pos += tplus; if (me->anim_pos >= me->anim_time || me->anim_time == 0 || !me->oldstate) { @@ -675,34 +912,24 @@ void midend_timer(midend_data *me, float tplus) me->flash_pos = me->flash_time = 0; } - midend_redraw(me); + if (need_redraw) + midend_redraw(me); if (me->timing) { float oldelapsed = me->elapsed; me->elapsed += tplus; if ((int)oldelapsed != (int)me->elapsed) - status_bar(me->frontend, me->laststatus ? me->laststatus : ""); + status_bar(me->drawing, me->laststatus ? me->laststatus : ""); } midend_set_timer(me); } -float *midend_colours(midend_data *me, int *ncolours) +float *midend_colours(midend *me, int *ncolours) { - game_state *state = NULL; float *ret; - if (me->nstates == 0) { - char *aux = NULL; - char *desc = me->ourgame->new_desc(me->params, me->random, - &aux, TRUE); - state = me->ourgame->new_game(me, me->params, desc); - sfree(desc); - sfree(aux); - } else - state = me->states[0].state; - - ret = me->ourgame->colours(me->frontend, state, ncolours); + ret = me->ourgame->colours(me->frontend, ncolours); { int i; @@ -716,27 +943,26 @@ float *midend_colours(midend_data *me, int *ncolours) for (i = 0; i < *ncolours; i++) { char buf[80], *e; unsigned int r, g, b; - int j; + int j, k; sprintf(buf, "%s_COLOUR_%d", me->ourgame->name, i); - for (j = 0; buf[j]; j++) - buf[j] = toupper((unsigned char)buf[j]); + for (j = k = 0; buf[j]; j++) + if (!isspace((unsigned char)buf[j])) + buf[k++] = toupper((unsigned char)buf[j]); + buf[k] = '\0'; if ((e = getenv(buf)) != NULL && sscanf(e, "%2x%2x%2x", &r, &g, &b) == 3) { - ret[i*3 + 0] = r / 255.0; - ret[i*3 + 1] = g / 255.0; - ret[i*3 + 2] = b / 255.0; + ret[i*3 + 0] = r / 255.0F; + ret[i*3 + 1] = g / 255.0F; + ret[i*3 + 2] = b / 255.0F; } } } - if (me->nstates == 0) - me->ourgame->free_game(state); - return ret; } -int midend_num_presets(midend_data *me) +int midend_num_presets(midend *me) { if (!me->npresets) { char *name; @@ -749,10 +975,14 @@ int midend_num_presets(midend_data *me) game_params *); me->preset_names = sresize(me->preset_names, me->presetsize, char *); + me->preset_encodings = sresize(me->preset_encodings, + me->presetsize, char *); } me->presets[me->npresets] = preset; me->preset_names[me->npresets] = name; + me->preset_encodings[me->npresets] = + me->ourgame->encode_params(preset, TRUE);; me->npresets++; } } @@ -766,11 +996,13 @@ int midend_num_presets(midend_data *me) * encoded parameter strings. */ char buf[80], *e, *p; - int j; + int j, k; sprintf(buf, "%s_PRESETS", me->ourgame->name); - for (j = 0; buf[j]; j++) - buf[j] = toupper((unsigned char)buf[j]); + for (j = k = 0; buf[j]; j++) + if (!isspace((unsigned char)buf[j])) + buf[k++] = toupper((unsigned char)buf[j]); + buf[k] = '\0'; if ((e = getenv(buf)) != NULL) { p = e = dupstr(e); @@ -801,19 +1033,24 @@ int midend_num_presets(midend_data *me) game_params *); me->preset_names = sresize(me->preset_names, me->presetsize, char *); + me->preset_encodings = sresize(me->preset_encodings, + me->presetsize, char *); } me->presets[me->npresets] = preset; me->preset_names[me->npresets] = dupstr(name); + me->preset_encodings[me->npresets] = + me->ourgame->encode_params(preset, TRUE); me->npresets++; } + sfree(e); } } return me->npresets; } -void midend_fetch_preset(midend_data *me, int n, +void midend_fetch_preset(midend *me, int n, char **name, game_params **params) { assert(n >= 0 && n < me->npresets); @@ -821,12 +1058,28 @@ void midend_fetch_preset(midend_data *me, int n, *params = me->presets[n]; } -int midend_wants_statusbar(midend_data *me) +int midend_which_preset(midend *me) +{ + char *encoding = me->ourgame->encode_params(me->params, TRUE); + int i, ret; + + ret = -1; + for (i = 0; i < me->npresets; i++) + if (!strcmp(encoding, me->preset_encodings[i])) { + ret = i; + break; + } + + sfree(encoding); + return ret; +} + +int midend_wants_statusbar(midend *me) { - return me->ourgame->wants_statusbar(); + return me->ourgame->wants_statusbar; } -void midend_supersede_game_desc(midend_data *me, char *desc, char *privdesc) +void midend_supersede_game_desc(midend *me, char *desc, char *privdesc) { sfree(me->desc); sfree(me->privdesc); @@ -834,7 +1087,7 @@ void midend_supersede_game_desc(midend_data *me, char *desc, char *privdesc) me->privdesc = privdesc ? dupstr(privdesc) : NULL; } -config_item *midend_get_config(midend_data *me, int which, char **wintitle) +config_item *midend_get_config(midend *me, int which, char **wintitle) { char *titlebuf, *parstr, *rest; config_item *ret; @@ -899,7 +1152,7 @@ config_item *midend_get_config(midend_data *me, int which, char **wintitle) return NULL; } -static char *midend_game_id_int(midend_data *me, char *id, int defmode) +static char *midend_game_id_int(midend *me, char *id, int defmode) { char *error, *par, *desc, *seed; game_params *newcurparams, *newparams, *oldparams1, *oldparams2; @@ -1033,12 +1286,25 @@ static char *midend_game_id_int(midend_data *me, char *id, int defmode) return NULL; } -char *midend_game_id(midend_data *me, char *id) +char *midend_game_id(midend *me, char *id) { return midend_game_id_int(me, id, DEF_PARAMS); } -char *midend_set_config(midend_data *me, int which, config_item *cfg) +char *midend_get_game_id(midend *me) +{ + char *parstr, *ret; + + parstr = me->ourgame->encode_params(me->curparams, FALSE); + assert(parstr); + assert(me->desc); + ret = snewn(strlen(parstr) + strlen(me->desc) + 2, char); + sprintf(ret, "%s:%s", parstr, me->desc); + sfree(parstr); + return ret; +} + +char *midend_set_config(midend *me, int which, config_item *cfg) { char *error; game_params *params; @@ -1069,15 +1335,24 @@ char *midend_set_config(midend_data *me, int which, config_item *cfg) return NULL; } -char *midend_text_format(midend_data *me) +int midend_can_format_as_text_now(midend *me) { - if (me->ourgame->can_format_as_text && me->statepos > 0) + if (me->ourgame->can_format_as_text_ever) + return me->ourgame->can_format_as_text_now(me->params); + else + return FALSE; +} + +char *midend_text_format(midend *me) +{ + if (me->ourgame->can_format_as_text_ever && me->statepos > 0 && + me->ourgame->can_format_as_text_now(me->params)) return me->ourgame->text_format(me->states[me->statepos-1].state); else return NULL; } -char *midend_solve(midend_data *me) +char *midend_solve(midend *me) { game_state *s; char *msg, *movestr; @@ -1088,12 +1363,15 @@ char *midend_solve(midend_data *me) if (me->statepos < 1) return "No game set up to solve"; /* _shouldn't_ happen! */ - msg = "Solve operation failed"; /* game _should_ overwrite on error */ + msg = NULL; movestr = me->ourgame->solve(me->states[0].state, me->states[me->statepos-1].state, me->aux_info, &msg); - if (!movestr) + if (!movestr) { + if (!msg) + msg = "Solve operation failed"; /* _shouldn't_ happen, but can */ return msg; + } s = me->ourgame->execute_move(me->states[me->statepos-1].state, movestr); assert(s); @@ -1101,8 +1379,7 @@ char *midend_solve(midend_data *me) * Now enter the solved state as the next move. */ midend_stop_anim(me); - while (me->nstates > me->statepos) - me->ourgame->free_game(me->states[--me->nstates].state); + midend_purge_states(me); ensure(me); me->states[me->nstates].state = s; me->states[me->nstates].movestr = movestr; @@ -1112,14 +1389,42 @@ char *midend_solve(midend_data *me) me->ourgame->changed_state(me->ui, me->states[me->statepos-2].state, me->states[me->statepos-1].state); - me->anim_time = 0.0; - midend_finish_move(me); - midend_redraw(me); + me->dir = +1; + if (me->ourgame->flags & SOLVE_ANIMATES) { + me->oldstate = me->ourgame->dup_game(me->states[me->statepos-2].state); + me->anim_time = + me->ourgame->anim_length(me->states[me->statepos-2].state, + me->states[me->statepos-1].state, + +1, me->ui); + me->anim_pos = 0.0; + } else { + me->anim_time = 0.0; + midend_finish_move(me); + } + if (me->drawing) + midend_redraw(me); midend_set_timer(me); return NULL; } -char *midend_rewrite_statusbar(midend_data *me, char *text) +int midend_status(midend *me) +{ + /* + * We should probably never be called when the state stack has no + * states on it at all - ideally, midends should never be left in + * that state for long enough to get put down and forgotten about. + * But if we are, I think we return _true_ - pedantically speaking + * a midend in that state is 'vacuously solved', and more + * practically, a user whose midend has been left in that state + * probably _does_ want the 'new game' option to be prominent. + */ + if (me->statepos == 0) + return +1; + + return me->ourgame->status(me->states[me->statepos-1].state); +} + +char *midend_rewrite_statusbar(midend *me, char *text) { /* * An important special case is that we are occasionally called @@ -1134,7 +1439,7 @@ char *midend_rewrite_statusbar(midend_data *me, char *text) char timebuf[100], *ret; int min, sec; - sec = me->elapsed; + sec = (int)me->elapsed; min = sec / 60; sec %= 60; sprintf(timebuf, "[%d:%02d] ", min, sec); @@ -1152,7 +1457,7 @@ char *midend_rewrite_statusbar(midend_data *me, char *text) #define SERIALISE_MAGIC "Simon Tatham's Portable Puzzle Collection" #define SERIALISE_VERSION "1" -void midend_serialise(midend_data *me, +void midend_serialise(midend *me, void (*write)(void *ctx, void *buf, int len), void *wctx) { @@ -1301,7 +1606,7 @@ void midend_serialise(midend_data *me, /* * This function returns NULL on success, or an error message. */ -char *midend_deserialise(midend_data *me, +char *midend_deserialise(midend *me, int (*read)(void *ctx, void *buf, int len), void *rctx) { @@ -1317,7 +1622,7 @@ char *midend_deserialise(midend_data *me, * We construct all the new state in local variables while we * check its sanity. Only once we have finished reading the * serialised data and detected no errors at all do we start - * modifying stuff in the midend_data passed in. + * modifying stuff in the midend passed in. */ char *seed = NULL, *parstr = NULL, *desc = NULL, *privdesc = NULL; char *auxinfo = NULL, *uistr = NULL, *cparstr = NULL; @@ -1438,7 +1743,7 @@ char *midend_deserialise(midend_data *me, uistr = val; val = NULL; } else if (!strcmp(key, "TIME")) { - elapsed = atof(val); + elapsed = (float)atof(val); } else if (!strcmp(key, "NSTATES")) { nstates = atoi(val); if (nstates <= 0) { @@ -1612,9 +1917,10 @@ char *midend_deserialise(midend_data *me, midend_set_timer(me); if (me->drawstate) - me->ourgame->free_drawstate(me->drawstate); + me->ourgame->free_drawstate(me->drawing, me->drawstate); me->drawstate = - me->ourgame->new_drawstate(me->states[me->statepos-1].state); + me->ourgame->new_drawstate(me->drawing, + me->states[me->statepos-1].state); midend_size_new_drawstate(me); ret = NULL; /* success! */ @@ -1647,3 +1953,148 @@ char *midend_deserialise(midend_data *me, return ret; } + +/* + * This function examines a saved game file just far enough to + * determine which game type it contains. It returns NULL on success + * and the game name string in 'name' (which will be dynamically + * allocated and should be caller-freed), or an error message on + * failure. + */ +char *identify_game(char **name, int (*read)(void *ctx, void *buf, int len), + void *rctx) +{ + int nstates = 0, statepos = -1, gotstates = 0; + int started = FALSE; + + char *val = NULL; + /* Initially all errors give the same report */ + char *ret = "Data does not appear to be a saved game file"; + + *name = NULL; + + /* + * Loop round and round reading one key/value pair at a time from + * the serialised stream, until we've found the game name. + */ + while (nstates <= 0 || statepos < 0 || gotstates < nstates-1) { + char key[9], c; + int len; + + do { + if (!read(rctx, key, 1)) { + /* unexpected EOF */ + goto cleanup; + } + } while (key[0] == '\r' || key[0] == '\n'); + + if (!read(rctx, key+1, 8)) { + /* unexpected EOF */ + goto cleanup; + } + + if (key[8] != ':') { + if (started) + ret = "Data was incorrectly formatted for a saved game file"; + goto cleanup; + } + len = strcspn(key, ": "); + assert(len <= 8); + key[len] = '\0'; + + len = 0; + while (1) { + if (!read(rctx, &c, 1)) { + /* unexpected EOF */ + goto cleanup; + } + + if (c == ':') { + break; + } else if (c >= '0' && c <= '9') { + len = (len * 10) + (c - '0'); + } else { + if (started) + ret = "Data was incorrectly formatted for a" + " saved game file"; + goto cleanup; + } + } + + val = snewn(len+1, char); + if (!read(rctx, val, len)) { + if (started) + goto cleanup; + } + val[len] = '\0'; + + if (!started) { + if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) { + /* ret already has the right message in it */ + goto cleanup; + } + /* Now most errors are this one, unless otherwise specified */ + ret = "Saved data ended unexpectedly"; + started = TRUE; + } else { + if (!strcmp(key, "VERSION")) { + if (strcmp(val, SERIALISE_VERSION)) { + ret = "Cannot handle this version of the saved game" + " file format"; + goto cleanup; + } + } else if (!strcmp(key, "GAME")) { + *name = dupstr(val); + ret = NULL; + goto cleanup; + } + } + + sfree(val); + val = NULL; + } + + cleanup: + sfree(val); + return ret; +} + +char *midend_print_puzzle(midend *me, document *doc, int with_soln) +{ + game_state *soln = NULL; + + if (me->statepos < 1) + return "No game set up to print";/* _shouldn't_ happen! */ + + if (with_soln) { + char *msg, *movestr; + + if (!me->ourgame->can_solve) + return "This game does not support the Solve operation"; + + msg = "Solve operation failed";/* game _should_ overwrite on error */ + movestr = me->ourgame->solve(me->states[0].state, + me->states[me->statepos-1].state, + me->aux_info, &msg); + if (!movestr) + return msg; + soln = me->ourgame->execute_move(me->states[me->statepos-1].state, + movestr); + assert(soln); + + sfree(movestr); + } else + soln = NULL; + + /* + * This call passes over ownership of the two game_states and + * the game_params. Hence we duplicate the ones we want to + * keep, and we don't have to bother freeing soln if it was + * non-NULL. + */ + document_add_puzzle(doc, me->ourgame, + me->ourgame->dup_params(me->curparams), + me->ourgame->dup_game(me->states[0].state), soln); + + return NULL; +}