OSDN Git Service

738910bad4260d2cbc880fe0c59ab734d5e158ab
[nazghul-jp/nazghul-jp.git] / src / menus.c
1 /* $Id: menus.c,v 1.41 2010/08/26 05:56:21 gmcnutt Exp $
2  *
3  * Copyright (C) 2006 Gordon McNutt
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the Free
7  * Software Foundation; either version 2 of the License, or (at your option)
8  * any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but WITHOUT
11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13  * more details.
14  *
15  * You should have received a copy of the GNU General Public License along with
16  * this program; if not, write to the Free Foundation, Inc., 59 Temple Place,
17  * Suite 330, Boston, MA 02111-1307 USA
18  */
19
20 #include "menus.h"
21
22 #include "ascii.h"
23 #include "cfg.h"
24 #include "conv.h"
25 #include "cmd.h"
26 #include "cmdwin.h"
27 #include "console.h"
28 #include "event.h"
29 #include "foogod.h"
30 #include "log.h"
31 #include "list.h"
32 #include "map.h"
33 #include "screen.h"
34 #include "status.h"
35 #include "file.h"
36 #include "nazghul.h"
37
38 #include "session.h" /* added for demo */
39 #include "place.h" /* added for demo */
40 #include "sprite.h" /* added for demo */
41 #include "tick.h" /* added for demo */
42
43 #include <assert.h>
44 #include <ctype.h>
45 #include <errno.h>
46 #include <png.h>
47 #include <time.h>
48 #include <SDL_image.h>
49 #include <sys/stat.h>
50 #include <sys/types.h>
51 #include <unistd.h>
52
53 #define LOADSAVE_HINT "\005\006=scroll ENT=select DEL=delete ESC=exit"
54
55 /**
56  * Enable this option to have the a "L)oad Game" menu item in addition to the
57  * "J)ourney Onward" menu item. When this is enabled J)ourney Onward
58  * automatically picks the last saved game. Otherwise, J)ourney Onward works
59  * like L)oad Game, providing a list of all saved games, with the most recently
60  * saved one at the top.
61  */
62 #ifndef CONFIG_LOAD_GAME_OPTION
63 #define CONFIG_LOAD_GAME_OPTION 0
64 #endif
65
66 /**
67  * Information about saved game files.
68  */
69 typedef struct saved_game {
70         struct list list;        /**< For menu_saved_games list */
71         char *fname;             /**< Filename (w/o path) */
72         char *path;              /**< Full pathname to save file */
73         time_t timestamp;        /**< Last modification time */
74         int ref;                 /**< Reference count on this struct */
75         SDL_Surface *screenshot; /**< Map screenshot */
76         char is_current;         /**< is this the currently loaded game? */
77 } saved_game_t;
78
79 /**
80  * The context used for the menu_scroll function. This keeps track of the
81  * currently selected saved game from the menu and communicates a player abort
82  * back to the menu code.
83  */
84 typedef struct {
85         saved_game_t *save; /**< Currently highlighted saved game */
86         char *entry;        /**< Currently highlighted string entry */
87         char *hotkeys;      /**< Hotkey characters, in order of listing */
88         const char **menu;        /**< Menu strings */
89         int n_menu;         /**< Number of menu strings */
90         const char *title;        /**< Menu title */
91         char abort : 1;     /**< The player aborted the selection */
92 } menu_scroll_data_t;
93
94
95 /**
96  * The list of saved games, built when we evaluate the saved-game script for
97  * the load and save menus.
98  */
99 static struct list menu_saved_games;
100
101 /**
102  * Keep track of the name of the currently loaded game so we can mark it in the
103  * load and save menus.
104  */
105 static saved_game_t *menu_current_saved_game = 0;
106
107 /**
108  * This is what the Save Game menu shows for the new game option.
109  */
110 static const char *MENU_NEW_GAME_STR = "N)ew Saved Game";
111
112 /**
113  * This is a hack added to support demo mode. I'll try to explain, because it's
114  * convoluted. Normally (before I added demo support) the main menu runs an
115  * event loop by calling eventHandle(), which returns when the user makes a
116  * choice.
117  *
118  * For demo mode, I added an extra wrinkle. When I setup the demo I push a tick
119  * handler (I mean animation ticks, which are usually disabled until we start
120  * the game, but for demo mode I need them). The tick handler calls
121  * place_exec() to run the demo. The idea is we would run the demo more or less
122  * concurrently with our processing of user keystrokes in the main menu, but of
123  * course we're doing it all on a single thread.
124  *
125  * Running place_exec() on every tick caused a lot of latency in our response
126  * to handling user keypresses. So I added the first hack: place_exec() will
127  * call eventHandlePending() after processing each object. That gives us a
128  * chance to process user keystrokes more frequently while the demo is
129  * running. The simplified call tree when the user hits <enter> to select a
130  * menu item is something like this:
131  *
132  * main_menu
133  * `-eventHandle
134  *   `-menu_demo_tick_handler
135  *     `-place_exec
136  *       `-eventHandlePending
137  *
138  * Normally we're directly in eventHandle when the player presses <enter>, and
139  * we fall back to main_menu and handle the selection. But if we're in
140  * eventHandlePending this doesn't happen, and we keep on handling events, so
141  * the player perceives this as a dropped keystroke.
142  *
143  * To "fix" it I added the main_menu_handled variable. This causes the
144  * menu_demo_tick_handler to return a value that kicks eventHandle out of its
145  * loop when the user selects <enter>.
146  */
147 int main_menu_handled = 0;
148
149 /**
150  * Another hack for demos! What fun. Little flag to let the main menu code know
151  * that the demo is done and doesn't need to keep running any more, thanks.
152  */
153 int demo_done = 0;
154
155 /**
156  * Delete a saved game struct and all it's strings. Don't call this, use
157  * saved_game_unref().
158  *
159  * @param save The struct to delete.
160  */
161 static void saved_game_del(saved_game_t *save)
162 {
163         assert(! save->ref);
164
165         if (save->fname)
166                 free(save->fname);
167         if (save->path)
168                 free(save->path);
169         if (save->screenshot)
170                 SDL_FreeSurface(save->screenshot);
171         free(save);
172 }
173
174 /**
175  * Infer the filename of a screenshot from the filename for the saved game.
176  *
177  * @param save The saved game that goes with the screenshot.
178  * @returns The full pathname to the screenshot file. The caller should free()
179  * the string when done using it.
180  */
181 static char *saved_game_mk_screenshot_fname(saved_game_t *save)
182 {
183         char *s_fname = (char*)malloc(strlen(save->path)+5);
184         assert(s_fname);
185         sprintf(s_fname, "%s.png", save->path);
186         return s_fname;
187 }
188
189 /**
190  * Create a new saved game struct and populate it's fields. This makes a copy
191  * of the fname, creates a copy of the full pathname, and gets the modification
192  * time of the file.
193  *
194  * @param fname Name of the saved game file.
195  * @returns The new struct or 0 if there was a problem accessing the file or
196  * allocating memory.
197  */
198 static saved_game_t *saved_game_new(char *fname)
199 {
200         struct stat fileinfo;
201         char *s_fname = 0;
202         saved_game_t *save = (saved_game_t*)malloc(sizeof(*save));
203         if (!save) {
204                 warn("Could not alloc save\n");
205                 return 0;
206         }
207
208         memset(save, 0, sizeof(*save));
209         
210         list_init(&save->list);        
211
212         /* Keep a copy of the file name. */
213         save->fname = strdup(fname);
214         if (! save->fname) {
215                 warn("Could not alloc fname\n");
216                 goto abort;
217         }
218
219         /* Build the full path. */
220         save->path = file_mkpath(cfg_get("saved-games-dirname"), fname);
221         if (!save->path) {
222                 warn("Could not alloc filename\n");
223                 goto abort;
224         }
225
226         /* Get the timestamp on the file. */
227         if (! stat(save->path, &fileinfo)) {
228                 save->timestamp = fileinfo.st_mtime;
229         } else {
230                 /* This is probably a new save that hasn't been written to file
231                  * yet. Use the current time as it's timestamp. */
232                 save->timestamp = time(0);
233         }
234
235         /* Load the screenshot. Ignore failure. */
236         s_fname = saved_game_mk_screenshot_fname(save);
237         if (file_exists(s_fname)) {
238                 save->screenshot = IMG_Load(s_fname);
239         }
240         free(s_fname);
241
242         save->ref = 1;
243         return save;
244
245  abort:
246         saved_game_del(save);
247         return 0;
248 }
249
250 /**
251  * Release a reference to a saved game struct. This could destroy it.
252  *
253  * @param save The struct to release.
254  */
255 static void saved_game_unref(saved_game_t *save)
256 {
257         assert(save->ref);
258         save->ref--;
259         if (!save->ref) {
260                 saved_game_del(save);
261         }
262 }
263
264 /**
265  * Called when the user kills the window during the main menu. Exits the
266  * program.
267  *
268  * @param kh Unused
269  * @returns Nothing, the program exits.
270  */
271 static bool main_menu_quit_handler(struct QuitHandler *kh)
272 {
273         exit(0);
274         return(0); /* for Sun compiler */
275 }
276
277 /**
278  * Shows the game credits in the status window.
279  */
280 static void show_credits(void)
281 {
282         struct KeyHandler kh;
283         const char *title = "Credits";
284         const char *text = 
285                 "Engine Programming\n"\
286                 "...Gordon McNutt\n"\
287                 "...Kris Parker\n"\
288                 "...Sam Glasby\n"\
289                 "...Tim Douglas\n"\
290                 "...Janne Johansson\n"\
291                 "...Karl Garrison\n"\
292                 "Build System\n"\
293                 "...Andreas Bauer\n"\
294                 "Game Scripting\n"\
295                 "...Gordon McNutt\n"\
296                 "...Kris Parker\n"\
297                 "...Sam Glasby\n"
298                 "Art Provided by\n"\
299                 "...Joshua Steele\n"\
300                 "...David Gervais\n"\
301                 "...Kris Parker\n"
302                 "...Kevin Gabbert\n"\
303                 "...Gordon McNutt\n"\
304                 "...Sam Glasby\n"\
305                 "...Steve Riberdy\n"\
306                 "Music Provided by\n"\
307                 "...Jim Paterson\n"\
308                 ;
309
310         statusSetPageText(title, text);
311         statusSetMode(Page);
312         consolePrint("[Hit ESC to continue]\n");
313
314         kh.fx = scroller;
315         kh.data = NULL;
316         eventPushKeyHandler(&kh);
317         eventHandle();
318         eventPopKeyHandler();
319 }
320
321 /**
322  * Prompts the user to confirm that they want to overwrite a saved game.
323  *
324  * @returns 1 On confirm, 0 on cancel.
325  */
326 static int confirm_selection()
327 {
328         int yesno;
329         log_msg("Existing saved game will be overwritten! Are you sure?");
330         cmdwin_clear();
331         cmdwin_spush("Confirm");
332         cmdwin_spush("<y/n>");
333         getkey(&yesno, yesnokey);
334         cmdwin_pop();
335         if (yesno=='y') {
336                 cmdwin_spush("yes!");
337                 log_msg("Ok!");
338                 return 1;
339         } else {
340                 cmdwin_spush("no!");
341                 log_msg("Canceled!");
342                 return 0;
343         }
344 }
345
346 /**
347  * Add another saved game to the list. This is called as a result of executing
348  * the saved-games script, which is just a sequence of (kern-add-saved-game
349  * <fname>) procedure calls, each of which lands here. Based on the filename
350  * we'll build out the other info associated with the saved game like the
351  * timestamp and screenshot image. This also adds the saved game to the list in
352  * order sorted by timestamp, using a simple insertion sort algorithm.
353  *
354  * @param fname The name of the saved game file.
355  */
356 void menu_add_saved_game(char *fname)
357 {
358         struct list *lptr;
359
360         /* Create a new saved game list element. */
361         saved_game_t *save = saved_game_new(fname);
362         if (!save) {
363                 warn("menu_add_saved_game: could not add '%s'\n", fname);
364                 return;
365         }
366
367         /* Insert it in the list ordered by timestamp. Find the first saved
368          * game in the list which has a timestamp after this one.  */
369         lptr = menu_saved_games.next;
370         while (lptr != &menu_saved_games) {
371                 saved_game_t *save2 = outcast(lptr, saved_game_t, list);
372                 if (save->timestamp > save2->timestamp) {
373                         break;
374                 }
375                 lptr = lptr->next;
376         }
377
378         /* Insert this one previous to it. Works for empty lists, too. */
379         list_add_tail(lptr, &save->list);
380 }
381
382 /**
383  * Print the saved game's name, timestamp and other info for display in the
384  * status window.
385  *
386  * @param buf The string buffer to hold the printed line.
387  * @param n The size in bytes of the string buffer (including a terminating 
388  * NULL).
389  * @param fname The name of the saved game file, not including the full path.
390  */
391 static int sprintf_game_info(char *buf, int n, saved_game_t *save, char hotkey)
392 {
393         struct tm *timeinfo;
394         int ret = -1;
395         char datebuf[n];
396         int padlen;
397         char mark = ' ';
398
399         /* Convert the timestamp from epoch to a time structure. */
400         timeinfo = localtime(&save->timestamp);
401
402         /* Print the date to a temp buffer to see how big it is. */
403         snprintf(datebuf, n, "%02d:%02d %02d/%02d/%d", timeinfo->tm_hour, 
404                  timeinfo->tm_min, timeinfo->tm_mon, timeinfo->tm_mday, 
405                  1900+timeinfo->tm_year);
406
407         /* Calculate necessary padding to right-justify the date. */
408         padlen = n - (strlen(save->fname) 
409                       + strlen(datebuf) 
410                       + 3 
411                       + (hotkey ? 3 : 0));
412
413         /* We'll mark the current game with an '*'. */
414         if (save->is_current) {
415                 mark = '*';
416         }
417
418         /* Print to the buffer. */
419         if (hotkey) {
420                 snprintf(buf, n, "%c) %s %*c%c%s", hotkey, save->fname, 
421                          padlen, ' ', mark, datebuf);
422         } else {
423                 snprintf(buf, n, "%s %*c%c%s", save->fname, 
424                          padlen, ' ', mark, datebuf);
425         }
426         return ret;
427                  
428 }
429
430 /**
431  * Extract the filename from the menu entry strings used in the load and save
432  * game menus.
433  * @returns A strdup'd copy of the fname.
434  */
435 static char *menu_entry_to_fname(char *entry)
436 {
437         char *fname, *end;
438         int mod;
439
440         /* Most saved games entries start with x), where x is a 0-9 */
441         if (isdigit(entry[0]) && ')' == entry[1]) {
442                 entry += 3;
443         }
444
445         end = strchr(entry, ' ');
446         mod = end ? 1 : 0;
447         if (mod)
448                 *end = 0;
449         fname = strdup(entry);
450         assert(fname);
451         if (mod)
452                 *end = ' ';
453         return fname;
454 }
455
456 /**
457  * Reset the current saved game.
458  * @param save A pointer to the new saved game struct.
459  */
460 static void menu_set_current_saved_game(saved_game_t *save)
461 {
462         if (menu_current_saved_game) {
463                 menu_current_saved_game->is_current = 0;
464                 saved_game_unref(menu_current_saved_game);
465                 menu_current_saved_game = 0;
466         }
467         menu_current_saved_game = save;
468         if (save) {
469                 save->is_current = 1;
470                 save->ref++;
471                 /* Move it to the front of the list. */
472                 list_remove(&save->list);
473                 list_add(&menu_saved_games, &save->list);
474         }
475 }
476
477 /**
478  * Search the list of saved games for one with the given file name.
479  *
480  * @param fname Filename to search for.
481  * @returns The saved game with the matching filename, or 0 if none found.
482  */
483 static saved_game_t *saved_game_lookup(char *fname)
484 {
485         struct list *lptr;
486         list_for_each(&menu_saved_games, lptr) {
487                 saved_game_t *save = outcast(lptr, saved_game_t, list);
488                 if (! strcmp(fname, save->fname))
489                         return save;
490         }
491         return 0;
492 }
493
494 /**
495  * Show a screenshot over the map viewer with the words "SCREEN SHOT" in the
496  * middle.
497  *
498  * @param screenshot The image to show, or 0 to just show a blank screen. For a
499  * blank screen the words "SCREEN SHOT" are still printed.
500  */
501 static void menu_show_screenshot(SDL_Surface *screenshot)
502 {
503         static const char *MENU_SCREEN_SHOT_STR = "^c+ySCREEN SHOT^c-";
504         SDL_Rect rect;
505
506         mapSetImage(screenshot);
507         rect.x = (((MAP_X + MAP_W) / 2) - (5 * ASCII_W));
508         rect.y = (MAP_Y + MAP_H)/4;
509         rect.w = strlen(MENU_SCREEN_SHOT_STR);
510         rect.h = ASCII_H;
511         screenPrint(&rect, 0, MENU_SCREEN_SHOT_STR);
512
513         rect.w *= ASCII_W;
514         screenUpdate(&rect);
515 }
516
517 /**
518  * Get the highlighted menu item from the status viewer and figure out which
519  * saved game it corresponds to.
520  *
521  * @returns The saved game struct that goes with the menu entry, or 0 if the
522  * "New Game" option is highlighted.
523  */
524 static saved_game_t *menu_scroller_get_selected()
525 {
526         char *fname;
527         char *entry_str = (char*)statusGetSelected(String);
528         if (!entry_str) {
529                 return 0;
530         }
531         if (! strcmp(entry_str, MENU_NEW_GAME_STR)) {
532                 return 0;
533         }
534         fname = menu_entry_to_fname(entry_str);
535         return saved_game_lookup(fname);
536 }
537
538 /**
539  * Rewrite the saved-game script, using the current list of saved games.
540  *
541  * @returns -1 on error, 0 on success.
542  */
543 static int menu_rewrite_saves()
544 {
545         FILE *file;
546         struct list *lptr;
547         char *fname = cfg_get("save-game-filename");
548
549         file = file_open_in_save_dir(fname, "w");
550         if (! file) {
551                 warn("Problem updating %s: %s\n", fname, file_get_error());
552                 return -1;
553         }
554         
555         list_for_each(&menu_saved_games, lptr) {
556                 saved_game_t *save = outcast(lptr, saved_game_t,  list);
557                 fprintf(file, "(kern-add-save-game \"%s\")\n", save->fname);
558         }
559
560         fclose(file);
561         return 0;
562 }
563
564 /**
565  * Prompt the user to delete the currently highlighted save-game. This checks
566  * if the highlighted entry is a valid save game, prompts the user to confirm
567  * the deletetion, deletes the save file and the screenshot file, removes the
568  * saved game struct from the list, unrefs it, and re-writes the saved game
569  * script.
570  *
571  * If the save-game or screenshot files can't be deleted the operation warns
572  * the user but continues to remove and unreference the saved game. It will
573  * appear again the next time nazghul is restarted.
574  */
575 static void menu_prompt_to_delete(menu_scroll_data_t *data)
576 {
577         char *selstr = 0;
578         saved_game_t *save = 0;
579         int yesno = 0;
580         int i1, i2;
581
582         /* Check if user tried to delete the N)ew Saved Game option */
583         selstr =  (char*)statusGetSelected(String);
584         if (! strcmp(selstr, MENU_NEW_GAME_STR))
585                 return;
586
587         /* Get the saved game struct for the selection. */
588         save =  menu_scroller_get_selected();
589         if (!save)
590                 return;
591
592         /* Prompt to confirm. */
593         log_begin("Delete %s?", save->fname);
594         log_flush();
595         cmdwin_clear();
596         cmdwin_push("Delete-");
597         cmdwin_push("<y/n>");
598         getkey(&yesno, yesnokey);
599         cmdwin_pop();
600
601         /* If confirmation denied then cancel. */
602         if (yesno == 'n') {
603                 cmdwin_spush("abort!");
604                 log_end(" Canceled!");
605                 return;
606         }
607
608         /* Confirmed, try to delete the save file. Abort if it doesn't work. */
609         cmdwin_push("yes!");
610         statusFlashSelected(Red);
611         if (unlink(save->path)) {
612                 log_continue(" WARNING! Failed to delete save file %s: %s", 
613                         save->path, strerror(errno));
614         }
615
616         /* Try to delete the screenshot. Warn the user and continue if it
617          * doesn't work. */
618         if (save->screenshot) {
619                 char *scr_fname = saved_game_mk_screenshot_fname(save);
620                 if (unlink(scr_fname)) {
621                         log_continue(" WARNING! Failed to delete screenshot "\
622                                      "file %s: %s:", scr_fname, 
623                                      strerror(errno));
624                 }
625                 free(scr_fname);
626         }
627
628         /* Remove and unreference the saved game struct. */
629         list_remove(&save->list);
630         if (save == menu_current_saved_game) {
631                 menu_set_current_saved_game(0);
632         }
633         saved_game_unref(save);
634
635         /* Re-write the saved game script. */
636         menu_rewrite_saves();
637
638         /* Reshuffle all the menu entries to close the gap and reset the status
639          * menu list. */
640         i1 = statusGetSelectedIndex(String);
641         assert(i1 >= 0);
642         for (i2 = i1; i2 < data->n_menu-1; i2++) {
643                 data->menu[i2] = data->menu[i2+1];
644         }
645         data->menu[i2] = 0;
646         data->n_menu--;
647         statusSetStringList(data->title, data->n_menu, data->menu);
648
649         /* Disable repainting while re-setting the mode to avoid the ugly
650          * flashes on the top line. */
651         statusDisableRepaint();
652         statusSetMode(StringList);
653         statusEnableRepaint();
654
655         statusSetSelectedIndex(i1 ? (i1 - 1) : 0);
656         /*statusRepaint();*/
657
658         log_end(" Removed!");
659 }
660
661 /**
662  * Scroll the status window for the load/save menus. As the player scrolls over
663  * a saved game, show its screenshot on the map window.
664  *
665  * @param kh Keyhandler with the menu context as its data element.
666  * @param key The key pressed by the player.
667  * @param keymod Reflects the status of the SHIFT, CTRL and ALT keys.
668  * @returns 0 to keep the Status window in scroll mode, 1 to end it.
669  */
670 int menu_scroller(struct KeyHandler * kh, int key, int keymod)
671 {
672         menu_scroll_data_t *data = (menu_scroll_data_t *) kh->data;
673         enum StatusScrollDir dir;
674         int i1;
675
676         if (data->hotkeys && key < 128) {
677                 char ckey = (char)key;
678                 char *hotkey = strchr(data->hotkeys, ckey);
679                 if (hotkey) {
680                         int index = hotkey - data->hotkeys;
681                         printf("ckey=%c index=%d\n", ckey, index);
682                         statusSetSelectedIndex(index);
683                         statusFlashSelected(Green);
684                         data->entry = (char*)statusGetSelected(String);
685                         data->save = menu_scroller_get_selected();
686                         return 1;
687                 }
688         }
689
690         switch (key) {
691         case KEY_NORTH:
692                 dir = ScrollUp;
693                 break;
694         case KEY_SOUTH:
695                 dir = ScrollDown;
696                 break;
697         case SDLK_PAGEUP:
698                 dir = ScrollPageUp;
699                 break;
700         case SDLK_PAGEDOWN:
701                 dir = ScrollPageDown;
702                 break;
703         case SDLK_HOME:
704                 dir = ScrollTop;
705                 break;
706         case SDLK_END:
707                 dir = ScrollBottom;
708                 break;
709         case SDLK_RETURN:
710         case SDLK_SPACE:
711         case KEY_HERE:
712         case '\n':
713                 i1 = statusGetSelectedIndex(String);
714                 statusFlashSelected(Green);
715                 data->entry = (char*)statusGetSelected(String);
716                 data->save = menu_scroller_get_selected();
717                 return 1;
718         case SDLK_ESCAPE:
719         case 'q':
720                 data->abort = 1;
721                 return 1;
722         case SDLK_DELETE:
723         case 'd':
724                 menu_prompt_to_delete(data);
725                 return data->n_menu ? 0 : 1;
726         default:
727                 return 0;
728         }
729
730         statusScroll(dir);
731         data->entry = (char*)statusGetSelected(String);
732         data->save = menu_scroller_get_selected();
733         menu_show_screenshot(data->save ? data->save->screenshot : 0);
734
735         return 0;
736 }
737
738 /**
739  * Scroll the status window for the main menu.
740  *
741  * @param kh Keyhandler with the menu context as its data element.
742  * @param key The key pressed by the player.
743  * @param keymod Reflects the status of the SHIFT, CTRL and ALT keys.
744  * @returns 0 to keep the Status window in scroll mode, 1 to end it.
745  */
746 int main_menu_scroller(struct KeyHandler * kh, int key, int keymod)
747 {
748         menu_scroll_data_t *data = (menu_scroll_data_t *) kh->data;
749         enum StatusScrollDir dir;
750
751         if (data->hotkeys && key < 128) {
752                 char ckey = (char)key;
753                 char *hotkey = strchr(data->hotkeys, ckey);
754                 if (hotkey) {
755                         int index = hotkey - data->hotkeys;
756                         printf("ckey=%c index=%d\n", ckey, index);
757                         statusSetSelectedIndex(index);
758                         data->entry = (char*)statusGetSelected(String);
759                         return 1;
760                 }
761         }
762
763         switch (key) {
764         case KEY_NORTH:
765                 dir = ScrollUp;
766                 break;
767         case KEY_SOUTH:
768                 dir = ScrollDown;
769                 break;
770         case SDLK_PAGEUP:
771                 dir = ScrollPageUp;
772                 break;
773         case SDLK_PAGEDOWN:
774                 dir = ScrollPageDown;
775                 break;
776         case SDLK_RETURN:
777         case SDLK_SPACE:
778         case KEY_HERE:
779         case '\n':
780                 data->entry = (char*)statusGetSelected(String);
781                 main_menu_handled = 1;
782                 return 1;
783         case SDLK_ESCAPE:
784         case 'q':
785                 data->abort = 1;
786                 main_menu_handled = 1;
787                 return 1;
788         default:
789                 return 0;
790         }
791
792         statusScroll(dir);
793         data->entry = (char*)statusGetSelected(String);
794
795         return 0;
796 }
797
798 /**
799  * Get a suitable hotkey for the numeral.
800  *
801  * @param i The number of the hotkey.
802  * @returns ASCII 0-9 or NULL if i is out of range.
803  */
804 static char menu_hotkey(int i)
805 {
806         return (i < 10) ? (i + '0') : 0;
807 }
808
809 /**
810  * Find the most recently saved game.
811  *
812  * @returns A copy of the full pathname of the most recently saved game, or 0
813  * if there are no saved games.
814  */
815 char * journey_onward(void)
816 {
817         char *ret = 0;
818         saved_game_t *save;
819
820         if (list_empty(&menu_saved_games)) {
821                 return 0;
822         }
823
824         /* Since the saved game list is kept sorted by modification time, the
825          * first element in the list is the most recent. */
826         save = outcast(menu_saved_games.next, saved_game_t, list);
827         ret = strdup(save->path);
828         menu_set_current_saved_game(save);
829         return ret;
830 }
831
832 /**
833  * Give the user a choice of saved games to load, with the most recently saved
834  * always at the top for easy access. If there is only one saved game then
835  * don't bother asking, just load it directly.
836  *
837  * @returns A copy of the full pathname of the game file to load, or 0 if the
838  * player canceled.
839  */
840 char * load_game_menu(void)
841 {
842         const char **menu = 0;
843         char *menubuf, *menubufptr;
844         int n = 0;
845         int i = 0;
846         struct list *lptr = 0;
847         struct KeyHandler kh;
848         menu_scroll_data_t data;
849         char *selection = 0;
850         enum StatusMode omode = statusGetMode();
851         int linew = STAT_CHARS_PER_LINE;
852
853         /* If there is only one saved game then just load it without prompting
854          * the user. */
855         if (1 == list_len(&menu_saved_games)) {
856                 return journey_onward();
857         }
858
859         memset(&data, 0, sizeof(data));
860
861         /* Allocate the memory for the menu strings. */
862         n = list_len(&menu_saved_games);
863         menubuf = (char*)calloc(n, linew+1);
864         assert(menubuf);
865         menu = (const char**)calloc(n, sizeof(menu[0]));
866         assert(menu);
867
868         data.hotkeys = (char*)calloc(n + 1, 1);
869         assert(data.hotkeys);
870         data.menu = menu;
871         data.n_menu = n;
872         data.title = "Load Game";
873
874         /* Add each saved game to the menu list. */
875         menubufptr = menubuf;
876         list_for_each(&menu_saved_games, lptr) {
877                 saved_game_t *save = outcast(lptr, saved_game_t, list);
878                 menu[i] = menubufptr;
879                 data.hotkeys[i] = menu_hotkey(i);
880                 sprintf_game_info(menubufptr, linew+1, save, data.hotkeys[i]);
881                 menubufptr += linew+1;
882                 i++;
883         }
884
885         foogodSetHintText(LOADSAVE_HINT);
886         foogodSetMode(FOOGOD_HINT);
887         statusSetStringList(data.title, n, menu);
888         statusSetMode(StringList);
889
890         /* Setup the initial screen shot */
891         if (list_empty(&menu_saved_games)) {
892                 menu_show_screenshot(0);
893         } else {
894                 saved_game_t *save = outcast(menu_saved_games.next, 
895                                              saved_game_t, list);
896                 menu_show_screenshot(save->screenshot);
897         }
898
899         kh.fx = menu_scroller;
900         kh.data = &data;
901         eventPushKeyHandler(&kh);
902         eventHandle();
903         eventPopKeyHandler();
904
905         /* If the player selected something then build the full pathname for
906          * return. */
907         if (! data.abort
908             && data.save) {
909                 selection = strdup(data.save->path);
910                 assert(selection);
911                 menu_set_current_saved_game(data.save);
912         }
913
914         mapClearImage();
915
916         /* If the original status mode was already StringList don't reset the
917          * mode, this will cause status to highlight the top list entry of our
918          * list and it looks funny while we're loading the game. */
919         if (omode != StringList)
920                 statusSetMode(omode);
921
922         foogodSetMode(FOOGOD_DEFAULT);
923         free(menu);
924         free(menubuf);
925         free(data.hotkeys);
926
927         return selection;
928 }
929
930 /**
931  * Filter characters we don't want to show in filenames.
932  *
933  * @param key A keypress code, usually ASCII.
934  * @returns 1 to reject the character, 0 to allow it.
935  */
936 static int menu_fname_filter(int key)
937 {
938         if (isalnum(key)
939             || (key=='_')
940             || (key=='-')
941             || (key=='.')) {
942                 return 0;
943         } else {
944                 return 1;
945         }
946 }
947
948 /**
949  * Get a file name from the player.
950  * @returns A strdup'd copy of the filename.
951  */
952 static char *prompt_for_fname()
953 {
954         char buf[32];
955
956         log_msg("Enter the new filename.");
957         cmdwin_clear();
958         cmdwin_push("Filename: ");
959
960         if (ui_getline_filtered(buf, sizeof(buf),  menu_fname_filter)) {
961                 return strdup(buf);
962         }
963         return 0;
964 }
965
966 /**
967  * Let the player select a file to save the current game.
968  *
969  * @returns A strdup'd copy of the name of the file to save to, or 0 if the
970  * player aborts. The filename is NOT the full path, like some of the other
971  * menu functions return, because we're not going to load anything with the
972  * result.
973  */
974 char * save_game_menu(void)
975 {
976         const char **menu = 0;
977         char *menubuf, *menubufptr;
978         int n = 0;
979         int i = 0;
980         struct list *lptr = 0;
981         struct KeyHandler kh;
982         menu_scroll_data_t data;
983         enum StatusMode omode = statusGetMode();
984         int linew = STAT_CHARS_PER_LINE;
985         saved_game_t *selected_game = 0;
986
987         memset(&data, 0, sizeof(data));
988
989         /* Allocate the string buffers to display the menu. */
990         n = list_len(&menu_saved_games) + 1;
991         menubuf = (char*)calloc(n, linew+1);
992         assert(menubuf);
993         menu = (const char**)calloc(n, sizeof(menu[0]));
994         assert(menu);
995
996         data.hotkeys = (char*)calloc(n+1, 1);
997         assert(data.hotkeys);
998         data.menu = menu;
999         data.n_menu = n;
1000         data.title = "Save Game";
1001
1002         /* Prepare to fill in the menu list. */
1003         i = 0;
1004         menubufptr = menubuf;
1005
1006         /* The first entry is always the New Save Game option. */
1007         sprintf(menubufptr, MENU_NEW_GAME_STR);
1008         data.hotkeys[i] = 'n';
1009         menu[i++] = menubufptr;
1010         menubufptr += linew+1;
1011
1012         /* Is there a game already loaded? */
1013         if (menu_current_saved_game) {
1014
1015                 /* It should be first in the list of saved games. */
1016                 assert(menu_saved_games.next 
1017                        == &menu_current_saved_game->list);
1018                 
1019                 data.hotkeys[i] = menu_hotkey(i);
1020
1021                 /* Put it as the next item in the menu. */
1022                 sprintf_game_info(menubufptr, linew+1, 
1023                                   menu_current_saved_game, data.hotkeys[i]);
1024                 menu[i++] = menubufptr;
1025                 menubufptr += linew+1;
1026         }
1027
1028         /* Prepare to list the remaining saved games. */
1029         if (menu_current_saved_game) {
1030                 lptr = menu_current_saved_game->list.next;
1031         } else {
1032                 lptr = menu_saved_games.next;
1033         }
1034
1035         /* The remaining saved games are in timestamp order on the list; add
1036          * them to the menu in this order. */
1037         while (lptr != &menu_saved_games) {
1038                 saved_game_t *save = outcast(lptr, saved_game_t, list);
1039                 lptr = lptr->next;
1040                 menu[i] = menubufptr;
1041                 data.hotkeys[i] = menu_hotkey(i);
1042                 sprintf_game_info(menubufptr, linew+1, save, data.hotkeys[i]);
1043                 menubufptr += linew+1;
1044                 i++;
1045         }
1046
1047         foogodSetHintText(LOADSAVE_HINT);
1048         foogodSetMode(FOOGOD_HINT);
1049
1050         /* Setup the menu in the status window. */
1051         statusSetStringList(data.title, n, menu);
1052         statusSetMode(StringList);
1053         
1054         /* Highlight the current saved game. */
1055         if (menu_current_saved_game) {
1056                 statusSetSelectedIndex(1);
1057         }
1058
1059         /* Show the initial screenshot. */
1060         menu_show_screenshot(menu_current_saved_game 
1061                              ? menu_current_saved_game->screenshot
1062                              : 0);
1063
1064 reselect:
1065         kh.fx = menu_scroller;
1066         kh.data = &data;
1067         eventPushKeyHandler(&kh);
1068         eventHandle();
1069         eventPopKeyHandler();
1070
1071         if (data.abort) {
1072                 selected_game = 0;
1073                 goto done;
1074         }
1075
1076         /* Did the player select an existing saved game? */
1077         if (data.save) {
1078
1079                 /* Yes. Overwrite? */
1080                 if (confirm_selection()) {
1081                         selected_game = data.save;
1082                 } else {
1083                         goto reselect;
1084                 }
1085         }
1086
1087         /* No. Did player abort? */
1088         else if (!data.abort) {
1089
1090                 /* No. Must be a new saved game. */
1091                 struct list *lptr;
1092                 char *new_name = prompt_for_fname();
1093                 if (!new_name)
1094                         goto reselect;
1095
1096                 /* Did player re-type an existing filename? */
1097                 list_for_each(&menu_saved_games, lptr) {
1098                         saved_game_t *exist = outcast(lptr, saved_game_t, 
1099                                                       list);
1100                         if (!strcmp(new_name, exist->fname)) {
1101
1102                                 /* Yes. Confirm overwrite? */
1103                                 if (!confirm_selection()) {
1104                                         /* No. Reselect. */
1105                                         free(new_name);
1106                                         goto reselect;
1107                                 }
1108
1109                                 /* Ok, overwrite. Nothing new to add to the
1110                                  * saved-game script. */
1111                                 break;
1112                         }
1113                 }
1114                 
1115                 /* Replace the "New Saved Game" menu line with what the player
1116                    typed. */
1117                 strncpy(menubuf, new_name, linew);
1118                 
1119                 /* Add a new saved game struct to the list. */
1120                 selected_game = saved_game_new(new_name);
1121                 list_add(&menu_saved_games, &selected_game->list);
1122                 
1123                 /* Re-write the saved games file to add the new
1124                  * save. */
1125                 menu_rewrite_saves();
1126
1127                 free(new_name);
1128         }
1129
1130         /* Save selected? */
1131         if (selected_game) {
1132
1133                 char *s_fname = saved_game_mk_screenshot_fname(selected_game);
1134                 SDL_Rect rect;
1135
1136                 /* Does it have an old screenshot? */
1137                 if (selected_game->screenshot) {
1138                         /* Yes. Get rid of it. */
1139                         SDL_FreeSurface(selected_game->screenshot);
1140                         selected_game->screenshot = 0;
1141                 }
1142
1143                 /* Restore map view so we can get a new screenshot. */
1144                 mapClearImage();
1145                 mapUpdate(0);
1146
1147                 /* Take a new screenshot. */
1148                 rect.x = MAP_X;
1149                 rect.y = MAP_Y;
1150                 rect.w = MAP_W;
1151                 rect.h = MAP_H;
1152                 screenCapture(s_fname, &rect);
1153                 selected_game->screenshot = IMG_Load(s_fname);
1154                 menu_set_current_saved_game(selected_game);
1155                 free(s_fname);
1156         }
1157
1158  done:
1159         /* Restore the original status mode before deleting the list. */
1160         statusSetMode(omode);
1161         foogodSetMode(FOOGOD_DEFAULT);
1162         foogodRepaint();
1163         mapClearImage();
1164
1165         free(menu);
1166         free(menubuf);
1167         free(data.hotkeys);
1168
1169         if (selected_game) {
1170                 return strdup(selected_game->fname);
1171         }
1172         return 0;
1173 }
1174
1175 static bool menus_demo_tick_handler(struct TickHandler *th)
1176 {
1177         static int in_tick = 0; /* hack: prevent recursive entry */
1178         if (Session && !in_tick) {
1179                 in_tick = 1;
1180                 Tick++;
1181                 sprite_advance_ticks(1);
1182                 if (Place) {
1183                         place_exec(Place);
1184                 }
1185                 in_tick = 0;
1186         }
1187
1188         if (Quit) {
1189                 demo_done = 1;
1190         }
1191
1192         /* See the comment over main_menu_handled. */
1193         return (bool)(main_menu_handled || demo_done);
1194 }
1195
1196 char * main_menu(void)
1197 {
1198         static const char *START_NEW_GAME="S)tart New Game";
1199         static const char *JOURNEY_ONWARD="J)ourney Onward";
1200         static const char *LOAD_GAME="L)oad Game";
1201         static const char *CREDITS="C)redits";
1202         static const char *QUIT="Q)uit";
1203         static const char *TUTORIAL="T)utorial";
1204         static const char *SETTINGS = "S(e)ttings";
1205         static const char *DEMO = "Show (I)ntro";
1206         const char *menu[8];
1207         char hotkeys[8+1];
1208         int n_items = 0;
1209         struct KeyHandler kh;
1210         menu_scroll_data_t data;
1211         char *selection = NULL;
1212         struct QuitHandler qh;
1213         static char *new_game_fname =
1214                 file_mkpath(cfg_get("include-dirname"),
1215                             cfg_get("new-game-filename"));
1216         static char *tutorial_fname =  
1217                 file_mkpath(cfg_get("include-dirname"),
1218                             cfg_get("tutorial-filename"));
1219         char *load_fname = 0;
1220         char *save_game_fname = cfg_get("save-game-filename");
1221         struct TickHandler th;
1222         int show_demo_option = 0;
1223         static int first_time = 1;
1224         char *demo_fname = 0;
1225
1226         /* If the player has a saved game, then he's already seen the
1227          * demo. Don't make him wait to load it before he can continue his
1228          * game. */
1229         int run_demo = ! file_exists_in_save_dir(save_game_fname);
1230
1231         /* setup main menu quit handler so player can click close window to
1232          * exit */
1233         qh.fx = main_menu_quit_handler;
1234         eventPushQuitHandler(&qh);
1235
1236         /* Does the config file mention a demo? */
1237         if (cfg_get("demo-filename")) {
1238                 demo_fname = file_mkpath(cfg_get("include-dirname"),
1239                                          cfg_get("demo-filename"));
1240
1241                 /* Can we find it? */
1242                 if (file_exists(demo_fname)) {
1243                         
1244                         /* Have we already run the demo once? */
1245                         if (! run_demo || ! first_time) {
1246
1247                                 /* Show the demo as a menu option but don't
1248                                  * automatically start it. */
1249                                 show_demo_option = 1;
1250
1251                         } else {
1252                                 /* Setup the demo to run in parallel with the
1253                                  * main menu. */
1254                                 run_demo = 1;
1255                                 session_load(demo_fname);
1256                                 foogodSetMode(FOOGOD_DEFAULT);
1257                                 Session->is_demo = 1;
1258                                 session_run_hook(Session, 
1259                                                  new_game_start_hook, "p", 
1260                                                  Session->player);
1261                                 th.fx = menus_demo_tick_handler;
1262                                 eventPushTickHandler(&th);
1263                                 Quit = false;
1264                                 cfg_set("demo-has-run", "yes");
1265                         }
1266                 }
1267         }
1268
1269
1270  start_main_menu:
1271         n_items = 0;
1272         cmdwin_clear();
1273
1274         if (! run_demo) {
1275                 nazghul_splash();
1276         }
1277
1278         /* check for a previously saved game to Journey Onward */
1279         if (file_exists_in_save_dir(save_game_fname)) {
1280                 menu[n_items] = JOURNEY_ONWARD;
1281                 hotkeys[n_items] = 'j';
1282                 n_items++;
1283                 if (CONFIG_LOAD_GAME_OPTION) {
1284                         menu[n_items] = LOAD_GAME;
1285                         hotkeys[n_items] = 'l';
1286                         n_items++;
1287                 }
1288         }
1289
1290         /* check for the default script for Start New Game */
1291         if (file_exists(new_game_fname)) {
1292                 menu[n_items] = START_NEW_GAME;
1293                 hotkeys[n_items] = 's';
1294                 n_items++;
1295         }
1296
1297         /* check for a tutorial script for Tutorial */
1298         if (file_exists(tutorial_fname)) {
1299                 menu[n_items] = TUTORIAL;
1300                 hotkeys[n_items] = 't';
1301                 n_items++;
1302         }
1303
1304         /* check for demo */
1305         if (show_demo_option) {
1306                 menu[n_items] = DEMO;
1307                 hotkeys[n_items] = 'i';
1308                 n_items++;
1309         }
1310
1311         menu[n_items] = SETTINGS;
1312         hotkeys[n_items] = 'e';
1313         n_items++;
1314
1315         menu[n_items] = CREDITS;
1316         hotkeys[n_items] = 'c';
1317         n_items++;
1318
1319         menu[n_items] = QUIT;
1320         hotkeys[n_items] = 'q';
1321         n_items++;
1322
1323         hotkeys[n_items] = 0;
1324
1325         foogodSetHintText("\005\006=scroll ENT=select");
1326         foogodSetMode(FOOGOD_HINT);
1327         statusSetStringList("Main Menu", n_items, menu);
1328         statusSetMode(StringList);
1329
1330         data.hotkeys = hotkeys;
1331         data.entry = NULL;
1332         kh.fx   = main_menu_scroller;
1333         kh.data = &data;
1334         main_menu_handled = 0;
1335
1336         /* If running a demo then start/resume it. */
1337         if (run_demo) {
1338                 tick_run();
1339         }
1340
1341         eventPushKeyHandler(&kh);
1342         eventHandle();
1343         eventPopKeyHandler();
1344
1345         /* Did we come back because the demo is done? */
1346         if (demo_done) {
1347
1348                 /* Yep. Setup for plain old splash and show the demo as a menu
1349                  * option. */
1350                 run_demo = 0;
1351                 show_demo_option = 1;
1352                 demo_done = 0;
1353                 Quit = false;
1354                 session_del(Session); /* wish me luck! */
1355                 Session = 0;
1356                 tick_pause();
1357                 eventPopTickHandler();
1358         }
1359
1360
1361         /* Pause the demo while running the submenus. This is really only
1362          * necessary for the load_game_menu(). */
1363         if (run_demo) {
1364                 tick_pause();
1365         }
1366
1367         selection = data.entry;
1368         if (! selection) {
1369                 goto start_main_menu;
1370         }
1371
1372         if (! strcmp(selection, START_NEW_GAME)) {
1373                 load_fname = new_game_fname;
1374                 assert(load_fname);
1375         }
1376         else if (! strcmp(selection, JOURNEY_ONWARD)) {
1377                 if (CONFIG_LOAD_GAME_OPTION) {
1378                         load_fname = journey_onward();
1379                 } else {
1380                         load_fname = load_game_menu();
1381                 }
1382                 if (! load_fname)
1383                         goto start_main_menu;
1384         }
1385         else if (CONFIG_LOAD_GAME_OPTION
1386                  && ! strcmp(selection, LOAD_GAME)) {                
1387                 load_fname = load_game_menu();
1388                 if (!load_fname)
1389                         goto start_main_menu;
1390         }
1391         else if (! strcmp(selection, SETTINGS)) {
1392                 options_menu();
1393                 goto start_main_menu;
1394         }
1395         else if (! strcmp(selection, CREDITS)) {
1396                 show_credits();
1397                 goto start_main_menu;
1398         }
1399         else if (! strcmp(selection, TUTORIAL)) {
1400                 load_fname = tutorial_fname;
1401         }
1402         else if (! strcmp(selection, QUIT)) {
1403                 exit(0);
1404         }
1405         else if (! strcmp(selection, DEMO)) {
1406                 /* FIXME: this means that once the demo is started, it won't
1407                  * appear as an option again until the user selects to leave
1408                  * the main menu and then comes back. Currently there is no way
1409                  * to tell when the demo is done, so we don't know when it's ok
1410                  * to put the option back in. */
1411                 show_demo_option = 0;
1412                 run_demo = 1;
1413
1414                 session_load(demo_fname);
1415                 Session->is_demo = 1;
1416                 foogodSetMode(FOOGOD_DEFAULT);
1417                 session_run_hook(Session, new_game_start_hook, "p", Session->player);
1418                 th.fx = menus_demo_tick_handler;
1419                 eventPushTickHandler(&th);
1420                 Quit = false;
1421                 goto start_main_menu;
1422         }
1423         else {
1424                 fprintf(stderr, "Invalid selection: '%s'\n", selection);
1425                 exit(-1);
1426         }
1427
1428         /* turn off status while new session is loading */
1429         statusSetMode(DisableStatus);
1430         foogodSetMode(FOOGOD_DEFAULT);
1431
1432         /* pop main menu quit handler, new one will be pushed in play.c */
1433         eventPopQuitHandler();
1434
1435         /* Cleanup after the demo. */
1436         if (run_demo) {
1437                 tick_pause();
1438                 eventPopTickHandler();
1439         }
1440         
1441         first_time = 0;
1442         return load_fname;
1443 }
1444
1445 int menu_init(void)
1446 {
1447         char *fname = cfg_get("save-game-filename");
1448         list_init(&menu_saved_games);
1449         if (file_exists_in_save_dir(fname)) {
1450                 file_load_from_save_dir(fname);
1451         }
1452         return 0;
1453 }
1454
1455 /*****************************************************************************
1456  * Options Menu
1457  */
1458 #include "cmd.h" /* for yesnokey() */
1459 #include "sound.h" 
1460
1461 #define OPTION_MAXNAMESTRLEN 32
1462 #define OPTION_MAXVALSTRLEN  16
1463 #define OPTION_MAXMENUSTRLEN (OPTION_MAXNAMESTRLEN + OPTION_MAXVALSTRLEN + 4)
1464
1465 enum {
1466         OPTION_SCREEN_DIMS = 0,
1467         OPTION_SOUND_ENABLE,
1468         OPTION_MUSIC_VOLUME,
1469         OPTION_KEYWORD_HIGHLIGHTING,
1470         OPTION_NUMOPTIONS /* keep last */
1471 };
1472
1473 struct option {
1474         const char *name;
1475         const char *comment;
1476         const char *key;
1477         const char *val;
1478         char *entry_val;
1479         char *startup_val;
1480         void (*handler)(struct option *);
1481         void (*enable)(int val);
1482         char restart : 1;
1483         char changed : 1;
1484         char restart_on_enable : 1;
1485 };
1486
1487 static void option_screen_dims(struct option *opt);
1488 static void option_yes_no(struct option *opt);
1489 static void option_music(struct option *opt);
1490
1491 #define DECL_OPTION(name,comment,key,handler) \
1492     { name, comment, key, 0, 0, 0, handler, NULL, 0, 0 }
1493
1494 #define DECL_YESNO_OPTION(name,comment,key,handler,ctrl,roo) \
1495     { name, comment, key, 0, 0, 0, handler, ctrl, 0, 0, roo }
1496
1497 static struct option options[OPTION_NUMOPTIONS] = {
1498         DECL_OPTION("Screen Size", "Set the dimensions of the game screen.", 
1499                     "screen-dims", option_screen_dims),
1500         DECL_YESNO_OPTION("Sound", "Turn sound on or off.",
1501                           "sound-enabled", option_yes_no, sound_enable, 1),
1502         DECL_OPTION("Music Volume", "Adjust volume of builtin music.", 
1503                     "music-volume", option_music),
1504         DECL_YESNO_OPTION("Keyword Highlighting", 
1505                           "Highlight keywords in conversations with NPC's.",
1506                           "keyword-highlighting", option_yes_no,
1507                           conv_enable_keyword_highlighting, 0),
1508 };
1509
1510 /* option_screen_dims -- let player select desired screen size from a list */
1511 static void option_screen_dims(struct option *opt)
1512 {
1513 #       define ADD_SCREEN_DIM(dim,mapw) (dim),
1514         const char *menu[] = {
1515 #               include "screen_dims.h"
1516         };
1517         struct KeyHandler kh;
1518         struct ScrollerContext data;
1519         
1520         log_msg("Choose your screen size");
1521         cmdwin_clear();
1522         cmdwin_spush("Screen size");
1523         cmdwin_spush("<select>");
1524
1525         statusSetStringList("Screen Dimensions", array_sz(menu), menu);
1526         statusSetMode(StringList);
1527         data.selection = NULL;
1528         data.selector  = String;
1529         kh.fx   = scroller;
1530         kh.data = &data;
1531         eventPushKeyHandler(&kh);
1532         eventHandle();
1533         eventPopKeyHandler();
1534
1535         if (!data.selection) {
1536                 return;
1537         }
1538
1539         if (! strcmp((char*)data.selection, opt->startup_val)) {
1540                 opt->restart = 0;
1541         } else {
1542                 opt->restart = 1;
1543         }
1544
1545         opt->val = (char*)data.selection;
1546 }
1547
1548 /**
1549  * Handler for simple yes-or-no options.
1550  *
1551  * @param opt is the option to modify.
1552  */
1553 static void option_yes_no(struct option *opt)
1554 {
1555         if (!strcmp(opt->val, "yes")) {
1556                 opt->val = "no";
1557                 opt->enable(0);
1558                 opt->restart = 0;
1559         } else {
1560                 opt->val = "yes";
1561                 opt->enable(1);
1562                 if (strcmp(opt->startup_val, "yes")) {
1563                         opt->restart = opt->restart_on_enable;
1564                 } else {
1565                         opt->restart = 0;
1566                 }
1567         }
1568 }
1569
1570 /* option_sound -- prompt player to enable music at given level */
1571 static void option_music(struct option *opt)
1572 {
1573         const char *menu[] = {
1574                 "Off","25%","50%","75%","100%"
1575         };
1576         struct KeyHandler kh;
1577         struct ScrollerContext data;
1578         
1579         log_msg("Choose music volume");
1580         cmdwin_clear();
1581         cmdwin_spush("Volume");
1582         cmdwin_spush("<select>");
1583         
1584         
1585         statusSetStringList("Music Volume", array_sz(menu), menu);
1586         statusSetMode(StringList);
1587         data.selection = NULL;
1588         data.selector  = String;
1589         kh.fx   = scroller;
1590         kh.data = &data;
1591         eventPushKeyHandler(&kh);
1592         eventHandle();
1593         eventPopKeyHandler();
1594         
1595         if (!data.selection)
1596         {
1597                 return;
1598         }
1599         
1600         opt->val = (char*)data.selection;
1601         set_music_volume((char*)data.selection);
1602
1603 }
1604
1605 static int options_save(void)
1606 {
1607         int i;
1608         char *fname = cfg_get("options-script-filename");
1609         FILE *file = file_open_in_save_dir(fname, "w");
1610         if (!file) {
1611                 warn("Could not open '%s': %s\n", fname, strerror(errno));
1612                 return -1;
1613         }
1614
1615         fprintf(file, 
1616                 ";; %s -- This file contains user-specified options that override the\n"
1617                 ";; game defaults.\n"
1618                 "(kern-cfg-set \n\n",
1619                 fname);
1620
1621         for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1622                 fprintf(file, ";; %s\n \"%s\" \"%s\"\n\n",
1623                         options[i].comment,
1624                         options[i].key,
1625                         options[i].val);
1626         }
1627
1628         fprintf(file, ")\n");
1629         fclose(file);
1630
1631         return 0;
1632 }        
1633
1634 /* options_menu -- show the user-configurable options. Upon exit prompt to save
1635  * the current settings to the options file. */
1636 void options_menu(void)
1637 {
1638         int i;
1639         int yesno;
1640         int any_changed = 0;
1641         int any_restart = 0;
1642         char menu_strings[OPTION_NUMOPTIONS][OPTION_MAXMENUSTRLEN] = {};
1643         const char *menu[OPTION_NUMOPTIONS];
1644         struct KeyHandler kh;
1645         struct ScrollerContext data;
1646
1647         /* Get current values and build the menu list. */
1648         for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1649
1650                 /* Make a copy of the original value on startup. */
1651                 if (! options[i].startup_val) {
1652                         options[i].startup_val = strdup(cfg_get(options[i].key));
1653                 }
1654
1655                 /* Make a copy of the values on entry. */
1656                 options[i].entry_val = strdup(cfg_get(options[i].key));
1657
1658                 /* Clear the flags on entry */
1659                 options[i].changed = 0;
1660
1661                 /* Init the current val. */
1662                 options[i].val = cfg_get(options[i].key);
1663
1664                 /* Build the menu entry. */
1665                 snprintf(menu_strings[i], OPTION_MAXMENUSTRLEN,
1666                          "%s [%s] %s",
1667                          options[i].name,
1668                          options[i].val,
1669                          options[i].restart ? "(restart)" : ""
1670                         );
1671
1672                 /* Add it to the menu list. */
1673                 menu[i] = menu_strings[i];
1674         }
1675
1676         /* Menu loop */
1677         for (;;) {
1678
1679                 /* Setup status browser (do this every time through the loop,
1680                  * as the handler functions might change things) */
1681                 cmdwin_clear();
1682                 cmdwin_spush("Settings");
1683                 cmdwin_push("<select/ESC>");
1684                 foogodSetHintText(SCROLLER_HINT);
1685                 foogodSetMode(FOOGOD_HINT);
1686                 statusSetStringList("Settings", OPTION_NUMOPTIONS, 
1687                                     (const char**)menu);
1688                 statusSetMode(StringList);
1689                 data.selection = NULL;
1690                 data.selector  = String;
1691                 kh.fx   = scroller;
1692                 kh.data = &data;
1693                 eventPushKeyHandler(&kh);
1694                 eventHandle();
1695                 eventPopKeyHandler();
1696
1697                 /* Player ESC */
1698                 if (! data.selection) {
1699                         break;
1700                 }
1701
1702                 /* Handle the option */
1703                 for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1704
1705                         if (strcmp((char*)data.selection, menu[i])) {
1706                                 continue;
1707                         }
1708                         
1709                         /* Invoke the option handler. */
1710                         options[i].handler(&options[i]);
1711
1712                         /* Update the menu string. */
1713                         options[i].changed = strcmp(options[i].entry_val, 
1714                                                     options[i].val);
1715                         snprintf(menu_strings[i], OPTION_MAXMENUSTRLEN,
1716                                  "%s [%s] %c %s",
1717                                  options[i].name,
1718                                  options[i].val,
1719                                  options[i].changed ? '*' : ' ',
1720                                  options[i].restart ? "(restart)" : ""
1721                                 );
1722
1723                         break;
1724                 }
1725         }
1726
1727         cmdwin_clear();
1728         foogodSetMode(FOOGOD_DEFAULT);
1729
1730         /* If none of the options changed from their entry values then return
1731          * without bothering the player. */
1732         for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1733                 any_changed |= options[i].changed;
1734                 any_restart |= options[i].restart;
1735         }
1736         if (! any_changed) {
1737                 return;
1738         }
1739
1740         /* Prompt to save */
1741         log_msg("Save settings?");
1742         cmdwin_spush("Save");
1743         cmdwin_push("<y/n>");
1744         getkey(&yesno, yesnokey);
1745         cmdwin_pop();
1746
1747         if (yesno == 'y') {
1748                 /* Set everything and save to the options file. */
1749                 for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1750                         cfg_set(options[i].key, options[i].val);
1751                 }
1752                 if (options_save()) {
1753                         log_msg("Error while saving!");
1754                 } else {
1755                         log_msg("Settings saved!");
1756                         if (any_restart) {
1757                                 log_msg("NOTE: some of your changes won't "
1758                                         "take effect until you Quit and "
1759                                         "restart the program. Sorry for the "
1760                                         "inconvenience.");
1761                         }
1762                 }
1763         } else {
1764                 log_msg("Settings unchanged.");
1765         }        
1766 }
1767
1768 void menu_startup_error(const char *fmt, ...)
1769 {
1770         va_list args;
1771         char buf[128], *ptr;
1772         SDL_Rect rect;
1773         int len;
1774         struct QuitHandler qh;
1775
1776         SCREEN_W = 320;
1777         SCREEN_H = 240;
1778         screenInit();
1779         asciiInit();
1780         eventInit();
1781
1782         va_start(args, fmt);
1783         vsnprintf(buf, sizeof(buf), fmt, args);
1784         va_end(args);
1785
1786         len = strlen(buf);
1787         ptr = buf;
1788         rect.x = 0;
1789         rect.y = 0;
1790         rect.w = screenWidth();
1791         rect.h = ASCII_H;
1792
1793         screenPrint(&rect, 0, "^c+r*** STARTUP ERROR ***^c-");
1794         rect.y += ASCII_H;
1795
1796         /* Print the error message, wrapping lines to make it fit on the
1797          * screen. */
1798         while (len) {
1799                 int n = min(len, rect.w/ASCII_W);
1800
1801                 screenPrint(&rect, 0, ptr);
1802                 len -= n;
1803                 ptr += n;
1804                 rect.y += ASCII_H;
1805         }
1806
1807         screenUpdate(0);
1808
1809         /* Wait for the user to close the window. */
1810         qh.fx = main_menu_quit_handler;
1811         eventPushQuitHandler(&qh);
1812
1813         /* This will block until the use closes the message window, which
1814          * terminates the program. */
1815         eventHandle();
1816 }