Files
nethack/win/curses/cursinvt.c
nhmall d5658018ac alternative to display_inventory for window-port
Several window ports that support perm_invent were
using a call back to the core display_inventory()
function.

While calling from the window port back to core functions
is arguably not ideal in the first place, it was recently
brought to light that code NetHack-3.7 code changes to
display_inventory() actually caused it to stop repopulating
the perm_invent window as intended under certain circumstances.

For now, provide an alternative function, repopulate_perminvent(),
that hopefullshould still work the way it did previously.

There will likely be some additional changes after this to
further improve things, at some point.

For now though, this
Resolves #1454
2025-11-08 14:26:07 -05:00

467 lines
16 KiB
C

/* 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" -*-*/
/* NetHack 3.7 cursinvt.c */
/* Copyright (c) Fredrik Ljungdahl, 2017. */
/* NetHack may be freely redistributed. See license for details. */
#include "curses.h"
#include "hack.h"
#include "wincurs.h"
#include "cursinvt.h"
static void curs_invt_updated(WINDOW *);
static unsigned pi_article_skip(const char *);
static int curs_scroll_invt(WINDOW *);
static void curs_show_invt(WINDOW *);
/*
* Persistent inventory (perm_invent) for curses interface.
* It resembles a menu but does not function like one.
*/
/* pseudo menu line data */
struct pi_line {
char *invtxt; /* class header or inventory item without letter prefix */
attr_t c_attr; /* attribute for class headers */
int color;
char letter; /* inventory letter; accelerator if this was really a menu;
* used to distinguish item lines from header lines and for
* display (no selection possible) */
};
static struct pi_line zero_pi_line;
/* full perm_invent data; added to array[] one line at a time */
struct pi_data {
struct pi_line *array; /* one element for each line of perminv */
unsigned allocsize, inuseindx; /* num elements allocated and populated */
unsigned rowoffset, coloffset; /* for displaying a subset due to space */
unsigned widest; /* longest array[].invtxt */
};
#define PERMINV_CHUNK 20 /* number of elements to grow array[] when needed */
/* current persistent inventory */
static struct pi_data pi = { (struct pi_line *) 0, 0, 0, 0, 0, 0 };
/* discard saved persistent inventory data */
void
curs_purge_perminv_data(boolean everything)
{
if (pi.array) {
unsigned idx;
for (idx = 0; idx < pi.inuseindx; ++idx)
free(pi.array[idx].invtxt), pi.array[idx].invtxt = 0;
if (everything)
free(pi.array), pi.array = 0, pi.allocsize = 0;
}
pi.inuseindx = 0;
pi.rowoffset = pi.coloffset = 0;
pi.widest = 0;
}
/* Runs when the game indicates that the inventory has been updated */
void
curs_update_invt(int arg)
{
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) {
/* [core_]status_initialize, curses_create_main_windows,
curses_last_messages, [core_]doredraw; doredraw will
call (*update_inventory) [curses_update_inventory] which
will call us but 'win' should be defined that time */
curs_reset_windows(TRUE, FALSE);
/* TODO: guard against window creation failure [if that's
possible] which would lead to uncontrolled recursion */
}
return;
}
/* clear anything displayed from previous update */
werase(win);
if (!arg) {
/*
* 'arg'==0 means basic inventory_update(): [re-]populate and
* [re-]display the persistent inventory window.
*/
if (pi.array) /* previous data is obsolete */
curs_purge_perminv_data(FALSE);
/* ask core to display full inventory in a PICK_NONE menu;
instead of setting up an ordinary menu, it will indirectly
call curs_add_invt() for each line (including class headers) */
repopulate_perminvent();
curs_invt_updated(win);
} else {
/*
* 'arg'!=0 means #perminv command is providing the player
* with opportunity to scroll the already displayed persistent
* inventory window. Aside from being non-zero, the value of
* 'arg' is ignored.
*/
int scrollingdone;
/* previous data is still valid; let player interactively scroll it */
do {
scrollingdone = curs_scroll_invt(win);
curs_invt_updated(win);
} while (!scrollingdone);
}
return;
}
/* persistent inventory has been updated or scrolled/panned; re-display it */
static void
curs_invt_updated(WINDOW *win)
{
/* display collected inventory data, probably clipped */
curs_show_invt(win);
if (curses_window_has_border(INV_WIN))
box(win, 0, 0);
wnoutrefresh(win);
}
/* scroll persistent inventory window forwards or backwards or side-to-side */
static int
curs_scroll_invt(WINDOW *win UNUSED)
{
char menukeys[QBUFSZ], qbuf[QBUFSZ];
unsigned uheight, uwidth, uhalfwidth, scrlmask;
int ch, menucmd, height, width;
int res = 0;
curses_get_window_size(INV_WIN, &height, &width);
uheight = (unsigned) height;
uwidth = (unsigned) width;
uhalfwidth = uwidth / 2;
menukeys[0] = '\0';
scrlmask = 0U;
if (pi.rowoffset > 0)
scrlmask |= 1U; /* include scroll backwards: ^ and < */
if (pi.rowoffset + uheight <= pi.inuseindx)
scrlmask |= 2U; /* include scroll forwards: > and | */
if (pi.coloffset > 0)
scrlmask |= 4U; /* include scroll left: { */
if (pi.widest > pi.coloffset + uwidth)
scrlmask |= 8U; /* include scroll right: } */
(void) collect_menu_keys(menukeys, scrlmask, TRUE);
Snprintf(qbuf, sizeof qbuf, "Inventory scroll: [%s%s%s] ",
menukeys, *menukeys ? " " : "", "Ret Esc");
curses_count_window(qbuf);
ch = getch();
curses_count_window((char *) 0);
curses_clear_unhighlight_message_window();
menucmd = (ch <= 0 || ch >= 255) ? ch : (int) (uchar) map_menu_cmd(ch);
switch (menucmd) {
case KEY_ESC:
case C('c'): /* ^C */
/* for <escape>, leave window with scrolling as-is */
res = -1;
break;
case '\n':
case '\r':
case '\b':
case '\177':
/* for <return>, or <space> when already on last page,
restore window to unscrolled */
pi.rowoffset = pi.coloffset = 0;
res = 1;
break;
case ' ':
if (pi.rowoffset + uheight <= pi.inuseindx) {
pi.rowoffset = pi.coloffset = 0;
res = 1;
break;
}
FALLTHROUGH;
/*FALLTHRU*/
case KEY_RIGHT:
case KEY_NPAGE:
case MENU_NEXT_PAGE:
if (pi.inuseindx <= uheight)
pi.rowoffset = 0;
else if (pi.rowoffset + 2 * uheight <= pi.inuseindx)
pi.rowoffset += uheight;
else
pi.rowoffset = pi.inuseindx - (uheight - 1);
break;
case KEY_LEFT:
case KEY_PPAGE:
case MENU_PREVIOUS_PAGE:
if (pi.rowoffset >= uheight)
pi.rowoffset -= uheight;
else
pi.rowoffset = 0;
break;
case KEY_END:
case MENU_LAST_PAGE:
if (pi.inuseindx > uheight)
pi.rowoffset = pi.inuseindx - (uheight - 1);
else
pi.rowoffset = 0;
break;
case KEY_HOME:
case MENU_FIRST_PAGE:
pi.rowoffset = 0;
break;
case KEY_DOWN:
if (pi.rowoffset + uheight <= pi.inuseindx)
pi.rowoffset += 1;
break;
case KEY_UP:
if (pi.rowoffset > 0)
pi.rowoffset -= 1;
else
pi.rowoffset = 0;
break;
case MENU_SHIFT_RIGHT:
if (pi.widest <= uwidth) {
pi.coloffset = 0;
} else {
pi.coloffset += uhalfwidth;
if (pi.coloffset + uwidth > pi.widest)
pi.coloffset = pi.widest - uwidth;
}
break;
case MENU_SHIFT_LEFT:
if (pi.coloffset >= uhalfwidth)
pi.coloffset -= uhalfwidth;
else
pi.coloffset = 0;
break;
#if 0
case MENU_SEARCH:
break;
#endif
case '\0':
default:
curses_nhbell();
break;
}
return res;
}
/* check 'str' for an article prefix and return length of that */
static unsigned
pi_article_skip(const char *str)
{
unsigned skip = 0; /* number of chars to skip when displaying str */
/*
* if (!strncmp(str, "a ", 2))
* skip = 2;
* else if (!strncmp(str, "an ", 3))
* skip = 3;
* else if (!strncmp(str, "the ", 4))
* skip = 4;
*/
if (str[0] == 'a') {
if (str[1] == ' ')
skip = 2;
else if (str[1] == 'n' && str[2] == ' ')
skip = 3;
} else if (str[0] == 't') {
if (str[1] == 'h' && str[2] == 'e' && str[3] == ' ')
skip = 4;
}
return skip;
}
/* store an inventory item or class header but don't display anything yet */
void
curs_add_invt(
int linenum, /* line index; 1..n rather than 0..n-1 */
char accelerator, /* selector letter for items, 0 for class headers */
attr_t attr, /* curses attribute for headers, 0 for items */
int clr, /* NetHack color for headers, NO_COLOR for items */
const char *str) /* formatted inventory item, without invlet prefix,
* or class header text */
{
unsigned idx, len;
struct pi_line newelement, *aptr = pi.array;
if ((unsigned) linenum > pi.allocsize) {
pi.allocsize += PERMINV_CHUNK;
pi.array = (struct pi_line *) alloc(pi.allocsize * sizeof *aptr);
for (idx = 0; idx < pi.allocsize; ++idx)
pi.array[idx] = (idx < pi.inuseindx) ? aptr[idx] : zero_pi_line;
if (aptr)
free(aptr);
aptr = pi.array;
}
newelement.invtxt = dupstr(str);
newelement.c_attr = attr; /* note: caller has already converted 'attr'
* from tty-style attribute to curses one */
newelement.color = clr;
newelement.letter = accelerator;
aptr[pi.inuseindx++] = newelement;
len = strlen(str);
if (accelerator) {
/* +4: " )c " inventory letter will be inserted before invtxt;
invtxt's "a "/"an "/"the " prefix, if any, will be skipped */
len += 4;
if (len > pi.widest)
len -= pi_article_skip(str);
}
if (len > pi.widest)
pi.widest = len;
}
/* display the inventory menu-like data collected in pi.array[] */
static void
curs_show_invt(WINDOW *win)
{
const char *str;
char accelerator, tmpbuf[BUFSZ];
int attr, color;
unsigned lineno, stroffset, widest, left_col, right_col,
first_shown = 0, last_shown = 0, item_count = 0, xtra_line = 0;
int x, y, width, height, available_width,
border = curses_window_has_border(INV_WIN) ? 1 : 0,
/* we actually care about the topmost of message, map, or status
but either they all have borders or none do so just check map */
otherborder = curses_window_has_border(MAP_WIN) ? 1 : 0;
x = border; /* same for every line; 1 if border, 0 otherwise */
curses_get_window_size(INV_WIN, &height, &width);
widest = pi.widest;
left_col = pi.coloffset + 1;
right_col = left_col + (unsigned) width - 1;
/*
* If there will be any blank lines left at the bottom, sometimes
* insert one blank line at the top. If the first line is an item
* (via !sortpack) which will be obscured by the column indicator,
* or there is only one line (such as "not carrying anything") if
* that line would be at the very top aligned with the top border
* of another window (windowborders=3,4) because that looks odd.
* This only handles the single page case [because scrolling always
* fills pages beyond the first rather than just continuing from
* last line of preceding page, hence no blank line(s) at bottom].
*/
if ((pi.array[0].letter && pi.inuseindx < (unsigned) height
&& widest > (unsigned) width)
|| (pi.inuseindx == 1 && otherborder && !border)) {
wmove(win, 1, x);
wprintw(win, "%.*s", width - 2 * border, "");
xtra_line = 1;
}
for (lineno = 0; lineno < pi.rowoffset; ++lineno)
if (pi.array[lineno].letter)
++item_count;
for (lineno = pi.rowoffset; lineno < pi.inuseindx; ++lineno) {
str = pi.array[lineno].invtxt;
accelerator = pi.array[lineno].letter;
attr = pi.array[lineno].c_attr; /* already converted when stored */
color = pi.array[lineno].color;
if (color == NO_COLOR)
color = NONE;
if (accelerator)
++item_count;
/* Figure out where to draw the line */
y = (int) (lineno + xtra_line - pi.rowoffset) + border;
if (y - border >= height) { /* height already -2 for Top+Btm border */
/* 'y' has grown too big; there are too many lines to fit */
continue; /* skip, but still loop to update 'item_count' */
}
available_width = width; /* width is already -2 for Lft+Rgt borders */
wmove(win, y, x);
stroffset = 0;
if (accelerator) { /* inventory item line */
if (!first_shown)
first_shown = item_count;
last_shown = item_count;
/* despite being shown as a menu, nothing is selectable from the
persistent inventory window so avoid the usual highlighting
of inventory letters */
wprintw(win, " %c) ", accelerator);
available_width -= 4; /* space+letter+parenthesis+space */
/*
* Narrow the entries to fit more of the interesting text,
* but defer the removal until after menu colors matching.
* Do so unconditionally rather than trying to figure whether
* it's needed. When 'sortpack' is enabled we could also strip
* out "<class> of" from "<prefix><class> of <item><suffix>"
* but if that's to be done, the core ought to do it.
*/
stroffset = pi_article_skip(str); /* to skip "a "/"an "/"the " */
/* if/when scrolled right, invtxt for item lines gets shifted */
stroffset += pi.coloffset;
}
if (stroffset < strlen(str)) {
curses_menu_color_attr(win, color, attr, ON);
wprintw(win, "%.*s", available_width, str + stroffset);
curses_menu_color_attr(win, color, attr, OFF);
}
wclrtoeol(win);
} /* lineno loop */
if (pi.inuseindx > (unsigned) height) {
/*
* More lines than will fit at one time so some lines aren't shown.
* Overwrite the rightmost portion of last line with something
* like "[1-24 of 30>" or "<7-30 of 30]" if we've already scrolled
* forward. Right justified so that the line might still show
* something useful. It could be on a line of its own, in which
* case we need to erase that first.
*/
y = height - (1 - border);
if ((unsigned) y == pi.inuseindx - pi.rowoffset) {
wmove(win, y, x);
wclrtoeol(win);
}
Sprintf(tmpbuf, "%c%u-%u of %u%c",
(first_shown > 1) ? '<' : '[',
first_shown, last_shown, item_count,
(last_shown < item_count) ? '>' : ']');
mvwaddstr(win, y, x + (width - (int) strlen(tmpbuf)), tmpbuf);
}
if (widest > (unsigned) width) {
/*
* More columns than the persistent inventory window can fit so
* some columns aren't shown. Overwrite the rightmost portion of
* first line with something like "[1-25 of 40}" or "{16-40 of 40]".
* For the usual case, that line will be an object class header
* so we won't be obscuring an item, but that might not be the
* situation on page 2 and definitely won't be if 'sortpack' is Off.
*/
Sprintf(tmpbuf, "%c%u-%u of %u%c",
(left_col > 1) ? '{' : '[',
left_col, right_col, widest,
(right_col < widest) ? '}' : ']');
mvwaddstr(win, border, x + (width - (int) strlen(tmpbuf)), tmpbuf);
}
return;
}