diff options
author | Franklin Wei <git@fwei.tk> | 2017-04-29 18:21:56 -0400 |
---|---|---|
committer | Franklin Wei <git@fwei.tk> | 2017-04-29 18:24:42 -0400 |
commit | 881746789a489fad85aae8317555f73dbe261556 (patch) | |
tree | cec2946362c4698c8db3c10f3242ef546c2c22dd /apps/plugins/puzzles/src/midend.c | |
parent | 03dd4b92be7dcd5c8ab06da3810887060e06abd5 (diff) | |
download | rockbox-881746789a489fad85aae8317555f73dbe261556.tar.gz rockbox-881746789a489fad85aae8317555f73dbe261556.zip |
puzzles: refactor and resync with upstream
This brings puzzles up-to-date with upstream revision
2d333750272c3967cfd5cd3677572cddeaad5932, though certain changes made
by me, including cursor-only Untangle and some compilation fixes
remain. Upstream code has been moved to its separate subdirectory and
future syncs can be done by simply copying over the new sources.
Change-Id: Ia6506ca5f78c3627165ea6791d38db414ace0804
Diffstat (limited to 'apps/plugins/puzzles/src/midend.c')
-rw-r--r-- | apps/plugins/puzzles/src/midend.c | 2237 |
1 files changed, 2237 insertions, 0 deletions
diff --git a/apps/plugins/puzzles/src/midend.c b/apps/plugins/puzzles/src/midend.c new file mode 100644 index 0000000000..2eb5ee93e8 --- /dev/null +++ b/apps/plugins/puzzles/src/midend.c | |||
@@ -0,0 +1,2237 @@ | |||
1 | /* | ||
2 | * midend.c: general middle fragment sitting between the | ||
3 | * platform-specific front end and game-specific back end. | ||
4 | * Maintains a move list, takes care of Undo and Redo commands, and | ||
5 | * processes standard keystrokes for undo/redo/new/quit. | ||
6 | */ | ||
7 | |||
8 | #include <stdio.h> | ||
9 | #include <string.h> | ||
10 | #include <assert.h> | ||
11 | #include <stdlib.h> | ||
12 | #include <ctype.h> | ||
13 | |||
14 | #include "puzzles.h" | ||
15 | |||
16 | enum { DEF_PARAMS, DEF_SEED, DEF_DESC }; /* for midend_game_id_int */ | ||
17 | |||
18 | enum { NEWGAME, MOVE, SOLVE, RESTART };/* for midend_state_entry.movetype */ | ||
19 | |||
20 | #define special(type) ( (type) != MOVE ) | ||
21 | |||
22 | struct midend_state_entry { | ||
23 | game_state *state; | ||
24 | char *movestr; | ||
25 | int movetype; | ||
26 | }; | ||
27 | |||
28 | struct midend { | ||
29 | frontend *frontend; | ||
30 | random_state *random; | ||
31 | const game *ourgame; | ||
32 | |||
33 | struct preset_menu *preset_menu; | ||
34 | char **encoded_presets; /* for midend_which_preset to check against */ | ||
35 | int n_encoded_presets; | ||
36 | |||
37 | /* | ||
38 | * `desc' and `privdesc' deserve a comment. | ||
39 | * | ||
40 | * `desc' is the game description as presented to the user when | ||
41 | * they ask for Game -> Specific. `privdesc', if non-NULL, is a | ||
42 | * different game description used to reconstruct the initial | ||
43 | * game_state when de-serialising. If privdesc is NULL, `desc' | ||
44 | * is used for both. | ||
45 | * | ||
46 | * For almost all games, `privdesc' is NULL and never used. The | ||
47 | * exception (as usual) is Mines: the initial game state has no | ||
48 | * squares open at all, but after the first click `desc' is | ||
49 | * rewritten to describe a game state with an initial click and | ||
50 | * thus a bunch of squares open. If we used that desc to | ||
51 | * serialise and deserialise, then the initial game state after | ||
52 | * deserialisation would look unlike the initial game state | ||
53 | * beforehand, and worse still execute_move() might fail on the | ||
54 | * attempted first click. So `privdesc' is also used in this | ||
55 | * case, to provide a game description describing the same | ||
56 | * fixed mine layout _but_ no initial click. (These game IDs | ||
57 | * may also be typed directly into Mines if you like.) | ||
58 | */ | ||
59 | char *desc, *privdesc, *seedstr; | ||
60 | char *aux_info; | ||
61 | enum { GOT_SEED, GOT_DESC, GOT_NOTHING } genmode; | ||
62 | |||
63 | int nstates, statesize, statepos; | ||
64 | struct midend_state_entry *states; | ||
65 | |||
66 | game_params *params, *curparams; | ||
67 | game_drawstate *drawstate; | ||
68 | game_ui *ui; | ||
69 | |||
70 | game_state *oldstate; | ||
71 | float anim_time, anim_pos; | ||
72 | float flash_time, flash_pos; | ||
73 | int dir; | ||
74 | |||
75 | int timing; | ||
76 | float elapsed; | ||
77 | char *laststatus; | ||
78 | |||
79 | drawing *drawing; | ||
80 | |||
81 | int pressed_mouse_button; | ||
82 | |||
83 | int preferred_tilesize, tilesize, winwidth, winheight; | ||
84 | |||
85 | void (*game_id_change_notify_function)(void *); | ||
86 | void *game_id_change_notify_ctx; | ||
87 | }; | ||
88 | |||
89 | #define ensure(me) do { \ | ||
90 | if ((me)->nstates >= (me)->statesize) { \ | ||
91 | (me)->statesize = (me)->nstates + 128; \ | ||
92 | (me)->states = sresize((me)->states, (me)->statesize, \ | ||
93 | struct midend_state_entry); \ | ||
94 | } \ | ||
95 | } while (0) | ||
96 | |||
97 | void midend_reset_tilesize(midend *me) | ||
98 | { | ||
99 | me->preferred_tilesize = me->ourgame->preferred_tilesize; | ||
100 | { | ||
101 | /* | ||
102 | * Allow an environment-based override for the default tile | ||
103 | * size by defining a variable along the lines of | ||
104 | * `NET_TILESIZE=15'. | ||
105 | */ | ||
106 | |||
107 | char buf[80], *e; | ||
108 | int j, k, ts; | ||
109 | |||
110 | sprintf(buf, "%s_TILESIZE", me->ourgame->name); | ||
111 | for (j = k = 0; buf[j]; j++) | ||
112 | if (!isspace((unsigned char)buf[j])) | ||
113 | buf[k++] = toupper((unsigned char)buf[j]); | ||
114 | buf[k] = '\0'; | ||
115 | if ((e = getenv(buf)) != NULL && sscanf(e, "%d", &ts) == 1 && ts > 0) | ||
116 | me->preferred_tilesize = ts; | ||
117 | } | ||
118 | } | ||
119 | |||
120 | midend *midend_new(frontend *fe, const game *ourgame, | ||
121 | const drawing_api *drapi, void *drhandle) | ||
122 | { | ||
123 | midend *me = snew(midend); | ||
124 | void *randseed; | ||
125 | int randseedsize; | ||
126 | |||
127 | get_random_seed(&randseed, &randseedsize); | ||
128 | |||
129 | me->frontend = fe; | ||
130 | me->ourgame = ourgame; | ||
131 | me->random = random_new(randseed, randseedsize); | ||
132 | me->nstates = me->statesize = me->statepos = 0; | ||
133 | me->states = NULL; | ||
134 | me->params = ourgame->default_params(); | ||
135 | me->game_id_change_notify_function = NULL; | ||
136 | me->game_id_change_notify_ctx = NULL; | ||
137 | |||
138 | /* | ||
139 | * Allow environment-based changing of the default settings by | ||
140 | * defining a variable along the lines of `NET_DEFAULT=25x25w' | ||
141 | * in which the value is an encoded parameter string. | ||
142 | */ | ||
143 | { | ||
144 | char buf[80], *e; | ||
145 | int j, k; | ||
146 | sprintf(buf, "%s_DEFAULT", me->ourgame->name); | ||
147 | for (j = k = 0; buf[j]; j++) | ||
148 | if (!isspace((unsigned char)buf[j])) | ||
149 | buf[k++] = toupper((unsigned char)buf[j]); | ||
150 | buf[k] = '\0'; | ||
151 | if ((e = getenv(buf)) != NULL) | ||
152 | me->ourgame->decode_params(me->params, e); | ||
153 | } | ||
154 | me->curparams = NULL; | ||
155 | me->desc = me->privdesc = NULL; | ||
156 | me->seedstr = NULL; | ||
157 | me->aux_info = NULL; | ||
158 | me->genmode = GOT_NOTHING; | ||
159 | me->drawstate = NULL; | ||
160 | me->oldstate = NULL; | ||
161 | me->preset_menu = NULL; | ||
162 | me->anim_time = me->anim_pos = 0.0F; | ||
163 | me->flash_time = me->flash_pos = 0.0F; | ||
164 | me->dir = 0; | ||
165 | me->ui = NULL; | ||
166 | me->pressed_mouse_button = 0; | ||
167 | me->laststatus = NULL; | ||
168 | me->timing = FALSE; | ||
169 | me->elapsed = 0.0F; | ||
170 | me->tilesize = me->winwidth = me->winheight = 0; | ||
171 | if (drapi) | ||
172 | me->drawing = drawing_new(drapi, me, drhandle); | ||
173 | else | ||
174 | me->drawing = NULL; | ||
175 | |||
176 | midend_reset_tilesize(me); | ||
177 | |||
178 | sfree(randseed); | ||
179 | |||
180 | return me; | ||
181 | } | ||
182 | |||
183 | const game *midend_which_game(midend *me) | ||
184 | { | ||
185 | return me->ourgame; | ||
186 | } | ||
187 | |||
188 | static void midend_purge_states(midend *me) | ||
189 | { | ||
190 | while (me->nstates > me->statepos) { | ||
191 | me->ourgame->free_game(me->states[--me->nstates].state); | ||
192 | if (me->states[me->nstates].movestr) | ||
193 | sfree(me->states[me->nstates].movestr); | ||
194 | } | ||
195 | } | ||
196 | |||
197 | static void midend_free_game(midend *me) | ||
198 | { | ||
199 | while (me->nstates > 0) { | ||
200 | me->nstates--; | ||
201 | me->ourgame->free_game(me->states[me->nstates].state); | ||
202 | sfree(me->states[me->nstates].movestr); | ||
203 | } | ||
204 | |||
205 | if (me->drawstate) | ||
206 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | ||
207 | } | ||
208 | |||
209 | static void midend_free_preset_menu(midend *me, struct preset_menu *menu) | ||
210 | { | ||
211 | if (menu) { | ||
212 | int i; | ||
213 | for (i = 0; i < menu->n_entries; i++) { | ||
214 | sfree(menu->entries[i].title); | ||
215 | if (menu->entries[i].params) | ||
216 | me->ourgame->free_params(menu->entries[i].params); | ||
217 | midend_free_preset_menu(me, menu->entries[i].submenu); | ||
218 | } | ||
219 | sfree(menu->entries); | ||
220 | sfree(menu); | ||
221 | } | ||
222 | } | ||
223 | |||
224 | void midend_free(midend *me) | ||
225 | { | ||
226 | midend_free_game(me); | ||
227 | |||
228 | if (me->drawing) | ||
229 | drawing_free(me->drawing); | ||
230 | random_free(me->random); | ||
231 | sfree(me->states); | ||
232 | sfree(me->desc); | ||
233 | sfree(me->privdesc); | ||
234 | sfree(me->seedstr); | ||
235 | sfree(me->aux_info); | ||
236 | me->ourgame->free_params(me->params); | ||
237 | midend_free_preset_menu(me, me->preset_menu); | ||
238 | if (me->ui) | ||
239 | me->ourgame->free_ui(me->ui); | ||
240 | if (me->curparams) | ||
241 | me->ourgame->free_params(me->curparams); | ||
242 | sfree(me->laststatus); | ||
243 | sfree(me); | ||
244 | } | ||
245 | |||
246 | static void midend_size_new_drawstate(midend *me) | ||
247 | { | ||
248 | /* | ||
249 | * Don't even bother, if we haven't worked out our tile size | ||
250 | * anyway yet. | ||
251 | */ | ||
252 | if (me->tilesize > 0) { | ||
253 | me->ourgame->compute_size(me->params, me->tilesize, | ||
254 | &me->winwidth, &me->winheight); | ||
255 | me->ourgame->set_size(me->drawing, me->drawstate, | ||
256 | me->params, me->tilesize); | ||
257 | } | ||
258 | } | ||
259 | |||
260 | void midend_size(midend *me, int *x, int *y, int user_size) | ||
261 | { | ||
262 | int min, max; | ||
263 | int rx, ry; | ||
264 | |||
265 | /* | ||
266 | * We can't set the size on the same drawstate twice. So if | ||
267 | * we've already sized one drawstate, we must throw it away and | ||
268 | * create a new one. | ||
269 | */ | ||
270 | if (me->drawstate && me->tilesize > 0) { | ||
271 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | ||
272 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | ||
273 | me->states[0].state); | ||
274 | } | ||
275 | |||
276 | /* | ||
277 | * Find the tile size that best fits within the given space. If | ||
278 | * `user_size' is TRUE, we must actually find the _largest_ such | ||
279 | * tile size, in order to get as close to the user's explicit | ||
280 | * request as possible; otherwise, we bound above at the game's | ||
281 | * preferred tile size, so that the game gets what it wants | ||
282 | * provided that this doesn't break the constraint from the | ||
283 | * front-end (which is likely to be a screen size or similar). | ||
284 | */ | ||
285 | if (user_size) { | ||
286 | max = 1; | ||
287 | do { | ||
288 | max *= 2; | ||
289 | me->ourgame->compute_size(me->params, max, &rx, &ry); | ||
290 | } while (rx <= *x && ry <= *y); | ||
291 | } else | ||
292 | max = me->preferred_tilesize + 1; | ||
293 | min = 1; | ||
294 | |||
295 | /* | ||
296 | * Now binary-search between min and max. We're looking for a | ||
297 | * boundary rather than a value: the point at which tile sizes | ||
298 | * stop fitting within the given dimensions. Thus, we stop when | ||
299 | * max and min differ by exactly 1. | ||
300 | */ | ||
301 | while (max - min > 1) { | ||
302 | int mid = (max + min) / 2; | ||
303 | me->ourgame->compute_size(me->params, mid, &rx, &ry); | ||
304 | if (rx <= *x && ry <= *y) | ||
305 | min = mid; | ||
306 | else | ||
307 | max = mid; | ||
308 | } | ||
309 | |||
310 | /* | ||
311 | * Now `min' is a valid size, and `max' isn't. So use `min'. | ||
312 | */ | ||
313 | |||
314 | me->tilesize = min; | ||
315 | if (user_size) | ||
316 | /* If the user requested a change in size, make it permanent. */ | ||
317 | me->preferred_tilesize = me->tilesize; | ||
318 | midend_size_new_drawstate(me); | ||
319 | *x = me->winwidth; | ||
320 | *y = me->winheight; | ||
321 | } | ||
322 | |||
323 | int midend_tilesize(midend *me) { return me->tilesize; } | ||
324 | |||
325 | void midend_set_params(midend *me, game_params *params) | ||
326 | { | ||
327 | me->ourgame->free_params(me->params); | ||
328 | me->params = me->ourgame->dup_params(params); | ||
329 | } | ||
330 | |||
331 | game_params *midend_get_params(midend *me) | ||
332 | { | ||
333 | return me->ourgame->dup_params(me->params); | ||
334 | } | ||
335 | |||
336 | static void midend_set_timer(midend *me) | ||
337 | { | ||
338 | me->timing = (me->ourgame->is_timed && | ||
339 | me->ourgame->timing_state(me->states[me->statepos-1].state, | ||
340 | me->ui)); | ||
341 | if (me->timing || me->flash_time || me->anim_time) | ||
342 | activate_timer(me->frontend); | ||
343 | else | ||
344 | deactivate_timer(me->frontend); | ||
345 | } | ||
346 | |||
347 | void midend_force_redraw(midend *me) | ||
348 | { | ||
349 | if (me->drawstate) | ||
350 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | ||
351 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | ||
352 | me->states[0].state); | ||
353 | midend_size_new_drawstate(me); | ||
354 | midend_redraw(me); | ||
355 | } | ||
356 | |||
357 | void midend_new_game(midend *me) | ||
358 | { | ||
359 | midend_stop_anim(me); | ||
360 | midend_free_game(me); | ||
361 | |||
362 | assert(me->nstates == 0); | ||
363 | |||
364 | if (me->genmode == GOT_DESC) { | ||
365 | me->genmode = GOT_NOTHING; | ||
366 | } else { | ||
367 | random_state *rs; | ||
368 | |||
369 | if (me->genmode == GOT_SEED) { | ||
370 | me->genmode = GOT_NOTHING; | ||
371 | } else { | ||
372 | /* | ||
373 | * Generate a new random seed. 15 digits comes to about | ||
374 | * 48 bits, which should be more than enough. | ||
375 | * | ||
376 | * I'll avoid putting a leading zero on the number, | ||
377 | * just in case it confuses anybody who thinks it's | ||
378 | * processed as an integer rather than a string. | ||
379 | */ | ||
380 | char newseed[16]; | ||
381 | int i; | ||
382 | newseed[15] = '\0'; | ||
383 | newseed[0] = '1' + (char)random_upto(me->random, 9); | ||
384 | for (i = 1; i < 15; i++) | ||
385 | newseed[i] = '0' + (char)random_upto(me->random, 10); | ||
386 | sfree(me->seedstr); | ||
387 | me->seedstr = dupstr(newseed); | ||
388 | |||
389 | if (me->curparams) | ||
390 | me->ourgame->free_params(me->curparams); | ||
391 | me->curparams = me->ourgame->dup_params(me->params); | ||
392 | } | ||
393 | |||
394 | sfree(me->desc); | ||
395 | sfree(me->privdesc); | ||
396 | sfree(me->aux_info); | ||
397 | me->aux_info = NULL; | ||
398 | |||
399 | rs = random_new(me->seedstr, strlen(me->seedstr)); | ||
400 | /* | ||
401 | * If this midend has been instantiated without providing a | ||
402 | * drawing API, it is non-interactive. This means that it's | ||
403 | * being used for bulk game generation, and hence we should | ||
404 | * pass the non-interactive flag to new_desc. | ||
405 | */ | ||
406 | me->desc = me->ourgame->new_desc(me->curparams, rs, | ||
407 | &me->aux_info, (me->drawing != NULL)); | ||
408 | me->privdesc = NULL; | ||
409 | random_free(rs); | ||
410 | } | ||
411 | |||
412 | ensure(me); | ||
413 | |||
414 | /* | ||
415 | * It might seem a bit odd that we're using me->params to | ||
416 | * create the initial game state, rather than me->curparams | ||
417 | * which is better tailored to this specific game and which we | ||
418 | * always know. | ||
419 | * | ||
420 | * It's supposed to be an invariant in the midend that | ||
421 | * me->params and me->curparams differ in no aspect that is | ||
422 | * important after generation (i.e. after new_desc()). By | ||
423 | * deliberately passing the _less_ specific of these two | ||
424 | * parameter sets, we provoke play-time misbehaviour in the | ||
425 | * case where a game has failed to encode a play-time parameter | ||
426 | * in the non-full version of encode_params(). | ||
427 | */ | ||
428 | me->states[me->nstates].state = | ||
429 | me->ourgame->new_game(me, me->params, me->desc); | ||
430 | |||
431 | /* | ||
432 | * As part of our commitment to self-testing, test the aux | ||
433 | * string to make sure nothing ghastly went wrong. | ||
434 | */ | ||
435 | if (me->ourgame->can_solve && me->aux_info) { | ||
436 | game_state *s; | ||
437 | char *msg, *movestr; | ||
438 | |||
439 | msg = NULL; | ||
440 | movestr = me->ourgame->solve(me->states[0].state, | ||
441 | me->states[0].state, | ||
442 | me->aux_info, &msg); | ||
443 | assert(movestr && !msg); | ||
444 | s = me->ourgame->execute_move(me->states[0].state, movestr); | ||
445 | assert(s); | ||
446 | me->ourgame->free_game(s); | ||
447 | sfree(movestr); | ||
448 | } | ||
449 | |||
450 | me->states[me->nstates].movestr = NULL; | ||
451 | me->states[me->nstates].movetype = NEWGAME; | ||
452 | me->nstates++; | ||
453 | me->statepos = 1; | ||
454 | me->drawstate = me->ourgame->new_drawstate(me->drawing, | ||
455 | me->states[0].state); | ||
456 | midend_size_new_drawstate(me); | ||
457 | me->elapsed = 0.0F; | ||
458 | me->flash_pos = me->flash_time = 0.0F; | ||
459 | me->anim_pos = me->anim_time = 0.0F; | ||
460 | if (me->ui) | ||
461 | me->ourgame->free_ui(me->ui); | ||
462 | me->ui = me->ourgame->new_ui(me->states[0].state); | ||
463 | midend_set_timer(me); | ||
464 | me->pressed_mouse_button = 0; | ||
465 | |||
466 | if (me->game_id_change_notify_function) | ||
467 | me->game_id_change_notify_function(me->game_id_change_notify_ctx); | ||
468 | } | ||
469 | |||
470 | int midend_can_undo(midend *me) | ||
471 | { | ||
472 | return (me->statepos > 1); | ||
473 | } | ||
474 | |||
475 | int midend_can_redo(midend *me) | ||
476 | { | ||
477 | return (me->statepos < me->nstates); | ||
478 | } | ||
479 | |||
480 | static int midend_undo(midend *me) | ||
481 | { | ||
482 | if (me->statepos > 1) { | ||
483 | if (me->ui) | ||
484 | me->ourgame->changed_state(me->ui, | ||
485 | me->states[me->statepos-1].state, | ||
486 | me->states[me->statepos-2].state); | ||
487 | me->statepos--; | ||
488 | me->dir = -1; | ||
489 | return 1; | ||
490 | } else | ||
491 | return 0; | ||
492 | } | ||
493 | |||
494 | static int midend_redo(midend *me) | ||
495 | { | ||
496 | if (me->statepos < me->nstates) { | ||
497 | if (me->ui) | ||
498 | me->ourgame->changed_state(me->ui, | ||
499 | me->states[me->statepos-1].state, | ||
500 | me->states[me->statepos].state); | ||
501 | me->statepos++; | ||
502 | me->dir = +1; | ||
503 | return 1; | ||
504 | } else | ||
505 | return 0; | ||
506 | } | ||
507 | |||
508 | static void midend_finish_move(midend *me) | ||
509 | { | ||
510 | float flashtime; | ||
511 | |||
512 | /* | ||
513 | * We do not flash if the later of the two states is special. | ||
514 | * This covers both forward Solve moves and backward (undone) | ||
515 | * Restart moves. | ||
516 | */ | ||
517 | if ((me->oldstate || me->statepos > 1) && | ||
518 | ((me->dir > 0 && !special(me->states[me->statepos-1].movetype)) || | ||
519 | (me->dir < 0 && me->statepos < me->nstates && | ||
520 | !special(me->states[me->statepos].movetype)))) { | ||
521 | flashtime = me->ourgame->flash_length(me->oldstate ? me->oldstate : | ||
522 | me->states[me->statepos-2].state, | ||
523 | me->states[me->statepos-1].state, | ||
524 | me->oldstate ? me->dir : +1, | ||
525 | me->ui); | ||
526 | if (flashtime > 0) { | ||
527 | me->flash_pos = 0.0F; | ||
528 | me->flash_time = flashtime; | ||
529 | } | ||
530 | } | ||
531 | |||
532 | if (me->oldstate) | ||
533 | me->ourgame->free_game(me->oldstate); | ||
534 | me->oldstate = NULL; | ||
535 | me->anim_pos = me->anim_time = 0; | ||
536 | me->dir = 0; | ||
537 | |||
538 | midend_set_timer(me); | ||
539 | } | ||
540 | |||
541 | void midend_stop_anim(midend *me) | ||
542 | { | ||
543 | if (me->oldstate || me->anim_time != 0) { | ||
544 | midend_finish_move(me); | ||
545 | midend_redraw(me); | ||
546 | } | ||
547 | } | ||
548 | |||
549 | void midend_restart_game(midend *me) | ||
550 | { | ||
551 | game_state *s; | ||
552 | |||
553 | assert(me->statepos >= 1); | ||
554 | if (me->statepos == 1) | ||
555 | return; /* no point doing anything at all! */ | ||
556 | |||
557 | /* | ||
558 | * During restart, we reconstruct the game from the (public) | ||
559 | * game description rather than from states[0], because that | ||
560 | * way Mines gets slightly more sensible behaviour (restart | ||
561 | * goes to _after_ the first click so you don't have to | ||
562 | * remember where you clicked). | ||
563 | */ | ||
564 | s = me->ourgame->new_game(me, me->params, me->desc); | ||
565 | |||
566 | /* | ||
567 | * Now enter the restarted state as the next move. | ||
568 | */ | ||
569 | midend_stop_anim(me); | ||
570 | midend_purge_states(me); | ||
571 | ensure(me); | ||
572 | me->states[me->nstates].state = s; | ||
573 | me->states[me->nstates].movestr = dupstr(me->desc); | ||
574 | me->states[me->nstates].movetype = RESTART; | ||
575 | me->statepos = ++me->nstates; | ||
576 | if (me->ui) | ||
577 | me->ourgame->changed_state(me->ui, | ||
578 | me->states[me->statepos-2].state, | ||
579 | me->states[me->statepos-1].state); | ||
580 | me->flash_pos = me->flash_time = 0.0F; | ||
581 | midend_finish_move(me); | ||
582 | midend_redraw(me); | ||
583 | midend_set_timer(me); | ||
584 | } | ||
585 | |||
586 | static int midend_really_process_key(midend *me, int x, int y, int button) | ||
587 | { | ||
588 | game_state *oldstate = | ||
589 | me->ourgame->dup_game(me->states[me->statepos - 1].state); | ||
590 | int type = MOVE, gottype = FALSE, ret = 1; | ||
591 | float anim_time; | ||
592 | game_state *s; | ||
593 | char *movestr; | ||
594 | |||
595 | movestr = | ||
596 | me->ourgame->interpret_move(me->states[me->statepos-1].state, | ||
597 | me->ui, me->drawstate, x, y, button); | ||
598 | |||
599 | if (!movestr) { | ||
600 | if (button == 'n' || button == 'N' || button == '\x0E') { | ||
601 | midend_new_game(me); | ||
602 | midend_redraw(me); | ||
603 | goto done; /* never animate */ | ||
604 | } else if (button == 'u' || button == 'U' || | ||
605 | button == '\x1A' || button == '\x1F') { | ||
606 | midend_stop_anim(me); | ||
607 | type = me->states[me->statepos-1].movetype; | ||
608 | gottype = TRUE; | ||
609 | if (!midend_undo(me)) | ||
610 | goto done; | ||
611 | } else if (button == 'r' || button == 'R' || | ||
612 | button == '\x12' || button == '\x19') { | ||
613 | midend_stop_anim(me); | ||
614 | if (!midend_redo(me)) | ||
615 | goto done; | ||
616 | } else if (button == '\x13' && me->ourgame->can_solve) { | ||
617 | if (midend_solve(me)) | ||
618 | goto done; | ||
619 | } else if (button == 'q' || button == 'Q' || button == '\x11') { | ||
620 | ret = 0; | ||
621 | goto done; | ||
622 | } else | ||
623 | goto done; | ||
624 | } else { | ||
625 | if (!*movestr) | ||
626 | s = me->states[me->statepos-1].state; | ||
627 | else { | ||
628 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, | ||
629 | movestr); | ||
630 | assert(s != NULL); | ||
631 | } | ||
632 | |||
633 | if (s == me->states[me->statepos-1].state) { | ||
634 | /* | ||
635 | * make_move() is allowed to return its input state to | ||
636 | * indicate that although no move has been made, the UI | ||
637 | * state has been updated and a redraw is called for. | ||
638 | */ | ||
639 | midend_redraw(me); | ||
640 | midend_set_timer(me); | ||
641 | goto done; | ||
642 | } else if (s) { | ||
643 | midend_stop_anim(me); | ||
644 | midend_purge_states(me); | ||
645 | ensure(me); | ||
646 | assert(movestr != NULL); | ||
647 | me->states[me->nstates].state = s; | ||
648 | me->states[me->nstates].movestr = movestr; | ||
649 | me->states[me->nstates].movetype = MOVE; | ||
650 | me->statepos = ++me->nstates; | ||
651 | me->dir = +1; | ||
652 | if (me->ui) | ||
653 | me->ourgame->changed_state(me->ui, | ||
654 | me->states[me->statepos-2].state, | ||
655 | me->states[me->statepos-1].state); | ||
656 | } else { | ||
657 | goto done; | ||
658 | } | ||
659 | } | ||
660 | |||
661 | if (!gottype) | ||
662 | type = me->states[me->statepos-1].movetype; | ||
663 | |||
664 | /* | ||
665 | * See if this move requires an animation. | ||
666 | */ | ||
667 | if (special(type) && !(type == SOLVE && | ||
668 | (me->ourgame->flags & SOLVE_ANIMATES))) { | ||
669 | anim_time = 0; | ||
670 | } else { | ||
671 | anim_time = me->ourgame->anim_length(oldstate, | ||
672 | me->states[me->statepos-1].state, | ||
673 | me->dir, me->ui); | ||
674 | } | ||
675 | |||
676 | me->oldstate = oldstate; oldstate = NULL; | ||
677 | if (anim_time > 0) { | ||
678 | me->anim_time = anim_time; | ||
679 | } else { | ||
680 | me->anim_time = 0.0; | ||
681 | midend_finish_move(me); | ||
682 | } | ||
683 | me->anim_pos = 0.0; | ||
684 | |||
685 | midend_redraw(me); | ||
686 | |||
687 | midend_set_timer(me); | ||
688 | |||
689 | done: | ||
690 | if (oldstate) me->ourgame->free_game(oldstate); | ||
691 | return ret; | ||
692 | } | ||
693 | |||
694 | int midend_process_key(midend *me, int x, int y, int button) | ||
695 | { | ||
696 | int ret = 1; | ||
697 | |||
698 | /* | ||
699 | * Harmonise mouse drag and release messages. | ||
700 | * | ||
701 | * Some front ends might accidentally switch from sending, say, | ||
702 | * RIGHT_DRAG messages to sending LEFT_DRAG, half way through a | ||
703 | * drag. (This can happen on the Mac, for example, since | ||
704 | * RIGHT_DRAG is usually done using Command+drag, and if the | ||
705 | * user accidentally releases Command half way through the drag | ||
706 | * then there will be trouble.) | ||
707 | * | ||
708 | * It would be an O(number of front ends) annoyance to fix this | ||
709 | * in the front ends, but an O(number of back ends) annoyance | ||
710 | * to have each game capable of dealing with it. Therefore, we | ||
711 | * fix it _here_ in the common midend code so that it only has | ||
712 | * to be done once. | ||
713 | * | ||
714 | * The possible ways in which things can go screwy in the front | ||
715 | * end are: | ||
716 | * | ||
717 | * - in a system containing multiple physical buttons button | ||
718 | * presses can inadvertently overlap. We can see ABab (caps | ||
719 | * meaning button-down and lowercase meaning button-up) when | ||
720 | * the user had semantically intended AaBb. | ||
721 | * | ||
722 | * - in a system where one button is simulated by means of a | ||
723 | * modifier key and another button, buttons can mutate | ||
724 | * between press and release (possibly during drag). So we | ||
725 | * can see Ab instead of Aa. | ||
726 | * | ||
727 | * Definite requirements are: | ||
728 | * | ||
729 | * - button _presses_ must never be invented or destroyed. If | ||
730 | * the user presses two buttons in succession, the button | ||
731 | * presses must be transferred to the backend unchanged. So | ||
732 | * if we see AaBb , that's fine; if we see ABab (the button | ||
733 | * presses inadvertently overlapped) we must somehow | ||
734 | * `correct' it to AaBb. | ||
735 | * | ||
736 | * - every mouse action must end up looking like a press, zero | ||
737 | * or more drags, then a release. This allows back ends to | ||
738 | * make the _assumption_ that incoming mouse data will be | ||
739 | * sane in this regard, and not worry about the details. | ||
740 | * | ||
741 | * So my policy will be: | ||
742 | * | ||
743 | * - treat any button-up as a button-up for the currently | ||
744 | * pressed button, or ignore it if there is no currently | ||
745 | * pressed button. | ||
746 | * | ||
747 | * - treat any drag as a drag for the currently pressed | ||
748 | * button, or ignore it if there is no currently pressed | ||
749 | * button. | ||
750 | * | ||
751 | * - if we see a button-down while another button is currently | ||
752 | * pressed, invent a button-up for the first one and then | ||
753 | * pass the button-down through as before. | ||
754 | * | ||
755 | * 2005-05-31: An addendum to the above. Some games might want | ||
756 | * a `priority order' among buttons, such that if one button is | ||
757 | * pressed while another is down then a fixed one of the | ||
758 | * buttons takes priority no matter what order they're pressed | ||
759 | * in. Mines, in particular, wants to treat a left+right click | ||
760 | * like a left click for the benefit of users of other | ||
761 | * implementations. So the last of the above points is modified | ||
762 | * in the presence of an (optional) button priority order. | ||
763 | * | ||
764 | * A further addition: we translate certain keyboard presses to | ||
765 | * cursor key 'select' buttons, so that a) frontends don't have | ||
766 | * to translate these themselves (like they do for CURSOR_UP etc), | ||
767 | * and b) individual games don't have to hard-code button presses | ||
768 | * of '\n' etc for keyboard-based cursors. The choice of buttons | ||
769 | * here could eventually be controlled by a runtime configuration | ||
770 | * option. | ||
771 | */ | ||
772 | if (IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) { | ||
773 | if (me->pressed_mouse_button) { | ||
774 | if (IS_MOUSE_DRAG(button)) { | ||
775 | button = me->pressed_mouse_button + | ||
776 | (LEFT_DRAG - LEFT_BUTTON); | ||
777 | } else { | ||
778 | button = me->pressed_mouse_button + | ||
779 | (LEFT_RELEASE - LEFT_BUTTON); | ||
780 | } | ||
781 | } else | ||
782 | return ret; /* ignore it */ | ||
783 | } else if (IS_MOUSE_DOWN(button) && me->pressed_mouse_button) { | ||
784 | /* | ||
785 | * If the new button has lower priority than the old one, | ||
786 | * don't bother doing this. | ||
787 | */ | ||
788 | if (me->ourgame->flags & | ||
789 | BUTTON_BEATS(me->pressed_mouse_button, button)) | ||
790 | return ret; /* just ignore it */ | ||
791 | |||
792 | /* | ||
793 | * Fabricate a button-up for the previously pressed button. | ||
794 | */ | ||
795 | ret = ret && midend_really_process_key | ||
796 | (me, x, y, (me->pressed_mouse_button + | ||
797 | (LEFT_RELEASE - LEFT_BUTTON))); | ||
798 | } | ||
799 | |||
800 | /* | ||
801 | * Translate keyboard presses to cursor selection. | ||
802 | */ | ||
803 | if (button == '\n' || button == '\r') | ||
804 | button = CURSOR_SELECT; | ||
805 | if (button == ' ') | ||
806 | button = CURSOR_SELECT2; | ||
807 | |||
808 | /* | ||
809 | * Normalise both backspace characters (8 and 127) to \b. Easier | ||
810 | * to do this once, here, than to require all front ends to | ||
811 | * carefully generate the same one - now each front end can | ||
812 | * generate whichever is easiest. | ||
813 | */ | ||
814 | if (button == '\177') | ||
815 | button = '\b'; | ||
816 | |||
817 | /* | ||
818 | * Now send on the event we originally received. | ||
819 | */ | ||
820 | ret = ret && midend_really_process_key(me, x, y, button); | ||
821 | |||
822 | /* | ||
823 | * And update the currently pressed button. | ||
824 | */ | ||
825 | if (IS_MOUSE_RELEASE(button)) | ||
826 | me->pressed_mouse_button = 0; | ||
827 | else if (IS_MOUSE_DOWN(button)) | ||
828 | me->pressed_mouse_button = button; | ||
829 | |||
830 | return ret; | ||
831 | } | ||
832 | |||
833 | void midend_redraw(midend *me) | ||
834 | { | ||
835 | assert(me->drawing); | ||
836 | |||
837 | if (me->statepos > 0 && me->drawstate) { | ||
838 | start_draw(me->drawing); | ||
839 | if (me->oldstate && me->anim_time > 0 && | ||
840 | me->anim_pos < me->anim_time) { | ||
841 | assert(me->dir != 0); | ||
842 | me->ourgame->redraw(me->drawing, me->drawstate, me->oldstate, | ||
843 | me->states[me->statepos-1].state, me->dir, | ||
844 | me->ui, me->anim_pos, me->flash_pos); | ||
845 | } else { | ||
846 | me->ourgame->redraw(me->drawing, me->drawstate, NULL, | ||
847 | me->states[me->statepos-1].state, +1 /*shrug*/, | ||
848 | me->ui, 0.0, me->flash_pos); | ||
849 | } | ||
850 | end_draw(me->drawing); | ||
851 | } | ||
852 | } | ||
853 | |||
854 | /* | ||
855 | * Nasty hacky function used to implement the --redo option in | ||
856 | * gtk.c. Only used for generating the puzzles' icons. | ||
857 | */ | ||
858 | void midend_freeze_timer(midend *me, float tprop) | ||
859 | { | ||
860 | me->anim_pos = me->anim_time * tprop; | ||
861 | midend_redraw(me); | ||
862 | deactivate_timer(me->frontend); | ||
863 | } | ||
864 | |||
865 | void midend_timer(midend *me, float tplus) | ||
866 | { | ||
867 | int need_redraw = (me->anim_time > 0 || me->flash_time > 0); | ||
868 | |||
869 | me->anim_pos += tplus; | ||
870 | if (me->anim_pos >= me->anim_time || | ||
871 | me->anim_time == 0 || !me->oldstate) { | ||
872 | if (me->anim_time > 0) | ||
873 | midend_finish_move(me); | ||
874 | } | ||
875 | |||
876 | me->flash_pos += tplus; | ||
877 | if (me->flash_pos >= me->flash_time || me->flash_time == 0) { | ||
878 | me->flash_pos = me->flash_time = 0; | ||
879 | } | ||
880 | |||
881 | if (need_redraw) | ||
882 | midend_redraw(me); | ||
883 | |||
884 | if (me->timing) { | ||
885 | float oldelapsed = me->elapsed; | ||
886 | me->elapsed += tplus; | ||
887 | if ((int)oldelapsed != (int)me->elapsed) | ||
888 | status_bar(me->drawing, me->laststatus ? me->laststatus : ""); | ||
889 | } | ||
890 | |||
891 | midend_set_timer(me); | ||
892 | } | ||
893 | |||
894 | float *midend_colours(midend *me, int *ncolours) | ||
895 | { | ||
896 | float *ret; | ||
897 | |||
898 | ret = me->ourgame->colours(me->frontend, ncolours); | ||
899 | |||
900 | { | ||
901 | int i; | ||
902 | |||
903 | /* | ||
904 | * Allow environment-based overrides for the standard | ||
905 | * colours by defining variables along the lines of | ||
906 | * `NET_COLOUR_4=6000c0'. | ||
907 | */ | ||
908 | |||
909 | for (i = 0; i < *ncolours; i++) { | ||
910 | char buf[80], *e; | ||
911 | unsigned int r, g, b; | ||
912 | int j, k; | ||
913 | |||
914 | sprintf(buf, "%s_COLOUR_%d", me->ourgame->name, i); | ||
915 | for (j = k = 0; buf[j]; j++) | ||
916 | if (!isspace((unsigned char)buf[j])) | ||
917 | buf[k++] = toupper((unsigned char)buf[j]); | ||
918 | buf[k] = '\0'; | ||
919 | if ((e = getenv(buf)) != NULL && | ||
920 | sscanf(e, "%2x%2x%2x", &r, &g, &b) == 3) { | ||
921 | ret[i*3 + 0] = r / 255.0F; | ||
922 | ret[i*3 + 1] = g / 255.0F; | ||
923 | ret[i*3 + 2] = b / 255.0F; | ||
924 | } | ||
925 | } | ||
926 | } | ||
927 | |||
928 | return ret; | ||
929 | } | ||
930 | |||
931 | struct preset_menu *preset_menu_new(void) | ||
932 | { | ||
933 | struct preset_menu *menu = snew(struct preset_menu); | ||
934 | menu->n_entries = 0; | ||
935 | menu->entries_size = 0; | ||
936 | menu->entries = NULL; | ||
937 | return menu; | ||
938 | } | ||
939 | |||
940 | static struct preset_menu_entry *preset_menu_add(struct preset_menu *menu, | ||
941 | char *title) | ||
942 | { | ||
943 | struct preset_menu_entry *toret; | ||
944 | if (menu->n_entries >= menu->entries_size) { | ||
945 | menu->entries_size = menu->n_entries * 5 / 4 + 10; | ||
946 | menu->entries = sresize(menu->entries, menu->entries_size, | ||
947 | struct preset_menu_entry); | ||
948 | } | ||
949 | toret = &menu->entries[menu->n_entries++]; | ||
950 | toret->title = title; | ||
951 | toret->params = NULL; | ||
952 | toret->submenu = NULL; | ||
953 | return toret; | ||
954 | } | ||
955 | |||
956 | struct preset_menu *preset_menu_add_submenu(struct preset_menu *parent, | ||
957 | char *title) | ||
958 | { | ||
959 | struct preset_menu_entry *entry = preset_menu_add(parent, title); | ||
960 | entry->submenu = preset_menu_new(); | ||
961 | return entry->submenu; | ||
962 | } | ||
963 | |||
964 | void preset_menu_add_preset(struct preset_menu *parent, | ||
965 | char *title, game_params *params) | ||
966 | { | ||
967 | struct preset_menu_entry *entry = preset_menu_add(parent, title); | ||
968 | entry->params = params; | ||
969 | } | ||
970 | |||
971 | game_params *preset_menu_lookup_by_id(struct preset_menu *menu, int id) | ||
972 | { | ||
973 | int i; | ||
974 | game_params *retd; | ||
975 | |||
976 | for (i = 0; i < menu->n_entries; i++) { | ||
977 | if (id == menu->entries[i].id) | ||
978 | return menu->entries[i].params; | ||
979 | if (menu->entries[i].submenu && | ||
980 | (retd = preset_menu_lookup_by_id( | ||
981 | menu->entries[i].submenu, id)) != NULL) | ||
982 | return retd; | ||
983 | } | ||
984 | |||
985 | return NULL; | ||
986 | } | ||
987 | |||
988 | static char *preset_menu_add_from_user_env( | ||
989 | midend *me, struct preset_menu *menu, char *p, int top_level) | ||
990 | { | ||
991 | while (*p) { | ||
992 | char *name, *val; | ||
993 | game_params *preset; | ||
994 | |||
995 | name = p; | ||
996 | while (*p && *p != ':') p++; | ||
997 | if (*p) *p++ = '\0'; | ||
998 | val = p; | ||
999 | while (*p && *p != ':') p++; | ||
1000 | if (*p) *p++ = '\0'; | ||
1001 | |||
1002 | if (!strcmp(val, "#")) { | ||
1003 | /* | ||
1004 | * Special case: either open a new submenu with the given | ||
1005 | * title, or terminate the current submenu. | ||
1006 | */ | ||
1007 | if (*name) { | ||
1008 | struct preset_menu *submenu = | ||
1009 | preset_menu_add_submenu(menu, dupstr(name)); | ||
1010 | p = preset_menu_add_from_user_env(me, submenu, p, FALSE); | ||
1011 | } else { | ||
1012 | /* | ||
1013 | * If we get a 'close submenu' indication at the top | ||
1014 | * level, there's not much we can do but quietly | ||
1015 | * ignore it. | ||
1016 | */ | ||
1017 | if (!top_level) | ||
1018 | return p; | ||
1019 | } | ||
1020 | continue; | ||
1021 | } | ||
1022 | |||
1023 | preset = me->ourgame->default_params(); | ||
1024 | me->ourgame->decode_params(preset, val); | ||
1025 | |||
1026 | if (me->ourgame->validate_params(preset, TRUE)) { | ||
1027 | /* Drop this one from the list. */ | ||
1028 | me->ourgame->free_params(preset); | ||
1029 | continue; | ||
1030 | } | ||
1031 | |||
1032 | preset_menu_add_preset(menu, dupstr(name), preset); | ||
1033 | } | ||
1034 | |||
1035 | return p; | ||
1036 | } | ||
1037 | |||
1038 | static void preset_menu_alloc_ids(midend *me, struct preset_menu *menu) | ||
1039 | { | ||
1040 | int i; | ||
1041 | |||
1042 | for (i = 0; i < menu->n_entries; i++) | ||
1043 | menu->entries[i].id = me->n_encoded_presets++; | ||
1044 | |||
1045 | for (i = 0; i < menu->n_entries; i++) | ||
1046 | if (menu->entries[i].submenu) | ||
1047 | preset_menu_alloc_ids(me, menu->entries[i].submenu); | ||
1048 | } | ||
1049 | |||
1050 | static void preset_menu_encode_params(midend *me, struct preset_menu *menu) | ||
1051 | { | ||
1052 | int i; | ||
1053 | |||
1054 | for (i = 0; i < menu->n_entries; i++) { | ||
1055 | if (menu->entries[i].params) { | ||
1056 | me->encoded_presets[menu->entries[i].id] = | ||
1057 | me->ourgame->encode_params(menu->entries[i].params, TRUE); | ||
1058 | } else { | ||
1059 | preset_menu_encode_params(me, menu->entries[i].submenu); | ||
1060 | } | ||
1061 | } | ||
1062 | } | ||
1063 | |||
1064 | struct preset_menu *midend_get_presets(midend *me, int *id_limit) | ||
1065 | { | ||
1066 | int i; | ||
1067 | |||
1068 | if (me->preset_menu) | ||
1069 | return me->preset_menu; | ||
1070 | |||
1071 | #if 0 | ||
1072 | /* Expect the game to implement exactly one of the two preset APIs */ | ||
1073 | assert(me->ourgame->fetch_preset || me->ourgame->preset_menu); | ||
1074 | assert(!(me->ourgame->fetch_preset && me->ourgame->preset_menu)); | ||
1075 | #endif | ||
1076 | |||
1077 | if (me->ourgame->fetch_preset) { | ||
1078 | char *name; | ||
1079 | game_params *preset; | ||
1080 | |||
1081 | /* Simple one-level menu */ | ||
1082 | assert(!me->ourgame->preset_menu); | ||
1083 | me->preset_menu = preset_menu_new(); | ||
1084 | for (i = 0; me->ourgame->fetch_preset(i, &name, &preset); i++) | ||
1085 | preset_menu_add_preset(me->preset_menu, name, preset); | ||
1086 | |||
1087 | } else { | ||
1088 | /* Hierarchical menu provided by the game backend */ | ||
1089 | me->preset_menu = me->ourgame->preset_menu(); | ||
1090 | } | ||
1091 | |||
1092 | { | ||
1093 | /* | ||
1094 | * Allow user extensions to the preset list by defining an | ||
1095 | * environment variable <gamename>_PRESETS whose value is a | ||
1096 | * colon-separated list of items, alternating between textual | ||
1097 | * titles in the menu and encoded parameter strings. For | ||
1098 | * example, "SOLO_PRESETS=2x3 Advanced:2x3da" would define | ||
1099 | * just one additional preset for Solo. | ||
1100 | */ | ||
1101 | char buf[80], *e; | ||
1102 | int j, k; | ||
1103 | |||
1104 | sprintf(buf, "%s_PRESETS", me->ourgame->name); | ||
1105 | for (j = k = 0; buf[j]; j++) | ||
1106 | if (!isspace((unsigned char)buf[j])) | ||
1107 | buf[k++] = toupper((unsigned char)buf[j]); | ||
1108 | buf[k] = '\0'; | ||
1109 | |||
1110 | if ((e = getenv(buf)) != NULL) { | ||
1111 | e = dupstr(e); | ||
1112 | preset_menu_add_from_user_env(me, me->preset_menu, e, TRUE); | ||
1113 | sfree(e); | ||
1114 | } | ||
1115 | } | ||
1116 | |||
1117 | /* | ||
1118 | * Finalise the menu: allocate an integer id to each entry, and | ||
1119 | * store string encodings of the presets' parameters in | ||
1120 | * me->encoded_presets. | ||
1121 | */ | ||
1122 | me->n_encoded_presets = 0; | ||
1123 | preset_menu_alloc_ids(me, me->preset_menu); | ||
1124 | me->encoded_presets = snewn(me->n_encoded_presets, char *); | ||
1125 | for (i = 0; i < me->n_encoded_presets; i++) | ||
1126 | me->encoded_presets[i] = NULL; | ||
1127 | preset_menu_encode_params(me, me->preset_menu); | ||
1128 | |||
1129 | if (id_limit) | ||
1130 | *id_limit = me->n_encoded_presets; | ||
1131 | return me->preset_menu; | ||
1132 | } | ||
1133 | |||
1134 | int midend_which_preset(midend *me) | ||
1135 | { | ||
1136 | char *encoding = me->ourgame->encode_params(me->params, TRUE); | ||
1137 | int i, ret; | ||
1138 | |||
1139 | ret = -1; | ||
1140 | for (i = 0; i < me->n_encoded_presets; i++) | ||
1141 | if (me->encoded_presets[i] && | ||
1142 | !strcmp(encoding, me->encoded_presets[i])) { | ||
1143 | ret = i; | ||
1144 | break; | ||
1145 | } | ||
1146 | |||
1147 | sfree(encoding); | ||
1148 | return ret; | ||
1149 | } | ||
1150 | |||
1151 | int midend_wants_statusbar(midend *me) | ||
1152 | { | ||
1153 | return me->ourgame->wants_statusbar; | ||
1154 | } | ||
1155 | |||
1156 | void midend_request_id_changes(midend *me, void (*notify)(void *), void *ctx) | ||
1157 | { | ||
1158 | me->game_id_change_notify_function = notify; | ||
1159 | me->game_id_change_notify_ctx = ctx; | ||
1160 | } | ||
1161 | |||
1162 | void midend_supersede_game_desc(midend *me, char *desc, char *privdesc) | ||
1163 | { | ||
1164 | sfree(me->desc); | ||
1165 | sfree(me->privdesc); | ||
1166 | me->desc = dupstr(desc); | ||
1167 | me->privdesc = privdesc ? dupstr(privdesc) : NULL; | ||
1168 | if (me->game_id_change_notify_function) | ||
1169 | me->game_id_change_notify_function(me->game_id_change_notify_ctx); | ||
1170 | } | ||
1171 | |||
1172 | config_item *midend_get_config(midend *me, int which, char **wintitle) | ||
1173 | { | ||
1174 | char *titlebuf, *parstr, *rest; | ||
1175 | config_item *ret; | ||
1176 | char sep; | ||
1177 | |||
1178 | assert(wintitle); | ||
1179 | titlebuf = snewn(40 + strlen(me->ourgame->name), char); | ||
1180 | |||
1181 | switch (which) { | ||
1182 | case CFG_SETTINGS: | ||
1183 | sprintf(titlebuf, "%s configuration", me->ourgame->name); | ||
1184 | *wintitle = titlebuf; | ||
1185 | return me->ourgame->configure(me->params); | ||
1186 | case CFG_SEED: | ||
1187 | case CFG_DESC: | ||
1188 | if (!me->curparams) { | ||
1189 | sfree(titlebuf); | ||
1190 | return NULL; | ||
1191 | } | ||
1192 | sprintf(titlebuf, "%s %s selection", me->ourgame->name, | ||
1193 | which == CFG_SEED ? "random" : "game"); | ||
1194 | *wintitle = titlebuf; | ||
1195 | |||
1196 | ret = snewn(2, config_item); | ||
1197 | |||
1198 | ret[0].type = C_STRING; | ||
1199 | if (which == CFG_SEED) | ||
1200 | ret[0].name = "Game random seed"; | ||
1201 | else | ||
1202 | ret[0].name = "Game ID"; | ||
1203 | ret[0].ival = 0; | ||
1204 | /* | ||
1205 | * For CFG_DESC the text going in here will be a string | ||
1206 | * encoding of the restricted parameters, plus a colon, | ||
1207 | * plus the game description. For CFG_SEED it will be the | ||
1208 | * full parameters, plus a hash, plus the random seed data. | ||
1209 | * Either of these is a valid full game ID (although only | ||
1210 | * the former is likely to persist across many code | ||
1211 | * changes). | ||
1212 | */ | ||
1213 | parstr = me->ourgame->encode_params(me->curparams, which == CFG_SEED); | ||
1214 | assert(parstr); | ||
1215 | if (which == CFG_DESC) { | ||
1216 | rest = me->desc ? me->desc : ""; | ||
1217 | sep = ':'; | ||
1218 | } else { | ||
1219 | rest = me->seedstr ? me->seedstr : ""; | ||
1220 | sep = '#'; | ||
1221 | } | ||
1222 | ret[0].sval = snewn(strlen(parstr) + strlen(rest) + 2, char); | ||
1223 | sprintf(ret[0].sval, "%s%c%s", parstr, sep, rest); | ||
1224 | sfree(parstr); | ||
1225 | |||
1226 | ret[1].type = C_END; | ||
1227 | ret[1].name = ret[1].sval = NULL; | ||
1228 | ret[1].ival = 0; | ||
1229 | |||
1230 | return ret; | ||
1231 | } | ||
1232 | |||
1233 | assert(!"We shouldn't be here"); | ||
1234 | return NULL; | ||
1235 | } | ||
1236 | |||
1237 | static char *midend_game_id_int(midend *me, char *id, int defmode) | ||
1238 | { | ||
1239 | char *error, *par, *desc, *seed; | ||
1240 | game_params *newcurparams, *newparams, *oldparams1, *oldparams2; | ||
1241 | int free_params; | ||
1242 | |||
1243 | seed = strchr(id, '#'); | ||
1244 | desc = strchr(id, ':'); | ||
1245 | |||
1246 | if (desc && (!seed || desc < seed)) { | ||
1247 | /* | ||
1248 | * We have a colon separating parameters from game | ||
1249 | * description. So `par' now points to the parameters | ||
1250 | * string, and `desc' to the description string. | ||
1251 | */ | ||
1252 | *desc++ = '\0'; | ||
1253 | par = id; | ||
1254 | seed = NULL; | ||
1255 | } else if (seed && (!desc || seed < desc)) { | ||
1256 | /* | ||
1257 | * We have a hash separating parameters from random seed. | ||
1258 | * So `par' now points to the parameters string, and `seed' | ||
1259 | * to the seed string. | ||
1260 | */ | ||
1261 | *seed++ = '\0'; | ||
1262 | par = id; | ||
1263 | desc = NULL; | ||
1264 | } else { | ||
1265 | /* | ||
1266 | * We only have one string. Depending on `defmode', we take | ||
1267 | * it to be either parameters, seed or description. | ||
1268 | */ | ||
1269 | if (defmode == DEF_SEED) { | ||
1270 | seed = id; | ||
1271 | par = desc = NULL; | ||
1272 | } else if (defmode == DEF_DESC) { | ||
1273 | desc = id; | ||
1274 | par = seed = NULL; | ||
1275 | } else { | ||
1276 | par = id; | ||
1277 | seed = desc = NULL; | ||
1278 | } | ||
1279 | } | ||
1280 | |||
1281 | /* | ||
1282 | * We must be reasonably careful here not to modify anything in | ||
1283 | * `me' until we have finished validating things. This function | ||
1284 | * must either return an error and do nothing to the midend, or | ||
1285 | * return success and do everything; nothing in between is | ||
1286 | * acceptable. | ||
1287 | */ | ||
1288 | newcurparams = newparams = oldparams1 = oldparams2 = NULL; | ||
1289 | |||
1290 | if (par) { | ||
1291 | /* | ||
1292 | * The params string may underspecify the game parameters, so | ||
1293 | * we must first initialise newcurparams with a full set of | ||
1294 | * params from somewhere else before we decode_params the | ||
1295 | * input string over the top. | ||
1296 | * | ||
1297 | * But which set? It depends on what other data we have. | ||
1298 | * | ||
1299 | * If we've been given a _descriptive_ game id, then that may | ||
1300 | * well underspecify by design, e.g. Solo game descriptions | ||
1301 | * often start just '3x3:' without specifying one of Solo's | ||
1302 | * difficulty settings, because it isn't necessary once a game | ||
1303 | * has been generated (and you might not even know it, if | ||
1304 | * you're manually transcribing a game description). In that | ||
1305 | * situation, I've always felt that the best thing to set the | ||
1306 | * difficulty to (for use if the user hits 'New Game' after | ||
1307 | * pasting in that game id) is whatever it was previously set | ||
1308 | * to. That is, we use whatever is already in me->params as | ||
1309 | * the basis for our decoding of this input string. | ||
1310 | * | ||
1311 | * A random-seed based game id, however, should use the real, | ||
1312 | * built-in default params, and not even check the | ||
1313 | * <game>_DEFAULT environment setting, because when people | ||
1314 | * paste each other random seeds - whether it's two users | ||
1315 | * arranging to generate the same game at the same time to | ||
1316 | * race solving them, or a user sending a bug report upstream | ||
1317 | * - the whole point is for the random game id to always be | ||
1318 | * interpreted the same way, even if it does underspecify. | ||
1319 | * | ||
1320 | * A parameter string typed in on its own, with no seed _or_ | ||
1321 | * description, gets treated the same way as a random seed, | ||
1322 | * because again I think the most likely reason for doing that | ||
1323 | * is to have a portable representation of a set of params. | ||
1324 | */ | ||
1325 | if (desc) { | ||
1326 | newcurparams = me->ourgame->dup_params(me->params); | ||
1327 | } else { | ||
1328 | newcurparams = me->ourgame->default_params(); | ||
1329 | } | ||
1330 | me->ourgame->decode_params(newcurparams, par); | ||
1331 | error = me->ourgame->validate_params(newcurparams, desc == NULL); | ||
1332 | if (error) { | ||
1333 | me->ourgame->free_params(newcurparams); | ||
1334 | return error; | ||
1335 | } | ||
1336 | oldparams1 = me->curparams; | ||
1337 | |||
1338 | /* | ||
1339 | * Now filter only the persistent parts of this state into | ||
1340 | * the long-term params structure, unless we've _only_ | ||
1341 | * received a params string in which case the whole lot is | ||
1342 | * persistent. | ||
1343 | */ | ||
1344 | oldparams2 = me->params; | ||
1345 | if (seed || desc) { | ||
1346 | char *tmpstr; | ||
1347 | |||
1348 | newparams = me->ourgame->dup_params(me->params); | ||
1349 | |||
1350 | tmpstr = me->ourgame->encode_params(newcurparams, FALSE); | ||
1351 | me->ourgame->decode_params(newparams, tmpstr); | ||
1352 | |||
1353 | sfree(tmpstr); | ||
1354 | } else { | ||
1355 | newparams = me->ourgame->dup_params(newcurparams); | ||
1356 | } | ||
1357 | free_params = TRUE; | ||
1358 | } else { | ||
1359 | newcurparams = me->curparams; | ||
1360 | newparams = me->params; | ||
1361 | free_params = FALSE; | ||
1362 | } | ||
1363 | |||
1364 | if (desc) { | ||
1365 | error = me->ourgame->validate_desc(newparams, desc); | ||
1366 | if (error) { | ||
1367 | if (free_params) { | ||
1368 | if (newcurparams) | ||
1369 | me->ourgame->free_params(newcurparams); | ||
1370 | if (newparams) | ||
1371 | me->ourgame->free_params(newparams); | ||
1372 | } | ||
1373 | return error; | ||
1374 | } | ||
1375 | } | ||
1376 | |||
1377 | /* | ||
1378 | * Now we've got past all possible error points. Update the | ||
1379 | * midend itself. | ||
1380 | */ | ||
1381 | me->params = newparams; | ||
1382 | me->curparams = newcurparams; | ||
1383 | if (oldparams1) | ||
1384 | me->ourgame->free_params(oldparams1); | ||
1385 | if (oldparams2) | ||
1386 | me->ourgame->free_params(oldparams2); | ||
1387 | |||
1388 | sfree(me->desc); | ||
1389 | sfree(me->privdesc); | ||
1390 | me->desc = me->privdesc = NULL; | ||
1391 | sfree(me->seedstr); | ||
1392 | me->seedstr = NULL; | ||
1393 | |||
1394 | if (desc) { | ||
1395 | me->desc = dupstr(desc); | ||
1396 | me->genmode = GOT_DESC; | ||
1397 | sfree(me->aux_info); | ||
1398 | me->aux_info = NULL; | ||
1399 | } | ||
1400 | |||
1401 | if (seed) { | ||
1402 | me->seedstr = dupstr(seed); | ||
1403 | me->genmode = GOT_SEED; | ||
1404 | } | ||
1405 | |||
1406 | return NULL; | ||
1407 | } | ||
1408 | |||
1409 | char *midend_game_id(midend *me, char *id) | ||
1410 | { | ||
1411 | return midend_game_id_int(me, id, DEF_PARAMS); | ||
1412 | } | ||
1413 | |||
1414 | char *midend_get_game_id(midend *me) | ||
1415 | { | ||
1416 | char *parstr, *ret; | ||
1417 | |||
1418 | parstr = me->ourgame->encode_params(me->curparams, FALSE); | ||
1419 | assert(parstr); | ||
1420 | assert(me->desc); | ||
1421 | ret = snewn(strlen(parstr) + strlen(me->desc) + 2, char); | ||
1422 | sprintf(ret, "%s:%s", parstr, me->desc); | ||
1423 | sfree(parstr); | ||
1424 | return ret; | ||
1425 | } | ||
1426 | |||
1427 | char *midend_get_random_seed(midend *me) | ||
1428 | { | ||
1429 | char *parstr, *ret; | ||
1430 | |||
1431 | if (!me->seedstr) | ||
1432 | return NULL; | ||
1433 | |||
1434 | parstr = me->ourgame->encode_params(me->curparams, TRUE); | ||
1435 | assert(parstr); | ||
1436 | ret = snewn(strlen(parstr) + strlen(me->seedstr) + 2, char); | ||
1437 | sprintf(ret, "%s#%s", parstr, me->seedstr); | ||
1438 | sfree(parstr); | ||
1439 | return ret; | ||
1440 | } | ||
1441 | |||
1442 | char *midend_set_config(midend *me, int which, config_item *cfg) | ||
1443 | { | ||
1444 | char *error; | ||
1445 | game_params *params; | ||
1446 | |||
1447 | switch (which) { | ||
1448 | case CFG_SETTINGS: | ||
1449 | params = me->ourgame->custom_params(cfg); | ||
1450 | error = me->ourgame->validate_params(params, TRUE); | ||
1451 | |||
1452 | if (error) { | ||
1453 | me->ourgame->free_params(params); | ||
1454 | return error; | ||
1455 | } | ||
1456 | |||
1457 | me->ourgame->free_params(me->params); | ||
1458 | me->params = params; | ||
1459 | break; | ||
1460 | |||
1461 | case CFG_SEED: | ||
1462 | case CFG_DESC: | ||
1463 | error = midend_game_id_int(me, cfg[0].sval, | ||
1464 | (which == CFG_SEED ? DEF_SEED : DEF_DESC)); | ||
1465 | if (error) | ||
1466 | return error; | ||
1467 | break; | ||
1468 | } | ||
1469 | |||
1470 | return NULL; | ||
1471 | } | ||
1472 | |||
1473 | int midend_can_format_as_text_now(midend *me) | ||
1474 | { | ||
1475 | if (me->ourgame->can_format_as_text_ever) | ||
1476 | return me->ourgame->can_format_as_text_now(me->params); | ||
1477 | else | ||
1478 | return FALSE; | ||
1479 | } | ||
1480 | |||
1481 | char *midend_text_format(midend *me) | ||
1482 | { | ||
1483 | if (me->ourgame->can_format_as_text_ever && me->statepos > 0 && | ||
1484 | me->ourgame->can_format_as_text_now(me->params)) | ||
1485 | return me->ourgame->text_format(me->states[me->statepos-1].state); | ||
1486 | else | ||
1487 | return NULL; | ||
1488 | } | ||
1489 | |||
1490 | char *midend_solve(midend *me) | ||
1491 | { | ||
1492 | game_state *s; | ||
1493 | char *msg, *movestr; | ||
1494 | |||
1495 | if (!me->ourgame->can_solve) | ||
1496 | return "This game does not support the Solve operation"; | ||
1497 | |||
1498 | if (me->statepos < 1) | ||
1499 | return "No game set up to solve"; /* _shouldn't_ happen! */ | ||
1500 | |||
1501 | msg = NULL; | ||
1502 | movestr = me->ourgame->solve(me->states[0].state, | ||
1503 | me->states[me->statepos-1].state, | ||
1504 | me->aux_info, &msg); | ||
1505 | if (!movestr) { | ||
1506 | if (!msg) | ||
1507 | msg = "Solve operation failed"; /* _shouldn't_ happen, but can */ | ||
1508 | return msg; | ||
1509 | } | ||
1510 | s = me->ourgame->execute_move(me->states[me->statepos-1].state, movestr); | ||
1511 | assert(s); | ||
1512 | |||
1513 | /* | ||
1514 | * Now enter the solved state as the next move. | ||
1515 | */ | ||
1516 | midend_stop_anim(me); | ||
1517 | midend_purge_states(me); | ||
1518 | ensure(me); | ||
1519 | me->states[me->nstates].state = s; | ||
1520 | me->states[me->nstates].movestr = movestr; | ||
1521 | me->states[me->nstates].movetype = SOLVE; | ||
1522 | me->statepos = ++me->nstates; | ||
1523 | if (me->ui) | ||
1524 | me->ourgame->changed_state(me->ui, | ||
1525 | me->states[me->statepos-2].state, | ||
1526 | me->states[me->statepos-1].state); | ||
1527 | me->dir = +1; | ||
1528 | if (me->ourgame->flags & SOLVE_ANIMATES) { | ||
1529 | me->oldstate = me->ourgame->dup_game(me->states[me->statepos-2].state); | ||
1530 | me->anim_time = | ||
1531 | me->ourgame->anim_length(me->states[me->statepos-2].state, | ||
1532 | me->states[me->statepos-1].state, | ||
1533 | +1, me->ui); | ||
1534 | me->anim_pos = 0.0; | ||
1535 | } else { | ||
1536 | me->anim_time = 0.0; | ||
1537 | midend_finish_move(me); | ||
1538 | } | ||
1539 | if (me->drawing) | ||
1540 | midend_redraw(me); | ||
1541 | midend_set_timer(me); | ||
1542 | return NULL; | ||
1543 | } | ||
1544 | |||
1545 | int midend_status(midend *me) | ||
1546 | { | ||
1547 | /* | ||
1548 | * We should probably never be called when the state stack has no | ||
1549 | * states on it at all - ideally, midends should never be left in | ||
1550 | * that state for long enough to get put down and forgotten about. | ||
1551 | * But if we are, I think we return _true_ - pedantically speaking | ||
1552 | * a midend in that state is 'vacuously solved', and more | ||
1553 | * practically, a user whose midend has been left in that state | ||
1554 | * probably _does_ want the 'new game' option to be prominent. | ||
1555 | */ | ||
1556 | if (me->statepos == 0) | ||
1557 | return +1; | ||
1558 | |||
1559 | return me->ourgame->status(me->states[me->statepos-1].state); | ||
1560 | } | ||
1561 | |||
1562 | char *midend_rewrite_statusbar(midend *me, char *text) | ||
1563 | { | ||
1564 | /* | ||
1565 | * An important special case is that we are occasionally called | ||
1566 | * with our own laststatus, to update the timer. | ||
1567 | */ | ||
1568 | if (me->laststatus != text) { | ||
1569 | sfree(me->laststatus); | ||
1570 | me->laststatus = dupstr(text); | ||
1571 | } | ||
1572 | |||
1573 | if (me->ourgame->is_timed) { | ||
1574 | char timebuf[100], *ret; | ||
1575 | int min, sec; | ||
1576 | |||
1577 | sec = (int)me->elapsed; | ||
1578 | min = sec / 60; | ||
1579 | sec %= 60; | ||
1580 | sprintf(timebuf, "[%d:%02d] ", min, sec); | ||
1581 | |||
1582 | ret = snewn(strlen(timebuf) + strlen(text) + 1, char); | ||
1583 | strcpy(ret, timebuf); | ||
1584 | strcat(ret, text); | ||
1585 | return ret; | ||
1586 | |||
1587 | } else { | ||
1588 | return dupstr(text); | ||
1589 | } | ||
1590 | } | ||
1591 | |||
1592 | #define SERIALISE_MAGIC "Simon Tatham's Portable Puzzle Collection" | ||
1593 | #define SERIALISE_VERSION "1" | ||
1594 | |||
1595 | void midend_serialise(midend *me, | ||
1596 | void (*write)(void *ctx, void *buf, int len), | ||
1597 | void *wctx) | ||
1598 | { | ||
1599 | int i; | ||
1600 | |||
1601 | /* | ||
1602 | * Each line of the save file contains three components. First | ||
1603 | * exactly 8 characters of header word indicating what type of | ||
1604 | * data is contained on the line; then a colon followed by a | ||
1605 | * decimal integer giving the length of the main string on the | ||
1606 | * line; then a colon followed by the string itself (exactly as | ||
1607 | * many bytes as previously specified, no matter what they | ||
1608 | * contain). Then a newline (of reasonably flexible form). | ||
1609 | */ | ||
1610 | #define wr(h,s) do { \ | ||
1611 | char hbuf[80]; \ | ||
1612 | char *str = (s); \ | ||
1613 | char lbuf[9]; \ | ||
1614 | copy_left_justified(lbuf, sizeof(lbuf), h); \ | ||
1615 | sprintf(hbuf, "%s:%d:", lbuf, (int)strlen(str)); \ | ||
1616 | write(wctx, hbuf, strlen(hbuf)); \ | ||
1617 | write(wctx, str, strlen(str)); \ | ||
1618 | write(wctx, "\n", 1); \ | ||
1619 | } while (0) | ||
1620 | |||
1621 | /* | ||
1622 | * Magic string identifying the file, and version number of the | ||
1623 | * file format. | ||
1624 | */ | ||
1625 | wr("SAVEFILE", SERIALISE_MAGIC); | ||
1626 | wr("VERSION", SERIALISE_VERSION); | ||
1627 | |||
1628 | /* | ||
1629 | * The game name. (Copied locally to avoid const annoyance.) | ||
1630 | */ | ||
1631 | { | ||
1632 | char *s = dupstr(me->ourgame->name); | ||
1633 | wr("GAME", s); | ||
1634 | sfree(s); | ||
1635 | } | ||
1636 | |||
1637 | /* | ||
1638 | * The current long-term parameters structure, in full. | ||
1639 | */ | ||
1640 | if (me->params) { | ||
1641 | char *s = me->ourgame->encode_params(me->params, TRUE); | ||
1642 | wr("PARAMS", s); | ||
1643 | sfree(s); | ||
1644 | } | ||
1645 | |||
1646 | /* | ||
1647 | * The current short-term parameters structure, in full. | ||
1648 | */ | ||
1649 | if (me->curparams) { | ||
1650 | char *s = me->ourgame->encode_params(me->curparams, TRUE); | ||
1651 | wr("CPARAMS", s); | ||
1652 | sfree(s); | ||
1653 | } | ||
1654 | |||
1655 | /* | ||
1656 | * The current game description, the privdesc, and the random seed. | ||
1657 | */ | ||
1658 | if (me->seedstr) | ||
1659 | wr("SEED", me->seedstr); | ||
1660 | if (me->desc) | ||
1661 | wr("DESC", me->desc); | ||
1662 | if (me->privdesc) | ||
1663 | wr("PRIVDESC", me->privdesc); | ||
1664 | |||
1665 | /* | ||
1666 | * The game's aux_info. We obfuscate this to prevent spoilers | ||
1667 | * (people are likely to run `head' or similar on a saved game | ||
1668 | * file simply to find out what it is, and don't necessarily | ||
1669 | * want to be told the answer to the puzzle!) | ||
1670 | */ | ||
1671 | if (me->aux_info) { | ||
1672 | unsigned char *s1; | ||
1673 | char *s2; | ||
1674 | int len; | ||
1675 | |||
1676 | len = strlen(me->aux_info); | ||
1677 | s1 = snewn(len, unsigned char); | ||
1678 | memcpy(s1, me->aux_info, len); | ||
1679 | obfuscate_bitmap(s1, len*8, FALSE); | ||
1680 | s2 = bin2hex(s1, len); | ||
1681 | |||
1682 | wr("AUXINFO", s2); | ||
1683 | |||
1684 | sfree(s2); | ||
1685 | sfree(s1); | ||
1686 | } | ||
1687 | |||
1688 | /* | ||
1689 | * Any required serialisation of the game_ui. | ||
1690 | */ | ||
1691 | if (me->ui) { | ||
1692 | char *s = me->ourgame->encode_ui(me->ui); | ||
1693 | if (s) { | ||
1694 | wr("UI", s); | ||
1695 | sfree(s); | ||
1696 | } | ||
1697 | } | ||
1698 | |||
1699 | /* | ||
1700 | * The game time, if it's a timed game. | ||
1701 | */ | ||
1702 | if (me->ourgame->is_timed) { | ||
1703 | char buf[80]; | ||
1704 | sprintf(buf, "%g", me->elapsed); | ||
1705 | wr("TIME", buf); | ||
1706 | } | ||
1707 | |||
1708 | /* | ||
1709 | * The length of, and position in, the states list. | ||
1710 | */ | ||
1711 | { | ||
1712 | char buf[80]; | ||
1713 | sprintf(buf, "%d", me->nstates); | ||
1714 | wr("NSTATES", buf); | ||
1715 | sprintf(buf, "%d", me->statepos); | ||
1716 | wr("STATEPOS", buf); | ||
1717 | } | ||
1718 | |||
1719 | /* | ||
1720 | * For each state after the initial one (which we know is | ||
1721 | * constructed from either privdesc or desc), enough | ||
1722 | * information for execute_move() to reconstruct it from the | ||
1723 | * previous one. | ||
1724 | */ | ||
1725 | for (i = 1; i < me->nstates; i++) { | ||
1726 | assert(me->states[i].movetype != NEWGAME); /* only state 0 */ | ||
1727 | switch (me->states[i].movetype) { | ||
1728 | case MOVE: | ||
1729 | wr("MOVE", me->states[i].movestr); | ||
1730 | break; | ||
1731 | case SOLVE: | ||
1732 | wr("SOLVE", me->states[i].movestr); | ||
1733 | break; | ||
1734 | case RESTART: | ||
1735 | wr("RESTART", me->states[i].movestr); | ||
1736 | break; | ||
1737 | } | ||
1738 | } | ||
1739 | |||
1740 | #undef wr | ||
1741 | } | ||
1742 | |||
1743 | /* | ||
1744 | * This function returns NULL on success, or an error message. | ||
1745 | */ | ||
1746 | char *midend_deserialise(midend *me, | ||
1747 | int (*read)(void *ctx, void *buf, int len), | ||
1748 | void *rctx) | ||
1749 | { | ||
1750 | int nstates = 0, statepos = -1, gotstates = 0; | ||
1751 | int started = FALSE; | ||
1752 | int i; | ||
1753 | |||
1754 | char *val = NULL; | ||
1755 | /* Initially all errors give the same report */ | ||
1756 | char *ret = "Data does not appear to be a saved game file"; | ||
1757 | |||
1758 | /* | ||
1759 | * We construct all the new state in local variables while we | ||
1760 | * check its sanity. Only once we have finished reading the | ||
1761 | * serialised data and detected no errors at all do we start | ||
1762 | * modifying stuff in the midend passed in. | ||
1763 | */ | ||
1764 | char *seed = NULL, *parstr = NULL, *desc = NULL, *privdesc = NULL; | ||
1765 | char *auxinfo = NULL, *uistr = NULL, *cparstr = NULL; | ||
1766 | float elapsed = 0.0F; | ||
1767 | game_params *params = NULL, *cparams = NULL; | ||
1768 | game_ui *ui = NULL; | ||
1769 | struct midend_state_entry *states = NULL; | ||
1770 | |||
1771 | /* | ||
1772 | * Loop round and round reading one key/value pair at a time | ||
1773 | * from the serialised stream, until we have enough game states | ||
1774 | * to finish. | ||
1775 | */ | ||
1776 | while (nstates <= 0 || statepos < 0 || gotstates < nstates-1) { | ||
1777 | char key[9], c; | ||
1778 | int len; | ||
1779 | |||
1780 | do { | ||
1781 | if (!read(rctx, key, 1)) { | ||
1782 | /* unexpected EOF */ | ||
1783 | goto cleanup; | ||
1784 | } | ||
1785 | } while (key[0] == '\r' || key[0] == '\n'); | ||
1786 | |||
1787 | if (!read(rctx, key+1, 8)) { | ||
1788 | /* unexpected EOF */ | ||
1789 | goto cleanup; | ||
1790 | } | ||
1791 | |||
1792 | if (key[8] != ':') { | ||
1793 | if (started) | ||
1794 | ret = "Data was incorrectly formatted for a saved game file"; | ||
1795 | goto cleanup; | ||
1796 | } | ||
1797 | len = strcspn(key, ": "); | ||
1798 | assert(len <= 8); | ||
1799 | key[len] = '\0'; | ||
1800 | |||
1801 | len = 0; | ||
1802 | while (1) { | ||
1803 | if (!read(rctx, &c, 1)) { | ||
1804 | /* unexpected EOF */ | ||
1805 | goto cleanup; | ||
1806 | } | ||
1807 | |||
1808 | if (c == ':') { | ||
1809 | break; | ||
1810 | } else if (c >= '0' && c <= '9') { | ||
1811 | len = (len * 10) + (c - '0'); | ||
1812 | } else { | ||
1813 | if (started) | ||
1814 | ret = "Data was incorrectly formatted for a" | ||
1815 | " saved game file"; | ||
1816 | goto cleanup; | ||
1817 | } | ||
1818 | } | ||
1819 | |||
1820 | val = snewn(len+1, char); | ||
1821 | if (!read(rctx, val, len)) { | ||
1822 | if (started) | ||
1823 | goto cleanup; | ||
1824 | } | ||
1825 | val[len] = '\0'; | ||
1826 | |||
1827 | if (!started) { | ||
1828 | if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) { | ||
1829 | /* ret already has the right message in it */ | ||
1830 | goto cleanup; | ||
1831 | } | ||
1832 | /* Now most errors are this one, unless otherwise specified */ | ||
1833 | ret = "Saved data ended unexpectedly"; | ||
1834 | started = TRUE; | ||
1835 | } else { | ||
1836 | if (!strcmp(key, "VERSION")) { | ||
1837 | if (strcmp(val, SERIALISE_VERSION)) { | ||
1838 | ret = "Cannot handle this version of the saved game" | ||
1839 | " file format"; | ||
1840 | goto cleanup; | ||
1841 | } | ||
1842 | } else if (!strcmp(key, "GAME")) { | ||
1843 | if (strcmp(val, me->ourgame->name)) { | ||
1844 | ret = "Save file is from a different game"; | ||
1845 | goto cleanup; | ||
1846 | } | ||
1847 | } else if (!strcmp(key, "PARAMS")) { | ||
1848 | sfree(parstr); | ||
1849 | parstr = val; | ||
1850 | val = NULL; | ||
1851 | } else if (!strcmp(key, "CPARAMS")) { | ||
1852 | sfree(cparstr); | ||
1853 | cparstr = val; | ||
1854 | val = NULL; | ||
1855 | } else if (!strcmp(key, "SEED")) { | ||
1856 | sfree(seed); | ||
1857 | seed = val; | ||
1858 | val = NULL; | ||
1859 | } else if (!strcmp(key, "DESC")) { | ||
1860 | sfree(desc); | ||
1861 | desc = val; | ||
1862 | val = NULL; | ||
1863 | } else if (!strcmp(key, "PRIVDESC")) { | ||
1864 | sfree(privdesc); | ||
1865 | privdesc = val; | ||
1866 | val = NULL; | ||
1867 | } else if (!strcmp(key, "AUXINFO")) { | ||
1868 | unsigned char *tmp; | ||
1869 | int len = strlen(val) / 2; /* length in bytes */ | ||
1870 | tmp = hex2bin(val, len); | ||
1871 | obfuscate_bitmap(tmp, len*8, TRUE); | ||
1872 | |||
1873 | sfree(auxinfo); | ||
1874 | auxinfo = snewn(len + 1, char); | ||
1875 | memcpy(auxinfo, tmp, len); | ||
1876 | auxinfo[len] = '\0'; | ||
1877 | sfree(tmp); | ||
1878 | } else if (!strcmp(key, "UI")) { | ||
1879 | sfree(uistr); | ||
1880 | uistr = val; | ||
1881 | val = NULL; | ||
1882 | } else if (!strcmp(key, "TIME")) { | ||
1883 | elapsed = (float)atof(val); | ||
1884 | } else if (!strcmp(key, "NSTATES")) { | ||
1885 | nstates = atoi(val); | ||
1886 | if (nstates <= 0) { | ||
1887 | ret = "Number of states in save file was negative"; | ||
1888 | goto cleanup; | ||
1889 | } | ||
1890 | if (states) { | ||
1891 | ret = "Two state counts provided in save file"; | ||
1892 | goto cleanup; | ||
1893 | } | ||
1894 | states = snewn(nstates, struct midend_state_entry); | ||
1895 | for (i = 0; i < nstates; i++) { | ||
1896 | states[i].state = NULL; | ||
1897 | states[i].movestr = NULL; | ||
1898 | states[i].movetype = NEWGAME; | ||
1899 | } | ||
1900 | } else if (!strcmp(key, "STATEPOS")) { | ||
1901 | statepos = atoi(val); | ||
1902 | } else if (!strcmp(key, "MOVE")) { | ||
1903 | gotstates++; | ||
1904 | states[gotstates].movetype = MOVE; | ||
1905 | states[gotstates].movestr = val; | ||
1906 | val = NULL; | ||
1907 | } else if (!strcmp(key, "SOLVE")) { | ||
1908 | gotstates++; | ||
1909 | states[gotstates].movetype = SOLVE; | ||
1910 | states[gotstates].movestr = val; | ||
1911 | val = NULL; | ||
1912 | } else if (!strcmp(key, "RESTART")) { | ||
1913 | gotstates++; | ||
1914 | states[gotstates].movetype = RESTART; | ||
1915 | states[gotstates].movestr = val; | ||
1916 | val = NULL; | ||
1917 | } | ||
1918 | } | ||
1919 | |||
1920 | sfree(val); | ||
1921 | val = NULL; | ||
1922 | } | ||
1923 | |||
1924 | params = me->ourgame->default_params(); | ||
1925 | me->ourgame->decode_params(params, parstr); | ||
1926 | if (me->ourgame->validate_params(params, TRUE)) { | ||
1927 | ret = "Long-term parameters in save file are invalid"; | ||
1928 | goto cleanup; | ||
1929 | } | ||
1930 | cparams = me->ourgame->default_params(); | ||
1931 | me->ourgame->decode_params(cparams, cparstr); | ||
1932 | if (me->ourgame->validate_params(cparams, FALSE)) { | ||
1933 | ret = "Short-term parameters in save file are invalid"; | ||
1934 | goto cleanup; | ||
1935 | } | ||
1936 | if (seed && me->ourgame->validate_params(cparams, TRUE)) { | ||
1937 | /* | ||
1938 | * The seed's no use with this version, but we can perfectly | ||
1939 | * well use the rest of the data. | ||
1940 | */ | ||
1941 | sfree(seed); | ||
1942 | seed = NULL; | ||
1943 | } | ||
1944 | if (!desc) { | ||
1945 | ret = "Game description in save file is missing"; | ||
1946 | goto cleanup; | ||
1947 | } else if (me->ourgame->validate_desc(params, desc)) { | ||
1948 | ret = "Game description in save file is invalid"; | ||
1949 | goto cleanup; | ||
1950 | } | ||
1951 | if (privdesc && me->ourgame->validate_desc(params, privdesc)) { | ||
1952 | ret = "Game private description in save file is invalid"; | ||
1953 | goto cleanup; | ||
1954 | } | ||
1955 | if (statepos < 0 || statepos >= nstates) { | ||
1956 | ret = "Game position in save file is out of range"; | ||
1957 | } | ||
1958 | |||
1959 | states[0].state = me->ourgame->new_game(me, params, | ||
1960 | privdesc ? privdesc : desc); | ||
1961 | for (i = 1; i < nstates; i++) { | ||
1962 | assert(states[i].movetype != NEWGAME); | ||
1963 | switch (states[i].movetype) { | ||
1964 | case MOVE: | ||
1965 | case SOLVE: | ||
1966 | states[i].state = me->ourgame->execute_move(states[i-1].state, | ||
1967 | states[i].movestr); | ||
1968 | if (states[i].state == NULL) { | ||
1969 | ret = "Save file contained an invalid move"; | ||
1970 | goto cleanup; | ||
1971 | } | ||
1972 | break; | ||
1973 | case RESTART: | ||
1974 | if (me->ourgame->validate_desc(params, states[i].movestr)) { | ||
1975 | ret = "Save file contained an invalid restart move"; | ||
1976 | goto cleanup; | ||
1977 | } | ||
1978 | states[i].state = me->ourgame->new_game(me, params, | ||
1979 | states[i].movestr); | ||
1980 | break; | ||
1981 | } | ||
1982 | } | ||
1983 | |||
1984 | ui = me->ourgame->new_ui(states[0].state); | ||
1985 | me->ourgame->decode_ui(ui, uistr); | ||
1986 | |||
1987 | /* | ||
1988 | * Now we've run out of possible error conditions, so we're | ||
1989 | * ready to start overwriting the real data in the current | ||
1990 | * midend. We'll do this by swapping things with the local | ||
1991 | * variables, so that the same cleanup code will free the old | ||
1992 | * stuff. | ||
1993 | */ | ||
1994 | { | ||
1995 | char *tmp; | ||
1996 | |||
1997 | tmp = me->desc; | ||
1998 | me->desc = desc; | ||
1999 | desc = tmp; | ||
2000 | |||
2001 | tmp = me->privdesc; | ||
2002 | me->privdesc = privdesc; | ||
2003 | privdesc = tmp; | ||
2004 | |||
2005 | tmp = me->seedstr; | ||
2006 | me->seedstr = seed; | ||
2007 | seed = tmp; | ||
2008 | |||
2009 | tmp = me->aux_info; | ||
2010 | me->aux_info = auxinfo; | ||
2011 | auxinfo = tmp; | ||
2012 | } | ||
2013 | |||
2014 | me->genmode = GOT_NOTHING; | ||
2015 | |||
2016 | me->statesize = nstates; | ||
2017 | nstates = me->nstates; | ||
2018 | me->nstates = me->statesize; | ||
2019 | { | ||
2020 | struct midend_state_entry *tmp; | ||
2021 | tmp = me->states; | ||
2022 | me->states = states; | ||
2023 | states = tmp; | ||
2024 | } | ||
2025 | me->statepos = statepos; | ||
2026 | |||
2027 | { | ||
2028 | game_params *tmp; | ||
2029 | |||
2030 | tmp = me->params; | ||
2031 | me->params = params; | ||
2032 | params = tmp; | ||
2033 | |||
2034 | tmp = me->curparams; | ||
2035 | me->curparams = cparams; | ||
2036 | cparams = tmp; | ||
2037 | } | ||
2038 | |||
2039 | me->oldstate = NULL; | ||
2040 | me->anim_time = me->anim_pos = me->flash_time = me->flash_pos = 0.0F; | ||
2041 | me->dir = 0; | ||
2042 | |||
2043 | { | ||
2044 | game_ui *tmp; | ||
2045 | |||
2046 | tmp = me->ui; | ||
2047 | me->ui = ui; | ||
2048 | ui = tmp; | ||
2049 | } | ||
2050 | |||
2051 | me->elapsed = elapsed; | ||
2052 | me->pressed_mouse_button = 0; | ||
2053 | |||
2054 | midend_set_timer(me); | ||
2055 | |||
2056 | if (me->drawstate) | ||
2057 | me->ourgame->free_drawstate(me->drawing, me->drawstate); | ||
2058 | me->drawstate = | ||
2059 | me->ourgame->new_drawstate(me->drawing, | ||
2060 | me->states[me->statepos-1].state); | ||
2061 | midend_size_new_drawstate(me); | ||
2062 | |||
2063 | ret = NULL; /* success! */ | ||
2064 | |||
2065 | cleanup: | ||
2066 | sfree(val); | ||
2067 | sfree(seed); | ||
2068 | sfree(parstr); | ||
2069 | sfree(cparstr); | ||
2070 | sfree(desc); | ||
2071 | sfree(privdesc); | ||
2072 | sfree(auxinfo); | ||
2073 | sfree(uistr); | ||
2074 | if (params) | ||
2075 | me->ourgame->free_params(params); | ||
2076 | if (cparams) | ||
2077 | me->ourgame->free_params(cparams); | ||
2078 | if (ui) | ||
2079 | me->ourgame->free_ui(ui); | ||
2080 | if (states) { | ||
2081 | int i; | ||
2082 | |||
2083 | for (i = 0; i < nstates; i++) { | ||
2084 | if (states[i].state) | ||
2085 | me->ourgame->free_game(states[i].state); | ||
2086 | sfree(states[i].movestr); | ||
2087 | } | ||
2088 | sfree(states); | ||
2089 | } | ||
2090 | |||
2091 | return ret; | ||
2092 | } | ||
2093 | |||
2094 | /* | ||
2095 | * This function examines a saved game file just far enough to | ||
2096 | * determine which game type it contains. It returns NULL on success | ||
2097 | * and the game name string in 'name' (which will be dynamically | ||
2098 | * allocated and should be caller-freed), or an error message on | ||
2099 | * failure. | ||
2100 | */ | ||
2101 | char *identify_game(char **name, int (*read)(void *ctx, void *buf, int len), | ||
2102 | void *rctx) | ||
2103 | { | ||
2104 | int nstates = 0, statepos = -1, gotstates = 0; | ||
2105 | int started = FALSE; | ||
2106 | |||
2107 | char *val = NULL; | ||
2108 | /* Initially all errors give the same report */ | ||
2109 | char *ret = "Data does not appear to be a saved game file"; | ||
2110 | |||
2111 | *name = NULL; | ||
2112 | |||
2113 | /* | ||
2114 | * Loop round and round reading one key/value pair at a time from | ||
2115 | * the serialised stream, until we've found the game name. | ||
2116 | */ | ||
2117 | while (nstates <= 0 || statepos < 0 || gotstates < nstates-1) { | ||
2118 | char key[9], c; | ||
2119 | int len; | ||
2120 | |||
2121 | do { | ||
2122 | if (!read(rctx, key, 1)) { | ||
2123 | /* unexpected EOF */ | ||
2124 | goto cleanup; | ||
2125 | } | ||
2126 | } while (key[0] == '\r' || key[0] == '\n'); | ||
2127 | |||
2128 | if (!read(rctx, key+1, 8)) { | ||
2129 | /* unexpected EOF */ | ||
2130 | goto cleanup; | ||
2131 | } | ||
2132 | |||
2133 | if (key[8] != ':') { | ||
2134 | if (started) | ||
2135 | ret = "Data was incorrectly formatted for a saved game file"; | ||
2136 | goto cleanup; | ||
2137 | } | ||
2138 | len = strcspn(key, ": "); | ||
2139 | assert(len <= 8); | ||
2140 | key[len] = '\0'; | ||
2141 | |||
2142 | len = 0; | ||
2143 | while (1) { | ||
2144 | if (!read(rctx, &c, 1)) { | ||
2145 | /* unexpected EOF */ | ||
2146 | goto cleanup; | ||
2147 | } | ||
2148 | |||
2149 | if (c == ':') { | ||
2150 | break; | ||
2151 | } else if (c >= '0' && c <= '9') { | ||
2152 | len = (len * 10) + (c - '0'); | ||
2153 | } else { | ||
2154 | if (started) | ||
2155 | ret = "Data was incorrectly formatted for a" | ||
2156 | " saved game file"; | ||
2157 | goto cleanup; | ||
2158 | } | ||
2159 | } | ||
2160 | |||
2161 | val = snewn(len+1, char); | ||
2162 | if (!read(rctx, val, len)) { | ||
2163 | if (started) | ||
2164 | goto cleanup; | ||
2165 | } | ||
2166 | val[len] = '\0'; | ||
2167 | |||
2168 | if (!started) { | ||
2169 | if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) { | ||
2170 | /* ret already has the right message in it */ | ||
2171 | goto cleanup; | ||
2172 | } | ||
2173 | /* Now most errors are this one, unless otherwise specified */ | ||
2174 | ret = "Saved data ended unexpectedly"; | ||
2175 | started = TRUE; | ||
2176 | } else { | ||
2177 | if (!strcmp(key, "VERSION")) { | ||
2178 | if (strcmp(val, SERIALISE_VERSION)) { | ||
2179 | ret = "Cannot handle this version of the saved game" | ||
2180 | " file format"; | ||
2181 | goto cleanup; | ||
2182 | } | ||
2183 | } else if (!strcmp(key, "GAME")) { | ||
2184 | *name = dupstr(val); | ||
2185 | ret = NULL; | ||
2186 | goto cleanup; | ||
2187 | } | ||
2188 | } | ||
2189 | |||
2190 | sfree(val); | ||
2191 | val = NULL; | ||
2192 | } | ||
2193 | |||
2194 | cleanup: | ||
2195 | sfree(val); | ||
2196 | return ret; | ||
2197 | } | ||
2198 | |||
2199 | char *midend_print_puzzle(midend *me, document *doc, int with_soln) | ||
2200 | { | ||
2201 | game_state *soln = NULL; | ||
2202 | |||
2203 | if (me->statepos < 1) | ||
2204 | return "No game set up to print";/* _shouldn't_ happen! */ | ||
2205 | |||
2206 | if (with_soln) { | ||
2207 | char *msg, *movestr; | ||
2208 | |||
2209 | if (!me->ourgame->can_solve) | ||
2210 | return "This game does not support the Solve operation"; | ||
2211 | |||
2212 | msg = "Solve operation failed";/* game _should_ overwrite on error */ | ||
2213 | movestr = me->ourgame->solve(me->states[0].state, | ||
2214 | me->states[me->statepos-1].state, | ||
2215 | me->aux_info, &msg); | ||
2216 | if (!movestr) | ||
2217 | return msg; | ||
2218 | soln = me->ourgame->execute_move(me->states[me->statepos-1].state, | ||
2219 | movestr); | ||
2220 | assert(soln); | ||
2221 | |||
2222 | sfree(movestr); | ||
2223 | } else | ||
2224 | soln = NULL; | ||
2225 | |||
2226 | /* | ||
2227 | * This call passes over ownership of the two game_states and | ||
2228 | * the game_params. Hence we duplicate the ones we want to | ||
2229 | * keep, and we don't have to bother freeing soln if it was | ||
2230 | * non-NULL. | ||
2231 | */ | ||
2232 | document_add_puzzle(doc, me->ourgame, | ||
2233 | me->ourgame->dup_params(me->curparams), | ||
2234 | me->ourgame->dup_game(me->states[0].state), soln); | ||
2235 | |||
2236 | return NULL; | ||
2237 | } | ||