diff --git a/win/curses/Bugs.txt b/win/curses/Bugs.txt new file mode 100644 index 000000000..86b731b43 --- /dev/null +++ b/win/curses/Bugs.txt @@ -0,0 +1,13 @@ +Here is a list of known issues with the curses interface at the time of +this writing. Send any others you discover to me (Karl Garrison) at +kgarrison@obox.com, along with how to reproduce the problem, if +possible. Missing features are listed in the file Todo.txt. + + * Resizing a window to 80 columns or less causes a crash (PDCurses for + SDL and X11 only). Windows starting at this size and below do not + cause a crash, however. + + * Cursor position is wrong on map for smaller terminal windows in all + versions of PDCurses (smaller than 80 width or 24 height). This is + due to an incomplete workaround for an issue with the wmove() function + in PDCurses. diff --git a/win/curses/Readme.txt b/win/curses/Readme.txt new file mode 100644 index 000000000..f9c95fdef --- /dev/null +++ b/win/curses/Readme.txt @@ -0,0 +1,116 @@ +INTRO +===== + +The "curses" windowport is a new text-based interface for NetHack, +using high-level curses routines to control the display. Currently, it +has been compiled and tested on Linux and Windows, but it should also +be portable to a number of other systems, such as other forms of UNIX, +Mac OS X, MSDOS, and OS/2. + +Some features of this interface compared to the traditional tty +interface include: + + * Dynamic window resizing (e.g. maximizing a terminal window) + * Dynamic configurable placement of status and message windows, + relative to the map + * Makes better use of larger terminal windows + * Fancier display (e.g. window borders, optional popup dialogs) + * "cursesgraphics" option for fancier line-drawing characters for + drawing the dungeon - this should work on most terminals/platforms + + +BUILDING +======== + +As of this writing code has been compiled on Linux and Windows. + +UNIX/Linux build instructions: Follow the instructions in +sys/unix/Install.unx. By default, the Makefile is setup to compile +against ncurses. Edit Makefile.src if you wish to compile against a +different curses library, such as PDCurses for SDL. + +Windows build instructions: If you are using Mingw32 as your compiler, +then follow the instructions in sys/winnt/Install.nt with the following +changes: + + * After running nhsetup, manually copy the file cursmake.gcc to the + src/ subdirectory + * Instead of typing "mingw32-make -f Makefile.gcc install" you will + type "mingw32-make -f cursmake.gcc install" + +If you are using a different compiler, you will have to manually modify +the appropriate Makefile to include the curses windowport files. + + +GAMEPLAY +======== + +Gameplay should be similar to the tty interface for NetHack; the +differences are primarily visual. This windowport supports dymanic +resizing of the terminal window, so you can play with it to see how it +looks best to you during a game. Also, the align_status and +align_message options may be set during the game, so you can experiment +to see what arraingement looks best to you. + +For menus, in addition to the normal configurable keybindings for menu +navigation descrived in the Guidebook, you can use the right and left +arrows to to forward or backward one page, respectively, and the home +and end keys to go to the first and last pages, respectively. + +Some configuration options that are specific to or relevant to the +curses windowport are shown below. Copy any of these that you like to +your nethack configuration file (e.g. .nethackrc for UNIX or +NetHack.cnf for Windows): +# +# Use this if the binary was compiled with multiple window interfaces, +# and curses is not the default +OPTIONS=windowtype:curses +# +# Set this for Windows systems, or for PDCurses for SDL on any system. +# The latter uses a cp437 font, which works with this option +#OPTIONS=IBMgraphics +# +# Set this if IBMgraphics above won't work for your system. Mutually +# exclusive with the above option, and should work on nearly any +# system. +OPTIONS=cursesgraphics +# +# Optionally specify the alignment of the message and status windows +# relative to the map window. If not specified, the code will default +# to the locations used in the tty interface: message window on top, +# and status window on bottom. Placing either of these on the right or +# left really only works well for winder terminal windows. +OPTIONS=align_message:bottom,align_status:right +# +# Use a small popup "window" for short prompts, e.g. "Really save?". +# If this is not set, the message window will be used for these as is +# done for the tty interface. +OPTIONS=popup_dialog +# +# Specify the initial window size for NetHack in units of characters. +# This is supported on PDCurses for SDL as well as PDCurses for +# Windows. +OPTIONS=term_cols:110,term_rows:32 +# +# Controls the usage of window borders for the main NetHack windows +# (message, map, and status windows). A value of 1 forces the borders +# to be drawn, a value of 2 forces them to be off, and a value of 3 +# allows the code to decide if they should be drawn based on the size +# of the terminal window. +OPTIONS=windowborders:3 + + +CONTACT +======= + +Please send any bug reports, suggestions, patches, or miscellaneous +feedback to me (Karl Garrison) at: kgarrison@pobox.com. Note that as +of this writing, I only have sporatic Internet access, so I may not get +back to you right away. + +Happy Hacking! + +Karl Garrison +March, 2009 + + diff --git a/win/curses/Todo.txt b/win/curses/Todo.txt new file mode 100644 index 000000000..b92327790 --- /dev/null +++ b/win/curses/Todo.txt @@ -0,0 +1,146 @@ +Below are some things I would like to see + +NETHACK INTERFACE +================= + +(These are the functions in cursmain.c called by the core NetHack code) + + * Implement curses_rip for optional fancier color tombstone, as well + as one that will display correctly on smaller terminals. + + * I am confused as to how mark_synch, wait_synch, and delay_output + should work. Help, please? + + * Both PDCurses and Ncurses have mouse support, so the poskey function + could probably be implemented easily enough. + + * raw_print is supposed to be able to work before the windowing system + has been initialized, as well as after, so I am unsure if curses + functions should be used here. Maybe check to see if initscr() has + been called, and use curses functions if so, and call initscr() from + there is not? Right now it is just a call to puts() with no support + for bold text. + + +DISPLAY +======= + + * Consolidate refreshes of the display for smoother output. + + * Horizontal scrollbar to show position for displays < 80 columns. + + * Calls to getch() should probably be turned into wgetch() for the + appropriate window. This causes quirty cursor behavior under + PDCurses, however. + + * Animation effects do not display properly - this could probably be + fixed with a correct implementation of the delay_output function. + + * Support option to set forground and background colors for individual + windows + + +MENUS +===== + +(cursdial.c) + + * Menus need to be able to accept a count as input, e.g. to specifiy + how many items to drop. + + * Currently the "preselected" flag for an individual menu item is + ignored. This should eventually be implemented. + + * Menus probably should never overlap with the message window, since + the user sometmes needs to be able to see the messages while the menu + is active, e.g. when identifying multiple items one at a time. + + * Perhaps allow for keyboard navigation of individual items, e.g. + using the up and down arrows to move among the selectable items, and + selecting individual items with the spacebar. Perhaps the tab key + could jump to the first selectable item after the next heading, and + shift-tab could jump to the first item of the previous heading. + + +MESSAGE WINDOW +============== + +(cursmesg.c) + + * Hitting Esc at the more prompt (which is '>>' for the curses + interface) should suppress the display of any further messages for + that turn like the tty interface does. + + +MAP WINDOW +========== + +(curswins.c) + + * The map window would probably benefit from a total redesign. Right + now, it uses a pad instead of a regular curses window, which causes a + number of special cases in the code to account for it, and a seperate + window behind it just to draw the border. It feels kludgy and + annoying! + + +STATUS WINDOW +============= + +(cursstat.c) + + * If the status window is on the right or left, then we have much more + room to work with for each item horizontally. Expand out some of the + labels for clarity. We can also list the current dungeon (e.g. + Gnomish Mines) and perhaps show thermometer bars for hit points and + magical power. + + * Conversely, if we have a narrower dislay, compress some of the + labels to save space, and do not display some items that never or + rarely change (e.g. name, level and title, and alignment). Perhaps + display changes to these fields in the message window if they do + happen to change (e.g. converting to a new alignment). + + * Maybe add some configuration options for what colors are used and + the like. + +OTHER DIALOGS +============= + +(cursdial.c) + + * curses_yn_function needs to accept a count if a '#' is present in + choices string. + + * Extended commands should be enterable letter-by-letter via a '#' + prompt if user does not have the extmenu command set to TRUE. + + * Character selection should allow for a random selection of any or + all choices. + + +OTHER PLATFORMS +=============== + + * PDCurses also work on DOS and OS/2. PDCurses for SDL and ncurses + exist for Mac OS X. Porting the curses interface to these platforms + should not be too difficult. + + +MISC +==== + + * Update documentation and in-game help to describe the newly-added + options: cursesgraphics, term_rows, term_cols, and windowborders. + + * Recognize "Alt" key in a platform-independant way to allow its use + to select extended commands. Currently this works for PDCurses. For + Ncurses, the Alt key works in an xterm or rxvt if the -meta8 flag is + passed, but I'd like to see a general way of detecting it. + + * PDCurses has a function named "addrawch" to output the visual + representation of a control character to the screen without having the + control character affect the display otherwise. I would like to find + a way to accomplish the same thing via Ncurses to e.g. be able to use + a font like nh10 with the correct symbol mappings in an xterm or the + like. diff --git a/win/curses/cursdial.c b/win/curses/cursdial.c new file mode 100644 index 000000000..274710db1 --- /dev/null +++ b/win/curses/cursdial.c @@ -0,0 +1,1387 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursdial.h" +#include "func_tab.h" +#include + +#if defined(FILENAME_CMP) +#define strcasecmp FILENAME_CMP +#endif +#if defined(STRNCMPI) +#define strncasecmp strncmpi +#endif + +/* Dialog windows for curses interface */ + + +/* Private declarations */ + +typedef struct nhmi { + winid wid; /* NetHack window id */ + int glyph; /* Menu glyphs */ + anything identifier; /* Value returned if item selected */ + CHAR_P accelerator; /* Character used to select item from menu */ + CHAR_P group_accel; /* Group accelerator for menu item, if any */ + int attr; /* Text attributes for item */ + const char *str; /* Text of menu item */ + BOOLEAN_P presel; /* Whether menu item should be preselected */ + boolean selected; /* Whether item is currently selected */ + int page_num; /* Display page number for entry */ + int line_num; /* Line number on page where entry begins */ + int num_lines; /* Number of lines entry uses on page */ + int count; /* Count for selected item */ + struct nhmi *prev_item; /* Pointer to previous entry */ + struct nhmi *next_item; /* Pointer to next entry */ +} nhmenu_item; + +typedef struct nhm { + winid wid; /* NetHack window id */ + const char *prompt; /* Menu prompt text */ + nhmenu_item *entries; /* Menu entries */ + int num_entries; /* Number of menu entries */ + int num_pages; /* Number of display pages for entry */ + int height; /* Window height of menu */ + int width; /* Window width of menu */ + boolean reuse_accels; /* Non-unique accelerators per page */ + struct nhm *prev_menu; /* Pointer to previous entry */ + struct nhm *next_menu; /* Pointer to next entry */ +} nhmenu; + +typedef enum menu_op_type { + SELECT, + DESELECT, + INVERT +} menu_op; + +static nhmenu *get_menu(winid wid); +static char menu_get_accel(boolean first); +static void menu_determine_pages(nhmenu *menu); +static boolean menu_is_multipage(nhmenu *menu, int width, int height); +static void menu_win_size(nhmenu *menu); +static void menu_display_page(nhmenu *menu, WINDOW * win, int page_num); +static int menu_get_selections(WINDOW * win, nhmenu *menu, int how); +static void menu_select_deselect(WINDOW * win, nhmenu_item *item, + menu_op operation); +static int menu_operation(WINDOW * win, nhmenu *menu, menu_op operation, + int page_num); +static void menu_clear_selections(nhmenu *menu); +static int menu_max_height(void); + +static nhmenu *nhmenus = NULL; /* NetHack menu array */ + + +/* Get a line of text from the player, such as asking for a character name or a wish */ + +void +curses_line_input_dialog(const char *prompt, char *answer, int buffer) +{ + int map_height, map_width, maxwidth, remaining_buf, winx, winy, count; + WINDOW *askwin, *bwin; +#if __STDC_VERSION__ >= 199901L + char input[buffer]; +#else +#ifndef BUFSZ +#define BUFSZ 256 +#endif + char input[BUFSZ]; + + buffer = BUFSZ - 1; +#endif + char *tmpstr; + int prompt_width = strlen(prompt) + buffer + 1; + int prompt_height = 1; + int height = prompt_height; + + maxwidth = term_cols - 2; + + if (iflags.window_inited) { + if (!iflags.wc_popup_dialog) { + curses_message_win_getline(prompt, answer, buffer); + return; + } + curses_get_window_size(MAP_WIN, &map_height, &map_width); + if ((prompt_width + 2) > map_width) + maxwidth = map_width - 2; + } + + if (prompt_width > maxwidth) { + prompt_height = curses_num_lines(prompt, maxwidth); + height = prompt_height; + prompt_width = maxwidth; + tmpstr = curses_break_str(prompt, maxwidth, prompt_height); + remaining_buf = buffer - (strlen(tmpstr) - 1); + if (remaining_buf > 0) { + height += (remaining_buf / prompt_width); + if ((remaining_buf % prompt_width) > 0) { + height++; + } + } + } + + if (iflags.window_inited) { + bwin = curses_create_window(prompt_width, height, UP); + wrefresh(bwin); + getbegyx(bwin, winy, winx); + askwin = newwin(height, prompt_width, winy + 1, winx + 1); + } else { + bwin = curses_create_window(prompt_width, height, CENTER); + wrefresh(bwin); + getbegyx(bwin, winy, winx); + askwin = newwin(height, prompt_width, winy + 1, winx + 1); + } + for (count = 0; count < prompt_height; count++) { + tmpstr = curses_break_str(prompt, maxwidth, count + 1); + if (count == (prompt_height - 1)) { /* Last line */ + mvwprintw(askwin, count, 0, "%s ", tmpstr); + } else { + mvwaddstr(askwin, count, 0, tmpstr); + } + free(tmpstr); + } + + echo(); + curs_set(1); + wgetnstr(askwin, input, buffer - 1); + curs_set(0); + strcpy(answer, input); + werase(bwin); + delwin(bwin); + curses_destroy_win(askwin); + noecho(); +} + + +/* Get a single character response from the player, such as a y/n prompt */ + +int +curses_character_input_dialog(const char *prompt, const char *choices, + CHAR_P def) +{ + WINDOW *askwin = NULL; + int answer, count, maxwidth, map_height, map_width; + char *linestr; + char askstr[BUFSZ + QBUFSZ]; + char choicestr[QBUFSZ]; + int prompt_width = strlen(prompt); + int prompt_height = 1; + boolean any_choice = FALSE; + boolean accept_count = FALSE; + + if (invent || (moves > 1)) { + curses_get_window_size(MAP_WIN, &map_height, &map_width); + } else { + map_height = term_rows; + map_width = term_cols; + } + + maxwidth = map_width - 2; + + if (choices != NULL) { + for (count = 0; choices[count] != '\0'; count++) { + if (choices[count] == '#') { /* Accept a count */ + accept_count = TRUE; + } + } + choicestr[0] = ' '; + choicestr[1] = '['; + for (count = 0; choices[count] != '\0'; count++) { + if (choices[count] == '\033') { /* Escape */ + break; + } + choicestr[count + 2] = choices[count]; + } + choicestr[count + 2] = ']'; + if (((def >= 'A') && (def <= 'Z')) || ((def >= 'a') && (def <= 'z'))) { + choicestr[count + 3] = ' '; + choicestr[count + 4] = '('; + choicestr[count + 5] = def; + choicestr[count + 6] = ')'; + choicestr[count + 7] = '\0'; + } else { /* No usable default choice */ + + choicestr[count + 3] = '\0'; + def = '\0'; /* Mark as no default */ + } + strcpy(askstr, prompt); + strcat(askstr, choicestr); + } else { + strcpy(askstr, prompt); + any_choice = TRUE; + } + + prompt_width = strlen(askstr); + + if ((prompt_width + 2) > maxwidth) { + prompt_height = curses_num_lines(askstr, maxwidth); + prompt_width = map_width - 2; + } + + if (iflags.wc_popup_dialog /*|| curses_stupid_hack*/) { + askwin = curses_create_window(prompt_width, prompt_height, UP); + for (count = 0; count < prompt_height; count++) { + linestr = curses_break_str(askstr, maxwidth, count + 1); + mvwaddstr(askwin, count + 1, 1, linestr); + free(linestr); + } + + wrefresh(askwin); + } else { + linestr = curses_copy_of(askstr); + pline("%s", linestr); + free(linestr); + curs_set(1); + } + + /*curses_stupid_hack = 0; */ + + while (1) { + answer = getch(); + + if (answer == ERR) { + answer = def; + break; + } + + answer = curses_convert_keys(answer); + + if (answer == KEY_ESC) { + if (choices == NULL) { + break; + } + answer = def; + for (count = 0; choices[count] != '\0'; count++) { + if (choices[count] == 'q') { /* q is preferred over n */ + answer = 'q'; + } else if ((choices[count] == 'n') && answer != 'q') { + answer = 'n'; + } + } + break; + } else if ((answer == '\n') || (answer == '\r') || (answer == ' ')) { + if ((choices != NULL) && (def != '\0')) { + answer = def; + } + break; + } + + if (digit(answer)) { + if (accept_count) { + if (answer != '0') { + yn_number = curses_get_count(answer - '0'); + touchwin(askwin); + refresh(); + } + + answer = '#'; + break; + } + } + + if (any_choice) { + break; + } + + if (choices != NULL) { + for (count = 0; (size_t) count < strlen(choices); count++) { + if (choices[count] == answer) { + break; + } + } + if (choices[count] == answer) { + break; + } + } + } + + if (iflags.wc_popup_dialog) { + /* Kludge to make prompt visible after window is dismissed + when inputting a number */ + if (digit(answer)) { + linestr = curses_copy_of(askstr); + pline("%s", linestr); + free(linestr); + curs_set(1); + } + + curses_destroy_win(askwin); + } else { + curses_clear_unhighlight_message_window(); + curs_set(0); + } + + return answer; +} + + +/* Return an extended command from the user */ + +int +curses_ext_cmd() +{ + int count, letter, prompt_width, startx, starty, winx, winy; + int messageh, messagew, maxlen = BUFSZ - 1; + int ret = -1; + char cur_choice[BUFSZ]; + int matches = 0; + WINDOW *extwin = NULL, *extwin2 = NULL; + + if (iflags.extmenu) { + return extcmd_via_menu(); + } + + startx = 0; + starty = 0; + if (iflags.wc_popup_dialog) { /* Prompt in popup window */ + int x0, y0, w, h; /* bounding coords of popup */ + extwin2 = curses_create_window(25, 1, UP); + wrefresh(extwin2); + /* create window inside window to prevent overwriting of border */ + getbegyx(extwin2,y0,x0); + getmaxyx(extwin2,h,w); + extwin = newwin(1, w-2, y0+1, x0+1); + if (w - 4 < maxlen) maxlen = w - 4; + } else { + curses_get_window_xy(MESSAGE_WIN, &winx, &winy); + curses_get_window_size(MESSAGE_WIN, &messageh, &messagew); + + if (curses_window_has_border(MESSAGE_WIN)) { + winx++; + winy++; + } + + winy += messageh - 1; + extwin = newwin(1, messagew-2, winy, winx); + if (messagew - 4 < maxlen) maxlen = messagew - 4; + pline("#"); + } + + cur_choice[0] = '\0'; + + while (1) { + wmove(extwin, starty, startx); + waddstr(extwin, "# "); + wmove(extwin, starty, startx + 2); + waddstr(extwin, cur_choice); + wmove(extwin, starty, strlen(cur_choice) + startx + 2); + wprintw(extwin, " "); + + /* if we have an autocomplete command, AND it matches uniquely */ + if (matches == 1) { + curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, ON); + wmove(extwin, starty, strlen(cur_choice) + startx + 2); + wprintw(extwin, "%s", extcmdlist[ret].ef_txt + strlen(cur_choice)); + curses_toggle_color_attr(extwin, NONE, A_UNDERLINE, OFF); + mvwprintw(extwin, starty, + strlen(extcmdlist[ret].ef_txt) + 2, " "); + } + + wrefresh(extwin); + letter = getch(); + prompt_width = strlen(cur_choice); + matches = 0; + + if (letter == '\033' || letter == ERR) { + ret = -1; + break; + } + + if ((letter == '\r') || (letter == '\n')) { + if (ret == -1) { + for (count = 0; extcmdlist[count].ef_txt; count++) { + if (!strcasecmp(cur_choice, extcmdlist[count].ef_txt)) { + ret = count; + break; + } + } + } + break; + } + + if ((letter == '\b') || (letter == KEY_BACKSPACE)) { + if (prompt_width == 0) { + ret = -1; + break; + } else { + cur_choice[prompt_width - 1] = '\0'; + letter = '*'; + prompt_width--; + } + } + if (letter != '*' && prompt_width < maxlen) { + cur_choice[prompt_width] = letter; + cur_choice[prompt_width + 1] = '\0'; + ret = -1; + } + for (count = 0; extcmdlist[count].ef_txt; count++) { + if (!(extcmdlist[count].flags & AUTOCOMPLETE)) + continue; + if (strlen(extcmdlist[count].ef_txt) > (size_t) prompt_width) { + if (strncasecmp(cur_choice, extcmdlist[count].ef_txt, + prompt_width) == 0) { + if ((extcmdlist[count].ef_txt[prompt_width] == + lowc(letter)) || letter == '*') { + if (matches == 0) { + ret = count; + } + + matches++; + } + } + } + } + } + + curses_destroy_win(extwin); + if (extwin2) curses_destroy_win(extwin2); + return ret; +} + + +/* Initialize a menu from given NetHack winid */ + +void +curses_create_nhmenu(winid wid) +{ + nhmenu *new_menu = NULL; + nhmenu *menuptr = nhmenus; + nhmenu_item *menu_item_ptr = NULL; + nhmenu_item *tmp_menu_item = NULL; + + new_menu = get_menu(wid); + + if (new_menu != NULL) { + /* Reuse existing menu, clearing out current entries */ + menu_item_ptr = new_menu->entries; + + if (menu_item_ptr != NULL) { + while (menu_item_ptr->next_item != NULL) { + tmp_menu_item = menu_item_ptr->next_item; + free(menu_item_ptr); + menu_item_ptr = tmp_menu_item; + } + free(menu_item_ptr); /* Last entry */ + new_menu->entries = NULL; + } + if (new_menu->prompt != NULL) { /* Reusing existing menu */ + free((char *) new_menu->prompt); + } + return; + } + + new_menu = malloc(sizeof (nhmenu)); + new_menu->wid = wid; + new_menu->prompt = NULL; + new_menu->entries = NULL; + new_menu->num_pages = 0; + new_menu->height = 0; + new_menu->width = 0; + new_menu->reuse_accels = FALSE; + new_menu->next_menu = NULL; + + if (nhmenus == NULL) { /* no menus in memory yet */ + new_menu->prev_menu = NULL; + nhmenus = new_menu; + } else { + while (menuptr->next_menu != NULL) { + menuptr = menuptr->next_menu; + } + new_menu->prev_menu = menuptr; + menuptr->next_menu = new_menu; + } +} + + +/* Add a menu item to the given menu window */ + +void +curses_add_nhmenu_item(winid wid, int glyph, const ANY_P * identifier, + CHAR_P accelerator, CHAR_P group_accel, int attr, + const char *str, BOOLEAN_P presel) +{ + char *new_str; + nhmenu_item *new_item, *current_items, *menu_item_ptr; + nhmenu *current_menu = get_menu(wid); + + if (str == NULL) { + return; + } + + new_str = curses_copy_of(str); + curses_rtrim((char *) new_str); + new_item = malloc(sizeof (nhmenu_item)); + new_item->wid = wid; + new_item->glyph = glyph; + new_item->identifier = *identifier; + new_item->accelerator = accelerator; + new_item->group_accel = group_accel; + new_item->attr = attr; + new_item->str = new_str; + new_item->presel = presel; + new_item->selected = FALSE; + new_item->page_num = 0; + new_item->line_num = 0; + new_item->num_lines = 0; + new_item->count = -1; + new_item->next_item = NULL; + + if (current_menu == NULL) { + panic + ("curses_add_nhmenu_item: attempt to add item to nonexistant menu"); + } + + current_items = current_menu->entries; + menu_item_ptr = current_items; + + if (current_items == NULL) { + new_item->prev_item = NULL; + current_menu->entries = new_item; + } else { + while (menu_item_ptr->next_item != NULL) { + menu_item_ptr = menu_item_ptr->next_item; + } + new_item->prev_item = menu_item_ptr; + menu_item_ptr->next_item = new_item; + } +} + + +/* No more entries are to be added to menu, so details of the menu can be + finalized in memory */ + +void +curses_finalize_nhmenu(winid wid, const char *prompt) +{ + int count = 0; + nhmenu *current_menu = get_menu(wid); + nhmenu_item *menu_item_ptr = current_menu->entries; + + if (current_menu == NULL) { + panic("curses_finalize_nhmenu: attempt to finalize nonexistant menu"); + } + + while (menu_item_ptr != NULL) { + menu_item_ptr = menu_item_ptr->next_item; + count++; + } + + current_menu->num_entries = count; + + current_menu->prompt = curses_copy_of(prompt); +} + + +/* Display a nethack menu, and return a selection, if applicable */ + +int +curses_display_nhmenu(winid wid, int how, MENU_ITEM_P ** _selected) +{ + nhmenu *current_menu = get_menu(wid); + nhmenu_item *menu_item_ptr; + int num_chosen, count; + WINDOW *win; + MENU_ITEM_P *selected = NULL; + + *_selected = NULL; + + if (current_menu == NULL) { + panic("curses_display_nhmenu: attempt to display nonexistant menu"); + } + + menu_item_ptr = current_menu->entries; + + if (menu_item_ptr == NULL) { + panic("curses_display_nhmenu: attempt to display empty menu"); + } + + /* Reset items to unselected to clear out selections from previous + invocations of this menu, and preselect appropriate items */ + while (menu_item_ptr != NULL) { + menu_item_ptr->selected = menu_item_ptr->presel; + menu_item_ptr = menu_item_ptr->next_item; + } + + menu_win_size(current_menu); + menu_determine_pages(current_menu); + + /* Display pre and post-game menus centered */ + if (((moves <= 1) && !invent) || program_state.gameover) { + win = curses_create_window(current_menu->width, + current_menu->height, CENTER); + } else { /* Display during-game menus on the right out of the way */ + + win = curses_create_window(current_menu->width, + current_menu->height, RIGHT); + } + + num_chosen = menu_get_selections(win, current_menu, how); + curses_destroy_win(win); + + if (num_chosen > 0) { + selected = (MENU_ITEM_P *) malloc(num_chosen * sizeof (MENU_ITEM_P)); + count = 0; + + menu_item_ptr = current_menu->entries; + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->selected) { + if (count == num_chosen) { + panic("curses_display_nhmenu: Selected items " + "exceeds expected number"); + } + selected[count].item = menu_item_ptr->identifier; + selected[count].count = menu_item_ptr->count; + count++; + } + menu_item_ptr = menu_item_ptr->next_item; + } + + if (count != num_chosen) { + panic("curses_display_nhmenu: Selected items less than " + "expected number"); + } + } + + *_selected = selected; + + return num_chosen; +} + + +boolean +curses_menu_exists(winid wid) +{ + if (get_menu(wid) != NULL) { + return TRUE; + } else { + return FALSE; + } +} + +/* Delete the menu associated with the given NetHack winid from memory */ + +void +curses_del_menu(winid wid) +{ + nhmenu_item *tmp_menu_item; + nhmenu_item *menu_item_ptr; + nhmenu *tmpmenu; + nhmenu *current_menu = get_menu(wid); + + if (current_menu == NULL) { + return; + } + + menu_item_ptr = current_menu->entries; + + /* First free entries associated with this menu from memory */ + if (menu_item_ptr != NULL) { + while (menu_item_ptr->next_item != NULL) { + tmp_menu_item = menu_item_ptr->next_item; + free(menu_item_ptr); + menu_item_ptr = tmp_menu_item; + } + free(menu_item_ptr); /* Last entry */ + current_menu->entries = NULL; + } + + /* Now unlink the menu from the list and free it as well */ + if (current_menu->prev_menu != NULL) { + tmpmenu = current_menu->prev_menu; + tmpmenu->next_menu = current_menu->next_menu; + } else { + nhmenus = current_menu->next_menu; /* New head mode or NULL */ + } + if (current_menu->next_menu != NULL) { + tmpmenu = current_menu->next_menu; + tmpmenu->prev_menu = current_menu->prev_menu; + } + + free(current_menu); + + curses_del_wid(wid); +} + + +/* return a pointer to the menu associated with the given NetHack winid */ + +static nhmenu * +get_menu(winid wid) +{ + nhmenu *menuptr = nhmenus; + + while (menuptr != NULL) { + if (menuptr->wid == wid) { + return menuptr; + } + menuptr = menuptr->next_menu; + } + + return NULL; /* Not found */ +} + + +static char +menu_get_accel(boolean first) +{ + char ret; + static char next_letter = 'a'; + + if (first) { + next_letter = 'a'; + } + + ret = next_letter; + + if (((next_letter < 'z') && (next_letter >= 'a')) || ((next_letter < 'Z') + && (next_letter >= + 'A')) || + ((next_letter < '9') && (next_letter >= '0'))) { + next_letter++; + } else if (next_letter == 'z') { + next_letter = 'A'; + } else if (next_letter == 'Z') { + next_letter = '0'; + } + + return ret; +} + + +/* Determine if menu will require multiple pages to display */ + +static boolean +menu_is_multipage(nhmenu *menu, int width, int height) +{ + int num_lines; + int curline = 0; + nhmenu_item *menu_item_ptr = menu->entries; + + if (strlen(menu->prompt) > 0) { + curline += curses_num_lines(menu->prompt, width) + 1; + } + + if (menu->num_entries <= (height - curline)) { + while (menu_item_ptr != NULL) { + menu_item_ptr->line_num = curline; + if (menu_item_ptr->identifier.a_void == NULL) { + num_lines = curses_num_lines(menu_item_ptr->str, width); + } else { + /* Add space for accelerator */ + num_lines = curses_num_lines(menu_item_ptr->str, width - 4); + } + menu_item_ptr->num_lines = num_lines; + curline += num_lines; + menu_item_ptr = menu_item_ptr->next_item; + if ((curline > height) || ((curline > height - 2) && + (height == menu_max_height()))) { + break; + } + } + if (menu_item_ptr == NULL) { + return FALSE; + } + } + return TRUE; +} + + +/* Determine which entries go on which page, and total number of pages */ + +static void +menu_determine_pages(nhmenu *menu) +{ + int tmpline, num_lines; + int curline = 0; + int page_num = 1; + nhmenu_item *menu_item_ptr = menu->entries; + int width = menu->width; + int height = menu->height; + int page_end = height; + + + if (strlen(menu->prompt) > 0) { + curline += curses_num_lines(menu->prompt, width) + 1; + } + + tmpline = curline; + + if (menu_is_multipage(menu, width, height)) { + page_end -= 2; /* Room to display current page number */ + } + + /* Determine what entries belong on which page */ + menu_item_ptr = menu->entries; + + while (menu_item_ptr != NULL) { + menu_item_ptr->page_num = page_num; + menu_item_ptr->line_num = curline; + if (menu_item_ptr->identifier.a_void == NULL) { + num_lines = curses_num_lines(menu_item_ptr->str, width); + } else { + /* Add space for accelerator */ + num_lines = curses_num_lines(menu_item_ptr->str, width - 4); + } + menu_item_ptr->num_lines = num_lines; + curline += num_lines; + if (curline > page_end) { + page_num++; + curline = tmpline; + /* Move ptr back so entry will be reprocessed on new page */ + menu_item_ptr = menu_item_ptr->prev_item; + } + menu_item_ptr = menu_item_ptr->next_item; + } + + menu->num_pages = page_num; +} + + +/* Determine dimensions of menu window based on term size and entries */ + +static void +menu_win_size(nhmenu *menu) +{ + int width, height, maxwidth, maxheight, curentrywidth, lastline; + int maxentrywidth = strlen(menu->prompt); + int maxheaderwidth = 0; + nhmenu_item *menu_item_ptr = menu->entries; + + maxwidth = 38; /* Reasonable minimum usable width */ + + if ((term_cols / 2) > maxwidth) { + maxwidth = (term_cols / 2); /* Half the screen */ + } + + maxheight = menu_max_height(); + + /* First, determine the width of the longest menu entry */ + while (menu_item_ptr != NULL) + { + if (menu_item_ptr->identifier.a_void == NULL) { + curentrywidth = strlen(menu_item_ptr->str); + + if (curentrywidth > maxheaderwidth) { + maxheaderwidth = curentrywidth; + } + } else { + /* Add space for accelerator */ + curentrywidth = strlen(menu_item_ptr->str) + 4; +#if 0 // FIXME: menu glyphs + if (menu_item_ptr->glyph != NO_GLYPH + && iflags.use_menu_glyphs) + curentrywidth += 2; +#endif + } + if (curentrywidth > maxentrywidth) { + maxentrywidth = curentrywidth; + } + menu_item_ptr = menu_item_ptr->next_item; + } + + /* If the widest entry is smaller than maxwidth, reduce maxwidth accordingly */ + if (maxentrywidth < maxwidth) { + maxwidth = maxentrywidth; + } + + /* Try not to wrap headers/normal text lines if possible. We can + go wider than half the screen for this purpose if need be */ + + if ((maxheaderwidth > maxwidth) && (maxheaderwidth < (term_cols - 2))) { + maxwidth = maxheaderwidth; + } + + width = maxwidth; + + /* Possibly reduce height if only 1 page */ + if (!menu_is_multipage(menu, maxwidth, maxheight)) { + menu_item_ptr = menu->entries; + + while (menu_item_ptr->next_item != NULL) { + menu_item_ptr = menu_item_ptr->next_item; + } + + lastline = (menu_item_ptr->line_num) + menu_item_ptr->num_lines; + + if (lastline < maxheight) { + maxheight = lastline; + } + } else { /* If multipage, make sure we have enough width for page footer */ + + if (width < 20) { + width = 20; + } + } + + height = maxheight; + menu->width = width; + menu->height = height; +} + + +/* Displays menu selections in the given window */ + +static void +menu_display_page(nhmenu *menu, WINDOW * win, int page_num) +{ + nhmenu_item *menu_item_ptr; + int count, curletter, entry_cols, start_col, num_lines, footer_x; + boolean first_accel = TRUE; + + int color = NO_COLOR; + int attr = A_NORMAL; + boolean menu_color = FALSE; + + /* Cycle through entries until we are on the correct page */ + + menu_item_ptr = menu->entries; + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->page_num == page_num) { + break; + } + menu_item_ptr = menu_item_ptr->next_item; + } + + if (menu_item_ptr == NULL) { /* Page not found */ + panic("menu_display_page: attempt to display nonexistant page"); + } + + werase(win); + + if (strlen(menu->prompt) > 0) { + num_lines = curses_num_lines(menu->prompt, menu->width); + + for (count = 0; count < num_lines; count++) { + mvwprintw(win, count + 1, 1, "%s", + curses_break_str(menu->prompt, menu->width, count + 1)); + } + } + + /* Display items for current page */ + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->page_num != page_num) { + break; + } + if (menu_item_ptr->identifier.a_void != NULL) { + if (menu_item_ptr->accelerator != 0) { + curletter = menu_item_ptr->accelerator; + } else { + if (first_accel) { + curletter = menu_get_accel(TRUE); + first_accel = FALSE; + if (!menu->reuse_accels && (menu->num_pages > 1)) { + menu->reuse_accels = TRUE; + } + } else { + curletter = menu_get_accel(FALSE); + } + menu_item_ptr->accelerator = curletter; + } + + if (menu_item_ptr->selected) { + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON); + mvwaddch(win, menu_item_ptr->line_num + 1, 1, '<'); + mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter); + mvwaddch(win, menu_item_ptr->line_num + 1, 3, '>'); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF); + } else { + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON); + mvwaddch(win, menu_item_ptr->line_num + 1, 2, curletter); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF); + mvwprintw(win, menu_item_ptr->line_num + 1, 3, ") "); + } + } + entry_cols = menu->width; + start_col = 1; + + if (menu_item_ptr->identifier.a_void != NULL) { + entry_cols -= 4; + start_col += 4; + } +#if 0 + //FIXME: menuglyphs not implemented yet + if (menu_item_ptr->glyph != NO_GLYPH && iflags.use_menu_glyphs) { + unsigned special; /*notused */ + + mapglyph(menu_item_ptr->glyph, &curletter, &color, &special, 0, 0); + curses_toggle_color_attr(win, color, NONE, ON); + mvwaddch(win, menu_item_ptr->line_num + 1, start_col, curletter); + curses_toggle_color_attr(win, color, NONE, OFF); + mvwaddch(win, menu_item_ptr->line_num + 1, start_col + 1, ' '); + entry_cols -= 2; + start_col += 2; + } +#endif + if (iflags.use_menu_color && (menu_color = get_menu_coloring + ((char *) menu_item_ptr->str, &color, + &attr))) { + if (color != NO_COLOR) { + curses_toggle_color_attr(win, color, NONE, ON); + } + attr = curses_convert_attr(attr); + if (attr != A_NORMAL) { + menu_item_ptr->attr = menu_item_ptr->attr | attr; + } + } + curses_toggle_color_attr(win, NONE, menu_item_ptr->attr, ON); + + num_lines = curses_num_lines(menu_item_ptr->str, entry_cols); + + for (count = 0; count < num_lines; count++) { + if (strlen(menu_item_ptr->str) > 0) { + mvwprintw(win, menu_item_ptr->line_num + count + 1, + start_col, "%s", curses_break_str(menu_item_ptr->str, + entry_cols, + count + 1)); + } + } + if (menu_color && (color != NO_COLOR)) { + curses_toggle_color_attr(win, color, NONE, OFF); + } + curses_toggle_color_attr(win, NONE, menu_item_ptr->attr, OFF); + menu_item_ptr = menu_item_ptr->next_item; + } + + if (menu->num_pages > 1) { + footer_x = menu->width - strlen("<- (Page X of Y) ->"); + if (menu->num_pages > 9) { /* Unlikely */ + footer_x -= 2; + } + mvwprintw(win, menu->height, footer_x + 3, "(Page %d of %d)", + page_num, menu->num_pages); + if (page_num != 1) { + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON); + mvwaddstr(win, menu->height, footer_x, "<="); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF); + } + if (page_num != menu->num_pages) { + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON); + mvwaddstr(win, menu->height, menu->width - 2, "=>"); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF); + } + } + curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, ON); + box(win, 0, 0); + curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, OFF); + wrefresh(win); +} + + +static int +menu_get_selections(WINDOW * win, nhmenu *menu, int how) +{ + int curletter; + int count = -1; + int count_letter = '\0'; + int curpage = 1; + int num_selected = 0; + boolean dismiss = FALSE; + char search_key[BUFSZ]; + nhmenu_item *menu_item_ptr = menu->entries; + + menu_display_page(menu, win, 1); + + while (!dismiss) { + curletter = getch(); + + if (curletter == ERR) { + num_selected = -1; + dismiss = TRUE; + } + + if (curletter == '\033') { + curletter = curses_convert_keys(curletter); + } + + switch (how) { + case PICK_NONE: + if (menu->num_pages == 1) { + if (curletter == KEY_ESC) { + num_selected = -1; + } else { + num_selected = 0; + + } + dismiss = TRUE; + break; + } + break; + case PICK_ANY: + switch (curletter) { + case MENU_SELECT_PAGE: + (void) menu_operation(win, menu, SELECT, curpage); + break; + case MENU_SELECT_ALL: + curpage = menu_operation(win, menu, SELECT, 0); + break; + case MENU_UNSELECT_PAGE: + (void) menu_operation(win, menu, DESELECT, curpage); + break; + case MENU_UNSELECT_ALL: + curpage = menu_operation(win, menu, DESELECT, 0); + break; + case MENU_INVERT_PAGE: + (void) menu_operation(win, menu, INVERT, curpage); + break; + case MENU_INVERT_ALL: + curpage = menu_operation(win, menu, INVERT, 0); + break; + } + default: + if (isdigit(curletter)) { + count = curses_get_count(curletter - '0'); + touchwin(win); + refresh(); + curletter = getch(); + if (count > 0) { + count_letter = curletter; + } + } + } + + switch (curletter) { + case KEY_ESC: + num_selected = -1; + dismiss = TRUE; + break; + case '\n': + case '\r': + dismiss = TRUE; + break; + case KEY_RIGHT: + case KEY_NPAGE: + case MENU_NEXT_PAGE: + case ' ': + if (curpage < menu->num_pages) { + curpage++; + menu_display_page(menu, win, curpage); + } else if (curletter == ' ') { + dismiss = TRUE; + break; + } + break; + case KEY_LEFT: + case KEY_PPAGE: + case MENU_PREVIOUS_PAGE: + if (curpage > 1) { + curpage--; + menu_display_page(menu, win, curpage); + } + break; + case KEY_END: + case MENU_LAST_PAGE: + if (curpage != menu->num_pages) { + curpage = menu->num_pages; + menu_display_page(menu, win, curpage); + } + break; + case KEY_HOME: + case MENU_FIRST_PAGE: + if (curpage != 1) { + curpage = 1; + menu_display_page(menu, win, curpage); + } + break; + case MENU_SEARCH: + curses_line_input_dialog("Search for:", search_key, BUFSZ); + + refresh(); + touchwin(win); + wrefresh(win); + + if (strlen(search_key) == 0) { + break; + } + + menu_item_ptr = menu->entries; + + while (menu_item_ptr != NULL) { + if ((menu_item_ptr->identifier.a_void != NULL) && + (strstri(menu_item_ptr->str, search_key))) { + if (how == PICK_ONE) { + menu_clear_selections(menu); + menu_select_deselect(win, menu_item_ptr, SELECT); + num_selected = 1; + dismiss = TRUE; + break; + } else { + menu_select_deselect(win, menu_item_ptr, INVERT); + } + } + + menu_item_ptr = menu_item_ptr->next_item; + } + + menu_item_ptr = menu->entries; + break; + default: + if (how == PICK_NONE) { + num_selected = 0; + dismiss = TRUE; + break; + } + } + + menu_item_ptr = menu->entries; + while (menu_item_ptr != NULL) { + if (menu_item_ptr->identifier.a_void != NULL) { + if (((curletter == menu_item_ptr->accelerator) && + ((curpage == menu_item_ptr->page_num) || + (!menu->reuse_accels))) || ((menu_item_ptr->group_accel) + && (curletter == + menu_item_ptr-> + group_accel))) { + if (curpage != menu_item_ptr->page_num) { + curpage = menu_item_ptr->page_num; + menu_display_page(menu, win, curpage); + } + + if (how == PICK_ONE) { + menu_clear_selections(menu); + menu_select_deselect(win, menu_item_ptr, SELECT); + num_selected = 1; + dismiss = TRUE; + break; + } else if ((how == PICK_ANY) && (curletter == count_letter)) { + menu_select_deselect(win, menu_item_ptr, SELECT); + menu_item_ptr->count = count; + count = 0; + count_letter = '\0'; + } else { + menu_select_deselect(win, menu_item_ptr, INVERT); + } + } + } + menu_item_ptr = menu_item_ptr->next_item; + } + } + + if ((how == PICK_ANY) && (num_selected != -1)) { + num_selected = 0; + menu_item_ptr = menu->entries; + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->identifier.a_void != NULL) { + if (menu_item_ptr->selected) { + num_selected++; + } + } + menu_item_ptr = menu_item_ptr->next_item; + } + } + + return num_selected; +} + + +/* Select, deselect, or toggle selected for the given menu entry */ + +static void +menu_select_deselect(WINDOW * win, nhmenu_item *item, menu_op operation) +{ + int curletter = item->accelerator; + + if ((operation == DESELECT) || (item->selected && (operation == INVERT))) { + item->selected = FALSE; + mvwaddch(win, item->line_num + 1, 1, ' '); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, ON); + mvwaddch(win, item->line_num + 1, 2, curletter); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, NONE, OFF); + mvwaddch(win, item->line_num + 1, 3, ')'); + } else { + item->selected = TRUE; + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, ON); + mvwaddch(win, item->line_num + 1, 1, '<'); + mvwaddch(win, item->line_num + 1, 2, curletter); + mvwaddch(win, item->line_num + 1, 3, '>'); + curses_toggle_color_attr(win, HIGHLIGHT_COLOR, A_REVERSE, OFF); + } + + wrefresh(win); +} + + +/* Perform the selected operation (select, unselect, invert selection) +on the given menu page. If menu_page is 0, then perform opetation on +all pages in menu. Returns last page displayed. */ + +static int +menu_operation(WINDOW * win, nhmenu *menu, menu_op + operation, int page_num) +{ + int first_page, last_page, current_page; + nhmenu_item *menu_item_ptr = menu->entries; + + if (page_num == 0) { /* Operation to occur on all pages */ + first_page = 1; + last_page = menu->num_pages; + } else { + first_page = page_num; + last_page = page_num; + } + + /* Cycle through entries until we are on the correct page */ + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->page_num == first_page) { + break; + } + menu_item_ptr = menu_item_ptr->next_item; + } + + current_page = first_page; + + if (page_num == 0) { + menu_display_page(menu, win, current_page); + } + + if (menu_item_ptr == NULL) { /* Page not found */ + panic("menu_display_page: attempt to display nonexistant page"); + } + + while (menu_item_ptr != NULL) { + if (menu_item_ptr->page_num != current_page) { + if (menu_item_ptr->page_num > last_page) { + break; + } + + current_page = menu_item_ptr->page_num; + menu_display_page(menu, win, current_page); + } + + if (menu_item_ptr->identifier.a_void != NULL) { + menu_select_deselect(win, menu_item_ptr, operation); + } + + menu_item_ptr = menu_item_ptr->next_item; + } + + return current_page; +} + + +/* Set all menu items to unselected in menu */ + +static void +menu_clear_selections(nhmenu *menu) +{ + nhmenu_item *menu_item_ptr = menu->entries; + + while (menu_item_ptr != NULL) { + menu_item_ptr->selected = FALSE; + menu_item_ptr = menu_item_ptr->next_item; + } +} + + +/* Get the maximum height for a menu */ + +static int +menu_max_height(void) +{ + return term_rows - 2; +} diff --git a/win/curses/cursdial.h b/win/curses/cursdial.h new file mode 100644 index 000000000..bf3f68d9e --- /dev/null +++ b/win/curses/cursdial.h @@ -0,0 +1,23 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSDIAL_H +# define CURSDIAL_H + +/* Global declarations */ + +void curses_line_input_dialog(const char *prompt, char *answer, int buffer); +int curses_character_input_dialog(const char *prompt, const char *choices, + CHAR_P def); +int curses_ext_cmd(void); +void curses_create_nhmenu(winid wid); +void curses_add_nhmenu_item(winid wid, int glyph, const ANY_P * identifier, + CHAR_P accelerator, CHAR_P group_accel, int attr, + const char *str, BOOLEAN_P presel); +void curses_finalize_nhmenu(winid wid, const char *prompt); +int curses_display_nhmenu(winid wid, int how, MENU_ITEM_P ** _selected); +boolean curses_menu_exists(winid wid); +void curses_del_menu(winid wid); + + + +#endif /* CURSDIAL_H */ diff --git a/win/curses/cursinit.c b/win/curses/cursinit.c new file mode 100644 index 000000000..3e87693a7 --- /dev/null +++ b/win/curses/cursinit.c @@ -0,0 +1,923 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursinit.h" +#include "patchlevel.h" + +#include + +/* Initialization and startup functions for curses interface */ + +/* Private declarations */ + +static void set_window_position(int *, int *, int *, int *, int, + int *, int *, int *, int *, int, + int, int); + +/* array to save initial terminal colors for later restoration */ + +typedef struct nhrgb_type { + short r; + short g; + short b; +} nhrgb; + +nhrgb orig_yellow; +nhrgb orig_white; +nhrgb orig_darkgray; +nhrgb orig_hired; +nhrgb orig_higreen; +nhrgb orig_hiyellow; +nhrgb orig_hiblue; +nhrgb orig_himagenta; +nhrgb orig_hicyan; +nhrgb orig_hiwhite; + +/* Banners used for an optional ASCII splash screen */ + +#define NETHACK_SPLASH_A \ +" _ _ _ _ _ _ " + +#define NETHACK_SPLASH_B \ +"| \\ | | | | | | | | | | " + +#define NETHACK_SPLASH_C \ +"| \\| | ___ | |_ | |__| | __ _ ___ | | __" + +#define NETHACK_SPLASH_D \ +"| . ` | / _ \\| __|| __ | / _` | / __|| |/ /" + +#define NETHACK_SPLASH_E \ +"| |\\ || __/| |_ | | | || (_| || (__ | < " + +#define NETHACK_SPLASH_F \ +"|_| \\_| \\___| \\__||_| |_| \\__,_| \\___||_|\\_\\" + + +/* win* is size and placement of window to change, x/y/w/h is baseline which can + decrease depending on alignment of win* in orientation. + Negative minh/minw: as much as possible, but at least as much as specified. */ +static void +set_window_position(int *winx, int *winy, int *winw, int *winh, int orientation, + int *x, int *y, int *w, int *h, int border_space, + int minh, int minw) +{ + *winw = *w; + *winh = *h; + + /* Set window height/width */ + if (orientation == ALIGN_TOP || orientation == ALIGN_BOTTOM) { + if (minh < 0) { + *winh = (*h - ROWNO - border_space); + if (-minh > *winh) + *winh = -minh; + } else + *winh = minh; + *h -= (*winh + border_space); + } else { + if (minw < 0) { + *winw = (*w - COLNO - border_space); + if (-minw > *winw) + *winw = -minw; + } else + *winw = minw; + *w -= (*winw + border_space); + } + + *winx = *w + border_space + *x; + *winy = *h + border_space + *y; + + /* Set window position */ + if (orientation != ALIGN_RIGHT) { + *winx = *x; + if (orientation == ALIGN_LEFT) + *x += *winw + border_space; + } + if (orientation != ALIGN_BOTTOM) { + *winy = *y; + if (orientation == ALIGN_TOP) + *y += *winh + border_space; + } +} + +/* Create the "main" nonvolitile windows used by nethack */ + +void +curses_create_main_windows() +{ + int min_message_height = 1; + int message_orientation = 0; + int status_orientation = 0; + int border_space = 0; + int hspace = term_cols - 80; + boolean borders = FALSE; + + switch (iflags.wc2_windowborders) { + case 1: /* On */ + borders = TRUE; + break; + case 2: /* Off */ + borders = FALSE; + break; + case 3: /* Auto */ + if ((term_cols > 81) && (term_rows > 25)) { + borders = TRUE; + } + break; + default: + borders = FALSE; + } + + + if (borders) { + border_space = 2; + hspace -= border_space; + } + + if ((term_cols - border_space) < COLNO) { + min_message_height++; + } + + /* Determine status window orientation */ + if (!iflags.wc_align_status || (iflags.wc_align_status == ALIGN_TOP) + || (iflags.wc_align_status == ALIGN_BOTTOM)) { + if (!iflags.wc_align_status) { + iflags.wc_align_status = ALIGN_BOTTOM; + } + status_orientation = iflags.wc_align_status; + } else { /* left or right alignment */ + + /* Max space for player name and title horizontally */ + if ((hspace >= 26) && (term_rows >= 24)) { + status_orientation = iflags.wc_align_status; + hspace -= (26 + border_space); + } else { + status_orientation = ALIGN_BOTTOM; + } + } + + /* Determine message window orientation */ + if (!iflags.wc_align_message || (iflags.wc_align_message == ALIGN_TOP) + || (iflags.wc_align_message == ALIGN_BOTTOM)) { + if (!iflags.wc_align_message) { + iflags.wc_align_message = ALIGN_TOP; + } + message_orientation = iflags.wc_align_message; + } else { /* left or right alignment */ + + if ((hspace - border_space) >= 25) { /* Arbitrary */ + message_orientation = iflags.wc_align_message; + } else { + message_orientation = ALIGN_TOP; + } + } + + /* Figure out window positions and placements. Status and message area can be aligned + based on configuration. The priority alignment-wise is: status > msgarea > game. + Define everything as taking as much space as possible and shrink/move based on + alignment positions. */ + int message_x = 0; + int message_y = 0; + int status_x = 0; + int status_y = 0; + int inv_x = 0; + int inv_y = 0; + int map_x = 0; + int map_y = 0; + + int message_height = 0; + int message_width = 0; + int status_height = 0; + int status_width = 0; + int inv_height = 0; + int inv_width = 0; + int map_height = (term_rows - border_space); + int map_width = (term_cols - border_space); + + boolean status_vertical = FALSE; + boolean msg_vertical = FALSE; + if (status_orientation == ALIGN_LEFT || + status_orientation == ALIGN_RIGHT) + status_vertical = TRUE; + if (message_orientation == ALIGN_LEFT || + message_orientation == ALIGN_RIGHT) + msg_vertical = TRUE; + + int statusheight = 3; + if (iflags.statuslines < 3) + statusheight = 2; + + /* Vertical windows have priority. Otherwise, priotity is: + status > inv > msg */ + if (status_vertical) + set_window_position(&status_x, &status_y, &status_width, &status_height, + status_orientation, &map_x, &map_y, &map_width, &map_height, + border_space, statusheight, 26); + + if (iflags.perm_invent) { + /* Take up all width unless msgbar is also vertical. */ + int width = -25; + if (msg_vertical) + width = 25; + + set_window_position(&inv_x, &inv_y, &inv_width, &inv_height, + ALIGN_RIGHT, &map_x, &map_y, &map_width, &map_height, + border_space, -1, width); + } + + if (msg_vertical) + set_window_position(&message_x, &message_y, &message_width, &message_height, + message_orientation, &map_x, &map_y, &map_width, &map_height, + border_space, -1, -25); + + /* Now draw horizontal windows */ + if (!status_vertical) + set_window_position(&status_x, &status_y, &status_width, &status_height, + status_orientation, &map_x, &map_y, &map_width, &map_height, + border_space, statusheight, 26); + + if (!msg_vertical) + set_window_position(&message_x, &message_y, &message_width, &message_height, + message_orientation, &map_x, &map_y, &map_width, &map_height, + border_space, -1, -25); + + if (map_width > COLNO) + map_width = COLNO; + + if (map_height > ROWNO) + map_height = ROWNO; + + if (curses_get_nhwin(STATUS_WIN)) { + curses_del_nhwin(STATUS_WIN); + curses_del_nhwin(MESSAGE_WIN); + curses_del_nhwin(MAP_WIN); + curses_del_nhwin(INV_WIN); + + clear(); + } + + curses_add_nhwin(STATUS_WIN, status_height, status_width, status_y, + status_x, status_orientation, borders); + + curses_add_nhwin(MESSAGE_WIN, message_height, message_width, message_y, + message_x, message_orientation, borders); + + if (iflags.perm_invent) + curses_add_nhwin(INV_WIN, inv_height, inv_width, inv_y, inv_x, + ALIGN_RIGHT, borders); + + curses_add_nhwin(MAP_WIN, map_height, map_width, map_y, map_x, 0, borders); + + refresh(); + + curses_refresh_nethack_windows(); +/* + if (iflags.window_inited) { + curses_update_stats(); + if (iflags.perm_invent) + curses_update_inventory(); + } else { + iflags.window_inited = TRUE; + } +*/ +} + + +/* Initialize curses colors to colors used by NetHack */ + +void +curses_init_nhcolors() +{ +#ifdef TEXTCOLOR + if (has_colors()) { + use_default_colors(); + init_pair(1, COLOR_BLACK, -1); + init_pair(2, COLOR_RED, -1); + init_pair(3, COLOR_GREEN, -1); + init_pair(4, COLOR_YELLOW, -1); + init_pair(5, COLOR_BLUE, -1); + init_pair(6, COLOR_MAGENTA, -1); + init_pair(7, COLOR_CYAN, -1); + init_pair(8, -1, -1); + + { + int i; + + int clr_remap[16] = { + COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, + COLOR_BLUE, + COLOR_MAGENTA, COLOR_CYAN, -1, COLOR_WHITE, + COLOR_RED + 8, COLOR_GREEN + 8, COLOR_YELLOW + 8, + COLOR_BLUE + 8, + COLOR_MAGENTA + 8, COLOR_CYAN + 8, COLOR_WHITE + 8 + }; + + for (i = 0; i < (COLORS >= 16 ? 16 : 8); i++) { + init_pair(17 + (i * 2) + 0, clr_remap[i], COLOR_RED); + init_pair(17 + (i * 2) + 1, clr_remap[i], COLOR_BLUE); + } + + boolean hicolor = FALSE; + if (COLORS >= 16) + hicolor = TRUE; + + /* Work around the crazy definitions above for more background colors... */ + for (i = 0; i < (COLORS >= 16 ? 16 : 8); i++) { + init_pair((hicolor ? 49 : 9) + i, clr_remap[i], COLOR_GREEN); + init_pair((hicolor ? 65 : 33) + i, clr_remap[i], COLOR_YELLOW); + init_pair((hicolor ? 81 : 41) + i, clr_remap[i], COLOR_MAGENTA); + init_pair((hicolor ? 97 : 49) + i, clr_remap[i], COLOR_CYAN); + init_pair((hicolor ? 113 : 57) + i, clr_remap[i], COLOR_WHITE); + } + } + + + if (COLORS >= 16) { + init_pair(9, COLOR_WHITE, -1); + init_pair(10, COLOR_RED + 8, -1); + init_pair(11, COLOR_GREEN + 8, -1); + init_pair(12, COLOR_YELLOW + 8, -1); + init_pair(13, COLOR_BLUE + 8, -1); + init_pair(14, COLOR_MAGENTA + 8, -1); + init_pair(15, COLOR_CYAN + 8, -1); + init_pair(16, COLOR_WHITE + 8, -1); + } + + if (can_change_color()) { + /* Preserve initial terminal colors */ + color_content(COLOR_YELLOW, &orig_yellow.r, &orig_yellow.g, + &orig_yellow.b); + color_content(COLOR_WHITE, &orig_white.r, &orig_white.g, + &orig_white.b); + + /* Set colors to appear as NetHack expects */ + init_color(COLOR_YELLOW, 500, 300, 0); + init_color(COLOR_WHITE, 600, 600, 600); + if (COLORS >= 16) { + /* Preserve initial terminal colors */ + color_content(COLOR_RED + 8, &orig_hired.r, + &orig_hired.g, &orig_hired.b); + color_content(COLOR_GREEN + 8, &orig_higreen.r, + &orig_higreen.g, &orig_higreen.b); + color_content(COLOR_YELLOW + 8, &orig_hiyellow.r, + &orig_hiyellow.g, &orig_hiyellow.b); + color_content(COLOR_BLUE + 8, &orig_hiblue.r, + &orig_hiblue.g, &orig_hiblue.b); + color_content(COLOR_MAGENTA + 8, &orig_himagenta.r, + &orig_himagenta.g, &orig_himagenta.b); + color_content(COLOR_CYAN + 8, &orig_hicyan.r, + &orig_hicyan.g, &orig_hicyan.b); + color_content(COLOR_WHITE + 8, &orig_hiwhite.r, + &orig_hiwhite.g, &orig_hiwhite.b); + + /* Set colors to appear as NetHack expects */ + init_color(COLOR_RED + 8, 1000, 500, 0); + init_color(COLOR_GREEN + 8, 0, 1000, 0); + init_color(COLOR_YELLOW + 8, 1000, 1000, 0); + init_color(COLOR_BLUE + 8, 0, 0, 1000); + init_color(COLOR_MAGENTA + 8, 1000, 0, 1000); + init_color(COLOR_CYAN + 8, 0, 1000, 1000); + init_color(COLOR_WHITE + 8, 1000, 1000, 1000); +# ifdef USE_DARKGRAY + if (COLORS > 16) { + color_content(CURSES_DARK_GRAY, &orig_darkgray.r, + &orig_darkgray.g, &orig_darkgray.b); + init_color(CURSES_DARK_GRAY, 300, 300, 300); + /* just override black colorpair entry here */ + init_pair(1, CURSES_DARK_GRAY, -1); + } +# endif + } else { + /* Set flag to use bold for bright colors */ + } + } + } +#endif +} + + +/* Allow player to pick character's role, race, gender, and alignment. +Borrowed from the Gnome window port. */ + +void +curses_choose_character() +{ + int n, i, sel, count_off, pick4u; + int count = 0; + int cur_character = 0; + const char **choices; + int *pickmap; + char *prompt; + char pbuf[QBUFSZ]; + char choice[QBUFSZ]; + char tmpchoice[QBUFSZ]; + + prompt = build_plselection_prompt(pbuf, QBUFSZ, flags.initrole, + flags.initrace, flags.initgend, + flags.initalign); + + /* This part is irritating: we have to strip the choices off of + the string and put them in a separate string in order to use + curses_character_input_dialog for this prompt. */ + + while (cur_character != '[') { + cur_character = prompt[count]; + count++; + } + + count_off = count; + + while (cur_character != ']') { + tmpchoice[count - count_off] = prompt[count]; + count++; + cur_character = prompt[count]; + } + + tmpchoice[count - count_off] = '\0'; + lcase(tmpchoice); + + while (!isspace(prompt[count_off])) { + count_off--; + } + + prompt[count_off] = '\0'; + sprintf(choice, "%s%c", tmpchoice, '\033'); + if (strchr(tmpchoice, 't')) { /* Tutorial mode */ + mvaddstr(0, 1, "New? Press t to enter a tutorial."); + } + + /* Add capital letters as choices that aren't displayed */ + + for (count = 0; tmpchoice[count]; count++) { + tmpchoice[count] = toupper(tmpchoice[count]); + } + + sprintf(choice, "%s%s", choice, tmpchoice); + + /* prevent an unnecessary prompt */ + rigid_role_checks(); + + if (!flags.randomall && + (flags.initrole == ROLE_NONE || flags.initrace == ROLE_NONE || + flags.initgend == ROLE_NONE || flags.initalign == ROLE_NONE)) { + pick4u = tolower(curses_character_input_dialog(prompt, choice, 'y')); + } else { + pick4u = 'y'; + } + + if (pick4u == 'q') { /* Quit or cancelled */ + clearlocks(); + curses_bail(0); + } + + if (pick4u == 'y') { + flags.randomall = TRUE; + } + + clear(); + refresh(); + + if (!flags.randomall && flags.initrole < 0) { + /* select a role */ + for (n = 0; roles[n].name.m; n++) + continue; + choices = (const char **) alloc(sizeof (char *) * (n + 1)); + pickmap = (int *) alloc(sizeof (int) * (n + 1)); + for (;;) { + for (n = 0, i = 0; roles[i].name.m; i++) { + if (ok_role(i, flags.initrace, flags.initgend, flags.initalign)) { + if (flags.initgend >= 0 && flags.female && roles[i].name.f) + choices[n] = roles[i].name.f; + else + choices[n] = roles[i].name.m; + pickmap[n++] = i; + } + } + if (n > 0) + break; + else if (flags.initalign >= 0) + flags.initalign = -1; /* reset */ + else if (flags.initgend >= 0) + flags.initgend = -1; + else if (flags.initrace >= 0) + flags.initrace = -1; + else + panic("no available ROLE+race+gender+alignment combinations"); + } + choices[n] = (const char *) 0; + if (n > 1) + sel = + curses_character_dialog(choices, + "Choose one of the following roles:"); + else + sel = 0; + if (sel >= 0) + sel = pickmap[sel]; + else if (sel == ROLE_NONE) { /* Quit */ + clearlocks(); + curses_bail(0); + } + free((genericptr_t) choices); + free((genericptr_t) pickmap); + } else if (flags.initrole < 0) + sel = ROLE_RANDOM; + else + sel = flags.initrole; + + if (sel == ROLE_RANDOM) { /* Random role */ + sel = pick_role(flags.initrace, flags.initgend, + flags.initalign, PICK_RANDOM); + if (sel < 0) + sel = randrole(); + } + + flags.initrole = sel; + + /* Select a race, if necessary */ + /* force compatibility with role, try for compatibility with + * pre-selected gender/alignment */ + if (flags.initrace < 0 || !validrace(flags.initrole, flags.initrace)) { + if (flags.initrace == ROLE_RANDOM || flags.randomall) { + flags.initrace = pick_race(flags.initrole, flags.initgend, + flags.initalign, PICK_RANDOM); + if (flags.initrace < 0) + flags.initrace = randrace(flags.initrole); + } else { + /* Count the number of valid races */ + n = 0; /* number valid */ + for (i = 0; races[i].noun; i++) { + if (ok_race(flags.initrole, i, flags.initgend, flags.initalign)) + n++; + } + if (n == 0) { + for (i = 0; races[i].noun; i++) { + if (validrace(flags.initrole, i)) + n++; + } + } + + choices = (const char **) alloc(sizeof (char *) * (n + 1)); + pickmap = (int *) alloc(sizeof (int) * (n + 1)); + for (n = 0, i = 0; races[i].noun; i++) { + if (ok_race(flags.initrole, i, flags.initgend, flags.initalign)) { + choices[n] = races[i].noun; + pickmap[n++] = i; + } + } + choices[n] = (const char *) 0; + /* Permit the user to pick, if there is more than one */ + if (n > 1) + sel = + curses_character_dialog(choices, + "Choose one of the following races:"); + else + sel = 0; + if (sel >= 0) + sel = pickmap[sel]; + else if (sel == ROLE_NONE) { /* Quit */ + clearlocks(); + curses_bail(0); + } + flags.initrace = sel; + free((genericptr_t) choices); + free((genericptr_t) pickmap); + } + if (flags.initrace == ROLE_RANDOM) { /* Random role */ + sel = pick_race(flags.initrole, flags.initgend, + flags.initalign, PICK_RANDOM); + if (sel < 0) + sel = randrace(flags.initrole); + flags.initrace = sel; + } + } + + /* Select a gender, if necessary */ + /* force compatibility with role/race, try for compatibility with + * pre-selected alignment */ + if (flags.initgend < 0 || + !validgend(flags.initrole, flags.initrace, flags.initgend)) { + if (flags.initgend == ROLE_RANDOM || flags.randomall) { + flags.initgend = pick_gend(flags.initrole, flags.initrace, + flags.initalign, PICK_RANDOM); + if (flags.initgend < 0) + flags.initgend = randgend(flags.initrole, flags.initrace); + } else { + /* Count the number of valid genders */ + n = 0; /* number valid */ + for (i = 0; i < ROLE_GENDERS; i++) { + if (ok_gend(flags.initrole, flags.initrace, i, flags.initalign)) + n++; + } + if (n == 0) { + for (i = 0; i < ROLE_GENDERS; i++) { + if (validgend(flags.initrole, flags.initrace, i)) + n++; + } + } + + choices = (const char **) alloc(sizeof (char *) * (n + 1)); + pickmap = (int *) alloc(sizeof (int) * (n + 1)); + for (n = 0, i = 0; i < ROLE_GENDERS; i++) { + if (ok_gend(flags.initrole, flags.initrace, i, flags.initalign)) { + choices[n] = genders[i].adj; + pickmap[n++] = i; + } + } + choices[n] = (const char *) 0; + /* Permit the user to pick, if there is more than one */ + if (n > 1) + sel = + curses_character_dialog(choices, + "Choose one of the following genders:"); + else + sel = 0; + if (sel >= 0) + sel = pickmap[sel]; + else if (sel == ROLE_NONE) { /* Quit */ + clearlocks(); + curses_bail(0); + } + flags.initgend = sel; + free((genericptr_t) choices); + free((genericptr_t) pickmap); + } + if (flags.initgend == ROLE_RANDOM) { /* Random gender */ + sel = pick_gend(flags.initrole, flags.initrace, + flags.initalign, PICK_RANDOM); + if (sel < 0) + sel = randgend(flags.initrole, flags.initrace); + flags.initgend = sel; + } + } + + /* Select an alignment, if necessary */ + /* force compatibility with role/race/gender */ + if (flags.initalign < 0 || + !validalign(flags.initrole, flags.initrace, flags.initalign)) { + if (flags.initalign == ROLE_RANDOM || flags.randomall) { + flags.initalign = pick_align(flags.initrole, flags.initrace, + flags.initgend, PICK_RANDOM); + if (flags.initalign < 0) + flags.initalign = randalign(flags.initrole, flags.initrace); + } else { + /* Count the number of valid alignments */ + n = 0; /* number valid */ + for (i = 0; i < ROLE_ALIGNS; i++) { + if (ok_align(flags.initrole, flags.initrace, flags.initgend, i)) + n++; + } + if (n == 0) { + for (i = 0; i < ROLE_ALIGNS; i++) + if (validalign(flags.initrole, flags.initrace, i)) + n++; + } + + choices = (const char **) alloc(sizeof (char *) * (n + 1)); + pickmap = (int *) alloc(sizeof (int) * (n + 1)); + for (n = 0, i = 0; i < ROLE_ALIGNS; i++) { + if (ok_align(flags.initrole, flags.initrace, flags.initgend, i)) { + choices[n] = aligns[i].adj; + pickmap[n++] = i; + } + } + choices[n] = (const char *) 0; + /* Permit the user to pick, if there is more than one */ + if (n > 1) + sel = + curses_character_dialog(choices, + "Choose one of the following alignments:"); + else + sel = 0; + if (sel >= 0) + sel = pickmap[sel]; + else if (sel == ROLE_NONE) { /* Quit */ + clearlocks(); + curses_bail(0); + } + flags.initalign = sel; + free((genericptr_t) choices); + free((genericptr_t) pickmap); + } + if (flags.initalign == ROLE_RANDOM) { + sel = pick_align(flags.initrole, flags.initrace, + flags.initgend, PICK_RANDOM); + if (sel < 0) + sel = randalign(flags.initrole, flags.initrace); + flags.initalign = sel; + } + } +} + + +/* Prompt user for character race, role, alignment, or gender */ + +int +curses_character_dialog(const char **choices, const char *prompt) +{ + int count, count2, ret, curletter; + char used_letters[52]; + anything identifier; + menu_item *selected = NULL; + winid wid = curses_get_wid(NHW_MENU); + + identifier.a_void = 0; + curses_start_menu(wid); + + for (count = 0; choices[count]; count++) { + curletter = tolower(choices[count][0]); + for (count2 = 0; count2 < count; count2++) { + if (curletter == used_letters[count2]) { + curletter = toupper(curletter); + } + } + + identifier.a_int = (count + 1); /* Must be non-zero */ + curses_add_menu(wid, NO_GLYPH, &identifier, curletter, 0, + A_NORMAL, choices[count], FALSE); + used_letters[count] = curletter; + } + + /* Random Selection */ + identifier.a_int = ROLE_RANDOM; + curses_add_menu(wid, NO_GLYPH, &identifier, '*', 0, A_NORMAL, "Random", + FALSE); + + /* Quit prompt */ + identifier.a_int = ROLE_NONE; + curses_add_menu(wid, NO_GLYPH, &identifier, 'q', 0, A_NORMAL, "Quit", + FALSE); + curses_end_menu(wid, prompt); + ret = curses_select_menu(wid, PICK_ONE, &selected); + if (ret == 1) { + ret = (selected->item.a_int); + } else { /* Cancelled selection */ + + ret = ROLE_NONE; + } + + if (ret > 0) { + ret--; + } + + free((genericptr_t) selected); + return ret; +} + + +/* Initialize and display options appropriately */ + +void +curses_init_options() +{ + set_wc_option_mod_status(WC_ALIGN_MESSAGE | WC_ALIGN_STATUS | WC_COLOR | + WC_HILITE_PET | WC_POPUP_DIALOG, SET_IN_GAME); + + set_wc2_option_mod_status(WC2_GUICOLOR, SET_IN_GAME); + + /* Remove a few options that are irrelevant to this windowport */ + /*set_option_mod_status("DECgraphics", SET_IN_FILE); */ + set_option_mod_status("eight_bit_tty", SET_IN_FILE); + + /* Add those that are */ + set_option_mod_status("statuslines", SET_IN_GAME); + + /* Make sure that DECgraphics is not set to true via the config + file, as this will cause display issues. We can't disable it in + options.c in case the game is compiled with both tty and curses. */ + if (!symset[PRIMARY].name || !strcmpi(symset[PRIMARY].name, "DECgraphics")) { + load_symset("curses",PRIMARY); + load_symset("default",ROGUESET); + } +#ifdef PDCURSES + /* PDCurses for SDL, win32 and OS/2 has the ability to set the + terminal size programatically. If the user does not specify a + size in the config file, we will set it to a nice big 110x32 to + take advantage of some of the nice features of this windowport. */ + if (iflags.wc2_term_cols == 0) { + iflags.wc2_term_cols = 110; + } + + if (iflags.wc2_term_rows == 0) { + iflags.wc2_term_rows = 32; + } + + resize_term(iflags.wc2_term_rows, iflags.wc2_term_cols); + getmaxyx(base_term, term_rows, term_cols); + + /* This is needed for an odd bug with PDCurses-SDL */ +/* How to deal with this? + switch_graphics(ASCII_GRAPHICS); + if (iflags.IBMgraphics) { + switch_graphics(IBM_GRAPHICS); + } else if (iflags.cursesgraphics) { + switch_graphics(CURS_GRAPHICS); + } else { + switch_graphics(ASCII_GRAPHICS); + } +*/ +#endif /* PDCURSES */ + if (!iflags.wc2_windowborders) { + iflags.wc2_windowborders = 3; /* Set to auto if not specified */ + } + + if (!iflags.wc2_petattr) { + iflags.wc2_petattr = A_REVERSE; + } else { /* Pet attribute specified, so hilite_pet should be true */ + + iflags.hilite_pet = TRUE; + } + +#ifdef NCURSES_MOUSE_VERSION + if (iflags.wc_mouse_support) { + mousemask(BUTTON1_CLICKED, NULL); + } +#endif +} + + +/* Display an ASCII splash screen if the splash_screen option is set */ + +void +curses_display_splash_window() +{ + int x_start; + int y_start; + + curses_get_window_xy(MAP_WIN, &x_start, &y_start); + + if ((term_cols < 70) || (term_rows < 20)) { + iflags.wc_splash_screen = FALSE; /* No room for s.s. */ + } + + curses_toggle_color_attr(stdscr, CLR_WHITE, A_NORMAL, ON); + if (iflags.wc_splash_screen) { + mvaddstr(y_start, x_start, NETHACK_SPLASH_A); + mvaddstr(y_start + 1, x_start, NETHACK_SPLASH_B); + mvaddstr(y_start + 2, x_start, NETHACK_SPLASH_C); + mvaddstr(y_start + 3, x_start, NETHACK_SPLASH_D); + mvaddstr(y_start + 4, x_start, NETHACK_SPLASH_E); + mvaddstr(y_start + 5, x_start, NETHACK_SPLASH_F); + y_start += 7; + } + + curses_toggle_color_attr(stdscr, CLR_WHITE, A_NORMAL, OFF); + +#ifdef COPYRIGHT_BANNER_A + mvaddstr(y_start, x_start, COPYRIGHT_BANNER_A); + y_start++; +#endif + +#ifdef COPYRIGHT_BANNER_B + mvaddstr(y_start, x_start, COPYRIGHT_BANNER_B); + y_start++; +#endif + +#ifdef COPYRIGHT_BANNER_C + mvaddstr(y_start, x_start, COPYRIGHT_BANNER_C); + y_start++; +#endif + +#ifdef COPYRIGHT_BANNER_D /* Just in case */ + mvaddstr(y_start, x_start, COPYRIGHT_BANNER_D); + y_start++; +#endif + refresh(); +} + + +/* Resore colors and cursor state before exiting */ + +void +curses_cleanup() +{ +#ifdef TEXTCOLOR + if (has_colors() && can_change_color()) { + init_color(COLOR_YELLOW, orig_yellow.r, orig_yellow.g, orig_yellow.b); + init_color(COLOR_WHITE, orig_white.r, orig_white.g, orig_white.b); + + if (COLORS >= 16) { + init_color(COLOR_RED + 8, orig_hired.r, orig_hired.g, orig_hired.b); + init_color(COLOR_GREEN + 8, orig_higreen.r, orig_higreen.g, + orig_higreen.b); + init_color(COLOR_YELLOW + 8, orig_hiyellow.r, + orig_hiyellow.g, orig_hiyellow.b); + init_color(COLOR_BLUE + 8, orig_hiblue.r, orig_hiblue.g, + orig_hiblue.b); + init_color(COLOR_MAGENTA + 8, orig_himagenta.r, + orig_himagenta.g, orig_himagenta.b); + init_color(COLOR_CYAN + 8, orig_hicyan.r, orig_hicyan.g, + orig_hicyan.b); + init_color(COLOR_WHITE + 8, orig_hiwhite.r, orig_hiwhite.g, + orig_hiwhite.b); +# ifdef USE_DARKGRAY + if (COLORS > 16) { + init_color(CURSES_DARK_GRAY, orig_darkgray.r, + orig_darkgray.g, orig_darkgray.b); + } +# endif + } + } +#endif +} diff --git a/win/curses/cursinit.h b/win/curses/cursinit.h new file mode 100644 index 000000000..c7f390047 --- /dev/null +++ b/win/curses/cursinit.h @@ -0,0 +1,17 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSINIT_H +# define CURSINIT_H + +/* Global declarations */ + +void curses_create_main_windows(void); +void curses_init_nhcolors(void); +void curses_choose_character(void); +int curses_character_dialog(const char **choices, const char *prompt); +void curses_init_options(void); +void curses_display_splash_window(void); +void curses_cleanup(void); + + +#endif /* CURSINIT_H */ diff --git a/win/curses/cursinvt.c b/win/curses/cursinvt.c new file mode 100644 index 000000000..f5a362d7d --- /dev/null +++ b/win/curses/cursinvt.c @@ -0,0 +1,111 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursinvt.h" + +/* Permanent inventory for curses interface */ + +/* Runs when the game indicates that the inventory has been updated */ +void +curses_update_inv(void) +{ + WINDOW *win = curses_get_nhwin(INV_WIN); + + /* Check if the inventory window is enabled in first place */ + if (!win) { + /* It's not. Re-initialize the main windows if the + option was enabled. */ + if (iflags.perm_invent) { + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + } + return; + } + + boolean border = curses_window_has_border(INV_WIN); + + /* Figure out drawing area */ + int x = 0; + int y = 0; + if (border) { + x++; + y++; + } + + /* Clear the window as it is at the moment. */ + werase(win); + + wmove(win, y, x); + attr_t attr = A_UNDERLINE; + wattron(win, attr); + wprintw(win, "Inventory:"); + wattroff(win, attr); + + /* The actual inventory will override this if we do carry stuff */ + wmove(win, y + 1, x); + wprintw(win, "Not carrying anything"); + + display_inventory(NULL, FALSE); + + if (border) + box(win, 0, 0); + + wnoutrefresh(win); +} + +/* Adds an inventory item. */ +void +curses_add_inv(int y, int glyph, CHAR_P accelerator, attr_t attr, + const char *str) +{ + WINDOW *win = curses_get_nhwin(INV_WIN); + + /* Figure out where to draw the line */ + int x = 0; + if (curses_window_has_border(INV_WIN)) { + x++; + y++; + } + + wmove(win, y, x); + if (accelerator) { + attr_t bold = A_BOLD; + wattron(win, bold); + waddch(win, accelerator); + wattroff(win, bold); + wprintw(win, ") "); + } +#if 0 // FIXME: MENU GLYPHS + if (accelerator && glyph != NO_GLYPH && iflags.use_menu_glyphs) { + unsigned dummy = 0; /* Not used */ + int color = 0; + int symbol = 0; + mapglyph(glyph, &symbol, &color, &dummy, + u.ux, u.uy); + attr_t glyphclr = curses_color_attr(color, 0); + wattron(win, glyphclr); + wprintw(win, "%c ", symbol); + wattroff(win, glyphclr); + } +#endif + int color = NO_COLOR; + if (accelerator && /* Don't colorize categories */ + iflags.use_menu_color) { + boolean menu_color = FALSE; + char str_mutable[BUFSZ]; + Strcpy(str_mutable, str); + attr = 0; + get_menu_coloring(str_mutable, &color, &attr); + attr = curses_convert_attr(attr); + } + if (color == NO_COLOR) color = NONE; + curses_toggle_color_attr(win, color, attr, ON); + //wattron(win, attr); + wprintw(win, "%s", str); + //wattroff(win, attr); + curses_toggle_color_attr(win, color, attr, OFF); + wclrtoeol(win); +} diff --git a/win/curses/cursinvt.h b/win/curses/cursinvt.h new file mode 100644 index 000000000..99c90d277 --- /dev/null +++ b/win/curses/cursinvt.h @@ -0,0 +1,11 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSINVT_H +# define CURSINVT_H + + +/* Global declarations */ + +void curses_update_inv(void); + +#endif /* CURSINVT_H */ diff --git a/win/curses/cursmain.c b/win/curses/cursmain.c new file mode 100644 index 000000000..a7addedea --- /dev/null +++ b/win/curses/cursmain.c @@ -0,0 +1,820 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "patchlevel.h" +#include "color.h" +#include "wincurs.h" + +/* Public functions for curses NetHack interface */ + +/* Interface definition, for windows.c */ +struct window_procs curses_procs = { + "curses", + WC_ALIGN_MESSAGE | WC_ALIGN_STATUS | WC_COLOR | WC_HILITE_PET | + WC_PERM_INVENT | WC_POPUP_DIALOG | WC_SPLASH_SCREEN, + WC2_DARKGRAY | WC2_HITPOINTBAR | +#if defined(STATUS_HILITES) + WC2_HILITE_STATUS | +#endif + WC2_HITPOINTBAR | WC2_FLUSH_STATUS | + WC2_TERM_SIZE | WC2_WINDOWBORDERS | WC2_PETATTR | WC2_GUICOLOR, + curses_init_nhwindows, + curses_player_selection, + curses_askname, + curses_get_nh_event, + curses_exit_nhwindows, + curses_suspend_nhwindows, + curses_resume_nhwindows, + curses_create_nhwindow, + curses_clear_nhwindow, + curses_display_nhwindow, + curses_destroy_nhwindow, + curses_curs, + curses_putstr, + genl_putmixed, + curses_display_file, + curses_start_menu, + curses_add_menu, + curses_end_menu, + curses_select_menu, + genl_message_menu, + curses_update_inventory, + curses_mark_synch, + curses_wait_synch, +#ifdef CLIPPING + curses_cliparound, +#endif +#ifdef POSITIONBAR + donull, +#endif + curses_print_glyph, + curses_raw_print, + curses_raw_print_bold, + curses_nhgetch, + curses_nh_poskey, + curses_nhbell, + curses_doprev_message, + curses_yn_function, + curses_getlin, + curses_get_ext_cmd, + curses_number_pad, + curses_delay_output, +#ifdef CHANGE_COLOR /* only a Mac option currently */ + donull, + donull, +#endif + curses_start_screen, + curses_end_screen, + genl_outrip, + curses_preference_update, + genl_getmsghistory, + genl_putmsghistory, + curses_status_init, + genl_status_finish, + genl_status_enablefield, + curses_status_update, + genl_can_suspend_yes, +}; + +/* Track if we're performing an update to the permanent window. + Needed since we aren't using the normal menu functions to handle + the inventory window. */ +static int inv_update = 0; + +/* +init_nhwindows(int* argcp, char** argv) + -- Initialize the windows used by NetHack. This can also + create the standard windows listed at the top, but does + not display them. + -- Any commandline arguments relevant to the windowport + should be interpreted, and *argcp and *argv should + be changed to remove those arguments. + -- When the message window is created, the variable + iflags.window_inited needs to be set to TRUE. Otherwise + all plines() will be done via raw_print(). + ** Why not have init_nhwindows() create all of the "standard" + ** windows? Or at least all but WIN_INFO? -dean +*/ +void +curses_init_nhwindows(int *argcp, char **argv) +{ +#ifdef PDCURSES + char window_title[BUFSZ]; +#endif + +#ifdef XCURSES + base_term = Xinitscr(*argcp, argv); +#else + base_term = initscr(); +#endif +#ifdef TEXTCOLOR + if (has_colors()) { + start_color(); + curses_init_nhcolors(); + } else { + iflags.use_color = FALSE; + set_option_mod_status("color", SET_IN_FILE); + iflags.wc2_guicolor = FALSE; + set_wc2_option_mod_status(WC2_GUICOLOR, SET_IN_FILE); + } +#else + iflags.use_color = FALSE; + set_option_mod_status("color", SET_IN_FILE); + iflags.wc2_guicolor = FALSE; + set_wc2_option_mod_status(WC2_GUICOLOR, SET_IN_FILE); +#endif + noecho(); + raw(); + meta(stdscr, TRUE); + orig_cursor = curs_set(0); + keypad(stdscr, TRUE); +#ifdef NCURSES_VERSION +# ifdef __APPLE__ + ESCDELAY = 25; +# else + set_escdelay(25); +# endif/* __APPLE__ */ +#endif /* NCURSES_VERSION */ +#ifdef PDCURSES +# ifdef DEF_GAME_NAME +# ifdef VERSION_STRING + sprintf(window_title, "%s %s", DEF_GAME_NAME, VERSION_STRING); +# else + sprintf(window_title, "%s", DEF_GAME_NAME); +# endif + /* VERSION_STRING */ +# else +# ifdef VERSION_STRING + sprintf(window_title, "%s %s", "NetHack", VERSION_STRING); +# else + sprintf(window_title, "%s", "NetHack"); +# endif + /* VERSION_STRING */ +# endif/* DEF_GAME_NAME */ + PDC_set_title(window_title); + PDC_set_blink(TRUE); /* Only if the user asks for it! */ + timeout(1); + (void) getch(); + timeout(-1); +#endif /* PDCURSES */ + getmaxyx(base_term, term_rows, term_cols); + counting = FALSE; + curses_init_options(); + if ((term_rows < 15) || (term_cols < 40)) { + panic("Terminal too small. Must be minumum 40 width and 15 height"); + } + + curses_create_main_windows(); + curses_init_mesg_history(); + curses_display_splash_window(); +} + +/* Do a window-port specific player type selection. If player_selection() + offers a Quit option, it is its responsibility to clean up and terminate + the process. You need to fill in pl_character[0]. +*/ +void +curses_player_selection() +{ + curses_choose_character(); +} + + +/* Ask the user for a player name. */ +void +curses_askname() +{ + curses_line_input_dialog("Who are you?", plname, PL_NSIZ); +} + + +/* Does window event processing (e.g. exposure events). + A noop for the tty and X window-ports. +*/ +void +curses_get_nh_event() +{ +#ifdef PDCURSES + if (is_termresized()) { + resize_term(0, 0); + getmaxyx(base_term, term_rows, term_cols); + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + } +#endif +#ifdef NCURSES_VERSION /* Is there a better way to detect ncurses? */ + if (is_term_resized(term_rows, term_cols)) { + if (!isendwin()) { + endwin(); + } + + refresh(); + getmaxyx(base_term, term_rows, term_cols); + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + } +#endif +} + +/* Exits the window system. This should dismiss all windows, + except the "window" used for raw_print(). str is printed if possible. +*/ +void +curses_exit_nhwindows(const char *str) +{ + curses_cleanup(); + curs_set(orig_cursor); + endwin(); + iflags.window_inited = 0; + if (str != NULL) { + raw_print(str); + } +} + +/* Prepare the window to be suspended. */ +void +curses_suspend_nhwindows(const char *str) +{ + endwin(); +} + + +/* Restore the windows after being suspended. */ +void +curses_resume_nhwindows() +{ + curses_refresh_nethack_windows(); +} + +/* Create a window of type "type" which can be + NHW_MESSAGE (top line) + NHW_STATUS (bottom lines) + NHW_MAP (main dungeon) + NHW_MENU (inventory or other "corner" windows) + NHW_TEXT (help/text, full screen paged window) +*/ +winid +curses_create_nhwindow(int type) +{ + winid wid = curses_get_wid(type); + + if (curses_is_menu(wid) || curses_is_text(wid)) { + curses_start_menu(wid); + curses_add_wid(wid); + } + + return wid; +} + + +/* Clear the given window, when asked to. */ +void +curses_clear_nhwindow(winid wid) +{ + if (wid != NHW_MESSAGE) { + curses_clear_nhwin(wid); + } +} + +/* -- Display the window on the screen. If there is data + pending for output in that window, it should be sent. + If blocking is TRUE, display_nhwindow() will not + return until the data has been displayed on the screen, + and acknowledged by the user where appropriate. + -- All calls are blocking in the tty window-port. + -- Calling display_nhwindow(WIN_MESSAGE,???) will do a + --more--, if necessary, in the tty window-port. +*/ +void +curses_display_nhwindow(winid wid, BOOLEAN_P block) +{ + menu_item *selected = NULL; + if (curses_is_menu(wid) || curses_is_text(wid)) { + curses_end_menu(wid, ""); + curses_select_menu(wid, PICK_NONE, &selected); + return; + } + + /* don't overwrite the splash screen first time through */ + if (!iflags.window_inited && wid == MAP_WIN) + iflags.window_inited = TRUE; + else { + /* actually display the window */ + wnoutrefresh(curses_get_nhwin(wid)); + /* flush pending writes from other windows too */ + doupdate(); + } + if ((wid == MAP_WIN) && block) { + (void) curses_more(); + } + + if ((wid == MESSAGE_WIN) && block) { + (void) curses_block(TRUE); + } +} + + +/* Destroy will dismiss the window if the window has not + * already been dismissed. +*/ +void +curses_destroy_nhwindow(winid wid) +{ + curses_del_nhwin(wid); +} + +/* Next output to window will start at (x,y), also moves + displayable cursor to (x,y). For backward compatibility, + 1 <= x < cols, 0 <= y < rows, where cols and rows are + the size of window. +*/ +void +curses_curs(winid wid, int x, int y) +{ + curses_move_cursor(wid, x, y); +} + +/* +putstr(window, attr, str) + -- Print str on the window with the given attribute. Only + printable ASCII characters (040-0126) must be supported. + Multiple putstr()s are output on separate lines. +Attributes + can be one of + ATR_NONE (or 0) + ATR_ULINE + ATR_BOLD + ATR_BLINK + ATR_INVERSE + If a window-port does not support all of these, it may map + unsupported attributes to a supported one (e.g. map them + all to ATR_INVERSE). putstr() may compress spaces out of + str, break str, or truncate str, if necessary for the + display. Where putstr() breaks a line, it has to clear + to end-of-line. + -- putstr should be implemented such that if two putstr()s + are done consecutively the user will see the first and + then the second. In the tty port, pline() achieves this + by calling more() or displaying both on the same line. +*/ +void +curses_putstr(winid wid, int attr, const char *text) +{ + int curses_attr = curses_convert_attr(attr); + + /* We need to convert NetHack attributes to curses attributes */ + curses_puts(wid, curses_attr, text); +} + +/* Display the file named str. Complain about missing files + iff complain is TRUE. +*/ +void +curses_display_file(const char *filename, BOOLEAN_P must_exist) +{ + curses_view_file(filename, must_exist); +} + +/* Start using window as a menu. You must call start_menu() + before add_menu(). After calling start_menu() you may not + putstr() to the window. Only windows of type NHW_MENU may + be used for menus. +*/ +void +curses_start_menu(winid wid) +{ + if (inv_update) + return; + + curses_create_nhmenu(wid); +} + +/* +add_menu(winid wid, int glyph, const anything identifier, + char accelerator, char groupacc, + int attr, char *str, boolean preselected) + -- Add a text line str to the given menu window. If identifier + is 0, then the line cannot be selected (e.g. a title). + Otherwise, identifier is the value returned if the line is + selected. Accelerator is a keyboard key that can be used + to select the line. If the accelerator of a selectable + item is 0, the window system is free to select its own + accelerator. It is up to the window-port to make the + accelerator visible to the user (e.g. put "a - " in front + of str). The value attr is the same as in putstr(). + Glyph is an optional glyph to accompany the line. If + window port cannot or does not want to display it, this + is OK. If there is no glyph applicable, then this + value will be NO_GLYPH. + -- All accelerators should be in the range [A-Za-z]. + -- It is expected that callers do not mix accelerator + choices. Either all selectable items have an accelerator + or let the window system pick them. Don't do both. + -- Groupacc is a group accelerator. It may be any character + outside of the standard accelerator (see above) or a + number. If 0, the item is unaffected by any group + accelerator. If this accelerator conflicts with + the menu command (or their user defined alises), it loses. + The menu commands and aliases take care not to interfere + with the default object class symbols. + -- If you want this choice to be preselected when the + menu is displayed, set preselected to TRUE. +*/ +void +curses_add_menu(winid wid, int glyph, const ANY_P * identifier, + CHAR_P accelerator, CHAR_P group_accel, int attr, + const char *str, BOOLEAN_P presel) +{ + int curses_attr = curses_convert_attr(attr); + + if (inv_update) { + curses_add_inv(inv_update, glyph, accelerator, curses_attr, str); + inv_update++; + return; + } + + curses_add_nhmenu_item(wid, glyph, identifier, accelerator, group_accel, + curses_attr, str, presel); +} + +/* +end_menu(window, prompt) + -- Stop adding entries to the menu and flushes the window + to the screen (brings to front?). Prompt is a prompt + to give the user. If prompt is NULL, no prompt will + be printed. + ** This probably shouldn't flush the window any more (if + ** it ever did). That should be select_menu's job. -dean +*/ +void +curses_end_menu(winid wid, const char *prompt) +{ + if (inv_update) + return; + + curses_finalize_nhmenu(wid, prompt); +} + +/* +int select_menu(winid window, int how, menu_item **selected) + -- Return the number of items selected; 0 if none were chosen, + -1 when explicitly cancelled. If items were selected, then + selected is filled in with an allocated array of menu_item + structures, one for each selected line. The caller must + free this array when done with it. The "count" field + of selected is a user supplied count. If the user did + not supply a count, then the count field is filled with + -1 (meaning all). A count of zero is equivalent to not + being selected and should not be in the list. If no items + were selected, then selected is NULL'ed out. How is the + mode of the menu. Three valid values are PICK_NONE, + PICK_ONE, and PICK_N, meaning: nothing is selectable, + only one thing is selectable, and any number valid items + may selected. If how is PICK_NONE, this function should + never return anything but 0 or -1. + -- You may call select_menu() on a window multiple times -- + the menu is saved until start_menu() or destroy_nhwindow() + is called on the window. + -- Note that NHW_MENU windows need not have select_menu() + called for them. There is no way of knowing whether + select_menu() will be called for the window at + create_nhwindow() time. +*/ +int +curses_select_menu(winid wid, int how, MENU_ITEM_P ** selected) +{ + if (inv_update) + return 0; + + return curses_display_nhmenu(wid, how, selected); +} + +void +curses_update_inventory(void) +{ + /* Don't do anything if perm_invent is off unless we + changed the option. */ + if (!iflags.perm_invent) { + if (curses_get_nhwin(INV_WIN)) { + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + } + return; + } + + /* Update inventory sidebar. NetHack uses normal menu functions + when drawing the inventory, and we don't want to change the + underlying code. So instead, track if an inventory update is + being performed with a static variable. */ + inv_update = 1; + curses_update_inv(); + inv_update = 0; +} + +/* +mark_synch() -- Don't go beyond this point in I/O on any channel until + all channels are caught up to here. Can be an empty call + for the moment +*/ +void +curses_mark_synch() +{ +} + +/* +wait_synch() -- Wait until all pending output is complete (*flush*() for + streams goes here). + -- May also deal with exposure events etc. so that the + display is OK when return from wait_synch(). +*/ +void +curses_wait_synch() +{ +} + +/* +cliparound(x, y)-- Make sure that the user is more-or-less centered on the + screen if the playing area is larger than the screen. + -- This function is only defined if CLIPPING is defined. +*/ +void +curses_cliparound(int x, int y) +{ + int sx, sy, ex, ey; + boolean redraw = curses_map_borders(&sx, &sy, &ex, &ey, x, y); + + if (redraw) { + curses_draw_map(sx, sy, ex, ey); + } +} + +/* +print_glyph(window, x, y, glyph, bkglyph) + -- Print the glyph at (x,y) on the given window. Glyphs are + integers at the interface, mapped to whatever the window- + port wants (symbol, font, color, attributes, ...there's + a 1-1 map between glyphs and distinct things on the map). + bkglyph is to render the background behind the glyph. + It's not used here. +*/ +void +curses_print_glyph(winid wid, XCHAR_P x, XCHAR_P y, int glyph, int bkglyph) +{ + int ch; + int color; + unsigned int special; + int attr = -1; + + /* map glyph to character and color */ + mapglyph(glyph, &ch, &color, &special, x, y); + if ((special & MG_PET) && iflags.hilite_pet) { + attr = iflags.wc2_petattr; + } + if ((special & MG_DETECT) && iflags.use_inverse) { + attr = A_REVERSE; + } + if (!symset[PRIMARY].name || !strcmpi(symset[PRIMARY].name, "curses")) { + ch = curses_convert_glyph(ch, glyph); + } + if (wid == NHW_MAP) { +/* hilite stairs not in 3.6, yet + if ((special & MG_STAIRS) && iflags.hilite_hidden_stairs) { + color = 16 + (color * 2); + } else +*/ + if ((special & MG_OBJPILE) && iflags.hilite_pile) { + color = 16 + (color * 2) + 1; + } + } + + curses_putch(wid, x, y, ch, color, attr); +} + +/* +raw_print(str) -- Print directly to a screen, or otherwise guarantee that + the user sees str. raw_print() appends a newline to str. + It need not recognize ASCII control characters. This is + used during startup (before windowing system initialization + -- maybe this means only error startup messages are raw), + for error messages, and maybe other "msg" uses. E.g. + updating status for micros (i.e, "saving"). +*/ +void +curses_raw_print(const char *str) +{ + puts(str); +} + +/* +raw_print_bold(str) + -- Like raw_print(), but prints in bold/standout (if possible). +*/ +void +curses_raw_print_bold(const char *str) +{ + curses_raw_print(str); +} + +/* +int nhgetch() -- Returns a single character input from the user. + -- In the tty window-port, nhgetch() assumes that tgetch() + will be the routine the OS provides to read a character. + Returned character _must_ be non-zero. +*/ +int +curses_nhgetch() +{ + int ch; + + curses_prehousekeeping(); + ch = curses_read_char(); + curses_posthousekeeping(); + + return ch; +} + +/* +int nh_poskey(int *x, int *y, int *mod) + -- Returns a single character input from the user or a + a positioning event (perhaps from a mouse). If the + return value is non-zero, a character was typed, else, + a position in the MAP window is returned in x, y and mod. + mod may be one of + + CLICK_1 -- mouse click type 1 + CLICK_2 -- mouse click type 2 + + The different click types can map to whatever the + hardware supports. If no mouse is supported, this + routine always returns a non-zero character. +*/ +int +curses_nh_poskey(int *x, int *y, int *mod) +{ + int key = curses_nhgetch(); + +#ifdef NCURSES_MOUSE_VERSION + /* Mouse event if mouse_support is true */ + if (key == KEY_MOUSE) { + key = curses_get_mouse(x, y, mod); + } +#endif + + return key; +} + +/* +nhbell() -- Beep at user. [This will exist at least until sounds are + redone, since sounds aren't attributable to windows anyway.] +*/ +void +curses_nhbell() +{ + beep(); +} + +/* +doprev_message() + -- Display previous messages. Used by the ^P command. + -- On the tty-port this scrolls WIN_MESSAGE back one line. +*/ +int +curses_doprev_message() +{ + curses_prev_mesg(); + return 0; +} + +/* +char yn_function(const char *ques, const char *choices, char default) + -- Print a prompt made up of ques, choices and default. + Read a single character response that is contained in + choices or default. If choices is NULL, all possible + inputs are accepted and returned. This overrides + everything else. The choices are expected to be in + lower case. Entering ESC always maps to 'q', or 'n', + in that order, if present in choices, otherwise it maps + to default. Entering any other quit character (SPACE, + RETURN, NEWLINE) maps to default. + -- If the choices string contains ESC, then anything after + it is an acceptable response, but the ESC and whatever + follows is not included in the prompt. + -- If the choices string contains a '#' then accept a count. + Place this value in the global "yn_number" and return '#'. + -- This uses the top line in the tty window-port, other + ports might use a popup. +*/ +char +curses_yn_function(const char *question, const char *choices, CHAR_P def) +{ + return (char) curses_character_input_dialog(question, choices, def); +} + +/* +getlin(const char *ques, char *input) + -- Prints ques as a prompt and reads a single line of text, + up to a newline. The string entered is returned without the + newline. ESC is used to cancel, in which case the string + "\033\000" is returned. + -- getlin() must call flush_screen(1) before doing anything. + -- This uses the top line in the tty window-port, other + ports might use a popup. +*/ +void +curses_getlin(const char *question, char *input) +{ + curses_line_input_dialog(question, input, BUFSZ); +} + +/* +int get_ext_cmd(void) + -- Get an extended command in a window-port specific way. + An index into extcmdlist[] is returned on a successful + selection, -1 otherwise. +*/ +int +curses_get_ext_cmd() +{ + return curses_ext_cmd(); +} + + +/* +number_pad(state) + -- Initialize the number pad to the given state. +*/ +void +curses_number_pad(int state) +{ +} + +/* +delay_output() -- Causes a visible delay of 50ms in the output. + Conceptually, this is similar to wait_synch() followed + by a nap(50ms), but allows asynchronous operation. +*/ +void +curses_delay_output() +{ + /* refreshing the whole display is a waste of time, + * but that's why we're here */ + refresh(); + napms(50); +} + +/* +start_screen() -- Only used on Unix tty ports, but must be declared for + completeness. Sets up the tty to work in full-screen + graphics mode. Look at win/tty/termcap.c for an + example. If your window-port does not need this function + just declare an empty function. +*/ +void +curses_start_screen() +{ +} + +/* +end_screen() -- Only used on Unix tty ports, but must be declared for + completeness. The complement of start_screen(). +*/ +void +curses_end_screen() +{ +} + +/* +outrip(winid, int) + -- The tombstone code. If you want the traditional code use + genl_outrip for the value and check the #if in rip.c. +*/ +void +curses_outrip(winid wid, int how) +{ +} + +/* +preference_update(preference) + -- The player has just changed one of the wincap preference + settings, and the NetHack core is notifying your window + port of that change. If your window-port is capable of + dynamically adjusting to the change then it should do so. + Your window-port will only be notified of a particular + change if it indicated that it wants to be by setting the + corresponding bit in the wincap mask. +*/ +void +curses_preference_update(const char *pref) +{ + if ((strcmp(pref, "align_status") == 0) || + (strcmp(pref, "align_message") == 0)) { + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + } +} + diff --git a/win/curses/cursmesg.c b/win/curses/cursmesg.c new file mode 100644 index 000000000..9fc1d6442 --- /dev/null +++ b/win/curses/cursmesg.c @@ -0,0 +1,630 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursmesg.h" +#include + + +/* Message window routines for curses interface */ + +/* Private declatations */ + +typedef struct nhpm { + char *str; /* Message text */ + long turn; /* Turn number for message */ + struct nhpm *prev_mesg; /* Pointer to previous message */ + struct nhpm *next_mesg; /* Pointer to next message */ +} nhprev_mesg; + +static void scroll_window(winid wid); +static void unscroll_window(winid wid); +static void directional_scroll(winid wid, int nlines); +static void mesg_add_line(char *mline); +static nhprev_mesg *get_msg_line(boolean reverse, int mindex); + +static int turn_lines = 1; +static int mx = 0; +static int my = 0; /* message window text location */ +static nhprev_mesg *first_mesg = NULL; +static nhprev_mesg *last_mesg = NULL; +static int max_messages; +static int num_messages = 0; + + + +/* Write a string to the message window. Attributes set by calling function. */ + +void +curses_message_win_puts(const char *message, boolean recursed) +{ + int height, width, linespace; + char *tmpstr; + WINDOW *win = curses_get_nhwin(MESSAGE_WIN); + boolean border = curses_window_has_border(MESSAGE_WIN); + int message_length = strlen(message); + int border_space = 0; + static long suppress_turn = -1; + + if (strncmp("Count:", message, 6) == 0) { + curses_count_window(message); + return; + } + + if (suppress_turn == moves) { + return; + } + + curses_get_window_size(MESSAGE_WIN, &height, &width); + if (border) { + border_space = 1; + if (mx < 1) { + mx = 1; + } + if (my < 1) { + my = 1; + } + } + + linespace = ((width + border_space) - 3) - mx; + + if (strcmp(message, "#") == 0) { /* Extended command or Count: */ + if ((strcmp(toplines, "#") != 0) && (my >= (height - 1 + border_space)) && (height != 1)) { /* Bottom of message window */ + scroll_window(MESSAGE_WIN); + mx = width; + my--; + strcpy(toplines, message); + } + + return; + } + + if (!recursed) { + strcpy(toplines, message); + mesg_add_line((char *) message); + } + + if (linespace < message_length) { + if (my >= (height - 1 + border_space)) { /* bottom of message win */ + if ((turn_lines > height) || (height == 1)) { + /* Pause until key is hit - Esc suppresses any further + messages that turn */ + if (curses_more() == '\033') { + suppress_turn = moves; + return; + } + } else { + scroll_window(MESSAGE_WIN); + turn_lines++; + } + } else { + if (mx != border_space) { + my++; + mx = border_space; + } + } + } + + if (height > 1) { + curses_toggle_color_attr(win, NONE, A_BOLD, ON); + } + + if ((mx == border_space) && ((message_length + 2) > width)) { + tmpstr = curses_break_str(message, (width - 2), 1); + mvwprintw(win, my, mx, "%s", tmpstr); + mx += strlen(tmpstr); + if (strlen(tmpstr) < (size_t) (width - 2)) { + mx++; + } + free(tmpstr); + if (height > 1) { + curses_toggle_color_attr(win, NONE, A_BOLD, OFF); + } + curses_message_win_puts(tmpstr = curses_str_remainder(message, (width - 2), 1), + TRUE); + free(tmpstr); + } else { + mvwprintw(win, my, mx, "%s", message); + curses_toggle_color_attr(win, NONE, A_BOLD, OFF); + mx += message_length + 1; + } + wrefresh(win); +} + + +int +curses_block(boolean noscroll) +/* noscroll - blocking because of msgtype = stop/alert */ +/* else blocking because window is full, so need to scroll after */ +{ + int height, width, ret; + WINDOW *win = curses_get_nhwin(MESSAGE_WIN); + char *resp = " \n\033"; /* space, enter, esc */ + + + curses_get_window_size(MESSAGE_WIN, &height, &width); + curses_toggle_color_attr(win, MORECOLOR, NONE, ON); + mvwprintw(win, my, mx, iflags.msg_is_alert ? "" : ">>"); + curses_toggle_color_attr(win, MORECOLOR, NONE, OFF); + if (iflags.msg_is_alert) + curses_alert_main_borders(TRUE); + wrefresh(win); + while (iflags.msg_is_alert && (ret = wgetch(win) != '\t')); + /* msgtype=stop should require space/enter rather than + * just any key, as we want to prevent YASD from + * riding direction keys. */ + while (!iflags.msg_is_alert && (ret = wgetch(win)) && !index(resp,(char)ret)); + if (iflags.msg_is_alert) + curses_alert_main_borders(FALSE); + if (height == 1) { + curses_clear_unhighlight_message_window(); + } else { + mvwprintw(win, my, mx, " "); + if (!noscroll) { + scroll_window(MESSAGE_WIN); + turn_lines = 1; + } + wrefresh(win); + } + + return ret; +} + +int +curses_more() +{ + return curses_block(FALSE); +} + + +/* Clear the message window if one line; otherwise unhighlight old messages */ + +void +curses_clear_unhighlight_message_window() +{ + int mh, mw, count; + boolean border = curses_window_has_border(MESSAGE_WIN); + WINDOW *win = curses_get_nhwin(MESSAGE_WIN); + + turn_lines = 1; + + curses_get_window_size(MESSAGE_WIN, &mh, &mw); + + mx = 0; + + if (border) { + mx++; + } + + if (mh == 1) { + curses_clear_nhwin(MESSAGE_WIN); + } else { + mx += mw; /* Force new line on new turn */ + + if (border) { + + for (count = 0; count < mh; count++) { + mvwchgat(win, count + 1, 1, mw, COLOR_PAIR(8), A_NORMAL, NULL); + } + } else { + for (count = 0; count < mh; count++) { + mvwchgat(win, count, 0, mw, COLOR_PAIR(8), A_NORMAL, NULL); + } + } + + wnoutrefresh(win); + } +} + + +/* Reset message window cursor to starting position, and display most +recent messages. */ + +void +curses_last_messages() +{ + boolean border = curses_window_has_border(MESSAGE_WIN); + + if (border) { + mx = 1; + my = 1; + } else { + mx = 0; + my = 0; + } + + nhprev_mesg *mesg; + int i; + for (i = (num_messages - 1); i > 0; i--) { + mesg = get_msg_line(TRUE, i); + if (mesg && mesg->str && strcmp(mesg->str, "")) + curses_message_win_puts(mesg->str, TRUE); + } + curses_message_win_puts(toplines, TRUE); +} + + +/* Initialize list for message history */ + +void +curses_init_mesg_history() +{ + max_messages = iflags.msg_history; + + if (max_messages < 1) { + max_messages = 1; + } + + if (max_messages > MESG_HISTORY_MAX) { + max_messages = MESG_HISTORY_MAX; + } +} + + +/* Display previous message window messages in reverse chron order */ + +void +curses_prev_mesg() +{ + int count; + winid wid; + long turn = 0; + anything *identifier; + nhprev_mesg *mesg; + menu_item *selected = NULL; + + wid = curses_get_wid(NHW_MENU); + curses_create_nhmenu(wid); + identifier = malloc(sizeof (anything)); + identifier->a_void = NULL; + + for (count = 0; count < num_messages; count++) { + mesg = get_msg_line(TRUE, count); + if ((turn != mesg->turn) && (count != 0)) { + curses_add_menu(wid, NO_GLYPH, identifier, 0, 0, A_NORMAL, + "---", FALSE); + } + curses_add_menu(wid, NO_GLYPH, identifier, 0, 0, A_NORMAL, + mesg->str, FALSE); + turn = mesg->turn; + } + + curses_end_menu(wid, ""); + curses_select_menu(wid, PICK_NONE, &selected); +} + + +/* Shows Count: in a separate window, or at the bottom of the message +window, depending on the user's settings */ + +void +curses_count_window(const char *count_text) +{ + int startx, starty, winx, winy; + int messageh, messagew; + static WINDOW *countwin = NULL; + + if ((count_text == NULL) && (countwin != NULL)) { + delwin(countwin); + countwin = NULL; + counting = FALSE; + return; + } + + counting = TRUE; + + if (iflags.wc_popup_dialog) { /* Display count in popup window */ + startx = 1; + starty = 1; + + if (countwin == NULL) { + countwin = curses_create_window(25, 1, UP); + } + + } else { /* Display count at bottom of message window */ + + curses_get_window_xy(MESSAGE_WIN, &winx, &winy); + curses_get_window_size(MESSAGE_WIN, &messageh, &messagew); + + if (curses_window_has_border(MESSAGE_WIN)) { + winx++; + winy++; + } + + winy += messageh - 1; + + if (countwin == NULL) { + pline("#"); +#ifndef PDCURSES + countwin = newwin(1, 25, winy, winx); +#endif /* !PDCURSES */ + } +#ifdef PDCURSES + else { + curses_destroy_win(countwin); + } + + countwin = newwin(1, 25, winy, winx); +#endif /* PDCURSES */ + startx = 0; + starty = 0; + } + + mvwprintw(countwin, starty, startx, "%s", count_text); + wrefresh(countwin); +} + + /* Gets a "line" (buffer) of input. */ +void +curses_message_win_getline(const char *prompt, char *answer, int buffer) +{ + int height, width; /* of window */ + char *tmpbuf, *p_answer; /* combined prompt + answer */ + int nlines, maxlines, i; /* prompt + answer */ + int promptline; + int promptx; + char **linestarts; /* pointers to start of each line */ + char *tmpstr; /* for free() */ + int maxy, maxx; /* linewrap / scroll */ + int ch; + + WINDOW *win = curses_get_nhwin(MESSAGE_WIN); + int border_space = 0; + int len = 0; /* of answer string */ + boolean border = curses_window_has_border(MESSAGE_WIN); + int orig_cursor = curs_set(0); + + curses_get_window_size(MESSAGE_WIN, &height, &width); + if (border) { + border_space = 1; + if (mx < 1) mx = 1; + if (my < 1) my = 1; + } + maxy = height - 1 + border_space; + maxx = width - 1 + border_space; + + tmpbuf = (char *)malloc(strlen(prompt) + buffer + 2); + maxlines = buffer / width * 2; + strcpy(tmpbuf, prompt); + strcat(tmpbuf, " "); + nlines = curses_num_lines(tmpbuf,width); + maxlines += nlines * 2; + linestarts = (char **)malloc(sizeof(char*) * maxlines); + p_answer = tmpbuf + strlen(tmpbuf); + linestarts[0] = tmpbuf; + + if (mx > border_space) { /* newline */ + if (my >= maxy) scroll_window(MESSAGE_WIN); + else my++; + mx = border_space; + } + + curses_toggle_color_attr(win, NONE, A_BOLD, ON); + + for (i = 0; i < nlines-1; i++) { + tmpstr = curses_break_str(linestarts[i],width-1,1); + linestarts[i+1] = linestarts[i] + strlen(tmpstr); + if (*linestarts[i+1] == ' ') linestarts[i+1]++; + mvwaddstr(win,my,mx,tmpstr); + free(tmpstr); + if (++my >= maxy) { + scroll_window(MESSAGE_WIN); + my--; + } + } + mvwaddstr(win,my,mx,linestarts[nlines-1]); + mx = promptx = strlen(linestarts[nlines-1]) + border_space; + promptline = nlines - 1; + + while(1) { + mx = strlen(linestarts[nlines - 1]) + border_space; + if (mx > maxx) { + if (nlines < maxlines) { + tmpstr = curses_break_str(linestarts[nlines - 1], width - 1, 1); + mx = strlen(tmpstr) + border_space; + mvwprintw(win, my, mx, "%*c", maxx - mx + 1, ' '); + if (++my > maxy) { + scroll_window(MESSAGE_WIN); + my--; + } + mx = border_space; + linestarts[nlines] = linestarts[nlines - 1] + strlen(tmpstr); + if (*linestarts[nlines] == ' ') linestarts[nlines]++; + mvwaddstr(win, my, mx, linestarts[nlines]); + mx = strlen(linestarts[nlines]) + border_space; + nlines++; + free(tmpstr); + } else { + p_answer[--len] = '\0'; + mvwaddch(win, my, --mx, ' '); + } + } + wmove(win, my, mx); + curs_set(1); + wrefresh(win); + ch = getch(); + curs_set(0); + switch(ch) { + case '\033': /* DOESCAPE */ + /* blank the input but don't exit */ + while(nlines - 1 > promptline) { + if (nlines-- > height) { + unscroll_window(MESSAGE_WIN); + tmpstr = curses_break_str(linestarts[nlines - height], width - 1, 1); + mvwaddstr(win, border_space, border_space, tmpstr); + free(tmpstr); + } else { + mx = border_space; + mvwprintw(win, my, mx, "%*c", maxx - mx, ' '); + my--; + } + } + mx = promptx; + mvwprintw(win, my, mx, "%*c", maxx - mx, ' '); + *p_answer = '\0'; + len = 0; + break; + case ERR: /* should not happen */ + *answer = '\0'; + free(tmpbuf); + free(linestarts); + curs_set(orig_cursor); + curses_toggle_color_attr(win, NONE, A_BOLD, OFF); + return; + case '\r': + case '\n': + free(linestarts); + strncpy(answer, p_answer, buffer); + strcpy(toplines, tmpbuf); + mesg_add_line((char *) tmpbuf); + free(tmpbuf); + curs_set(orig_cursor); + curses_toggle_color_attr(win, NONE, A_BOLD, OFF); + if (++my > maxy) { + scroll_window(MESSAGE_WIN); + my--; + } + mx = border_space; + return; + case '\b': + case KEY_BACKSPACE: + if (len < 1) { + len = 1; + mx = promptx; + } + p_answer[--len] = '\0'; + mvwaddch(win, my, --mx, ' '); + /* try to unwrap back to the previous line if there is one */ + if (nlines > 1 && strlen(linestarts[nlines - 2]) < (size_t) width) { + mvwaddstr(win, my - 1, border_space, linestarts[nlines - 2]); + if (nlines-- > height) { + unscroll_window(MESSAGE_WIN); + tmpstr = curses_break_str(linestarts[nlines - height], width - 1, 1); + mvwaddstr(win, border_space, border_space, tmpstr); + free(tmpstr); + } else { + /* clean up the leftovers on the next line, if we didn't scroll it away */ + mvwprintw(win, my--, border_space, "%*c", strlen(linestarts[nlines]), ' '); + } + } + break; + default: + p_answer[len++] = ch; + if (len >= buffer) len = buffer-1; + else mvwaddch(win, my, mx, ch); + p_answer[len] = '\0'; + } + } +} + +/* Scroll lines upward in given window, or clear window if only one line. */ +static void +scroll_window(winid wid) +{ + directional_scroll(wid,1); +} + +static void +unscroll_window(winid wid) +{ + directional_scroll(wid,-1); +} + +static void +directional_scroll(winid wid, int nlines) +{ + int wh, ww, s_top, s_bottom; + boolean border = curses_window_has_border(wid); + WINDOW *win = curses_get_nhwin(wid); + + curses_get_window_size(wid, &wh, &ww); + if (wh == 1) { + curses_clear_nhwin(wid); + return; + } + if (border) { + s_top = 1; + s_bottom = wh; + } else { + s_top = 0; + s_bottom = wh - 1; + } + scrollok(win, TRUE); + wsetscrreg(win, s_top, s_bottom); + wscrl(win, nlines); + scrollok(win, FALSE); + if (wid == MESSAGE_WIN) { + if (border) + mx = 1; + else + mx = 0; + } + if (border) { + box(win, 0, 0); + } + wrefresh(win); +} + + +/* Add given line to message history */ + +static void +mesg_add_line(char *mline) +{ + nhprev_mesg *tmp_mesg = NULL; + nhprev_mesg *current_mesg = malloc(sizeof (nhprev_mesg)); + + current_mesg->str = curses_copy_of(mline); + current_mesg->turn = moves; + current_mesg->next_mesg = NULL; + + if (num_messages == 0) { + first_mesg = current_mesg; + } + + if (last_mesg != NULL) { + last_mesg->next_mesg = current_mesg; + } + current_mesg->prev_mesg = last_mesg; + last_mesg = current_mesg; + + + if (num_messages < max_messages) { + num_messages++; + } else { + tmp_mesg = first_mesg->next_mesg; + free(first_mesg); + first_mesg = tmp_mesg; + } +} + + +/* Returns specified line from message history, or NULL if out of bounds */ + +static nhprev_mesg * +get_msg_line(boolean reverse, int mindex) +{ + int count; + nhprev_mesg *current_mesg; + + if (reverse) { + current_mesg = last_mesg; + for (count = 0; count < mindex; count++) { + if (current_mesg == NULL) { + return NULL; + } + current_mesg = current_mesg->prev_mesg; + } + return current_mesg; + } else { + current_mesg = first_mesg; + for (count = 0; count < mindex; count++) { + if (current_mesg == NULL) { + return NULL; + } + current_mesg = current_mesg->next_mesg; + } + return current_mesg; + } +} diff --git a/win/curses/cursmesg.h b/win/curses/cursmesg.h new file mode 100644 index 000000000..54af5f9b3 --- /dev/null +++ b/win/curses/cursmesg.h @@ -0,0 +1,19 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSMESG_H +# define CURSMESG_H + + +/* Global declarations */ + +void curses_message_win_puts(const char *message, boolean recursed); +int curses_block(boolean require_tab); +int curses_more(void); +void curses_clear_unhighlight_message_window(void); +void curses_message_win_getline(const char *prompt, char *answer, int buffer); +void curses_last_messages(void); +void curses_init_mesg_history(void); +void curses_prev_mesg(void); +void curses_count_window(const char *count_text); + +#endif /* CURSMESG_H */ diff --git a/win/curses/cursmisc.c b/win/curses/cursmisc.c new file mode 100644 index 000000000..4b424884d --- /dev/null +++ b/win/curses/cursmisc.c @@ -0,0 +1,880 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursmisc.h" +#include "func_tab.h" +#include "dlb.h" + +#include + +/* Misc. curses interface functions */ + +/* Private declarations */ + +static int curs_x = -1; +static int curs_y = -1; + +static int parse_escape_sequence(void); + +/* Macros for Control and Alt keys */ + +#ifndef M +# ifndef NHSTDC +# define M(c) (0x80 | (c)) +# else +# define M(c) ((c) - 128) +# endif/* NHSTDC */ +#endif +#ifndef C +# define C(c) (0x1f & (c)) +#endif + + +/* Read a character of input from the user */ + +int +curses_read_char() +{ + int ch, tmpch; + + ch = getch(); + tmpch = ch; + ch = curses_convert_keys(ch); + + if (ch == 0) { + ch = '\033'; /* map NUL to ESC since nethack doesn't expect NUL */ + } +#if defined(ALT_0) && defined(ALT_9) /* PDCurses, maybe others */ + if ((ch >= ALT_0) && (ch <= ALT_9)) { + tmpch = (ch - ALT_0) + '0'; + ch = M(tmpch); + } +#endif + +#if defined(ALT_A) && defined(ALT_Z) /* PDCurses, maybe others */ + if ((ch >= ALT_A) && (ch <= ALT_Z)) { + tmpch = (ch - ALT_A) + 'a'; + ch = M(tmpch); + } +#endif + +#ifdef KEY_RESIZE + /* Handle resize events via get_nh_event, not this code */ + if (ch == KEY_RESIZE) { + ch = '\033'; /* NetHack doesn't know what to do with KEY_RESIZE */ + } +#endif + + if (counting && !isdigit(ch)) { /* Dismiss count window if necissary */ + curses_count_window(NULL); + curses_refresh_nethack_windows(); + } + + return ch; +} + +/* Turn on or off the specified color and / or attribute */ + +void +curses_toggle_color_attr(WINDOW * win, int color, int attr, int onoff) +{ +#ifdef TEXTCOLOR + int curses_color; + + /* Map color disabled */ + if ((!iflags.wc_color) && (win == mapwin)) { + return; + } + + /* GUI color disabled */ +// if ((!iflags.wc2_guicolor) && (win != mapwin)) { +// return; +// } + + if (color == 0) { /* make black fg visible */ +# ifdef USE_DARKGRAY + if (iflags.wc2_darkgray) { + if (can_change_color() && (COLORS > 16)) { + /* colorpair for black is already darkgray */ + } else { /* Use bold for a bright black */ + + wattron(win, A_BOLD); + } + } else +# endif/* USE_DARKGRAY */ + color = CLR_BLUE; + } + curses_color = color + 1; + if (COLORS < 16) { + if (curses_color > 8 && curses_color < 17) + curses_color -= 8; + else if (curses_color > (17 + 16)) + curses_color -= 16; + } + if (onoff == ON) { /* Turn on color/attributes */ + if (color != NONE) { + if ((((color > 7) && (color < 17)) || + (color > 17 + 17)) && (COLORS < 16)) { + wattron(win, A_BOLD); + } + wattron(win, COLOR_PAIR(curses_color)); + } + + if (attr != NONE) { + wattron(win, attr); + } + } else { /* Turn off color/attributes */ + + if (color != NONE) { + if ((color > 7) && (COLORS < 16)) { + wattroff(win, A_BOLD); + } +# ifdef USE_DARKGRAY + if ((color == 0) && (!can_change_color() || (COLORS <= 16))) { + wattroff(win, A_BOLD); + } +# else + if (iflags.use_inverse) { + wattroff(win, A_REVERSE); + } +# endif/* DARKGRAY */ + wattroff(win, COLOR_PAIR(curses_color)); + } + + if (attr != NONE) { + wattroff(win, attr); + } + } +#endif /* TEXTCOLOR */ +} + + +/* clean up and quit - taken from tty port */ + +void +curses_bail(const char *mesg) +{ + clearlocks(); + curses_exit_nhwindows(mesg); + nh_terminate(EXIT_SUCCESS); +} + + +/* Return a winid for a new window of the given type */ + +winid +curses_get_wid(int type) +{ + winid ret; + static winid menu_wid = 20; /* Always even */ + static winid text_wid = 21; /* Always odd */ + + switch (type) { + case NHW_MESSAGE: + return MESSAGE_WIN; + case NHW_MAP: + return MAP_WIN; + case NHW_STATUS: + return STATUS_WIN; + case NHW_MENU: + ret = menu_wid; + break; + case NHW_TEXT: + ret = text_wid; + break; + default: + panic("curses_get_wid: unsupported window type"); + ret = -1; /* Not reached */ + } + + while (curses_window_exists(ret)) { + ret += 2; + if ((ret + 2) > 10000) { /* Avoid "wid2k" problem */ + ret -= 9900; + } + } + + if (type == NHW_MENU) { + menu_wid += 2; + } else { + text_wid += 2; + } + + return ret; +} + + +/* + * Allocate a copy of the given string. If null, return a string of + * zero length. + * + * This is taken from copy_of() in tty/wintty.c. + */ + +char * +curses_copy_of(const char *s) +{ + if (!s) + s = ""; + return strcpy((char *) alloc((unsigned) (strlen(s) + 1)), s); +} + + +/* Determine the number of lines needed for a string for a dialog window +of the given width */ + +int +curses_num_lines(const char *str, int width) +{ + int last_space, count; + int curline = 1; + char substr[BUFSZ]; + char tmpstr[BUFSZ]; + + strncpy(substr, str, BUFSZ-1); + substr[BUFSZ-1] = '\0'; + + while (strlen(substr) > (size_t) width) { + last_space = 0; + + for (count = 0; count <= width; count++) { + if (substr[count] == ' ') + last_space = count; + + } + if (last_space == 0) { /* No spaces found */ + last_space = count - 1; + } + for (count = (last_space + 1); (size_t) count < strlen(substr); count++) { + tmpstr[count - (last_space + 1)] = substr[count]; + } + tmpstr[count - (last_space + 1)] = '\0'; + strcpy(substr, tmpstr); + curline++; + } + + return curline; +} + + +/* Break string into smaller lines to fit into a dialog window of the +given width */ + +char * +curses_break_str(const char *str, int width, int line_num) +{ + int last_space, count; + char *retstr; + int curline = 0; + int strsize = strlen(str) + 1; +#if __STDC_VERSION__ >= 199901L + char substr[strsize]; + char curstr[strsize]; + char tmpstr[strsize]; + + strcpy(substr, str); +#else +#ifndef BUFSZ +#define BUFSZ 256 +#endif + char substr[BUFSZ * 2]; + char curstr[BUFSZ * 2]; + char tmpstr[BUFSZ * 2]; + + if (strsize > (BUFSZ * 2) - 1) { + paniclog("curses", "curses_break_str() string too long."); + strncpy(substr, str, (BUFSZ * 2) - 2); + substr[(BUFSZ * 2) - 1] = '\0'; + } else + strcpy(substr, str); +#endif + + while (curline < line_num) { + if (strlen(substr) == 0) { + break; + } + curline++; + last_space = 0; + for (count = 0; count <= width; count++) { + if (substr[count] == ' ') { + last_space = count; + } else if (substr[count] == '\0') { + last_space = count; + break; + } + } + if (last_space == 0) { /* No spaces found */ + last_space = count - 1; + } + for (count = 0; count < last_space; count++) { + curstr[count] = substr[count]; + } + curstr[count] = '\0'; + if (substr[count] == '\0') { + break; + } + for (count = (last_space + 1); (size_t) count < strlen(substr); count++) { + tmpstr[count - (last_space + 1)] = substr[count]; + } + tmpstr[count - (last_space + 1)] = '\0'; + strcpy(substr, tmpstr); + } + + if (curline < line_num) { + return NULL; + } + + retstr = curses_copy_of(curstr); + + return retstr; +} + + +/* Return the remaining portion of a string after hacking-off line_num lines */ + +char * +curses_str_remainder(const char *str, int width, int line_num) +{ + int last_space, count; + char *retstr; + int curline = 0; + int strsize = strlen(str) + 1; +#if __STDC_VERSION__ >= 199901L + char substr[strsize]; + char curstr[strsize]; + char tmpstr[strsize]; + + strcpy(substr, str); +#else +#ifndef BUFSZ +#define BUFSZ 256 +#endif + char substr[BUFSZ * 2]; + char curstr[BUFSZ * 2]; + char tmpstr[BUFSZ * 2]; + + if (strsize > (BUFSZ * 2) - 1) { + paniclog("curses", "curses_str_remainder() string too long."); + strncpy(substr, str, (BUFSZ * 2) - 2); + substr[(BUFSZ * 2) - 1] = '\0'; + } else + strcpy(substr, str); +#endif + + while (curline < line_num) { + if (strlen(substr) == 0) { + break; + } + curline++; + last_space = 0; + for (count = 0; count <= width; count++) { + if (substr[count] == ' ') { + last_space = count; + } else if (substr[count] == '\0') { + last_space = count; + break; + } + } + if (last_space == 0) { /* No spaces found */ + last_space = count - 1; + } + for (count = 0; count < last_space; count++) { + curstr[count] = substr[count]; + } + curstr[count] = '\0'; + if (substr[count] == '\0') { + break; + } + for (count = (last_space + 1); (size_t) count < strlen(substr); count++) { + tmpstr[count - (last_space + 1)] = substr[count]; + } + tmpstr[count - (last_space + 1)] = '\0'; + strcpy(substr, tmpstr); + } + + if (curline < line_num) { + return NULL; + } + + retstr = curses_copy_of(substr); + + return retstr; +} + + +/* Determine if the given NetHack winid is a menu window */ + +boolean +curses_is_menu(winid wid) +{ + if ((wid > 19) && !(wid % 2)) { /* Even number */ + return TRUE; + } else { + return FALSE; + } +} + + +/* Determine if the given NetHack winid is a text window */ + +boolean +curses_is_text(winid wid) +{ + if ((wid > 19) && (wid % 2)) { /* Odd number */ + return TRUE; + } else { + return FALSE; + } +} + + +/* Replace certain characters with portable drawing characters if +cursesgraphics option is enabled */ + +int +curses_convert_glyph(int ch, int glyph) +{ + int symbol; + + if (Is_rogue_level(&u.uz)) { + return ch; + } + + /* Save some processing time by returning if the glyph represents + an object that we don't have custom characters for */ + if (!glyph_is_cmap(glyph)) { + return ch; + } + + symbol = glyph_to_cmap(glyph); + + /* If user selected a custom character for this object, don't + override this. */ + if (((glyph_is_cmap(glyph)) && (ch != showsyms[symbol]))) { + return ch; + } + + switch (symbol) { + case S_vwall: + return ACS_VLINE; + case S_hwall: + return ACS_HLINE; + case S_tlcorn: + return ACS_ULCORNER; + case S_trcorn: + return ACS_URCORNER; + case S_blcorn: + return ACS_LLCORNER; + case S_brcorn: + return ACS_LRCORNER; + case S_crwall: + return ACS_PLUS; + case S_tuwall: + return ACS_BTEE; + case S_tdwall: + return ACS_TTEE; + case S_tlwall: + return ACS_RTEE; + case S_trwall: + return ACS_LTEE; + case S_tree: + return ACS_PLMINUS; + case S_corr: + return ACS_CKBOARD; + case S_litcorr: + return ACS_CKBOARD; + } + + return ch; +} + + +/* Move text cursor to specified coordinates in the given NetHack window */ + +void +curses_move_cursor(winid wid, int x, int y) +{ + int sx, sy, ex, ey; + int xadj = 0; + int yadj = 0; + +#ifndef PDCURSES + WINDOW *win = curses_get_nhwin(MAP_WIN); +#endif + + if (wid != MAP_WIN) { + return; + } +#ifdef PDCURSES + /* PDCurses seems to not handle wmove correctly, so we use move and + physical screen coordinates instead */ + curses_get_window_xy(wid, &xadj, &yadj); +#endif + curs_x = x + xadj; + curs_y = y + yadj; + curses_map_borders(&sx, &sy, &ex, &ey, x, y); + + if (curses_window_has_border(wid)) { + curs_x++; + curs_y++; + } + + if ((x >= sx) && (x <= ex) && (y >= sy) && (y <= ey)) { + curs_x -= sx; + curs_y -= sy; +#ifdef PDCURSES + move(curs_y, curs_x); +#else + wmove(win, curs_y, curs_x); +#endif + } +} + + +/* Perform actions that should be done every turn before nhgetch() */ + +void +curses_prehousekeeping() +{ +#ifndef PDCURSES + WINDOW *win = curses_get_nhwin(MAP_WIN); +#endif /* PDCURSES */ + + if ((curs_x > -1) && (curs_y > -1)) { + curs_set(1); +#ifdef PDCURSES + /* PDCurses seems to not handle wmove correctly, so we use move + and physical screen coordinates instead */ + move(curs_y, curs_x); +#else + wmove(win, curs_y, curs_x); +#endif /* PDCURSES */ + curses_refresh_nhwin(MAP_WIN); + } +} + + +/* Perform actions that should be done every turn after nhgetch() */ + +void +curses_posthousekeeping() +{ + curs_set(0); + //curses_decrement_highlights(FALSE); + curses_clear_unhighlight_message_window(); +} + + +void +curses_view_file(const char *filename, boolean must_exist) +{ + winid wid; + anything *identifier; + char buf[BUFSZ]; + menu_item *selected = NULL; + dlb *fp = dlb_fopen(filename, "r"); + + if ((fp == NULL) && (must_exist)) { + pline("Cannot open %s for reading!", filename); + } + + if (fp == NULL) { + return; + } + + wid = curses_get_wid(NHW_MENU); + curses_create_nhmenu(wid); + identifier = malloc(sizeof (anything)); + identifier->a_void = NULL; + + while (dlb_fgets(buf, BUFSZ, fp) != NULL) { + curses_add_menu(wid, NO_GLYPH, identifier, 0, 0, A_NORMAL, buf, FALSE); + } + + dlb_fclose(fp); + curses_end_menu(wid, ""); + curses_select_menu(wid, PICK_NONE, &selected); +} + + +void +curses_rtrim(char *str) +{ + char *s; + + for (s = str; *s != '\0'; ++s); + for (--s; isspace(*s) && s > str; --s); + if (s == str) + *s = '\0'; + else + *(++s) = '\0'; +} + + +/* Read numbers until non-digit is encountered, and return number +in int form. */ + +int +curses_get_count(int first_digit) +{ + long current_count = first_digit; + int current_char; + + current_char = curses_read_char(); + + while (isdigit(current_char)) { + current_count = (current_count * 10) + (current_char - '0'); + if (current_count > LARGEST_INT) { + current_count = LARGEST_INT; + } + + pline("Count: %ld", current_count); + current_char = curses_read_char(); + } + + ungetch(current_char); + + if (current_char == '\033') { /* Cancelled with escape */ + current_count = -1; + } + + return current_count; +} + + +/* Convert the given NetHack text attributes into the format curses +understands, and return that format mask. */ + +int +curses_convert_attr(int attr) +{ + int curses_attr; + + switch (attr) { + case ATR_NONE: + curses_attr = A_NORMAL; + break; + case ATR_ULINE: + curses_attr = A_UNDERLINE; + break; + case ATR_BOLD: + curses_attr = A_BOLD; + break; + case ATR_BLINK: + curses_attr = A_BLINK; + break; + case ATR_INVERSE: + curses_attr = A_REVERSE; + break; + default: + curses_attr = A_NORMAL; + } + + return curses_attr; +} + + +/* Map letter attributes from a string to bitmask. Return mask on +success, or 0 if not found */ + +int +curses_read_attrs(char *attrs) +{ + int retattr = 0; + + if (strchr(attrs, 'b') || strchr(attrs, 'B')) { + retattr = retattr | A_BOLD; + } + if (strchr(attrs, 'i') || strchr(attrs, 'I')) { + retattr = retattr | A_REVERSE; + } + if (strchr(attrs, 'u') || strchr(attrs, 'U')) { + retattr = retattr | A_UNDERLINE; + } + if (strchr(attrs, 'k') || strchr(attrs, 'K')) { + retattr = retattr | A_BLINK; + } +#ifdef A_ITALIC + if (strchr(attrs, 't') || strchr(attrs, 'T')) { + retattr = retattr | A_ITALIC; + } +#endif +#ifdef A_RIGHTLINE + if (strchr(attrs, 'r') || strchr(attrs, 'R')) { + retattr = retattr | A_RIGHTLINE; + } +#endif +#ifdef A_LEFTLINE + if (strchr(attrs, 'l') || strchr(attrs, 'L')) { + retattr = retattr | A_LEFTLINE; + } +#endif + + return retattr; +} + + +/* Convert special keys into values that NetHack can understand. +Currently this is limited to arrow keys, but this may be expanded. */ + +int +curses_convert_keys(int key) +{ + int ret = key; + + if (ret == '\033') { + ret = parse_escape_sequence(); + } + + /* Handle arrow keys */ + switch (key) { + case KEY_LEFT: + if (iflags.num_pad) { + ret = '4'; + } else { + ret = 'h'; + } + break; + case KEY_RIGHT: + if (iflags.num_pad) { + ret = '6'; + } else { + ret = 'l'; + } + break; + case KEY_UP: + if (iflags.num_pad) { + ret = '8'; + } else { + ret = 'k'; + } + break; + case KEY_DOWN: + if (iflags.num_pad) { + ret = '2'; + } else { + ret = 'j'; + } + break; +#ifdef KEY_A1 + case KEY_A1: + if (iflags.num_pad) { + ret = '7'; + } else { + ret = 'y'; + } + break; +#endif /* KEY_A1 */ +#ifdef KEY_A3 + case KEY_A3: + if (iflags.num_pad) { + ret = '9'; + } else { + ret = 'u'; + } + break; +#endif /* KEY_A3 */ +#ifdef KEY_C1 + case KEY_C1: + if (iflags.num_pad) { + ret = '1'; + } else { + ret = 'b'; + } + break; +#endif /* KEY_C1 */ +#ifdef KEY_C3 + case KEY_C3: + if (iflags.num_pad) { + ret = '3'; + } else { + ret = 'n'; + } + break; +#endif /* KEY_C3 */ +#ifdef KEY_B2 + case KEY_B2: + if (iflags.num_pad) { + ret = '5'; + } else { + ret = 'g'; + } + break; +#endif /* KEY_B2 */ + } + + return ret; +} + + +/* Process mouse events. Mouse movement is processed until no further +mouse movement events are available. Returns 0 for a mouse click +event, or the first non-mouse key event in the case of mouse +movement. */ + +int +curses_get_mouse(int *mousex, int *mousey, int *mod) +{ + int key = '\033'; + +#ifdef NCURSES_MOUSE_VERSION + MEVENT event; + + if (getmouse(&event) == OK) { /* When the user clicks left mouse button */ + if (event.bstate & BUTTON1_CLICKED) { + /* See if coords are in map window & convert coords */ + if (wmouse_trafo(mapwin, &event.y, &event.x, TRUE)) { + key = 0; /* Flag mouse click */ + *mousex = event.x; + *mousey = event.y; + + if (curses_window_has_border(MAP_WIN)) { + (*mousex)--; + (*mousey)--; + } + + *mod = CLICK_1; + } + } + } +#endif /* NCURSES_MOUSE_VERSION */ + + return key; +} + + +static int +parse_escape_sequence(void) +{ +#ifndef PDCURSES + int ret; + + timeout(10); + + ret = getch(); + + if (ret != ERR) { /* Likely an escape sequence */ + if (((ret >= 'a') && (ret <= 'z')) || ((ret >= '0') && (ret <= '9'))) { + ret |= 0x80; /* Meta key support for most terminals */ + } else if (ret == 'O') { /* Numeric keypad */ + ret = getch(); + if ((ret != ERR) && (ret >= 112) && (ret <= 121)) { + ret = ret - 112 + '0'; /* Convert to number */ + } else { + ret = '\033'; /* Escape */ + } + } + } else { + ret = '\033'; /* Just an escape character */ + } + + timeout(-1); + + return ret; +#else + return '\033'; +#endif /* !PDCURSES */ +} + diff --git a/win/curses/cursmisc.h b/win/curses/cursmisc.h new file mode 100644 index 000000000..50821c81a --- /dev/null +++ b/win/curses/cursmisc.h @@ -0,0 +1,30 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSMISC_H +# define CURSMISC_H + +/* Global declarations */ + +int curses_read_char(void); +void curses_toggle_color_attr(WINDOW * win, int color, int attr, int onoff); +void curses_bail(const char *mesg); +winid curses_get_wid(int type); +char *curses_copy_of(const char *s); +int curses_num_lines(const char *str, int width); +char *curses_break_str(const char *str, int width, int line_num); +char *curses_str_remainder(const char *str, int width, int line_num); +boolean curses_is_menu(winid wid); +boolean curses_is_text(winid wid); +int curses_convert_glyph(int ch, int glyph); +void curses_move_cursor(winid wid, int x, int y); +void curses_prehousekeeping(void); +void curses_posthousekeeping(void); +void curses_view_file(const char *filename, boolean must_exist); +void curses_rtrim(char *str); +int curses_get_count(int first_digit); +int curses_convert_attr(int attr); +int curses_read_attrs(char *attrs); +int curses_convert_keys(int key); +int curses_get_mouse(int *mousex, int *mousey, int *mod); + +#endif /* CURSMISC_H */ diff --git a/win/curses/cursstat.c b/win/curses/cursstat.c new file mode 100644 index 000000000..ab3ca0ca5 --- /dev/null +++ b/win/curses/cursstat.c @@ -0,0 +1,1583 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include /* toupper() */ +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "cursstat.h" + +/* Status window functions for curses interface */ + +/* + * The following data structures come from the genl_ routines in + * src/windows.c and as such are considered to be on the window-port + * "side" of things, rather than the NetHack-core "side" of things. + */ + +extern const char *status_fieldfmt[MAXBLSTATS]; +extern const char *status_fieldnm[MAXBLSTATS]; +extern char *status_vals[MAXBLSTATS]; +extern boolean status_activefields[MAXBLSTATS]; + +/* Long format fields for vertical window */ +static char *status_vals_long[MAXBLSTATS]; + +#ifdef STATUS_HILITES +static long curses_condition_bits; +static int curses_status_colors[MAXBLSTATS]; +int hpbar_percent, hpbar_color; + +static int FDECL(condcolor, (long, unsigned long *)); +static int FDECL(condattr, (long, unsigned long *)); +#endif /* STATUS_HILITES */ +static void FDECL(draw_status, (unsigned long *)); +static void FDECL(draw_classic, (boolean, unsigned long *)); +static void FDECL(draw_vertical, (boolean, unsigned long *)); +static void FDECL(draw_horizontal, (boolean, unsigned long *)); + +void +curses_status_init() +{ +#ifdef STATUS_HILITES + int i; + + for (i = 0; i < MAXBLSTATS; ++i) { + curses_status_colors[i] = NO_COLOR; /* no color */ + status_vals_long[i] = (char *) alloc(MAXCO); + *status_vals_long[i] = '\0'; + } + curses_condition_bits = 0L; + hpbar_percent = 0, hpbar_color = NO_COLOR; +#endif /* STATUS_HILITES */ + + /* let genl_status_init do most of the initialization */ + genl_status_init(); +} + +/* + * *_status_update() + * -- update the value of a status field. + * -- the fldindex identifies which field is changing and + * is an integer index value from botl.h + * -- fldindex could be any one of the following from botl.h: + * BL_TITLE, BL_STR, BL_DX, BL_CO, BL_IN, BL_WI, BL_CH, + * BL_ALIGN, BL_SCORE, BL_CAP, BL_GOLD, BL_ENE, BL_ENEMAX, + * BL_XP, BL_AC, BL_HD, BL_TIME, BL_HUNGER, BL_HP, BL_HPMAX, + * BL_LEVELDESC, BL_EXP, BL_CONDITION + * -- fldindex could also be BL_FLUSH (-1), which is not really + * a field index, but is a special trigger to tell the + * windowport that it should redisplay all its status fields, + * even if no changes have been presented to it. + * -- ptr is usually a "char *", unless fldindex is BL_CONDITION. + * If fldindex is BL_CONDITION, then ptr is a long value with + * any or none of the following bits set (from botl.h): + * BL_MASK_STONE 0x00000001L + * BL_MASK_SLIME 0x00000002L + * BL_MASK_STRNGL 0x00000004L + * BL_MASK_FOODPOIS 0x00000008L + * BL_MASK_TERMILL 0x00000010L + * BL_MASK_BLIND 0x00000020L + * BL_MASK_DEAF 0x00000040L + * BL_MASK_STUN 0x00000080L + * BL_MASK_CONF 0x00000100L + * BL_MASK_HALLU 0x00000200L + * BL_MASK_LEV 0x00000400L + * BL_MASK_FLY 0x00000800L + * BL_MASK_RIDE 0x00001000L + * -- The value passed for BL_GOLD includes an encoded leading + * symbol for GOLD "\GXXXXNNNN:nnn". If the window port needs to use + * the textual gold amount without the leading "$:" the port will + * have to skip past ':' in the passed "ptr" for the BL_GOLD case. + * -- color is an unsigned int. + * color_index = color & 0x00FF; CLR_* value + * attribute = color & 0xFF00 >> 8; BL_* values + * This holds the color and attribute that the field should + * be displayed in. + * This is relevant for everything except BL_CONDITION fldindex. + * If fldindex is BL_CONDITION, this parameter should be ignored, + * as condition hilighting is done via the next colormasks + * parameter instead. + * + * -- colormasks - pointer to cond_hilites[] array of colormasks. + * Only relevant for BL_CONDITION fldindex. The window port + * should ignore this parameter for other fldindex values. + * Each condition bit must only ever appear in one of the + * CLR_ array members, but can appear in multiple HL_ATTCLR_ + * offsets (because more than one attribute can co-exist). + * See doc/window.doc for more details. + */ + +/* new approach through status_update() only */ +#define Begin_Attr(m) \ + if (m) { \ + if ((m) & HL_BOLD) \ + wattron(win, A_BOLD); \ + if ((m) & HL_INVERSE) \ + wattron(win,A_REVERSE); \ + if ((m) & HL_ULINE) \ + wattron(win,A_UNDERLINE); \ + if ((m) & HL_BLINK) \ + wattron(win,A_BLINK); \ + if ((m) & HL_DIM) \ + wattron(win,A_DIM); \ + } + +#define End_Attr(m) \ + if (m) { \ + if ((m) & HL_DIM) \ + wattroff(win,A_DIM); \ + if ((m) & HL_BLINK) \ + wattroff(win,A_BLINK); \ + if ((m) & HL_ULINE) \ + wattroff(win,A_UNDERLINE); \ + if ((m) & HL_INVERSE) \ + wattroff(win,A_REVERSE); \ + if ((m) & HL_BOLD) \ + wattroff(win,A_BOLD); \ + } + +#ifdef STATUS_HILITES + +#ifdef TEXTCOLOR +#define MaybeDisplayCond(bm,txt) \ + if (curses_condition_bits & (bm)) { \ + putstr(STATUS_WIN, 0, " "); \ + if (iflags.hilite_delta) { \ + attrmask = condattr((bm), colormasks); \ + Begin_Attr(attrmask); \ + if ((coloridx = condcolor((bm), colormasks)) != NO_COLOR) \ + curses_toggle_color_attr(win, coloridx, NONE, ON); \ + } \ + putstr(STATUS_WIN, 0, (txt)); \ + if (iflags.hilite_delta) { \ + if (coloridx != NO_COLOR) \ + curses_toggle_color_attr(win, coloridx, NONE, OFF); \ + End_Attr(attrmask); \ + } \ + } +#else +#define MaybeDisplayCond(bm,txt) \ + if (curses_condition_bits & (bm)) { \ + putstr(STATUS_WIN, 0, " "); \ + if (iflags.hilite_delta) { \ + attrmask = condattr((bm), colormasks); \ + Begin_Attr(attrmask); \ + } \ + putstr(STATUS_WIN, 0, (txt)); \ + if (iflags.hilite_delta) { \ + End_Attr(attrmask); \ + } \ + } +#endif + +void +curses_status_update(fldidx, ptr, chg, percent, color, colormasks) +int fldidx, chg UNUSED, percent, color; +genericptr_t ptr; +unsigned long *colormasks; +{ + long *condptr = (long *) ptr; + char *text = (char *) ptr; + char *goldnum = NULL; + static boolean oncearound = FALSE; /* prevent premature partial display */ + boolean use_name = TRUE; + + if (fldidx != BL_FLUSH) { + if (!status_activefields[fldidx]) + return; + if (fldidx == BL_GOLD) + goldnum = index(text,':') + 1; + switch (fldidx) { + case BL_CONDITION: + curses_condition_bits = *condptr; + oncearound = TRUE; + break; + case BL_TITLE: + case BL_HPMAX: + case BL_ENEMAX: + case BL_HUNGER: + case BL_CAP: + case BL_EXP: + use_name = FALSE; + /* FALLTHROUGH */ + default: + Sprintf(status_vals[fldidx], + (fldidx == BL_TITLE && iflags.wc2_hitpointbar) ? "%-30s" : + status_fieldfmt[fldidx] ? status_fieldfmt[fldidx] : "%s", + text); + if (use_name) { + Sprintf(status_vals_long[fldidx], "%-16s: %s", + status_fieldnm[fldidx], goldnum ? goldnum : text); + *(status_vals_long[fldidx]) = toupper((*status_vals_long[fldidx])); + } else + Strcpy(status_vals_long[fldidx], status_vals[fldidx]); +#ifdef TEXTCOLOR + curses_status_colors[fldidx] = color; +#else + curses_status_colors[fldidx] = NO_COLOR; +#endif + if (iflags.wc2_hitpointbar && fldidx == BL_HP) { + hpbar_percent = percent; +#ifdef TEXTCOLOR + hpbar_color = color; +#else + hpbar_color = NO_COLOR; +#endif + } + break; + } + } + + if (!oncearound) return; + draw_status(colormasks); +} + +void +draw_status(colormasks) +unsigned long *colormasks; +{ + boolean horiz = FALSE; + WINDOW *win = curses_get_nhwin(STATUS_WIN); + int orient = curses_get_window_orientation(STATUS_WIN); + boolean border = curses_window_has_border(STATUS_WIN); + + if ((orient != ALIGN_RIGHT) && (orient != ALIGN_LEFT)) + horiz = TRUE; + + /* Figure out if we have proper window dimensions for horizontal statusbar. */ + if (horiz) { + /* correct y */ + int cy = 3; + if (iflags.statuslines < 3) + cy = 2; + + /* actual y (and x) */ + int ax = 0; + int ay = 0; + getmaxyx(win, ay, ax); + if (border) + ay -= 2; + + if (cy != ay) { /* something changed. Redo everything */ + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + win = curses_get_nhwin(STATUS_WIN); + } + } + + werase(win); + if (horiz) { + if (iflags.statuslines < 3) + draw_classic(border, colormasks); + else + draw_horizontal(border, colormasks); + } else + draw_vertical(border, colormasks); + + if (border) + box(win, 0, 0); + wnoutrefresh(win); +} + +/* The 'classic' NetHack 3.x status layout */ +void +draw_classic(border, colormasks) +boolean border; +unsigned long *colormasks; +{ + enum statusfields fieldorder[2][15] = { + { BL_TITLE, BL_STR, BL_DX, BL_CO, BL_IN, BL_WI, BL_CH, BL_ALIGN, + BL_SCORE, BL_FLUSH, BL_FLUSH, BL_FLUSH, BL_FLUSH, BL_FLUSH, + BL_FLUSH }, + { BL_LEVELDESC, BL_GOLD, BL_HP, BL_HPMAX, BL_ENE, BL_ENEMAX, + BL_AC, BL_XP, BL_EXP, BL_HD, BL_TIME, BL_HUNGER, + BL_CAP, BL_CONDITION, BL_FLUSH } + }; +#ifdef TEXTCOLOR + int coloridx = NO_COLOR; +#endif + int i, attrmask = 0; + char *text; + int attridx = 0; + + WINDOW *win = curses_get_nhwin(STATUS_WIN); + if (border) wmove(win, 1, 1); + else wmove(win, 0, 0); + for (i = 0; fieldorder[0][i] != BL_FLUSH; ++i) { + int fldidx1 = fieldorder[0][i]; + if (status_activefields[fldidx1]) { + if (fldidx1 != BL_TITLE || !iflags.wc2_hitpointbar) { +#ifdef TEXTCOLOR + coloridx = curses_status_colors[fldidx1] & 0x00FF; +#endif + attridx = (curses_status_colors[fldidx1] & 0xFF00) >> 8; + text = status_vals[fldidx1]; + if (iflags.hilite_delta) { + if (*text == ' ') { + putstr(STATUS_WIN, 0, " "); + text++; + } + /* multiple attributes can be in effect concurrently */ + Begin_Attr(attridx); +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR && coloridx != CLR_MAX) + curses_toggle_color_attr(win, coloridx, NONE, ON); +#endif + } + + putstr(STATUS_WIN, 0, text); + + if (iflags.hilite_delta) { +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win, coloridx,NONE,OFF); +#endif + End_Attr(attridx); + } + } else { + /* hitpointbar using hp percent calculation */ + int bar_pos, bar_len; + char *bar2 = (char *)0; + char bar[MAXCO], savedch; + boolean twoparts = FALSE; + + text = status_vals[fldidx1]; + bar_len = strlen(text); + if (bar_len < MAXCO-1) { + Strcpy(bar, text); + bar_pos = (bar_len * hpbar_percent) / 100; + if (bar_pos < 1 && hpbar_percent > 0) + bar_pos = 1; + if (bar_pos >= bar_len && hpbar_percent < 100) + bar_pos = bar_len - 1; + if (bar_pos > 0 && bar_pos < bar_len) { + twoparts = TRUE; + bar2 = &bar[bar_pos]; + savedch = *bar2; + *bar2 = '\0'; + } + } + if (iflags.hilite_delta && iflags.wc2_hitpointbar) { + putstr(STATUS_WIN, 0, "["); +#ifdef TEXTCOLOR + coloridx = hpbar_color & 0x00FF; + /* attridx = (hpbar_color & 0xFF00) >> 8; */ + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win,coloridx,NONE,ON); +#endif + wattron(win,A_REVERSE); + putstr(STATUS_WIN, 0, bar); + wattroff(win,A_REVERSE); +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win,coloridx,NONE,OFF); +#endif + if (twoparts) { + *bar2 = savedch; + putstr(STATUS_WIN, 0, bar2); + } + putstr(STATUS_WIN, 0, "]"); + } else + putstr(STATUS_WIN, 0, text); + } + } + } + wclrtoeol(win); + if (border) wmove(win, 2, 1); + else wmove (win, 1, 0); + for (i = 0; fieldorder[1][i] != BL_FLUSH; ++i) { + int fldidx2 = fieldorder[1][i]; + + if (status_activefields[fldidx2]) { + if (fldidx2 != BL_CONDITION) { +#ifdef TEXTCOLOR + coloridx = curses_status_colors[fldidx2] & 0x00FF; +#endif + attridx = (curses_status_colors[fldidx2] & 0xFF00) >> 8; + text = status_vals[fldidx2]; + if (iflags.hilite_delta) { + if (*text == ' ') { + putstr(STATUS_WIN, 0, " "); + text++; + } + /* multiple attributes can be in effect concurrently */ + Begin_Attr(attridx); +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR && coloridx != CLR_MAX) + curses_toggle_color_attr(win,coloridx,NONE,ON); +#endif + } + + if (fldidx2 == BL_GOLD) { + /* putmixed() due to GOLD glyph */ + putmixed(STATUS_WIN, 0, text); + } else { + putstr(STATUS_WIN, 0, text); + } + + if (iflags.hilite_delta) { +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win,coloridx,NONE,OFF); +#endif + End_Attr(attridx); + } + } else { + MaybeDisplayCond(BL_MASK_STONE, "Stone"); + MaybeDisplayCond(BL_MASK_SLIME, "Slime"); + MaybeDisplayCond(BL_MASK_STRNGL, "Strngl"); + MaybeDisplayCond(BL_MASK_FOODPOIS, "FoodPois"); + MaybeDisplayCond(BL_MASK_TERMILL, "TermIll"); + MaybeDisplayCond(BL_MASK_BLIND, "Blind"); + MaybeDisplayCond(BL_MASK_DEAF, "Deaf"); + MaybeDisplayCond(BL_MASK_STUN, "Stun"); + MaybeDisplayCond(BL_MASK_CONF, "Conf"); + MaybeDisplayCond(BL_MASK_HALLU, "Hallu"); + MaybeDisplayCond(BL_MASK_LEV, "Lev"); + MaybeDisplayCond(BL_MASK_FLY, "Fly"); + MaybeDisplayCond(BL_MASK_RIDE, "Ride"); + } + } + } + wclrtoeol(win); + return; +} + +/* The new NH4-style horizontal layout on 3 lines */ +void +draw_horizontal(border, colormasks) +boolean border; +unsigned long *colormasks; +{ + /* TODO: implement this */ + /* for now, just draw classic */ + draw_classic(border, colormasks); +} + +/* The vertical layout from the original curses implementation */ +void +draw_vertical(border, colormasks) +boolean border; +unsigned long *colormasks; +{ + enum statusfields fieldorder[24] = { + BL_TITLE, BL_STR, BL_DX, BL_CO, BL_IN, BL_WI, BL_CH, BL_ALIGN, + BL_SCORE, BL_LEVELDESC, BL_GOLD, BL_HP, BL_HPMAX, BL_ENE, BL_ENEMAX, + BL_AC, BL_XP, BL_EXP, BL_HD, BL_TIME, BL_HUNGER, + BL_CAP, BL_CONDITION, BL_FLUSH + }; +#ifdef TEXTCOLOR + int coloridx = NO_COLOR; +#endif + int i, attrmask = 0; + char *text; + int attridx = 0; + int x = 0, y = 0; + + WINDOW *win = curses_get_nhwin(STATUS_WIN); + if (border) x++,y++; + for (i = 0; fieldorder[i] != BL_FLUSH; ++i) { + int fldidx1 = fieldorder[i]; + if (status_activefields[fldidx1]) { + if (fldidx1 != BL_TITLE || !iflags.wc2_hitpointbar) { + + if (fldidx1 != BL_CONDITION) { +#ifdef TEXTCOLOR + coloridx = curses_status_colors[fldidx1] & 0x00FF; +#endif + attridx = (curses_status_colors[fldidx1] & 0xFF00) >> 8; + text = status_vals_long[fldidx1]; + if (iflags.hilite_delta) { + if (*text == ' ') { + putstr(STATUS_WIN, 0, " "); + text++; + } + /* multiple attributes can be in effect concurrently */ + Begin_Attr(attridx); +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR && coloridx != CLR_MAX) + curses_toggle_color_attr(win, coloridx, NONE, ON); +#endif + } + + if (fldidx1 != BL_HPMAX + && fldidx1 != BL_ENEMAX + && fldidx1 != BL_EXP) + wmove(win, y++, x); /* everything on a new line except the above */ + + putstr(STATUS_WIN, 0, text); + + if (fldidx1 == BL_TITLE) y++; + + if (iflags.hilite_delta) { +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win, coloridx,NONE,OFF); +#endif + End_Attr(attridx); + } + } else { /* condition */ + MaybeDisplayCond(BL_MASK_STONE, "Stone"); + MaybeDisplayCond(BL_MASK_SLIME, "Slime"); + MaybeDisplayCond(BL_MASK_STRNGL, "Strngl"); + MaybeDisplayCond(BL_MASK_FOODPOIS, "FoodPois"); + MaybeDisplayCond(BL_MASK_TERMILL, "TermIll"); + MaybeDisplayCond(BL_MASK_BLIND, "Blind"); + MaybeDisplayCond(BL_MASK_DEAF, "Deaf"); + MaybeDisplayCond(BL_MASK_STUN, "Stun"); + MaybeDisplayCond(BL_MASK_CONF, "Conf"); + MaybeDisplayCond(BL_MASK_HALLU, "Hallu"); + MaybeDisplayCond(BL_MASK_LEV, "Lev"); + MaybeDisplayCond(BL_MASK_FLY, "Fly"); + MaybeDisplayCond(BL_MASK_RIDE, "Ride"); + } + } else { + /* hitpointbar using hp percent calculation */ + int bar_pos, bar_len; + char *bar2 = (char *)0; + char bar[MAXCO], savedch; + boolean twoparts = FALSE; + int height,width; + + text = status_vals[fldidx1]; + getmaxyx(win, height, width); + bar_len = min(strlen(text), (size_t)width - (border ? 4 : 2)); + text[bar_len] = '\0'; + if (bar_len < MAXCO-1) { + Strcpy(bar, text); + bar_pos = (bar_len * hpbar_percent) / 100; + if (bar_pos < 1 && hpbar_percent > 0) + bar_pos = 1; + if (bar_pos >= bar_len && hpbar_percent < 100) + bar_pos = bar_len - 1; + if (bar_pos > 0 && bar_pos < bar_len) { + twoparts = TRUE; + bar2 = &bar[bar_pos]; + savedch = *bar2; + *bar2 = '\0'; + } + } + wmove(win, y++, x); + if (iflags.hilite_delta && iflags.wc2_hitpointbar) { + putstr(STATUS_WIN, 0, "["); +#ifdef TEXTCOLOR + coloridx = hpbar_color & 0x00FF; + /* attridx = (hpbar_color & 0xFF00) >> 8; */ + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win,coloridx,NONE,ON); +#endif + wattron(win,A_REVERSE); + putstr(STATUS_WIN, 0, bar); + wattroff(win,A_REVERSE); +#ifdef TEXTCOLOR + if (coloridx != NO_COLOR) + curses_toggle_color_attr(win,coloridx,NONE,OFF); +#endif + if (twoparts) { + *bar2 = savedch; + putstr(STATUS_WIN, 0, bar2); + } + putstr(STATUS_WIN, 0, "]"); + } else + putstr(STATUS_WIN, 0, text); + y++; /* blank line after title */ + } + } + } + return; +} + +#ifdef TEXTCOLOR +/* + * Return what color this condition should + * be displayed in based on user settings. + */ +int condcolor(bm, bmarray) +long bm; +unsigned long *bmarray; +{ + int i; + + if (bm && bmarray) + for (i = 0; i < CLR_MAX; ++i) { + if (bmarray[i] && (bm & bmarray[i])) + return i; + } + return NO_COLOR; +} +#endif /* TEXTCOLOR */ + +int condattr(bm, bmarray) +long bm; +unsigned long *bmarray; +{ + int attr = 0; + int i; + + if (bm && bmarray) { + for (i = HL_ATTCLR_DIM; i < BL_ATTCLR_MAX; ++i) { + if (bmarray[i] && (bm & bmarray[i])) { + switch(i) { + case HL_ATTCLR_DIM: + attr |= HL_DIM; + break; + case HL_ATTCLR_BLINK: + attr |= HL_BLINK; + break; + case HL_ATTCLR_ULINE: + attr |= HL_ULINE; + break; + case HL_ATTCLR_INVERSE: + attr |= HL_INVERSE; + break; + case HL_ATTCLR_BOLD: + attr |= HL_BOLD; + break; + } + } + } + } + return attr; +} +#endif /* STATUS_HILITES */ + +#if 0 //old stuff to be re-incorporated +/* Private declarations */ + +/* Used to track previous value of things, to highlight changes. */ +typedef struct nhs { + long value; + int highlight_turns; + int highlight_color; +} nhstat; + +static attr_t get_trouble_color(const char *); +static void draw_trouble_str(const char *); +static void print_statdiff(const char *append, nhstat *, int, int); +static void get_playerrank(char *); +static int hpen_color(boolean, int, int); +static void draw_bar(boolean, int, int, const char *); +static void draw_horizontal(int, int, int, int); +static void draw_horizontal_new(int, int, int, int); +static void draw_vertical(int, int, int, int); +static void curses_add_statuses(WINDOW *, boolean, boolean, int *, int *); +static void curses_add_status(WINDOW *, boolean, boolean, int *, int *, + const char *, int); +static int decrement_highlight(nhstat *, boolean); + +#ifdef STATUS_COLORS +static attr_t hpen_color_attr(boolean, int, int); +extern struct color_option text_color_of(const char *text, + const struct text_color_option *color_options); +struct color_option percentage_color_of(int value, int max, + const struct percent_color_option *color_options); + +extern const struct text_color_option *text_colors; +extern const struct percent_color_option *hp_colors; +extern const struct percent_color_option *pw_colors; +#endif + +/* Whether or not we have printed status window content at least once. + Used to ensure that prev* doesn't end up highlighted on game start. */ +static boolean first = TRUE; +static nhstat prevdepth; +static nhstat prevstr; +static nhstat prevint; +static nhstat prevwis; +static nhstat prevdex; +static nhstat prevcon; +static nhstat prevcha; +static nhstat prevau; +static nhstat prevlevel; +static nhstat prevac; +static nhstat prevexp; +static nhstat prevtime; + +#ifdef SCORE_ON_BOTL +static nhstat prevscore; +#endif + +extern const char *hu_stat[]; /* from eat.c */ +extern const char *enc_stat[]; /* from botl.c */ + +/* If the statuscolors patch isn't enabled, have some default colors for status problems + anyway */ + +struct statcolor { + const char *txt; /* For status problems */ + int color; /* Default color assuming STATUS_COLORS isn't enabled */ +}; + +static const struct statcolor default_colors[] = { + {"Satiated", CLR_YELLOW}, + {"Hungry", CLR_YELLOW}, + {"Weak", CLR_ORANGE}, + {"Fainted", CLR_BRIGHT_MAGENTA}, + {"Fainting", CLR_BRIGHT_MAGENTA}, + {"Burdened", CLR_RED}, + {"Stressed", CLR_RED}, + {"Strained", CLR_ORANGE}, + {"Overtaxed", CLR_ORANGE}, + {"Overloaded", CLR_BRIGHT_MAGENTA}, + {"Conf", CLR_BRIGHT_BLUE}, + {"Blind", CLR_BRIGHT_BLUE}, + {"Stun", CLR_BRIGHT_BLUE}, + {"Hallu", CLR_BRIGHT_BLUE}, + {"Ill", CLR_BRIGHT_MAGENTA}, + {"FoodPois", CLR_BRIGHT_MAGENTA}, + {"Slime", CLR_BRIGHT_MAGENTA}, + {NULL, NULL, NO_COLOR}, +}; + +static attr_t +get_trouble_color(const char *stat) +{ + attr_t res = curses_color_attr(CLR_GRAY, 0); + const struct statcolor *clr; + for (clr = default_colors; clr->txt; clr++) { + if (stat && !strcmp(clr->txt, stat)) { +#ifdef STATUS_COLORS + /* Check if we have a color enabled with statuscolors */ + if (!iflags.use_status_colors) + return curses_color_attr(CLR_GRAY, 0); /* no color configured */ + + struct color_option stat_color; + + stat_color = text_color_of(clr->txt, text_colors); + if (stat_color.color == NO_COLOR && !stat_color.attr_bits) + return curses_color_attr(CLR_GRAY, 0); + + if (stat_color.color != NO_COLOR) + res = curses_color_attr(stat_color.color, 0); + + res = curses_color_attr(stat_color.color, 0); + int count; + for (count = 0; (1 << count) <= stat_color.attr_bits; count++) { + if (count != ATR_NONE && + (stat_color.attr_bits & (1 << count))) + res |= curses_convert_attr(count); + } + + return res; +#else + return curses_color_attr(clr->color, 0); +#endif + } + } + + return res; +} + +/* TODO: This is in the wrong place. */ +void +get_playerrank(char *rank) +{ + char buf[BUFSZ]; + if (Upolyd) { + int k = 0; + + Strcpy(buf, mons[u.umonnum].mname); + while(buf[k] != 0) { + if ((k == 0 || (k > 0 && buf[k-1] == ' ')) && + 'a' <= buf[k] && buf[k] <= 'z') + buf[k] += 'A' - 'a'; + k++; + } + Strcpy(rank, buf); + } else + Strcpy(rank, rank_of(u.ulevel, Role_switch, flags.female)); +} + +/* Handles numerical stat changes of various kinds. + type is generally STAT_OTHER (generic "do nothing special"), + but is used if the stat needs to be handled in a special way. */ +static void +print_statdiff(const char *append, nhstat *stat, int new, int type) +{ + char buf[BUFSZ]; + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + int color = CLR_GRAY; + + /* Turncount isn't highlighted, or it would be highlighted constantly. */ + if (type != STAT_TIME && new != stat->value) { + /* Less AC is better */ + if ((type == STAT_AC && new < stat->value) || + (type != STAT_AC && new > stat->value)) { + color = STAT_UP_COLOR; + if (type == STAT_GOLD) + color = HI_GOLD; + } else + color = STAT_DOWN_COLOR; + + stat->value = new; + stat->highlight_color = color; + stat->highlight_turns = 5; + } else if (stat->highlight_turns) + color = stat->highlight_color; + + attr_t attr = curses_color_attr(color, 0); + wattron(win, attr); + wprintw(win, "%s", append); + if (type == STAT_STR && new > 18) { + if (new > 118) + wprintw(win, "%d", new - 100); + else if (new == 118) + wprintw(win, "18/**"); + else + wprintw(win, "18/%02d", new - 18); + } else + wprintw(win, "%d", new); + + wattroff(win, attr); +} + +static void +draw_trouble_str(const char *str) +{ + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + attr_t attr = get_trouble_color(str); + wattron(win, attr); + wprintw(win, "%s", str); + wattroff(win, attr); +} + +/* Returns a ncurses attribute for foreground and background. + This should probably be in cursinit.c or something. */ +attr_t +curses_color_attr(int nh_color, int bg_color) +{ + int color = nh_color + 1; + attr_t cattr = A_NORMAL; + + if (!nh_color) { +#ifdef USE_DARKGRAY + if (iflags.wc2_darkgray) { + if (!can_change_color() || COLORS <= 16) + cattr |= A_BOLD; + } else +#endif + color = COLOR_BLUE; + } + + if (COLORS < 16 && color > 8) { + color -= 8; + cattr = A_BOLD; + } + + /* Can we do background colors? We can if we have more than + 16*7 colors (more than 8*7 for terminals with bold) */ + if (COLOR_PAIRS > (COLORS >= 16 ? 16 : 8) * 7) { + /* NH3 has a rather overcomplicated way of defining + its colors past the first 16: + Pair Foreground Background + 17 Black Red + 18 Black Blue + 19 Red Red + 20 Red Blue + 21 Green Red + ... + (Foreground order: Black, Red, Green, Yellow, Blue, + Magenta, Cyan, Gray/White) + + To work around these oddities, we define backgrounds + by the following pairs: + + 16 COLORS + 49-64: Green + 65-80: Yellow + 81-96: Magenta + 97-112: Cyan + 113-128: Gray/White + + 8 COLORS + 9-16: Green + 33-40: Yellow + 41-48: Magenta + 49-56: Cyan + 57-64: Gray/White */ + + if (bg_color == nh_color) + color = 1; /* Make foreground black if fg==bg */ + + if (bg_color == CLR_RED || bg_color == CLR_BLUE) { + /* already defined before extension */ + color *= 2; + color += 16; + if (bg_color == CLR_RED) + color--; + } else { + boolean hicolor = FALSE; + if (COLORS >= 16) + hicolor = TRUE; + + switch (bg_color) { + case CLR_GREEN: + color = (hicolor ? 48 : 8) + color; + break; + case CLR_BROWN: + color = (hicolor ? 64 : 32) + color; + break; + case CLR_MAGENTA: + color = (hicolor ? 80 : 40) + color; + break; + case CLR_CYAN: + color = (hicolor ? 96 : 48) + color; + break; + case CLR_GRAY: + color = (hicolor ? 112 : 56) + color; + break; + default: + break; + } + } + } + cattr |= COLOR_PAIR(color); + + return cattr; +} + +/* Returns a complete curses attribute. Used to possibly bold/underline/etc HP/Pw. */ +#ifdef STATUS_COLORS +static attr_t +hpen_color_attr(boolean is_hp, int cur, int max) +{ + struct color_option stat_color; + int count; + attr_t attr = 0; + if (!iflags.use_status_colors) + return curses_color_attr(CLR_GRAY, 0); + + stat_color = percentage_color_of(cur, max, is_hp ? hp_colors : pw_colors); + + if (stat_color.color != NO_COLOR) + attr |= curses_color_attr(stat_color.color, 0); + + for (count = 0; (1 << count) <= stat_color.attr_bits; count++) { + if (count != ATR_NONE && (stat_color.attr_bits & (1 << count))) + attr |= curses_convert_attr(count); + } + + return attr; +} +#endif + +/* Return color for the HP bar. + With status colors ON, this respect its configuration (defaulting to gray), but + only obeys the color (no weird attributes for the HP bar). + With status colors OFF, this returns reasonable defaults which are also used + for the HP/Pw text itself. */ +static int +hpen_color(boolean is_hp, int cur, int max) +{ +#ifdef STATUS_COLORS + if (iflags.use_status_colors) { + struct color_option stat_color; + stat_color = percentage_color_of(cur, max, is_hp ? hp_colors : pw_colors); + + if (stat_color.color == NO_COLOR) + return CLR_GRAY; + else + return stat_color.color; + } else + return CLR_GRAY; +#endif + + int color = CLR_GRAY; + if (cur == max) + color = CLR_GRAY; + else if (cur * 3 > max * 2) /* >2/3 */ + color = is_hp ? CLR_GREEN : CLR_CYAN; + else if (cur * 3 > max) /* >1/3 */ + color = is_hp ? CLR_YELLOW : CLR_BLUE; + else if (cur * 7 > max) /* >1/7 */ + color = is_hp ? CLR_RED : CLR_MAGENTA; + else + color = is_hp ? CLR_ORANGE : CLR_BRIGHT_MAGENTA; + + return color; +} + +/* Draws a bar + is_hp: TRUE if we're drawing HP, Pw otherwise (determines colors) + cur/max: Current/max HP/Pw + title: Not NULL if we are drawing as part of an existing title. + Otherwise, the format is as follows: [ 11 / 11 ] */ +static void +draw_bar(boolean is_hp, int cur, int max, const char *title) +{ + WINDOW *win = curses_get_nhwin(STATUS_WIN); + +#ifdef STATUS_COLORS + if (!iflags.hitpointbar) { + wprintw(win, "%s", !title ? "---" : title); + return; + } +#endif + + char buf[BUFSZ]; + if (title) + Strcpy(buf, title); + else { + int len = 5; + sprintf(buf, "%*d / %-*d", len, cur, len, max); + } + + /* Colors */ + attr_t fillattr, attr; + int color = hpen_color(is_hp, cur, max); + int invcolor = color & 7; + + fillattr = curses_color_attr(color, invcolor); + attr = curses_color_attr(color, 0); + + /* Figure out how much of the bar to fill */ + int fill = 0; + int len = strlen(buf); + if (cur > 0 && max > 0) + fill = len * cur / max; + if (fill > len) + fill = len; + + waddch(win, '['); + wattron(win, fillattr); + wprintw(win, "%.*s", fill, buf); + wattroff(win, fillattr); + wattron(win, attr); + wprintw(win, "%.*s", len - fill, &buf[fill]); + wattroff(win, attr); + waddch(win, ']'); +} + +/* Update the status win - this is called when NetHack would normally + write to the status window, so we know somwthing has changed. We + override the write and update what needs to be updated ourselves. */ +void +curses_update_stats(void) +{ + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + /* Clear the window */ + werase(win); + + int orient = curses_get_window_orientation(STATUS_WIN); + + boolean horiz = FALSE; + if ((orient != ALIGN_RIGHT) && (orient != ALIGN_LEFT)) + horiz = TRUE; + boolean border = curses_window_has_border(STATUS_WIN); + + /* Figure out if we have proper window dimensions for horizontal statusbar. */ + if (horiz) { + /* correct y */ + int cy = 3; + if (iflags.statuslines < 3) + cy = 2; + + /* actual y (and x) */ + int ax = 0; + int ay = 0; + getmaxyx(win, ay, ax); + if (border) + ay -= 2; + + if (cy != ay) { + curses_create_main_windows(); + curses_last_messages(); + doredraw(); + + /* Reset XP highlight (since classic_status and new show different numbers) */ + prevexp.highlight_turns = 0; + curses_update_stats(); + return; + } + } + + /* Starting x/y. Passed to draw_horizontal/draw_vertical to keep track of + window positioning. */ + int x = 0; + int y = 0; + + /* Don't start at border position if applicable */ + if (border) { + x++; + y++; + } + + /* Get HP values. */ + int hp = u.uhp; + int hpmax = u.uhpmax; + if (Upolyd) { + hp = u.mh; + hpmax = u.mhmax; + } + + if (orient != ALIGN_RIGHT && orient != ALIGN_LEFT) + draw_horizontal(x, y, hp, hpmax); + else + draw_vertical(x, y, hp, hpmax); + + if (border) + box(win, 0, 0); + + wnoutrefresh(win); + + if (first) { + first = FALSE; + + /* Zero highlight timers. This will call curses_update_status again if needed */ + curses_decrement_highlights(TRUE); + } +} + +static void +draw_horizontal(int x, int y, int hp, int hpmax) +{ + if (iflags.statuslines >= 3) { + /* Draw new-style statusbar */ + draw_horizontal_new(x, y, hp, hpmax); + return; + } + char buf[BUFSZ]; + char rank[BUFSZ]; + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + /* Line 1 */ + wmove(win, y, x); + + get_playerrank(rank); + sprintf(buf, "%s the %s", plname, rank); + + /* Use the title as HP bar (similar to hitpointbar) */ + draw_bar(TRUE, hp, hpmax, buf); + + /* Attributes */ + print_statdiff(" St:", &prevstr, ACURR(A_STR), STAT_STR); + print_statdiff(" Dx:", &prevdex, ACURR(A_DEX), STAT_OTHER); + print_statdiff(" Co:", &prevcon, ACURR(A_CON), STAT_OTHER); + print_statdiff(" In:", &prevint, ACURR(A_INT), STAT_OTHER); + print_statdiff(" Wi:", &prevwis, ACURR(A_WIS), STAT_OTHER); + print_statdiff(" Ch:", &prevcha, ACURR(A_CHA), STAT_OTHER); + + wprintw(win, (u.ualign.type == A_CHAOTIC ? " Chaotic" : + u.ualign.type == A_NEUTRAL ? " Neutral" : " Lawful")); + +#ifdef SCORE_ON_BOTL + if (flags.showscore) + print_statdiff(" S:", &prevscore, botl_score(), STAT_OTHER); +#endif /* SCORE_ON_BOTL */ + + + /* Line 2 */ + y++; + wmove(win, y, x); + + describe_level(buf); + + wprintw(win, "%s", buf); + + print_statdiff("$", &prevau, money_cnt(invent), STAT_GOLD); + + /* HP/Pw use special coloring rules */ + attr_t hpattr, pwattr; +#ifdef STATUS_COLORS + hpattr = hpen_color_attr(TRUE, hp, hpmax); + pwattr = hpen_color_attr(FALSE, u.uen, u.uenmax); +#else + int hpcolor, pwcolor; + hpcolor = hpen_color(TRUE, hp, hpmax); + pwcolor = hpen_color(FALSE, u.uen, u.uenmax); + hpattr = curses_color_attr(hpcolor, 0); + pwattr = curses_color_attr(pwcolor, 0); +#endif + wprintw(win, " HP:"); + wattron(win, hpattr); + wprintw(win, "%d(%d)", (hp < 0) ? 0 : hp, hpmax); + wattroff(win, hpattr); + + wprintw(win, " Pw:"); + wattron(win, pwattr); + wprintw(win, "%d(%d)", u.uen, u.uenmax); + wattroff(win, pwattr); + + print_statdiff(" AC:", &prevac, u.uac, STAT_AC); + + if (Upolyd) + print_statdiff(" HD:", &prevlevel, mons[u.umonnum].mlevel, STAT_OTHER); + else if (flags.showexp) { + print_statdiff(" Xp:", &prevlevel, u.ulevel, STAT_OTHER); + /* use waddch, we don't want to highlight the '/' */ + waddch(win, '/'); + print_statdiff("", &prevexp, u.uexp, STAT_OTHER); + } + else + print_statdiff(" Exp:", &prevlevel, u.ulevel, STAT_OTHER); + + if (flags.time) + print_statdiff(" T:", &prevtime, moves, STAT_TIME); + + curses_add_statuses(win, FALSE, FALSE, NULL, NULL); +} + +static void +draw_horizontal_new(int x, int y, int hp, int hpmax) +{ + char buf[BUFSZ]; + char rank[BUFSZ]; + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + /* Line 1 */ + wmove(win, y, x); + + get_playerrank(rank); + char race[BUFSZ]; + Strcpy(race, urace.adj); + race[0] = highc(race[0]); + wprintw(win, "%s the %s %s%s%s", plname, + (u.ualign.type == A_CHAOTIC ? "Chaotic" : + u.ualign.type == A_NEUTRAL ? "Neutral" : "Lawful"), + Upolyd ? "" : race, Upolyd ? "" : " ", + rank); + + /* Line 2 */ + y++; + wmove(win, y, x); + wprintw(win, "HP:"); + draw_bar(TRUE, hp, hpmax, NULL); + print_statdiff(" AC:", &prevac, u.uac, STAT_AC); + if (Upolyd) + print_statdiff(" HD:", &prevlevel, mons[u.umonnum].mlevel, STAT_OTHER); + else if (flags.showexp) { + /* Ensure that Xp have proper highlight on level change. */ + int levelchange = 0; + if (prevlevel.value != u.ulevel) { + if (prevlevel.value < u.ulevel) + levelchange = 1; + else + levelchange = 2; + } + print_statdiff(" Xp:", &prevlevel, u.ulevel, STAT_OTHER); + /* use waddch, we don't want to highlight the '/' */ + waddch(win, '('); + + /* Figure out amount of Xp needed to next level */ + int xp_left = 0; + if (u.ulevel < 30) + xp_left = (newuexp(u.ulevel) - u.uexp); + + if (levelchange) { + prevexp.value = (xp_left + 1); + if (levelchange == 2) + prevexp.value = (xp_left - 1); + } + print_statdiff("", &prevexp, xp_left, STAT_AC); + waddch(win, ')'); + } + else + print_statdiff(" Exp:", &prevlevel, u.ulevel, STAT_OTHER); + + waddch(win, ' '); + describe_level(buf); + + wprintw(win, "%s", buf); + + /* Line 3 */ + y++; + wmove(win, y, x); + wprintw(win, "Pw:"); + draw_bar(FALSE, u.uen, u.uenmax, NULL); + + print_statdiff(" $", &prevau, money_cnt(invent), STAT_GOLD); + +#ifdef SCORE_ON_BOTL + if (flags.showscore) + print_statdiff(" S:", &prevscore, botl_score(), STAT_OTHER); +#endif /* SCORE_ON_BOTL */ + + if (flags.time) + print_statdiff(" T:", &prevtime, moves, STAT_TIME); + + curses_add_statuses(win, TRUE, FALSE, &x, &y); + + /* Right-aligned attributes */ + int stat_length = 6; /* " Dx:xx" */ + int str_length = 6; + if (ACURR(A_STR) > 18 && ACURR(A_STR) < 119) + str_length = 9; + + getmaxyx(win, y, x); + + /* We want to deal with top line of y. getmaxx would do what we want, but it only + exist for compatibility reasons and might not exist at all in some versions. */ + y = 0; + if (curses_window_has_border(STATUS_WIN)) { + x--; + y++; + } + + x -= stat_length; + int orig_x = x; + wmove(win, y, x); + print_statdiff(" Co:", &prevcon, ACURR(A_CON), STAT_OTHER); + x -= stat_length; + wmove(win, y, x); + print_statdiff(" Dx:", &prevdex, ACURR(A_DEX), STAT_OTHER); + x -= str_length; + wmove(win, y, x); + print_statdiff(" St:", &prevstr, ACURR(A_STR), STAT_STR); + + x = orig_x; + y++; + wmove(win, y, x); + print_statdiff(" Ch:", &prevcha, ACURR(A_CHA), STAT_OTHER); + x -= stat_length; + wmove(win, y, x); + print_statdiff(" Wi:", &prevwis, ACURR(A_WIS), STAT_OTHER); + x -= str_length; + wmove(win, y, x); + print_statdiff(" In:", &prevint, ACURR(A_INT), STAT_OTHER); +} + +/* Personally I never understood the point of a vertical status bar. But removing the + option would be silly, so keep the functionality. */ +static void +draw_vertical(int x, int y, int hp, int hpmax) +{ + char buf[BUFSZ]; + char rank[BUFSZ]; + WINDOW *win = curses_get_nhwin(STATUS_WIN); + + /* Print title and dungeon branch */ + wmove(win, y++, x); + + get_playerrank(rank); + int ranklen = strlen(rank); + int namelen = strlen(plname); + int maxlen = 19; +#ifdef STATUS_COLORS + if (!iflags.hitpointbar) + maxlen += 2; /* With no hitpointbar, we can fit more since there's no "[]" */ +#endif + + if ((ranklen + namelen) > maxlen) { + /* The result doesn't fit. Strip name if >10 characters, then strip title */ + if (namelen > 10) { + while (namelen > 10 && (ranklen + namelen) > maxlen) + namelen--; + } + + while ((ranklen + namelen) > maxlen) + ranklen--; /* Still doesn't fit, strip rank */ + } + sprintf(buf, "%-*s the %-*s", namelen, plname, ranklen, rank); + draw_bar(TRUE, hp, hpmax, buf); + wmove(win, y++, x); + wprintw(win, "%s", dungeons[u.uz.dnum].dname); + + y++; /* Blank line inbetween */ + wmove(win, y++, x); + + /* Attributes. Old vertical order is preserved */ + print_statdiff("Strength: ", &prevstr, ACURR(A_STR), STAT_STR); + wmove(win, y++, x); + print_statdiff("Intelligence: ", &prevint, ACURR(A_INT), STAT_OTHER); + wmove(win, y++, x); + print_statdiff("Wisdom: ", &prevwis, ACURR(A_WIS), STAT_OTHER); + wmove(win, y++, x); + print_statdiff("Dexterity: ", &prevdex, ACURR(A_DEX), STAT_OTHER); + wmove(win, y++, x); + print_statdiff("Constitution: ", &prevcon, ACURR(A_CON), STAT_OTHER); + wmove(win, y++, x); + print_statdiff("Charisma: ", &prevcha, ACURR(A_CHA), STAT_OTHER); + wmove(win, y++, x); + wprintw(win, "Alignment: "); + wprintw(win, (u.ualign.type == A_CHAOTIC ? "Chaotic" : + u.ualign.type == A_NEUTRAL ? "Neutral" : "Lawful")); + wmove(win, y++, x); + wprintw(win, "Dungeon Level: "); + + /* Astral Plane doesn't fit */ + if (In_endgame(&u.uz)) + wprintw(win, "%s", Is_astralevel(&u.uz) ? "Astral" : "End Game"); + else + wprintw(win, "%d", depth(&u.uz)); + wmove(win, y++, x); + + print_statdiff("Gold: ", &prevau, money_cnt(invent), STAT_GOLD); + wmove(win, y++, x); + + /* HP/Pw use special coloring rules */ + attr_t hpattr, pwattr; +#ifdef STATUS_COLORS + hpattr = hpen_color_attr(TRUE, hp, hpmax); + pwattr = hpen_color_attr(FALSE, u.uen, u.uenmax); +#else + int hpcolor, pwcolor; + hpcolor = hpen_color(TRUE, hp, hpmax); + pwcolor = hpen_color(FALSE, u.uen, u.uenmax); + hpattr = curses_color_attr(hpcolor, 0); + pwattr = curses_color_attr(pwcolor, 0); +#endif + + wprintw(win, "Hit Points: "); + wattron(win, hpattr); + wprintw(win, "%d/%d", (hp < 0) ? 0 : hp, hpmax); + wattroff(win, hpattr); + wmove(win, y++, x); + + wprintw(win, "Magic Power: "); + wattron(win, pwattr); + wprintw(win, "%d/%d", u.uen, u.uenmax); + wattroff(win, pwattr); + wmove(win, y++, x); + + print_statdiff("Armor Class: ", &prevac, u.uac, STAT_AC); + wmove(win, y++, x); + + if (Upolyd) + print_statdiff("Hit Dice: ", &prevlevel, mons[u.umonnum].mlevel, STAT_OTHER); + else if (flags.showexp) { + print_statdiff("Experience: ", &prevlevel, u.ulevel, STAT_OTHER); + /* use waddch, we don't want to highlight the '/' */ + waddch(win, '/'); + print_statdiff("", &prevexp, u.uexp, STAT_OTHER); + } + else + print_statdiff("Level: ", &prevlevel, u.ulevel, STAT_OTHER); + wmove(win, y++, x); + + if (flags.time) { + print_statdiff("Time: ", &prevtime, moves, STAT_TIME); + wmove(win, y++, x); + } + +#ifdef SCORE_ON_BOTL + if (flags.showscore) { + print_statdiff("Score: ", &prevscore, botl_score(), STAT_OTHER); + wmove(win, y++, x); + } +#endif /* SCORE_ON_BOTL */ + + curses_add_statuses(win, FALSE, TRUE, &x, &y); +} + +static void +curses_add_statuses(WINDOW *win, boolean align_right, + boolean vertical, int *x, int *y) +{ + if (align_right) { + /* Right-aligned statuses. Since add_status decrease one x more + (to separate them with spaces), add 1 to x unless we have borders + (which would offset what add_status does) */ + int mx = *x; + int my = *y; + getmaxyx(win, my, mx); + if (!curses_window_has_border(STATUS_WIN)) + mx++; + + *x = mx; + } + +#define statprob(str, trouble) \ + curses_add_status(win, align_right, vertical, x, y, str, trouble) + + /* Hunger */ + statprob(hu_stat[u.uhs], u.uhs != 1); /* 1 is NOT_HUNGRY (not defined here) */ + + /* General troubles */ + statprob("Conf", Confusion); + statprob("Blind", Blind); + statprob("Stun", Stunned); + statprob("Hallu", Hallucination); + statprob("Ill", (u.usick_type & SICK_NONVOMITABLE)); + statprob("FoodPois", (u.usick_type & SICK_VOMITABLE)); + statprob("Slime", Slimed); + + /* Encumbrance */ + int enc = near_capacity(); + statprob(enc_stat[enc], enc > UNENCUMBERED); +#undef statprob +} + +static void +curses_add_status(WINDOW *win, boolean align_right, boolean vertical, + int *x, int *y, const char *str, int trouble) +{ + /* If vertical is TRUE here with no x/y, that's an error. But handle + it gracefully since NH3 doesn't recover well in crashes. */ + if (!x || !y) + vertical = FALSE; + + if (!trouble) + return; + + if (!vertical && !align_right) + waddch(win, ' '); + + /* For whatever reason, hunger states have trailing spaces. Get rid of them. */ + char buf[BUFSZ]; + Strcpy(buf, str); + int i; + for (i = 0; (buf[i] != ' ' && buf[i] != '\0'); i++) ; + + buf[i] = '\0'; + if (align_right) { + *x -= (strlen(buf) + 1); /* add spacing */ + wmove(win, *y, *x); + } + + draw_trouble_str(buf); + + if (vertical) { + wmove(win, *y, *x); + *y += 1; /* ++ advances the pointer addr */ + } +} + +/* Decrement a single highlight, return 1 if decremented to zero. zero is TRUE if we're + zeroing the highlight. */ +static int +decrement_highlight(nhstat *stat, boolean zero) +{ + if (stat->highlight_turns > 0) { + if (zero) { + stat->highlight_turns = 0; + return 1; + } + + stat->highlight_turns--; + if (stat->highlight_turns == 0) + return 1; + } + return 0; +} + +/* Decrement the highlight_turns for all stats. Call curses_update_stats + if needed to unhighlight a stat */ +void +curses_decrement_highlights(boolean zero) +{ + int unhighlight = 0; + + unhighlight |= decrement_highlight(&prevdepth, zero); + unhighlight |= decrement_highlight(&prevstr, zero); + unhighlight |= decrement_highlight(&prevdex, zero); + unhighlight |= decrement_highlight(&prevcon, zero); + unhighlight |= decrement_highlight(&prevint, zero); + unhighlight |= decrement_highlight(&prevwis, zero); + unhighlight |= decrement_highlight(&prevcha, zero); + unhighlight |= decrement_highlight(&prevau, zero); + unhighlight |= decrement_highlight(&prevlevel, zero); + unhighlight |= decrement_highlight(&prevac, zero); + unhighlight |= decrement_highlight(&prevexp, zero); + unhighlight |= decrement_highlight(&prevtime, zero); +#ifdef SCORE_ON_BOTL + unhighlight |= decrement_highlight(&prevscore, zero); +#endif + + if (unhighlight) + curses_update_stats(); +} +#endif diff --git a/win/curses/cursstat.h b/win/curses/cursstat.h new file mode 100644 index 000000000..7773d422b --- /dev/null +++ b/win/curses/cursstat.h @@ -0,0 +1,21 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSSTAT_H +# define CURSSTAT_H + +/* Used by handle_stat_change to handle some stats differently. Not an enum + because this is how NetHack code generally handles them. */ +# define STAT_OTHER 0 +# define STAT_STR 1 +# define STAT_GOLD 2 +# define STAT_AC 4 +# define STAT_TIME 5 +# define STAT_TROUBLE 6 + +/* Global declarations */ + +void curses_update_stats(); +void curses_decrement_highlights(boolean); +attr_t curses_color_attr(int nh_color, int bg_color); + +#endif /* CURSSTAT_H */ diff --git a/win/curses/curswins.c b/win/curses/curswins.c new file mode 100644 index 000000000..b04fd94c7 --- /dev/null +++ b/win/curses/curswins.c @@ -0,0 +1,752 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#include "curses.h" +#include "hack.h" +#include "wincurs.h" +#include "curswins.h" + +/* Window handling for curses interface */ + +/* Private declarations */ + +typedef struct nhw { + winid nhwin; /* NetHack window id */ + WINDOW *curwin; /* Curses window pointer */ + int width; /* Usable width not counting border */ + int height; /* Usable height not counting border */ + int x; /* start of window on terminal (left) */ + int y; /* start of window on termial (top) */ + int orientation; /* Placement of window relative to map */ + boolean border; /* Whether window has a visible border */ +} nethack_window; + +typedef struct nhwd { + winid nhwid; /* NetHack window id */ + struct nhwd *prev_wid; /* Pointer to previous entry */ + struct nhwd *next_wid; /* Pointer to next entry */ +} nethack_wid; + +typedef struct nhchar { + int ch; /* character */ + int color; /* color info for character */ + int attr; /* attributes of character */ +} nethack_char; + +static boolean map_clipped; /* Map window smaller than 80x21 */ +static nethack_window nhwins[NHWIN_MAX]; /* NetHack window array */ +static nethack_char map[ROWNO][COLNO]; /* Map window contents */ +static nethack_wid *nhwids = NULL; /* NetHack wid array */ + +static boolean is_main_window(winid wid); +static void write_char(WINDOW * win, int x, int y, nethack_char ch); +static void clear_map(void); + +/* Create a window with the specified size and orientation */ + +WINDOW * +curses_create_window(int width, int height, orient orientation) +{ + int mapx, mapy, maph, mapw = 0; + int startx = 0; + int starty = 0; + WINDOW *win; + boolean map_border = FALSE; + int mapb_offset = 0; + + if ((orientation == UP) || (orientation == DOWN) || + (orientation == LEFT) || (orientation == RIGHT)) { + if (invent || (moves > 1)) { + map_border = curses_window_has_border(MAP_WIN); + curses_get_window_xy(MAP_WIN, &mapx, &mapy); + curses_get_window_size(MAP_WIN, &maph, &mapw); + } else { + map_border = TRUE; + mapx = 0; + mapy = 0; + maph = term_rows; + mapw = term_cols; + } + } + + if (map_border) { + mapb_offset = 1; + } + + width += 2; /* leave room for bounding box */ + height += 2; + + if ((width > term_cols) || (height > term_rows)) + panic("curses_create_window: Terminal too small for dialog window"); + switch (orientation) { + case CENTER: + startx = (term_cols / 2) - (width / 2); + starty = (term_rows / 2) - (height / 2); + break; + case UP: + if (invent || (moves > 1)) { + startx = (mapw / 2) - (width / 2) + mapx + mapb_offset; + } else { + startx = 0; + } + + starty = mapy + mapb_offset; + break; + case DOWN: + if (invent || (moves > 1)) { + startx = (mapw / 2) - (width / 2) + mapx + mapb_offset; + } else { + startx = 0; + } + + starty = height - mapy - 1 - mapb_offset; + break; + case LEFT: + if (map_border && (width < term_cols)) + startx = 1; + else + startx = 0; + starty = term_rows - height; + break; + case RIGHT: + if (invent || (moves > 1)) { + startx = (mapw + mapx + (mapb_offset * 2)) - width; + } else { + startx = term_cols - width; + } + + starty = 0; + break; + default: + panic("curses_create_window: Bad orientation"); + break; + } + + if (startx < 0) { + startx = 0; + } + + if (starty < 0) { + starty = 0; + } + + win = newwin(height, width, starty, startx); + curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, ON); + box(win, 0, 0); + curses_toggle_color_attr(win, DIALOG_BORDER_COLOR, NONE, OFF); + return win; +} + + +/* Erase and delete curses window, and refresh standard windows */ + +void +curses_destroy_win(WINDOW * win) +{ + werase(win); + wrefresh(win); + delwin(win); + curses_refresh_nethack_windows(); +} + + +/* Refresh nethack windows if they exist, or base window if not */ + +void +curses_refresh_nethack_windows() +{ + WINDOW *status_window, *message_window, *map_window, *inv_window; + + status_window = curses_get_nhwin(STATUS_WIN); + message_window = curses_get_nhwin(MESSAGE_WIN); + map_window = curses_get_nhwin(MAP_WIN); + inv_window = curses_get_nhwin(INV_WIN); + + if ((moves <= 1) && !invent) { + /* Main windows not yet displayed; refresh base window instead */ + touchwin(stdscr); + refresh(); + } else { + touchwin(status_window); + wnoutrefresh(status_window); + touchwin(map_window); + wnoutrefresh(map_window); + touchwin(message_window); + wnoutrefresh(message_window); + if (inv_window) { + touchwin(inv_window); + wnoutrefresh(inv_window); + } + doupdate(); + } +} + + +/* Return curses window pointer for given NetHack winid */ + +WINDOW * +curses_get_nhwin(winid wid) +{ + if (!is_main_window(wid)) { + panic("curses_get_nhwin: wid out of range. Not a main window."); + } + + return nhwins[wid].curwin; +} + + +/* Add curses window pointer and window info to list for given NetHack winid */ + +void +curses_add_nhwin(winid wid, int height, int width, int y, int x, + orient orientation, boolean border) +{ + WINDOW *win; + int real_width = width; + int real_height = height; + + if (!is_main_window(wid)) { + panic("curses_add_nhwin: wid out of range. Not a main window."); + } + + nhwins[wid].nhwin = wid; + nhwins[wid].border = border; + nhwins[wid].width = width; + nhwins[wid].height = height; + nhwins[wid].x = x; + nhwins[wid].y = y; + nhwins[wid].orientation = orientation; + + if (border) { + real_width += 2; /* leave room for bounding box */ + real_height += 2; + } + + win = newwin(real_height, real_width, y, x); + + switch (wid) { + case MESSAGE_WIN: + messagewin = win; + break; + case STATUS_WIN: + statuswin = win; + break; + case MAP_WIN: + mapwin = win; + + if ((width < COLNO) || (height < ROWNO)) { + map_clipped = TRUE; + } else { + map_clipped = FALSE; + } + + break; + } + + if (border) { + box(win, 0, 0); + } + + nhwins[wid].curwin = win; +} + + +/* Add wid to list of known window IDs */ + +void +curses_add_wid(winid wid) +{ + nethack_wid *new_wid; + nethack_wid *widptr = nhwids; + + new_wid = malloc(sizeof (nethack_wid)); + new_wid->nhwid = wid; + + new_wid->next_wid = NULL; + + if (widptr == NULL) { + new_wid->prev_wid = NULL; + nhwids = new_wid; + } else { + while (widptr->next_wid != NULL) { + widptr = widptr->next_wid; + } + new_wid->prev_wid = widptr; + widptr->next_wid = new_wid; + } +} + + +/* refresh a curses window via given nethack winid */ + +void +curses_refresh_nhwin(winid wid) +{ + wnoutrefresh(curses_get_nhwin(wid)); + doupdate(); +} + + +/* Delete curses window via given NetHack winid and remove entry from list */ + +void +curses_del_nhwin(winid wid) +{ + if (curses_is_menu(wid) || curses_is_text(wid)) { + curses_del_menu(wid); + return; + } + + if (!is_main_window(wid)) { + panic("curses_del_nhwin: wid out of range. Not a main window."); + } + + nhwins[wid].curwin = NULL; + nhwins[wid].nhwin = -1; +} + + +/* Delete wid from list of known window IDs */ + +void +curses_del_wid(winid wid) +{ + nethack_wid *tmpwid; + nethack_wid *widptr = nhwids; + + if (curses_is_menu(wid) || curses_is_text(wid)) { + curses_del_menu(wid); + } + + while (widptr != NULL) { + if (widptr->nhwid == wid) { + if (widptr->prev_wid != NULL) { + tmpwid = widptr->prev_wid; + tmpwid->next_wid = widptr->next_wid; + } else { + nhwids = widptr->next_wid; /* New head mode, or NULL */ + } + if (widptr->next_wid != NULL) { + tmpwid = widptr->next_wid; + tmpwid->prev_wid = widptr->prev_wid; + } + free(widptr); + break; + } + widptr = widptr->next_wid; + } +} + + +/* Print a single character in the given window at the given coordinates */ + +void +curses_putch(winid wid, int x, int y, int ch, int color, int attr) +{ + int sx, sy, ex, ey; + boolean border = curses_window_has_border(wid); + nethack_char nch; + static boolean map_initted = FALSE; +/* + if (wid == STATUS_WIN) { + curses_update_stats(); + } +*/ + if (wid != MAP_WIN) { + return; + } + + if (!map_initted) { + clear_map(); + map_initted = TRUE; + } + + map[y][x].ch = ch; + map[y][x].color = color; + map[y][x].attr = attr; + nch = map[y][x]; + + (void) curses_map_borders(&sx, &sy, &ex, &ey, -1, -1); + + if ((x >= sx) && (x <= ex) && (y >= sy) && (y <= ey)) { + if (border) { + x++; + y++; + } + + write_char(mapwin, x - sx, y - sy, nch); + } + /* refresh after every character? + * Fair go, mate! Some of us are playing from Australia! */ + /* wrefresh(mapwin); */ +} + + +/* Get x, y coordinates of curses window on the physical terminal window */ + +void +curses_get_window_xy(winid wid, int *x, int *y) +{ + if (!is_main_window(wid)) { + panic("curses_get_window_xy: wid out of range. Not a main window."); + } + + *x = nhwins[wid].x; + *y = nhwins[wid].y; +} + + +/* Get usable width and height curses window on the physical terminal window */ + +void +curses_get_window_size(winid wid, int *height, int *width) +{ + *height = nhwins[wid].height; + *width = nhwins[wid].width; +} + + +/* Determine if given window has a visible border */ + +boolean +curses_window_has_border(winid wid) +{ + return nhwins[wid].border; +} + + +/* Determine if window for given winid exists */ + +boolean +curses_window_exists(winid wid) +{ + nethack_wid *widptr = nhwids; + + while (widptr != NULL) { + if (widptr->nhwid == wid) { + return TRUE; + } + + widptr = widptr->next_wid; + } + + return FALSE; +} + + +/* Return the orientation of the specified window */ + +int +curses_get_window_orientation(winid wid) +{ + if (!is_main_window(wid)) { + panic + ("curses_get_window_orientation: wid out of range. Not a main window."); + } + + return nhwins[wid].orientation; +} + + +/* Output a line of text to specified NetHack window with given coordinates +and text attributes */ + +void +curses_puts(winid wid, int attr, const char *text) +{ + anything *identifier; + WINDOW *win = NULL; + + if (is_main_window(wid)) { + win = curses_get_nhwin(wid); + } + + if (wid == MESSAGE_WIN) { + curses_message_win_puts(text, FALSE); + return; + } + +#if 0 + if (wid == STATUS_WIN) { + curses_update_stats(); /* We will do the write ourselves */ + /* Inventory updating isn't performed on redraws, so + also update inventory here... */ + curses_update_inventory(); + return; + } +#endif + + if (curses_is_menu(wid) || curses_is_text(wid)) { + if (!curses_menu_exists(wid)) { + panic("curses_puts: Attempted write to nonexistant window!"); + } + identifier = malloc(sizeof (anything)); + identifier->a_void = NULL; + curses_add_nhmenu_item(wid, NO_GLYPH, identifier, 0, 0, attr, text, + FALSE); + } else { + waddstr(win, text); + wnoutrefresh(win); + } +} + + +/* Clear the contents of a window via the given NetHack winid */ + +void +curses_clear_nhwin(winid wid) +{ + WINDOW *win = curses_get_nhwin(wid); + boolean border = curses_window_has_border(wid); + + if (wid == MAP_WIN) { + clearok(win, TRUE); /* Redraw entire screen when refreshed */ + clear_map(); + } + + werase(win); + + if (border) { + box(win, 0, 0); + } +} + +/* Change colour of window border to alert player to something */ +void +curses_alert_win_border(winid wid, boolean onoff) +{ + WINDOW *win = curses_get_nhwin(wid); + + if (!win || !curses_window_has_border(wid)) + return; + if (onoff) + curses_toggle_color_attr(win, ALERT_BORDER_COLOR, NONE, ON); + box(win, 0, 0); + if (onoff) + curses_toggle_color_attr(win, ALERT_BORDER_COLOR, NONE, OFF); + wnoutrefresh(win); +} + + +void +curses_alert_main_borders(boolean onoff) +{ + curses_alert_win_border(MAP_WIN, onoff); + curses_alert_win_border(MESSAGE_WIN, onoff); + curses_alert_win_border(STATUS_WIN, onoff); + curses_alert_win_border(INV_WIN, onoff); +} + +/* Return true if given wid is a main NetHack window */ + +static boolean +is_main_window(winid wid) +{ + if ((wid == MESSAGE_WIN) || (wid == MAP_WIN) || (wid == STATUS_WIN) || wid == INV_WIN) { + return TRUE; + } else { + return FALSE; + } +} + + +/* Unconditionally write a single character to a window at the given +coordinates without a refresh. Currently only used for the map. */ + +static void +write_char(WINDOW * win, int x, int y, nethack_char nch) +{ + curses_toggle_color_attr(win, nch.color, nch.attr, ON); +#ifdef PDCURSES + mvwaddrawch(win, y, x, nch.ch); +#else + mvwaddch(win, y, x, nch.ch); +#endif + curses_toggle_color_attr(win, nch.color, nch.attr, OFF); +} + + +/* Draw the entire visible map onto the screen given the visible map +boundaries */ + +void +curses_draw_map(int sx, int sy, int ex, int ey) +{ + int curx, cury; + int bspace = 0; + +#ifdef MAP_SCROLLBARS + int sbsx, sbsy, sbex, sbey, count; + nethack_char hsb_back, hsb_bar, vsb_back, vsb_bar; +#endif + + if (curses_window_has_border(MAP_WIN)) { + bspace++; + } +#ifdef MAP_SCROLLBARS + hsb_back.ch = '-'; + hsb_back.color = SCROLLBAR_BACK_COLOR; + hsb_back.attr = A_NORMAL; + hsb_bar.ch = '*'; + hsb_bar.color = SCROLLBAR_COLOR; + hsb_bar.attr = A_NORMAL; + vsb_back.ch = '|'; + vsb_back.color = SCROLLBAR_BACK_COLOR; + vsb_back.attr = A_NORMAL; + vsb_bar.ch = '*'; + vsb_bar.color = SCROLLBAR_COLOR; + vsb_bar.attr = A_NORMAL; + + /* Horizontal scrollbar */ + if ((sx > 0) || (ex < (COLNO - 1))) { + sbsx = (sx * ((long) (ex - sx + 1) / COLNO)); + sbex = (ex * ((long) (ex - sx + 1) / COLNO)); + + for (count = 0; count < sbsx; count++) { + write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_back); + } + + for (count = sbsx; count <= sbex; count++) { + write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_bar); + } + + for (count = sbex + 1; count <= (ex - sx); count++) { + write_char(mapwin, count + bspace, ey - sy + 1 + bspace, hsb_back); + } + } + + /* Vertical scrollbar */ + if ((sy > 0) || (ey < (ROWNO - 1))) { + sbsy = (sy * ((long) (ey - sy + 1) / ROWNO)); + sbey = (ey * ((long) (ey - sy + 1) / ROWNO)); + + for (count = 0; count < sbsy; count++) { + write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_back); + } + + for (count = sbsy; count <= sbey; count++) { + write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_bar); + } + + for (count = sbey + 1; count <= (ey - sy); count++) { + write_char(mapwin, ex - sx + 1 + bspace, count + bspace, vsb_back); + } + } +#endif /* MAP_SCROLLBARS */ + + for (curx = sx; curx <= ex; curx++) { + for (cury = sy; cury <= ey; cury++) { + write_char(mapwin, curx - sx + bspace, cury - sy + bspace, + map[cury][curx]); + } + } +} + + +/* Init map array to blanks */ + +static void +clear_map() +{ + int x, y; + + for (x = 0; x < COLNO; x++) { + for (y = 0; y < ROWNO; y++) { + map[y][x].ch = ' '; + map[y][x].color = NO_COLOR; + map[y][x].attr = A_NORMAL; + } + } +} + + +/* Determine visible boundaries of map, and determine if it needs to be +based on the location of the player. */ + +boolean +curses_map_borders(int *sx, int *sy, int *ex, int *ey, int ux, int uy) +{ + static int width = 0; + static int height = 0; + static int osx = 0; + static int osy = 0; + static int oex = 0; + static int oey = 0; + static int oux = -1; + static int ouy = -1; + + if ((oux == -1) || (ouy == -1)) { + oux = u.ux; + ouy = u.uy; + } + + if (ux == -1) { + ux = oux; + } else { + oux = ux; + } + + if (uy == -1) { + uy = ouy; + } else { + ouy = uy; + } + + curses_get_window_size(MAP_WIN, &height, &width); + +#ifdef MAP_SCROLLBARS + if (width < COLNO) { + height--; /* room for horizontal scrollbar */ + } + + if (height < ROWNO) { + width--; /* room for vertical scrollbar */ + + if (width == COLNO) { + height--; + } + } +#endif /* MAP_SCROLLBARS */ + + if (width >= COLNO) { + *sx = 0; + *ex = COLNO - 1; + } else { + *ex = (width / 2) + ux; + *sx = *ex - (width - 1); + + if (*ex >= COLNO) { + *sx = COLNO - width; + *ex = COLNO - 1; + } else if (*sx < 0) { + *sx = 0; + *ex = width - 1; + } + } + + if (height >= ROWNO) { + *sy = 0; + *ey = ROWNO - 1; + } else { + *ey = (height / 2) + uy; + *sy = *ey - (height - 1); + + if (*ey >= ROWNO) { + *sy = ROWNO - height; + *ey = ROWNO - 1; + } else if (*sy < 0) { + *sy = 0; + *ey = height - 1; + } + } + + if ((*sx != osx) || (*sy != osy) || (*ex != oex) || (*ey != oey) || + map_clipped) { + osx = *sx; + osy = *sy; + oex = *ex; + oey = *ey; + return TRUE; + } + + return FALSE; +} diff --git a/win/curses/curswins.h b/win/curses/curswins.h new file mode 100644 index 000000000..ce1935e07 --- /dev/null +++ b/win/curses/curswins.h @@ -0,0 +1,33 @@ +/* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/ + +#ifndef CURSWIN_H +# define CURSWIN_H + + +/* Global declarations */ + +WINDOW *curses_create_window(int width, int height, orient orientation); + +void curses_destroy_win(WINDOW * win); +void curses_refresh_nethack_windows(void); +WINDOW *curses_get_nhwin(winid wid); +void curses_add_nhwin(winid wid, int height, int width, int y, int x, + orient orientation, boolean border); +void curses_add_wid(winid wid); +void curses_refresh_nhwin(winid wid); +void curses_del_nhwin(winid wid); +void curses_del_wid(winid wid); +void curses_putch(winid wid, int x, int y, int ch, int color, int attrs); +void curses_get_window_xy(winid wid, int *x, int *y); +boolean curses_window_has_border(winid wid); +boolean curses_window_exists(winid wid); +int curses_get_window_orientation(winid wid); +void curses_puts(winid wid, int attr, const char *text); +void curses_clear_nhwin(winid wid); +void curses_alert_win_border(winid wid, boolean onoff); +void curses_alert_main_borders(boolean onoff); +void curses_draw_map(int sx, int sy, int ex, int ey); +boolean curses_map_borders(int *sx, int *sy, int *ex, int *ey, int ux, int uy); + + +#endif /* CURSWIN_H */