From 7096c68492caa73df17c44b4865f6436da0fc3f3 Mon Sep 17 00:00:00 2001 From: PatR Date: Tue, 13 Dec 2022 15:59:05 -0800 Subject: [PATCH] more interactive role selection This ended up combining several unrelated changes. Add missing 'fixes' entry for curses-specific item in New Features. When answering "Shall I pick ... for you? [ynaq]", accept \m as well as \n and space for choosing the default of 'y', same as normal ynaq() would. Also add '*' to '@' as not-shown potential answers; they force 'random'. When tty tore down any of the menus, things were reasonable if they were short enough for corner windows, but tall ones that switch to full screen weren't fully erased. The parts of those outside of the map window stayed behind when the tall menu was closed and cleared. Mainly affects picking the "~ - reset filtering" choice but also affected the role menu on 24 line tty screens. (Didn't affect curses because it tracks and refreshes its base window when some overlaying window goes away.) The role menu used 25 lines so required a second page for the case of a 24 line screen on tty. Dealing with that is a bit ugly but it wasn't an issue when this form of role selection was tty-only (because the info about choices made so far was displayed on the base window rather than in an extra menu line back then) so I added a hack for it. If the role menu will take one more line than the screen height, the separator between 'random' (below 'Wizard') and 'pick race first' gets squeezed out. If the menu needs two more lines (doesn't happen now, except by changing screen size to 23 lines for testing), a second line gets squeezed out. (Not attempted for curses because it wouldn't help. 'windowborders' and one or two extra separators it adds make menus taller. I doubt if many players use curses on 24-line screens but if they do, they'll be using something new rather than going from something that used to fit on one page with 3.6.x.) --- doc/fixes3-7-0.txt | 1 + include/decl.h | 1 + include/extern.h | 2 +- src/role.c | 128 +++++++++++++++++++++++++----------------- win/curses/cursmain.c | 2 +- win/tty/wintty.c | 24 +++++--- 6 files changed, 95 insertions(+), 63 deletions(-) diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index bcda940cf..e8f7323b4 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -1534,6 +1534,7 @@ curses: if a menu of objects contains at least one iron ball, and player is not already in the midst of entering a count, recognize '0' as a group accelerator rather than the start of a count curses: obey timed_delay option +curses: support tty-style extended role/race/&c selection at start of new game macOS: Xcode project was failing to build if the path to the NetHack source tree contained a space; the issue was within some shell script code contained within the project diff --git a/include/decl.h b/include/decl.h index 4682ca49d..3a24c8370 100644 --- a/include/decl.h +++ b/include/decl.h @@ -101,6 +101,7 @@ struct sinfo { int in_self_recover; /* processsing orphaned level files */ int in_checkpoint; /* saving insurance checkpoint */ int in_parseoptions; /* in parseoptions */ + int in_role_selection; /* role/race/&c selection menus in progress */ int config_error_ready; /* config_error_add is ready, available */ int beyond_savefile_load; /* set when past savefile loading */ #ifdef PANICLOG diff --git a/include/extern.h b/include/extern.h index 3e446f7a6..795ac92ad 100644 --- a/include/extern.h +++ b/include/extern.h @@ -2418,7 +2418,7 @@ extern const char *Hello(struct monst *); extern const char *Goodbye(void); extern const struct Race *character_race(short); extern void genl_player_selection(void); -extern int genl_player_setup(void); +extern int genl_player_setup(int); /* ### rumors.c ### */ diff --git a/src/role.c b/src/role.c index 188125e3c..4301cb97a 100644 --- a/src/role.c +++ b/src/role.c @@ -2101,7 +2101,7 @@ character_race(short pmindex) void genl_player_selection(void) { - if (genl_player_setup()) + if (genl_player_setup(0)) return; /* player cancelled role/race/&c selection, so quit */ @@ -2113,7 +2113,8 @@ genl_player_selection(void) /* ['#else' far below] */ static boolean reset_role_filtering(void); -static winid plsel_startmenu(void); +static winid plsel_startmenu(int, int); +static int maybe_skip_seps(int, int); static void setup_rolemenu(winid, boolean, int, int, int); static void setup_racemenu(winid, boolean, int, int, int); static void setup_gendmenu(winid, boolean, int, int, int); @@ -2127,7 +2128,7 @@ static void setup_algnmenu(winid, boolean, int, int, int); /* guts of tty's player_selection() */ int -genl_player_setup(void) +genl_player_setup(int screenheight) { char pbuf[QBUFSZ]; anything any; @@ -2137,7 +2138,9 @@ genl_player_setup(void) menu_item *selected = 0; int clr = 0; char pick4u = 'n'; + int result = 0; /* assume failure (player chooses to 'quit') */ + gp.program_state.in_role_selection++; /* affects tty menu cleanup */ /* Used to avoid "Is this ok?" if player has already specified all * four facets of role. * Note that rigid_role_checks might force any unspecified facets to @@ -2183,13 +2186,13 @@ genl_player_setup(void) pick4u = yn_function(prompt, (char *) 0, '\0', FALSE); pick4u = lowc(pick4u); if (pick4u == '\033' || pick4u == 'q') /* handle [q] */ - return 0; - if (pick4u == ' ' || pick4u == '\n') /* default is 'y' */ - pick4u = 'y'; - else if (pick4u == '@') /* similar to '-@' on command line */ - pick4u = 'a'; + goto setup_done; + if (pick4u == ' ' || pick4u == '\n' || pick4u == '\r') + pick4u = 'y'; /* default */ + else if (pick4u == '@' || pick4u == '*') + pick4u = 'a'; /* similar to '-@' on command line */ /* TODO? handle response of '?' */ - } while (pick4u != 'y' && pick4u != 'a' && pick4u != 'n'); /* [yna] */ + } while (pick4u != 'y' && pick4u != 'n' && pick4u != 'a'); /* [yna] */ #else /* slightly simpler but more likely to end up being wrapped */ @@ -2203,7 +2206,7 @@ genl_player_setup(void) user's and to 'y', to 'q' */ pick4u = yn_function(prompt, ynaqchars, 'y', FALSE); if (pick4u != 'y' && pick4u != 'a' && pick4u != 'n') - return 0; /* bail */ + goto setup_done; /* bail */ #endif } @@ -2225,15 +2228,19 @@ genl_player_setup(void) k = randrole(FALSE); } } else { + /* 'excess' is used to try to avoid tty pagination */ + int excess = maybe_skip_seps(screenheight, RS_ROLE); + /* prompt for a role */ - win = plsel_startmenu(); + win = plsel_startmenu(screenheight, RS_ROLE); /* populate the menu with role choices */ setup_rolemenu(win, TRUE, RACE, GEND, ALGN); /* add miscellaneous menu entries */ role_menu_extra(ROLE_RANDOM, win, TRUE); any = cg.zeroany; /* separator, not a choice */ - add_menu(win, &nul_glyphinfo, &any, 0, 0, - ATR_NONE, clr, "", MENU_ITEMFLAGS_NONE); + if (excess < 1 || excess > 2) + add_menu(win, &nul_glyphinfo, &any, 0, 0, + ATR_NONE, clr, "", MENU_ITEMFLAGS_NONE); role_menu_extra(RS_RACE, win, FALSE); role_menu_extra(RS_GENDER, win, FALSE); role_menu_extra(RS_ALGNMNT, win, FALSE); @@ -2263,7 +2270,7 @@ genl_player_setup(void) destroy_nhwindow(win), win = WIN_ERR; if (choice == ROLE_NONE) { - return 0; /* selected quit */ + goto setup_done; /* selected quit */ } else if (choice == RS_menu_arg(RS_ALGNMNT)) { ALGN = k = ROLE_NONE; nextpick = RS_ALGNMNT; @@ -2320,7 +2327,7 @@ genl_player_setup(void) } /* Permit the user to pick, if there is more than one */ if (n > 1) { - win = plsel_startmenu(); + win = plsel_startmenu(screenheight, RS_RACE); any = cg.zeroany; /* zero out all bits */ /* populate the menu with role choices */ setup_racemenu(win, TRUE, ROLE, GEND, ALGN); @@ -2348,7 +2355,7 @@ genl_player_setup(void) destroy_nhwindow(win), win = WIN_ERR; if (choice == ROLE_NONE) { - return 0; /* selected quit */ + goto setup_done; /* selected quit */ } else if (choice == RS_menu_arg(RS_ALGNMNT)) { ALGN = k = ROLE_NONE; nextpick = RS_ALGNMNT; @@ -2409,7 +2416,7 @@ genl_player_setup(void) } /* Permit the user to pick, if there is more than one */ if (n > 1) { - win = plsel_startmenu(); + win = plsel_startmenu(screenheight, RS_GENDER); any = cg.zeroany; /* zero out all bits */ /* populate the menu with gender choices */ setup_gendmenu(win, TRUE, ROLE, RACE, ALGN); @@ -2437,7 +2444,7 @@ genl_player_setup(void) destroy_nhwindow(win), win = WIN_ERR; if (choice == ROLE_NONE) { - return 0; /* selected quit */ + goto setup_done; /* selected quit */ } else if (choice == RS_menu_arg(RS_ALGNMNT)) { ALGN = k = ROLE_NONE; nextpick = RS_ALGNMNT; @@ -2496,7 +2503,7 @@ genl_player_setup(void) } /* Permit the user to pick, if there is more than one */ if (n > 1) { - win = plsel_startmenu(); + win = plsel_startmenu(screenheight, RS_ALGNMNT); any = cg.zeroany; /* zero out all bits */ setup_algnmenu(win, TRUE, ROLE, RACE, GEND); role_menu_extra(ROLE_RANDOM, win, TRUE); @@ -2522,7 +2529,7 @@ genl_player_setup(void) destroy_nhwindow(win), win = WIN_ERR; if (choice == ROLE_NONE) { - return 0; /* selected quit */ + goto setup_done; /* selected quit */ } else if (choice == RS_menu_arg(RS_GENDER)) { GEND = k = ROLE_NONE; nextpick = RS_GENDER; @@ -2572,29 +2579,8 @@ genl_player_setup(void) */ getconfirmation = (picksomething && pick4u != 'a' && !flags.randomall); while (getconfirmation) { - win = plsel_startmenu(); + win = plsel_startmenu(screenheight, RS_filter); /* filter: not ROLE */ any = cg.zeroany; /* zero out all bits */ -#if 0 - start_menu(win, MENU_BEHAVE_STANDARD); - any.a_int = 0; - char plbuf[QBUFSZ]; - if (!roles[ROLE].name.f - && ((roles[ROLE].allow & ROLE_GENDMASK) - == (ROLE_MALE | ROLE_FEMALE))) - Sprintf(plbuf, " %s", genders[GEND].adj); - else - *plbuf = '\0'; /* omit redundant gender */ - Snprintf(pbuf, sizeof pbuf, "%s, %s%s %s %s", gp.plname, - aligns[ALGN].adj, plbuf, races[RACE].adj, - (GEND == 1 && roles[ROLE].name.f) ? roles[ROLE].name.f - : roles[ROLE].name.m); - add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr, pbuf, - MENU_ITEMFLAGS_NONE); - /* blank separator */ - any.a_int = 0; - add_menu(win, &nul_glyphinfo, &any, 0, 0, - ATR_NONE, clr, "", MENU_ITEMFLAGS_NONE); -#endif /* [ynaq] menu choices */ any.a_int = 1; add_menu(win, &nul_glyphinfo, &any, 'y', 0, @@ -2621,7 +2607,7 @@ genl_player_setup(void) switch (choice) { default: /* 'q' or ESC */ - return 0; /* quit */ + goto setup_done; /* quit */ break; case 3: { /* 'a' */ /* @@ -2631,11 +2617,11 @@ genl_player_setup(void) */ int saveROLE, saveRACE, saveGEND, saveALGN; - iflags.renameinprogress = TRUE; + iflags.renameinprogress = TRUE; /* affects main() in unixmain.c */ /* plnamesuffix() can change any or all of ROLE, RACE, GEND, ALGN; we'll override that and honor only the name */ saveROLE = ROLE, saveRACE = RACE, saveGEND = GEND, saveALGN = ALGN; - *gp.plname = '\0'; + gp.plname[0] = '\0'; plnamesuffix(); /* calls askname() when gp.plname[] is empty */ ROLE = saveROLE, RACE = saveRACE, GEND = saveGEND, ALGN = saveALGN; break; /* getconfirmation is still True */ @@ -2653,10 +2639,13 @@ genl_player_setup(void) getconfirmation = FALSE; break; } - } - + } /* while 'getconfirmation' */ /* Success! */ - return 1; + result = 1; + + setup_done: + gp.program_state.in_role_selection--; + return result; } static boolean @@ -2713,9 +2702,41 @@ reset_role_filtering(void) return (n > 0) ? TRUE : FALSE; } +/* the change in format when this extended role selection was converted from + tty-only to tty+curses+? made the role selection menu require two pages + on a traditional 24-line tty; that wasn't fair to tty, so squeeze out + some blank separator lines from the menu if that will make it fit on one */ +static int +maybe_skip_seps(int rows, int aspect) +{ + int i, n = 0; + + /* not much point to generalizing this to other aspects */ + if (aspect != RS_ROLE) + return 0; + /* + * If there are one or two excess lines, setup_rolemenu() will omit + * the separator between 'random' and 'pick race first'. If there are + * two, plsel_startmenu() will omit the one between role info so far + * (" ...") and the set of role entries. + */ + + n += 4; /* title and ensuing separator, role info so far and separator */ + for (i = 0; roles[i].name.m; ++i) + if (ok_role(i, RACE, GEND, ALGN) && ok_race(i, RACE, GEND, ALGN) + && ok_gend(i, RACE, GEND, ALGN) && ok_align(i, RACE, GEND, ALGN)) + ++n; + n += 2; /* 'random' and separator */ + n += 5; /* race 1st, gender 1st, alignment 1st, reset filter, quit */ + n += 1; /* footer/prompt */ + if (rows > 0 && n > rows) + return n - rows; + return 0; +} + /* start a menu; show role aspects specified so far as a header line */ static winid -plsel_startmenu(void) +plsel_startmenu(int ttyrows, int aspect) { char qbuf[QBUFSZ]; winid win; @@ -2730,7 +2751,7 @@ plsel_startmenu(void) rolename = (ROLE < 0) ? "" : (GEND == 1 && roles[ROLE].name.f) ? roles[ROLE].name.f : roles[ROLE].name.m; - if (!*gp.plname || ROLE < 0 || RACE < 0 || GEND < 0 || ALGN < 0) { + if (!gp.plname[0] || ROLE < 0 || RACE < 0 || GEND < 0 || ALGN < 0) { /* " " */ Sprintf(qbuf, "%.20s %.20s %.20s %.20s", rolename, @@ -2755,8 +2776,9 @@ plsel_startmenu(void) any = cg.zeroany; add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr, qbuf, MENU_ITEMFLAGS_NONE); - add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr, - "", MENU_ITEMFLAGS_NONE); + if (maybe_skip_seps(ttyrows, aspect) != 2) + add_menu(win, &nul_glyphinfo, &any, 0, 0, ATR_NONE, clr, + "", MENU_ITEMFLAGS_NONE); return win; } @@ -2930,7 +2952,7 @@ setup_algnmenu( #else /* !TTY_GRAPHICS */ int -genl_player_setup(void) +genl_player_setup(int screenheight UNUSED) { return 0; } diff --git a/win/curses/cursmain.c b/win/curses/cursmain.c index ab018c711..3d370e872 100644 --- a/win/curses/cursmain.c +++ b/win/curses/cursmain.c @@ -275,7 +275,7 @@ void curses_player_selection(void) { #if 1 - if (genl_player_setup()) + if (genl_player_setup(0)) return; /* success */ /* quit/cancel */ diff --git a/win/tty/wintty.c b/win/tty/wintty.c index fb0846cf6..95ce1aa6d 100644 --- a/win/tty/wintty.c +++ b/win/tty/wintty.c @@ -575,7 +575,7 @@ tty_preference_update(const char *pref) void tty_player_selection(void) { - if (genl_player_setup()) + if (genl_player_setup(ttyDisplay->rows)) return; bail((char *) 0); @@ -1859,14 +1859,22 @@ tty_dismiss_nhwindow(winid window) case NHW_MENU: case NHW_TEXT: if (cw->active) { + /* skip erasure if window_inited has been reset to 0 during + final run-down in case this is the end-of-game window; + the contents of that window should remain shown even when + the window itself has gone away */ if (iflags.window_inited) { - /* otherwise dismissing the text endwin after other windows - * are dismissed tries to redraw the map and panics. since - * the whole reason for dismissing the other windows was to - * leave the ending window on the screen, we don't want to - * erase it anyway. - */ - erase_menu_or_text(window, cw, FALSE); + boolean clearscreen = FALSE; /* just erase the menu */ + + /* during role/race/&c selection, menus are put up on top + of the base window; we don't track what was there so + can't refresh--force the screen to be cleared instead + (affects dismissal of 'reset role filtering' menu if + screen height forces that to need a second page) */ + if (gp.program_state.in_role_selection) + clearscreen = TRUE; + + erase_menu_or_text(window, cw, clearscreen); } cw->active = 0; }