1 /* $Id: menus.c,v 1.41 2010/08/26 05:56:21 gmcnutt Exp $
3 * Copyright (C) 2006 Gordon McNutt
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)
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
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
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 */
48 #include <SDL_image.h>
50 #include <sys/types.h>
53 #define LOADSAVE_HINT "\005\006=scroll ENT=select DEL=delete ESC=exit"
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.
62 #ifndef CONFIG_LOAD_GAME_OPTION
63 #define CONFIG_LOAD_GAME_OPTION 0
67 * Information about saved game files.
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? */
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.
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 */
96 * The list of saved games, built when we evaluate the saved-game script for
97 * the load and save menus.
99 static struct list menu_saved_games;
102 * Keep track of the name of the currently loaded game so we can mark it in the
103 * load and save menus.
105 static saved_game_t *menu_current_saved_game = 0;
108 * This is what the Save Game menu shows for the new game option.
110 static const char *MENU_NEW_GAME_STR = "N)ew Saved Game";
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
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.
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:
134 * `-menu_demo_tick_handler
136 * `-eventHandlePending
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.
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>.
147 int main_menu_handled = 0;
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.
156 * Delete a saved game struct and all it's strings. Don't call this, use
157 * saved_game_unref().
159 * @param save The struct to delete.
161 static void saved_game_del(saved_game_t *save)
169 if (save->screenshot)
170 SDL_FreeSurface(save->screenshot);
175 * Infer the filename of a screenshot from the filename for the saved game.
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.
181 static char *saved_game_mk_screenshot_fname(saved_game_t *save)
183 char *s_fname = (char*)malloc(strlen(save->path)+5);
185 sprintf(s_fname, "%s.png", save->path);
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
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
198 static saved_game_t *saved_game_new(char *fname)
200 struct stat fileinfo;
202 saved_game_t *save = (saved_game_t*)malloc(sizeof(*save));
204 warn("Could not alloc save\n");
208 memset(save, 0, sizeof(*save));
210 list_init(&save->list);
212 /* Keep a copy of the file name. */
213 save->fname = strdup(fname);
215 warn("Could not alloc fname\n");
219 /* Build the full path. */
220 save->path = file_mkpath(cfg_get("saved-games-dirname"), fname);
222 warn("Could not alloc filename\n");
226 /* Get the timestamp on the file. */
227 if (! stat(save->path, &fileinfo)) {
228 save->timestamp = fileinfo.st_mtime;
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);
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);
246 saved_game_del(save);
251 * Release a reference to a saved game struct. This could destroy it.
253 * @param save The struct to release.
255 static void saved_game_unref(saved_game_t *save)
260 saved_game_del(save);
265 * Called when the user kills the window during the main menu. Exits the
269 * @returns Nothing, the program exits.
271 static bool main_menu_quit_handler(struct QuitHandler *kh)
274 return(0); /* for Sun compiler */
278 * Shows the game credits in the status window.
280 static void show_credits(void)
282 struct KeyHandler kh;
283 const char *title = "Credits";
285 "Engine Programming\n"\
286 "...Gordon McNutt\n"\
290 "...Janne Johansson\n"\
291 "...Karl Garrison\n"\
293 "...Andreas Bauer\n"\
295 "...Gordon McNutt\n"\
299 "...Joshua Steele\n"\
300 "...David Gervais\n"\
302 "...Kevin Gabbert\n"\
303 "...Gordon McNutt\n"\
305 "...Steve Riberdy\n"\
306 "Music Provided by\n"\
310 statusSetPageText(title, text);
312 consolePrint("[Hit ESC to continue]\n");
316 eventPushKeyHandler(&kh);
318 eventPopKeyHandler();
322 * Prompts the user to confirm that they want to overwrite a saved game.
324 * @returns 1 On confirm, 0 on cancel.
326 static int confirm_selection()
329 log_msg("Existing saved game will be overwritten! Are you sure?");
331 cmdwin_spush("Confirm");
332 cmdwin_spush("<y/n>");
333 getkey(&yesno, yesnokey);
336 cmdwin_spush("yes!");
341 log_msg("Canceled!");
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.
354 * @param fname The name of the saved game file.
356 void menu_add_saved_game(char *fname)
360 /* Create a new saved game list element. */
361 saved_game_t *save = saved_game_new(fname);
363 warn("menu_add_saved_game: could not add '%s'\n", fname);
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) {
378 /* Insert this one previous to it. Works for empty lists, too. */
379 list_add_tail(lptr, &save->list);
383 * Print the saved game's name, timestamp and other info for display in the
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
389 * @param fname The name of the saved game file, not including the full path.
391 static int sprintf_game_info(char *buf, int n, saved_game_t *save, char hotkey)
399 /* Convert the timestamp from epoch to a time structure. */
400 timeinfo = localtime(&save->timestamp);
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);
407 /* Calculate necessary padding to right-justify the date. */
408 padlen = n - (strlen(save->fname)
413 /* We'll mark the current game with an '*'. */
414 if (save->is_current) {
418 /* Print to the buffer. */
420 snprintf(buf, n, "%c) %s %*c%c%s", hotkey, save->fname,
421 padlen, ' ', mark, datebuf);
423 snprintf(buf, n, "%s %*c%c%s", save->fname,
424 padlen, ' ', mark, datebuf);
431 * Extract the filename from the menu entry strings used in the load and save
433 * @returns A strdup'd copy of the fname.
435 static char *menu_entry_to_fname(char *entry)
440 /* Most saved games entries start with x), where x is a 0-9 */
441 if (isdigit(entry[0]) && ')' == entry[1]) {
445 end = strchr(entry, ' ');
449 fname = strdup(entry);
457 * Reset the current saved game.
458 * @param save A pointer to the new saved game struct.
460 static void menu_set_current_saved_game(saved_game_t *save)
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;
467 menu_current_saved_game = save;
469 save->is_current = 1;
471 /* Move it to the front of the list. */
472 list_remove(&save->list);
473 list_add(&menu_saved_games, &save->list);
478 * Search the list of saved games for one with the given file name.
480 * @param fname Filename to search for.
481 * @returns The saved game with the matching filename, or 0 if none found.
483 static saved_game_t *saved_game_lookup(char *fname)
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))
495 * Show a screenshot over the map viewer with the words "SCREEN SHOT" in the
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.
501 static void menu_show_screenshot(SDL_Surface *screenshot)
503 static const char *MENU_SCREEN_SHOT_STR = "^c+ySCREEN SHOT^c-";
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);
511 screenPrint(&rect, 0, MENU_SCREEN_SHOT_STR);
518 * Get the highlighted menu item from the status viewer and figure out which
519 * saved game it corresponds to.
521 * @returns The saved game struct that goes with the menu entry, or 0 if the
522 * "New Game" option is highlighted.
524 static saved_game_t *menu_scroller_get_selected()
527 char *entry_str = (char*)statusGetSelected(String);
531 if (! strcmp(entry_str, MENU_NEW_GAME_STR)) {
534 fname = menu_entry_to_fname(entry_str);
535 return saved_game_lookup(fname);
539 * Rewrite the saved-game script, using the current list of saved games.
541 * @returns -1 on error, 0 on success.
543 static int menu_rewrite_saves()
547 char *fname = cfg_get("save-game-filename");
549 file = file_open_in_save_dir(fname, "w");
551 warn("Problem updating %s: %s\n", fname, file_get_error());
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);
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
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.
575 static void menu_prompt_to_delete(menu_scroll_data_t *data)
578 saved_game_t *save = 0;
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))
587 /* Get the saved game struct for the selection. */
588 save = menu_scroller_get_selected();
592 /* Prompt to confirm. */
593 log_begin("Delete %s?", save->fname);
596 cmdwin_push("Delete-");
597 cmdwin_push("<y/n>");
598 getkey(&yesno, yesnokey);
601 /* If confirmation denied then cancel. */
603 cmdwin_spush("abort!");
604 log_end(" Canceled!");
608 /* Confirmed, try to delete the save file. Abort if it doesn't work. */
610 statusFlashSelected(Red);
611 if (unlink(save->path)) {
612 log_continue(" WARNING! Failed to delete save file %s: %s",
613 save->path, strerror(errno));
616 /* Try to delete the screenshot. Warn the user and continue if it
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,
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);
633 saved_game_unref(save);
635 /* Re-write the saved game script. */
636 menu_rewrite_saves();
638 /* Reshuffle all the menu entries to close the gap and reset the status
640 i1 = statusGetSelectedIndex(String);
642 for (i2 = i1; i2 < data->n_menu-1; i2++) {
643 data->menu[i2] = data->menu[i2+1];
647 statusSetStringList(data->title, data->n_menu, data->menu);
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();
655 statusSetSelectedIndex(i1 ? (i1 - 1) : 0);
658 log_end(" Removed!");
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.
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.
670 int menu_scroller(struct KeyHandler * kh, int key, int keymod)
672 menu_scroll_data_t *data = (menu_scroll_data_t *) kh->data;
673 enum StatusScrollDir dir;
676 if (data->hotkeys && key < 128) {
677 char ckey = (char)key;
678 char *hotkey = strchr(data->hotkeys, ckey);
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();
701 dir = ScrollPageDown;
713 i1 = statusGetSelectedIndex(String);
714 statusFlashSelected(Green);
715 data->entry = (char*)statusGetSelected(String);
716 data->save = menu_scroller_get_selected();
724 menu_prompt_to_delete(data);
725 return data->n_menu ? 0 : 1;
731 data->entry = (char*)statusGetSelected(String);
732 data->save = menu_scroller_get_selected();
733 menu_show_screenshot(data->save ? data->save->screenshot : 0);
739 * Scroll the status window for the main menu.
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.
746 int main_menu_scroller(struct KeyHandler * kh, int key, int keymod)
748 menu_scroll_data_t *data = (menu_scroll_data_t *) kh->data;
749 enum StatusScrollDir dir;
751 if (data->hotkeys && key < 128) {
752 char ckey = (char)key;
753 char *hotkey = strchr(data->hotkeys, ckey);
755 int index = hotkey - data->hotkeys;
756 printf("ckey=%c index=%d\n", ckey, index);
757 statusSetSelectedIndex(index);
758 data->entry = (char*)statusGetSelected(String);
774 dir = ScrollPageDown;
780 data->entry = (char*)statusGetSelected(String);
781 main_menu_handled = 1;
786 main_menu_handled = 1;
793 data->entry = (char*)statusGetSelected(String);
799 * Get a suitable hotkey for the numeral.
801 * @param i The number of the hotkey.
802 * @returns ASCII 0-9 or NULL if i is out of range.
804 static char menu_hotkey(int i)
806 return (i < 10) ? (i + '0') : 0;
810 * Find the most recently saved game.
812 * @returns A copy of the full pathname of the most recently saved game, or 0
813 * if there are no saved games.
815 char * journey_onward(void)
820 if (list_empty(&menu_saved_games)) {
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);
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.
837 * @returns A copy of the full pathname of the game file to load, or 0 if the
840 char * load_game_menu(void)
842 const char **menu = 0;
843 char *menubuf, *menubufptr;
846 struct list *lptr = 0;
847 struct KeyHandler kh;
848 menu_scroll_data_t data;
850 enum StatusMode omode = statusGetMode();
851 int linew = STAT_CHARS_PER_LINE;
853 /* If there is only one saved game then just load it without prompting
855 if (1 == list_len(&menu_saved_games)) {
856 return journey_onward();
859 memset(&data, 0, sizeof(data));
861 /* Allocate the memory for the menu strings. */
862 n = list_len(&menu_saved_games);
863 menubuf = (char*)calloc(n, linew+1);
865 menu = (const char**)calloc(n, sizeof(menu[0]));
868 data.hotkeys = (char*)calloc(n + 1, 1);
869 assert(data.hotkeys);
872 data.title = "Load Game";
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;
885 foogodSetHintText(LOADSAVE_HINT);
886 foogodSetMode(FOOGOD_HINT);
887 statusSetStringList(data.title, n, menu);
888 statusSetMode(StringList);
890 /* Setup the initial screen shot */
891 if (list_empty(&menu_saved_games)) {
892 menu_show_screenshot(0);
894 saved_game_t *save = outcast(menu_saved_games.next,
896 menu_show_screenshot(save->screenshot);
899 kh.fx = menu_scroller;
901 eventPushKeyHandler(&kh);
903 eventPopKeyHandler();
905 /* If the player selected something then build the full pathname for
909 selection = strdup(data.save->path);
911 menu_set_current_saved_game(data.save);
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);
922 foogodSetMode(FOOGOD_DEFAULT);
931 * Filter characters we don't want to show in filenames.
933 * @param key A keypress code, usually ASCII.
934 * @returns 1 to reject the character, 0 to allow it.
936 static int menu_fname_filter(int key)
949 * Get a file name from the player.
950 * @returns A strdup'd copy of the filename.
952 static char *prompt_for_fname()
956 log_msg("Enter the new filename.");
958 cmdwin_push("Filename: ");
960 if (ui_getline_filtered(buf, sizeof(buf), menu_fname_filter)) {
967 * Let the player select a file to save the current game.
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
974 char * save_game_menu(void)
976 const char **menu = 0;
977 char *menubuf, *menubufptr;
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;
987 memset(&data, 0, sizeof(data));
989 /* Allocate the string buffers to display the menu. */
990 n = list_len(&menu_saved_games) + 1;
991 menubuf = (char*)calloc(n, linew+1);
993 menu = (const char**)calloc(n, sizeof(menu[0]));
996 data.hotkeys = (char*)calloc(n+1, 1);
997 assert(data.hotkeys);
1000 data.title = "Save Game";
1002 /* Prepare to fill in the menu list. */
1004 menubufptr = menubuf;
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;
1012 /* Is there a game already loaded? */
1013 if (menu_current_saved_game) {
1015 /* It should be first in the list of saved games. */
1016 assert(menu_saved_games.next
1017 == &menu_current_saved_game->list);
1019 data.hotkeys[i] = menu_hotkey(i);
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;
1028 /* Prepare to list the remaining saved games. */
1029 if (menu_current_saved_game) {
1030 lptr = menu_current_saved_game->list.next;
1032 lptr = menu_saved_games.next;
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);
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;
1047 foogodSetHintText(LOADSAVE_HINT);
1048 foogodSetMode(FOOGOD_HINT);
1050 /* Setup the menu in the status window. */
1051 statusSetStringList(data.title, n, menu);
1052 statusSetMode(StringList);
1054 /* Highlight the current saved game. */
1055 if (menu_current_saved_game) {
1056 statusSetSelectedIndex(1);
1059 /* Show the initial screenshot. */
1060 menu_show_screenshot(menu_current_saved_game
1061 ? menu_current_saved_game->screenshot
1065 kh.fx = menu_scroller;
1067 eventPushKeyHandler(&kh);
1069 eventPopKeyHandler();
1076 /* Did the player select an existing saved game? */
1079 /* Yes. Overwrite? */
1080 if (confirm_selection()) {
1081 selected_game = data.save;
1087 /* No. Did player abort? */
1088 else if (!data.abort) {
1090 /* No. Must be a new saved game. */
1092 char *new_name = prompt_for_fname();
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,
1100 if (!strcmp(new_name, exist->fname)) {
1102 /* Yes. Confirm overwrite? */
1103 if (!confirm_selection()) {
1109 /* Ok, overwrite. Nothing new to add to the
1110 * saved-game script. */
1115 /* Replace the "New Saved Game" menu line with what the player
1117 strncpy(menubuf, new_name, linew);
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);
1123 /* Re-write the saved games file to add the new
1125 menu_rewrite_saves();
1130 /* Save selected? */
1131 if (selected_game) {
1133 char *s_fname = saved_game_mk_screenshot_fname(selected_game);
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;
1143 /* Restore map view so we can get a new screenshot. */
1147 /* Take a new screenshot. */
1152 screenCapture(s_fname, &rect);
1153 selected_game->screenshot = IMG_Load(s_fname);
1154 menu_set_current_saved_game(selected_game);
1159 /* Restore the original status mode before deleting the list. */
1160 statusSetMode(omode);
1161 foogodSetMode(FOOGOD_DEFAULT);
1169 if (selected_game) {
1170 return strdup(selected_game->fname);
1175 static bool menus_demo_tick_handler(struct TickHandler *th)
1177 static int in_tick = 0; /* hack: prevent recursive entry */
1178 if (Session && !in_tick) {
1181 sprite_advance_ticks(1);
1192 /* See the comment over main_menu_handled. */
1193 return (bool)(main_menu_handled || demo_done);
1196 char * main_menu(void)
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];
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;
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
1229 int run_demo = ! file_exists_in_save_dir(save_game_fname);
1231 /* setup main menu quit handler so player can click close window to
1233 qh.fx = main_menu_quit_handler;
1234 eventPushQuitHandler(&qh);
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"));
1241 /* Can we find it? */
1242 if (file_exists(demo_fname)) {
1244 /* Have we already run the demo once? */
1245 if (! run_demo || ! first_time) {
1247 /* Show the demo as a menu option but don't
1248 * automatically start it. */
1249 show_demo_option = 1;
1252 /* Setup the demo to run in parallel with the
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",
1261 th.fx = menus_demo_tick_handler;
1262 eventPushTickHandler(&th);
1264 cfg_set("demo-has-run", "yes");
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';
1283 if (CONFIG_LOAD_GAME_OPTION) {
1284 menu[n_items] = LOAD_GAME;
1285 hotkeys[n_items] = 'l';
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';
1297 /* check for a tutorial script for Tutorial */
1298 if (file_exists(tutorial_fname)) {
1299 menu[n_items] = TUTORIAL;
1300 hotkeys[n_items] = 't';
1304 /* check for demo */
1305 if (show_demo_option) {
1306 menu[n_items] = DEMO;
1307 hotkeys[n_items] = 'i';
1311 menu[n_items] = SETTINGS;
1312 hotkeys[n_items] = 'e';
1315 menu[n_items] = CREDITS;
1316 hotkeys[n_items] = 'c';
1319 menu[n_items] = QUIT;
1320 hotkeys[n_items] = 'q';
1323 hotkeys[n_items] = 0;
1325 foogodSetHintText("\005\006=scroll ENT=select");
1326 foogodSetMode(FOOGOD_HINT);
1327 statusSetStringList("Main Menu", n_items, menu);
1328 statusSetMode(StringList);
1330 data.hotkeys = hotkeys;
1332 kh.fx = main_menu_scroller;
1334 main_menu_handled = 0;
1336 /* If running a demo then start/resume it. */
1341 eventPushKeyHandler(&kh);
1343 eventPopKeyHandler();
1345 /* Did we come back because the demo is done? */
1348 /* Yep. Setup for plain old splash and show the demo as a menu
1351 show_demo_option = 1;
1354 session_del(Session); /* wish me luck! */
1357 eventPopTickHandler();
1361 /* Pause the demo while running the submenus. This is really only
1362 * necessary for the load_game_menu(). */
1367 selection = data.entry;
1369 goto start_main_menu;
1372 if (! strcmp(selection, START_NEW_GAME)) {
1373 load_fname = new_game_fname;
1376 else if (! strcmp(selection, JOURNEY_ONWARD)) {
1377 if (CONFIG_LOAD_GAME_OPTION) {
1378 load_fname = journey_onward();
1380 load_fname = load_game_menu();
1383 goto start_main_menu;
1385 else if (CONFIG_LOAD_GAME_OPTION
1386 && ! strcmp(selection, LOAD_GAME)) {
1387 load_fname = load_game_menu();
1389 goto start_main_menu;
1391 else if (! strcmp(selection, SETTINGS)) {
1393 goto start_main_menu;
1395 else if (! strcmp(selection, CREDITS)) {
1397 goto start_main_menu;
1399 else if (! strcmp(selection, TUTORIAL)) {
1400 load_fname = tutorial_fname;
1402 else if (! strcmp(selection, QUIT)) {
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;
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);
1421 goto start_main_menu;
1424 fprintf(stderr, "Invalid selection: '%s'\n", selection);
1428 /* turn off status while new session is loading */
1429 statusSetMode(DisableStatus);
1430 foogodSetMode(FOOGOD_DEFAULT);
1432 /* pop main menu quit handler, new one will be pushed in play.c */
1433 eventPopQuitHandler();
1435 /* Cleanup after the demo. */
1438 eventPopTickHandler();
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);
1455 /*****************************************************************************
1458 #include "cmd.h" /* for yesnokey() */
1461 #define OPTION_MAXNAMESTRLEN 32
1462 #define OPTION_MAXVALSTRLEN 16
1463 #define OPTION_MAXMENUSTRLEN (OPTION_MAXNAMESTRLEN + OPTION_MAXVALSTRLEN + 4)
1466 OPTION_SCREEN_DIMS = 0,
1467 OPTION_SOUND_ENABLE,
1468 OPTION_MUSIC_VOLUME,
1469 OPTION_KEYWORD_HIGHLIGHTING,
1470 OPTION_NUMOPTIONS /* keep last */
1475 const char *comment;
1480 void (*handler)(struct option *);
1481 void (*enable)(int val);
1484 char restart_on_enable : 1;
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);
1491 #define DECL_OPTION(name,comment,key,handler) \
1492 { name, comment, key, 0, 0, 0, handler, NULL, 0, 0 }
1494 #define DECL_YESNO_OPTION(name,comment,key,handler,ctrl,roo) \
1495 { name, comment, key, 0, 0, 0, handler, ctrl, 0, 0, roo }
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),
1510 /* option_screen_dims -- let player select desired screen size from a list */
1511 static void option_screen_dims(struct option *opt)
1513 # define ADD_SCREEN_DIM(dim,mapw) (dim),
1514 const char *menu[] = {
1515 # include "screen_dims.h"
1517 struct KeyHandler kh;
1518 struct ScrollerContext data;
1520 log_msg("Choose your screen size");
1522 cmdwin_spush("Screen size");
1523 cmdwin_spush("<select>");
1525 statusSetStringList("Screen Dimensions", array_sz(menu), menu);
1526 statusSetMode(StringList);
1527 data.selection = NULL;
1528 data.selector = String;
1531 eventPushKeyHandler(&kh);
1533 eventPopKeyHandler();
1535 if (!data.selection) {
1539 if (! strcmp((char*)data.selection, opt->startup_val)) {
1545 opt->val = (char*)data.selection;
1549 * Handler for simple yes-or-no options.
1551 * @param opt is the option to modify.
1553 static void option_yes_no(struct option *opt)
1555 if (!strcmp(opt->val, "yes")) {
1562 if (strcmp(opt->startup_val, "yes")) {
1563 opt->restart = opt->restart_on_enable;
1570 /* option_sound -- prompt player to enable music at given level */
1571 static void option_music(struct option *opt)
1573 const char *menu[] = {
1574 "Off","25%","50%","75%","100%"
1576 struct KeyHandler kh;
1577 struct ScrollerContext data;
1579 log_msg("Choose music volume");
1581 cmdwin_spush("Volume");
1582 cmdwin_spush("<select>");
1585 statusSetStringList("Music Volume", array_sz(menu), menu);
1586 statusSetMode(StringList);
1587 data.selection = NULL;
1588 data.selector = String;
1591 eventPushKeyHandler(&kh);
1593 eventPopKeyHandler();
1595 if (!data.selection)
1600 opt->val = (char*)data.selection;
1601 set_music_volume((char*)data.selection);
1605 static int options_save(void)
1608 char *fname = cfg_get("options-script-filename");
1609 FILE *file = file_open_in_save_dir(fname, "w");
1611 warn("Could not open '%s': %s\n", fname, strerror(errno));
1616 ";; %s -- This file contains user-specified options that override the\n"
1617 ";; game defaults.\n"
1618 "(kern-cfg-set \n\n",
1621 for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1622 fprintf(file, ";; %s\n \"%s\" \"%s\"\n\n",
1628 fprintf(file, ")\n");
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)
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;
1647 /* Get current values and build the menu list. */
1648 for (i = 0; i < OPTION_NUMOPTIONS; i++) {
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));
1655 /* Make a copy of the values on entry. */
1656 options[i].entry_val = strdup(cfg_get(options[i].key));
1658 /* Clear the flags on entry */
1659 options[i].changed = 0;
1661 /* Init the current val. */
1662 options[i].val = cfg_get(options[i].key);
1664 /* Build the menu entry. */
1665 snprintf(menu_strings[i], OPTION_MAXMENUSTRLEN,
1669 options[i].restart ? "(restart)" : ""
1672 /* Add it to the menu list. */
1673 menu[i] = menu_strings[i];
1679 /* Setup status browser (do this every time through the loop,
1680 * as the handler functions might change things) */
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;
1693 eventPushKeyHandler(&kh);
1695 eventPopKeyHandler();
1698 if (! data.selection) {
1702 /* Handle the option */
1703 for (i = 0; i < OPTION_NUMOPTIONS; i++) {
1705 if (strcmp((char*)data.selection, menu[i])) {
1709 /* Invoke the option handler. */
1710 options[i].handler(&options[i]);
1712 /* Update the menu string. */
1713 options[i].changed = strcmp(options[i].entry_val,
1715 snprintf(menu_strings[i], OPTION_MAXMENUSTRLEN,
1719 options[i].changed ? '*' : ' ',
1720 options[i].restart ? "(restart)" : ""
1728 foogodSetMode(FOOGOD_DEFAULT);
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;
1736 if (! any_changed) {
1740 /* Prompt to save */
1741 log_msg("Save settings?");
1742 cmdwin_spush("Save");
1743 cmdwin_push("<y/n>");
1744 getkey(&yesno, yesnokey);
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);
1752 if (options_save()) {
1753 log_msg("Error while saving!");
1755 log_msg("Settings saved!");
1757 log_msg("NOTE: some of your changes won't "
1758 "take effect until you Quit and "
1759 "restart the program. Sorry for the "
1764 log_msg("Settings unchanged.");
1768 void menu_startup_error(const char *fmt, ...)
1771 char buf[128], *ptr;
1774 struct QuitHandler qh;
1782 va_start(args, fmt);
1783 vsnprintf(buf, sizeof(buf), fmt, args);
1790 rect.w = screenWidth();
1793 screenPrint(&rect, 0, "^c+r*** STARTUP ERROR ***^c-");
1796 /* Print the error message, wrapping lines to make it fit on the
1799 int n = min(len, rect.w/ASCII_W);
1801 screenPrint(&rect, 0, ptr);
1809 /* Wait for the user to close the window. */
1810 qh.fx = main_menu_quit_handler;
1811 eventPushQuitHandler(&qh);
1813 /* This will block until the use closes the message window, which
1814 * terminates the program. */