The prior behavior was an ugly kludge that treated rumors as only-eligible-to-appear-from-fortune-cookies if they contained the string "fortune" or "pity". This commit gets rid of that system and introduces a system where each rumor can be specified as such by prefixing it with "[cookie]". In order to avoid changing existing behavior, I have applied the [cookie] prefix to all the rumors which were affected under the old rules. However, several rumors seem like they were misclassified under the old rules, and I am in favor of changing them as follows: Should be made cookie-only (they only really make sense in context of eating a fortune cookie): - "Ouch. I hate when that happens." - "PLEASE ignore previous rumor." - "Suddenly, the dungeon will collapse..." - "Unfortunately, this message was left intentionally blank." Should be made non-cookie-only (they make sense if they appear as engravings, Oracle messages, or artifact whispers): - "They say that a gypsy could tell your fortune for a price." - "They say that fortune cookies are food for thought."
949 lines
35 KiB
C
949 lines
35 KiB
C
/* NetHack 3.7 rumors.c $NHDT-Date: 1594370241 2020/07/10 08:37:21 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.56 $ */
|
|
/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
|
|
/*-Copyright (c) Robert Patrick Rankin, 2012. */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "hack.h"
|
|
#include "dlb.h"
|
|
|
|
/* [Note: this comment is fairly old, but still accurate for 3.1;
|
|
* it's no longer accurate for 3.7 but may still be of interest.]
|
|
* Rumors have been entirely rewritten to speed up the access. This is
|
|
* essential when working from floppies. Using fseek() the way that's done
|
|
* here means rumors following longer rumors are output more often than those
|
|
* following shorter rumors. Also, you may see the same rumor more than once
|
|
* in a particular game (although the odds are highly against it), but
|
|
* this also happens with real fortune cookies. -dgk
|
|
*/
|
|
|
|
/* 3.6
|
|
* The rumors file consists of a "do not edit" line, then a line containing
|
|
* three sets of three counts (first two in decimal, third in hexadecimal).
|
|
* The first set has the number of true rumors, the count in bytes for all
|
|
* true rumors, and the file offset to the first one. The second set has
|
|
* the same group of numbers for the false rumors. The third set has 0 for
|
|
* count, 0 for size, and the file offset for end-of-file. The offset of
|
|
* the first true rumor plus the size of the true rumors matches the offset
|
|
* of the first false rumor. Likewise, the offset of the first false rumor
|
|
* plus the size of the false rumors matches the offset for end-of-file.
|
|
*/
|
|
|
|
/* 3.1 [now obsolete for rumors but still accurate for oracles]
|
|
* The rumors file consists of a "do not edit" line, a hexadecimal number
|
|
* giving the number of bytes of useful/true rumors, followed by those
|
|
* true rumors (one per line), followed by the useless/false/misleading/cute
|
|
* rumors (also one per line). Number of bytes of untrue rumors is derived
|
|
* via fseek(EOF)+ftell().
|
|
*
|
|
* The oracles file consists of a "do not edit" comment, a decimal count N
|
|
* and set of N+1 hexadecimal fseek offsets, followed by N multiple-line
|
|
* records, separated by "---" lines. The first oracle is a special case,
|
|
* and placed there by 'makedefs'.
|
|
*/
|
|
|
|
static void unpadline(char *);
|
|
static void init_rumors(dlb *);
|
|
static char *get_rnd_line(dlb *, char *, unsigned, int (*)(int),
|
|
long, long, unsigned);
|
|
static void init_oracles(dlb *);
|
|
static void others_check(const char *ftype, const char *, winid *);
|
|
static void couldnt_open_file(const char *);
|
|
static void init_CapMons(void);
|
|
|
|
/* used by CapitalMon(); set up by init_CapMons(), released by free_CapMons();
|
|
there's no need for these to be put into 'struct instance_globals g' */
|
|
static unsigned CapMonstCnt = 0, CapBogonCnt = 0,
|
|
CapMonSiz = 0; /* CapMonstCnt+CapBogonCnt+1 when non-zero */
|
|
static const char **CapMons = 0;
|
|
|
|
/* list of bogusmons prefixes used to indicate special monster type such as
|
|
unique or always a particular gender; see dat/bogusmon.txt */
|
|
extern const char bogon_codes[]; /* from do_name.c */
|
|
|
|
/* makedefs pads short rumors, epitaphs, engravings, and hallucinatory
|
|
monster names with trailing underscores; strip those off */
|
|
static void
|
|
unpadline(char *line)
|
|
{
|
|
char *p = eos(line);
|
|
|
|
/* remove newline if still present; caller should have stripped it */
|
|
if (p > line && p[-1] == '\n')
|
|
--p;
|
|
|
|
/* remove padding */
|
|
while (p > line && p[-1] == '_')
|
|
--p;
|
|
|
|
*p = '\0';
|
|
}
|
|
|
|
DISABLE_WARNING_FORMAT_NONLITERAL
|
|
|
|
static void
|
|
init_rumors(dlb *fp)
|
|
{
|
|
static const char rumors_header[] = "%d,%ld,%lx;%d,%ld,%lx;0,0,%lx\n";
|
|
int true_count, false_count; /* in file but not used here */
|
|
unsigned long eof_offset;
|
|
char line[BUFSZ];
|
|
|
|
(void) dlb_fgets(line, sizeof line, fp); /* skip "don't edit" comment */
|
|
(void) dlb_fgets(line, sizeof line, fp);
|
|
if (sscanf(line, rumors_header, &true_count, >.true_rumor_size,
|
|
>.true_rumor_start, &false_count, &gf.false_rumor_size,
|
|
&gf.false_rumor_start, &eof_offset) == 7
|
|
&& gt.true_rumor_size > 0L
|
|
&& gf.false_rumor_size > 0L) {
|
|
gt.true_rumor_end = (long) gt.true_rumor_start + gt.true_rumor_size;
|
|
/* assert( gt.true_rumor_end == false_rumor_start ); */
|
|
gf.false_rumor_end = (long) gf.false_rumor_start + gf.false_rumor_size;
|
|
/* assert( gf.false_rumor_end == eof_offset ); */
|
|
} else {
|
|
gt.true_rumor_size = -1L; /* init failed */
|
|
(void) dlb_fclose(fp);
|
|
}
|
|
}
|
|
|
|
RESTORE_WARNING_FORMAT_NONLITERAL
|
|
|
|
/* exclude_cookie is a hack used because we sometimes want to get rumors in a
|
|
* context where messages such as "You swallowed the fortune!" that refer to
|
|
* cookies should not appear. This has no effect for true rumors since none
|
|
* of them contain such references anyway.
|
|
*/
|
|
char *
|
|
getrumor(
|
|
int truth, /* 1=true, -1=false, 0=either */
|
|
char *rumor_buf,
|
|
boolean exclude_cookie)
|
|
{
|
|
dlb *rumors;
|
|
long beginning, ending;
|
|
char line[BUFSZ];
|
|
static const char *cookie_marker = "[cookie] ";
|
|
const int marklen = strlen(cookie_marker);
|
|
|
|
rumor_buf[0] = '\0';
|
|
if (gt.true_rumor_size < 0L) /* a previous try failed to open RUMORFILE */
|
|
return rumor_buf;
|
|
|
|
rumors = dlb_fopen(RUMORFILE, "r");
|
|
if (rumors) {
|
|
int count = 0;
|
|
int adjtruth;
|
|
|
|
do {
|
|
rumor_buf[0] = '\0';
|
|
if (gt.true_rumor_size == 0L) { /* if this is 1st outrumor() */
|
|
init_rumors(rumors);
|
|
if (gt.true_rumor_size < 0L) { /* init failed */
|
|
Sprintf(rumor_buf, "Error reading \"%.80s\".", RUMORFILE);
|
|
return rumor_buf;
|
|
}
|
|
}
|
|
/*
|
|
* input: 1 0 -1
|
|
* rn2 \ +1 2=T 1=T 0=F
|
|
* adj./ +0 1=T 0=F -1=F
|
|
*/
|
|
switch (adjtruth = truth + rn2(2)) {
|
|
case 2: /*(might let a bogus input arg sneak thru)*/
|
|
case 1:
|
|
beginning = (long) gt.true_rumor_start;
|
|
ending = gt.true_rumor_end;
|
|
break;
|
|
case 0: /* once here, 0 => false rather than "either"*/
|
|
case -1:
|
|
beginning = (long) gf.false_rumor_start;
|
|
ending = gf.false_rumor_end;
|
|
break;
|
|
default:
|
|
impossible("strange truth value for rumor");
|
|
return strcpy(rumor_buf, "Oops...");
|
|
}
|
|
Strcpy(rumor_buf,
|
|
get_rnd_line(rumors, line, (unsigned) sizeof line, rn2,
|
|
beginning, ending, MD_PAD_RUMORS));
|
|
} while (count++ < 50 && exclude_cookie
|
|
&& !strncmp(rumor_buf, cookie_marker, marklen));
|
|
(void) dlb_fclose(rumors);
|
|
if (count >= 50)
|
|
impossible("Can't find non-cookie rumor?");
|
|
else if (!gi.in_mklev) /* avoid exercizing wisdom for graffiti */
|
|
exercise(A_WIS, (adjtruth > 0));
|
|
} else {
|
|
couldnt_open_file(RUMORFILE);
|
|
gt.true_rumor_size = -1; /* don't try to open it again */
|
|
}
|
|
if (!exclude_cookie
|
|
&& !strncmp(rumor_buf, cookie_marker, marklen)) {
|
|
/* remove cookie_marker from the string */
|
|
char *src = rumor_buf + marklen;
|
|
char *dst = rumor_buf;
|
|
for (; *src != '\0'; ++src, ++dst) {
|
|
*dst = *src;
|
|
}
|
|
*dst = '\0'; /* terminator wasn't copied */
|
|
}
|
|
return rumor_buf;
|
|
}
|
|
|
|
/* test that the true/false rumor boundaries are valid and show the first
|
|
two and very last epitaphs, engravings, and bogus monsters */
|
|
void
|
|
rumor_check(void)
|
|
{
|
|
dlb *rumors;
|
|
winid tmpwin = WIN_ERR;
|
|
char *endp, line[BUFSZ], xbuf[BUFSZ], rumor_buf[BUFSZ];
|
|
|
|
rumors = (gt.true_rumor_size >= 0) ? dlb_fopen(RUMORFILE, "r") : 0;
|
|
if (rumors) {
|
|
long ftell_rumor_start = 0L;
|
|
|
|
rumor_buf[0] = '\0';
|
|
if (gt.true_rumor_size == 0L) { /* if this is 1st outrumor() */
|
|
init_rumors(rumors);
|
|
if (gt.true_rumor_size < 0L) {
|
|
rumors = (dlb *) 0; /* init_rumors() closes it upon failure */
|
|
goto no_rumors; /* init failed */
|
|
}
|
|
}
|
|
tmpwin = create_nhwindow(NHW_TEXT);
|
|
|
|
/*
|
|
* reveal the values.
|
|
*/
|
|
Sprintf(rumor_buf,
|
|
"T start=%06ld (%06lx), end=%06ld (%06lx), size=%06ld (%06lx)",
|
|
(long) gt.true_rumor_start, gt.true_rumor_start,
|
|
gt.true_rumor_end, (unsigned long) gt.true_rumor_end,
|
|
gt.true_rumor_size,(unsigned long) gt.true_rumor_size);
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
Sprintf(rumor_buf,
|
|
"F start=%06ld (%06lx), end=%06ld (%06lx), size=%06ld (%06lx)",
|
|
(long) gf.false_rumor_start, gf.false_rumor_start,
|
|
gf.false_rumor_end, (unsigned long) gf.false_rumor_end,
|
|
gf.false_rumor_size, (unsigned long) gf.false_rumor_size);
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
|
|
/*
|
|
* check the first rumor (start of true rumors) by
|
|
* skipping the first two lines.
|
|
*
|
|
* Then seek to the start of the false rumors (based on
|
|
* the value read in rumors, and display it.
|
|
*/
|
|
rumor_buf[0] = '\0';
|
|
(void) dlb_fseek(rumors, (long) gt.true_rumor_start, SEEK_SET);
|
|
ftell_rumor_start = dlb_ftell(rumors);
|
|
(void) dlb_fgets(line, sizeof line, rumors);
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
Sprintf(rumor_buf, "T %06ld %s", ftell_rumor_start,
|
|
xcrypt(line, xbuf));
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
/* find last true rumor */
|
|
while (dlb_fgets(line, sizeof line, rumors)
|
|
&& dlb_ftell(rumors) < gt.true_rumor_end)
|
|
continue;
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
Sprintf(rumor_buf, " %6s %s", "", xcrypt(line, xbuf));
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
|
|
rumor_buf[0] = '\0';
|
|
(void) dlb_fseek(rumors, (long) gf.false_rumor_start, SEEK_SET);
|
|
ftell_rumor_start = dlb_ftell(rumors);
|
|
(void) dlb_fgets(line, sizeof line, rumors);
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
Sprintf(rumor_buf, "F %06ld %s", ftell_rumor_start,
|
|
xcrypt(line, xbuf));
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
/* find last false rumor */
|
|
while (dlb_fgets(line, sizeof line, rumors)
|
|
&& dlb_ftell(rumors) < gf.false_rumor_end)
|
|
continue;
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
Sprintf(rumor_buf, " %6s %s", "", xcrypt(line, xbuf));
|
|
putstr(tmpwin, 0, rumor_buf);
|
|
|
|
(void) dlb_fclose(rumors);
|
|
|
|
/* if a previous attempt couldn't open file or rejected its contents,
|
|
we didn't bother trying again this time */
|
|
} else if (gt.true_rumor_size < 0L) {
|
|
no_rumors: /* file could be opened but init_rumors() didn't like it */
|
|
pline("rumors not accessible.");
|
|
/* engravings, epitaphs, and bogus monsters will still be shown,
|
|
and in tmpwin rather than via additional pline() calls */
|
|
display_nhwindow(WIN_MESSAGE, TRUE); /* --more-- */
|
|
|
|
/* first attempt to open file has just failed */
|
|
} else {
|
|
couldnt_open_file(RUMORFILE);
|
|
gt.true_rumor_size = -1; /* don't try to open it again */
|
|
}
|
|
|
|
/* initial implementation of default epitaph/engraving/bogusmon
|
|
contained an error; check those along with rumors */
|
|
others_check("Engravings:", ENGRAVEFILE, &tmpwin);
|
|
others_check("Epitaphs:", EPITAPHFILE, &tmpwin);
|
|
others_check("Bogus monsters:", BOGUSMONFILE, &tmpwin);
|
|
|
|
if (tmpwin != WIN_ERR) {
|
|
display_nhwindow(tmpwin, TRUE);
|
|
destroy_nhwindow(tmpwin);
|
|
}
|
|
}
|
|
|
|
DISABLE_WARNING_FORMAT_NONLITERAL
|
|
|
|
/* 3.7: augments rumors_check(); test 'engrave' or 'epitaph' or 'bogusmon' */
|
|
static void
|
|
others_check(
|
|
const char *ftype, /* header: "{Engravings|Epitaphs|Bogus monsters}:" */
|
|
const char *fname, /* filename: {ENGRAVEFILE|EPITAPHFILE|BOGUSMONFILE} */
|
|
winid *winptr) /* text window for output; created here if necessary */
|
|
{
|
|
static const char errfmt[] = "others_check(\"%s\"): %s";
|
|
dlb *fh;
|
|
char line[BUFSZ], xbuf[BUFSZ], *endp;
|
|
winid tmpwin = *winptr;
|
|
int entrycount = 0;
|
|
|
|
fh = dlb_fopen(fname, "r");
|
|
if (fh) {
|
|
if (tmpwin == WIN_ERR) {
|
|
*winptr = tmpwin = create_nhwindow(NHW_TEXT);
|
|
if (tmpwin == WIN_ERR) {
|
|
/* should panic, but won't for wizard mode check operation */
|
|
impossible(errfmt, fname, "can't create temporary window");
|
|
goto closeit;
|
|
}
|
|
}
|
|
putstr(tmpwin, 0, "");
|
|
putstr(tmpwin, 0, ftype);
|
|
/* "don't edit" comment */
|
|
*line = '\0';
|
|
if (!dlb_fgets(line, sizeof line, fh)) {
|
|
Sprintf(xbuf, errfmt, fname, "error; can't read comment line");
|
|
putstr(tmpwin, 0, xbuf);
|
|
goto closeit;
|
|
}
|
|
if (*line != '#') {
|
|
Sprintf(xbuf, errfmt, fname,
|
|
"malformed; first line is not a comment line:");
|
|
putstr(tmpwin, 0, xbuf);
|
|
/* show the bad line; we don't know whether it has been
|
|
encrypted via xcrypt() so show it both ways */
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
putstr(tmpwin, 0, "- first line, as is");
|
|
putstr(tmpwin, 0, line);
|
|
putstr(tmpwin, 0, "- xcrypt of first line");
|
|
putstr(tmpwin, 0, xcrypt(line, xbuf));
|
|
goto closeit;
|
|
}
|
|
/* first line; should be default one inserted by makedefs when
|
|
building the file but we don't have the expected value so
|
|
can only require a line to exist */
|
|
*line = '\0';
|
|
if (!dlb_fgets(line, sizeof line, fh) || *line == '\n') {
|
|
Sprintf(xbuf, errfmt, fname,
|
|
!*line ? "can't read first non-comment line"
|
|
: "first non-comment line is empty");
|
|
putstr(tmpwin, 0, xbuf);
|
|
goto closeit;
|
|
}
|
|
++entrycount;
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
putstr(tmpwin, 0, xcrypt(line, xbuf));
|
|
if (!dlb_fgets(line, sizeof line, fh)) {
|
|
putstr(tmpwin, 0, "(no second entry)");
|
|
} else {
|
|
++entrycount;
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
putstr(tmpwin, 0, xcrypt(line, xbuf));
|
|
while (dlb_fgets(line, sizeof line, fh)) {
|
|
++entrycount;
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
(void) xcrypt(line, xbuf);
|
|
}
|
|
/* count will be 2 if the default entry and the first ordinary
|
|
entry are the only ones present (if either of those were
|
|
missing, we wouldn't have gotten here...) */
|
|
if (entrycount == 2) {
|
|
putstr(tmpwin, 0, "(only two entries)");
|
|
} else {
|
|
/* showing an ellipsis avoids ambiguity about whether
|
|
there are other lines; doing so three times (once for
|
|
each file) results in total output being 24 lines,
|
|
forcing a --More-- prompt if using a 24 line screen;
|
|
displaying 23 lines and --More-- followed by second
|
|
page with 1 line doesn't look very good but isn't
|
|
incorrect, and taller screens where that won't be an
|
|
issue are more common than 24 line terminals nowadays */
|
|
if (entrycount > 3)
|
|
putstr(tmpwin, 0, " ...");
|
|
putstr(tmpwin, 0, xbuf); /* already decrypted */
|
|
}
|
|
}
|
|
|
|
closeit:
|
|
(void) dlb_fclose(fh);
|
|
} else {
|
|
/* since this comes out via impossible(), it won't be integrated
|
|
with the text window of values, but it shouldn't ever happen
|
|
so we won't waste effort integrating it */
|
|
couldnt_open_file(fname);
|
|
}
|
|
}
|
|
|
|
RESTORE_WARNING_FORMAT_NONLITERAL
|
|
|
|
/* load one randomly chosen line from a section of a file; undoes
|
|
decryption and strips trailing underscore padding and final newline;
|
|
if padlength is non-zero, every line is expected to be at least that
|
|
long and every line in the file will have an equal chance of being
|
|
chosen; however, if padlength is 0, lines following long lines are
|
|
more likely than average to be picked, and lines after short lines
|
|
are less likely */
|
|
static char *
|
|
get_rnd_line(
|
|
dlb *fh, /* already opened file */
|
|
char *buf, /* output buffer */
|
|
unsigned bufsiz, /* (unsigned) sizeof buf */
|
|
int (*rng)(int), /* random number routine; rn2(N) or similar, 0..N-1 */
|
|
long startpos, /* location in file of first line of interest */
|
|
long endpos, /* location one byte past last line of interest;
|
|
* if 0, end-of-file will be used */
|
|
unsigned padlength) /* expected line length; 0 if no expectations */
|
|
{
|
|
char *newl, *xbufp, xbuf[BUFSZ];
|
|
long filechunksize, chunkoffset;
|
|
int trylimit;
|
|
|
|
*buf = '\0';
|
|
if (!endpos) {
|
|
(void) dlb_fseek(fh, 0L, SEEK_END);
|
|
endpos = dlb_ftell(fh);
|
|
}
|
|
filechunksize = endpos - startpos;
|
|
|
|
/* might be zero (only if file is empty); should complain in that
|
|
case but it could happen over and over, also the suggestion
|
|
that save and restore might fix the problem wouldn't be useful */
|
|
if (filechunksize < 1L)
|
|
return buf;
|
|
/* 'rumors' is about 3/4 of the way to the limit on a 16-bit config
|
|
for the whole, roughly 3/8 of the way for either half; all active
|
|
configuations these days are at least 32-bits anyway */
|
|
nhassert(filechunksize <= INT_MAX); /* essential for rn2() */
|
|
|
|
/*
|
|
* Position randomly which will probably be in the middle of a line.
|
|
* (Occasionally by chance it will happen to be at the very start of
|
|
* a line, but we'll have no way of knowing that so have to behave
|
|
* as if it were positioned in the middle.)
|
|
* Read the rest of that line, then use the next one. If there's no
|
|
* next line (ie, end of file), go back to beginning and use first.
|
|
*
|
|
* When short lines have been padded to length N, only accept long
|
|
* lines if we land within last N+1 characters (+1 is for newline
|
|
* which hasn't been stripped away yet), effectively shortening
|
|
* them to normal length. That yields even selection distribution.
|
|
*/
|
|
for (trylimit = 10; trylimit > 0; --trylimit) {
|
|
chunkoffset = (long) (*rng)((int) filechunksize);
|
|
(void) dlb_fseek(fh, startpos + chunkoffset, SEEK_SET);
|
|
(void) dlb_fgets(buf, bufsiz, fh);
|
|
/* if padlength is 0, accept any position; when non-zero,
|
|
padlength does not count the newline but strlen(buf) does */
|
|
if (!padlength || (unsigned) strlen(buf) <= padlength + 1)
|
|
break;
|
|
}
|
|
/* use next line; for rumors, caller takes care of whether startpos
|
|
and endpos cover just true rumors or just false rumors; reaching
|
|
endpos is equivalent to end-of-file in order to avoid using the
|
|
first false rumor if fseek for a true one lands within the last one */
|
|
if (dlb_ftell(fh) >= endpos || !dlb_fgets(buf, bufsiz, fh)) {
|
|
/* assume failure is due to end-of-file; go back to start */
|
|
(void) dlb_fseek(fh, startpos, SEEK_SET);
|
|
(void) dlb_fgets(buf, bufsiz, fh);
|
|
}
|
|
if ((newl = strchr(buf, '\n')) != 0)
|
|
*newl = '\0';
|
|
/* decrypt line; make sure that our intermediate buffer is big enough */
|
|
xbufp = (strlen(buf) <= sizeof xbuf - 1) ? &xbuf[0]
|
|
: (char *) alloc((unsigned) strlen(buf) + 1);
|
|
Strcpy(buf, xcrypt(buf, xbufp));
|
|
if (xbufp != &xbuf[0])
|
|
free((genericptr_t) xbufp);
|
|
/* strip padding that makedefs adds to short lines */
|
|
if (padlength)
|
|
unpadline(buf);
|
|
return buf;
|
|
}
|
|
|
|
/* Gets a random line of text from file 'fname', and returns it.
|
|
rng is the random number generator to use, and should act like rn2 does. */
|
|
char *
|
|
get_rnd_text(
|
|
const char *fname,
|
|
char *buf,
|
|
int (*rng)(int),
|
|
unsigned padlength)
|
|
{
|
|
dlb *fh = dlb_fopen(fname, "r");
|
|
|
|
buf[0] = '\0';
|
|
if (fh) {
|
|
long starttxt = 0L;
|
|
char line[BUFSZ];
|
|
|
|
/* skip "don't edit" comment */
|
|
(void) dlb_fgets(line, sizeof line, fh);
|
|
/* obtain current file position */
|
|
(void) dlb_fseek(fh, 0L, SEEK_CUR);
|
|
starttxt = dlb_ftell(fh);
|
|
|
|
/* get a randomly chosen line; it comes back decrypted and unpadded */
|
|
Strcpy(buf, get_rnd_line(fh, line, (unsigned) sizeof line, rng,
|
|
starttxt, 0L, padlength));
|
|
(void) dlb_fclose(fh);
|
|
} else {
|
|
couldnt_open_file(fname);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
void
|
|
outrumor(
|
|
int truth, /* 1=true, -1=false, 0=either */
|
|
int mechanism)
|
|
{
|
|
static const char fortune_msg[] =
|
|
"This cookie has a scrap of paper inside.";
|
|
const char *line;
|
|
char buf[BUFSZ];
|
|
boolean reading = (mechanism == BY_COOKIE || mechanism == BY_PAPER);
|
|
|
|
if (reading) {
|
|
/* deal with various things that prevent reading */
|
|
if (is_fainted() && mechanism == BY_COOKIE) {
|
|
return;
|
|
} else if (Blind) {
|
|
if (mechanism == BY_COOKIE)
|
|
pline(fortune_msg);
|
|
pline("What a pity that you cannot read it!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
line = getrumor(truth, buf, reading ? FALSE : TRUE);
|
|
if (!*line)
|
|
line = "NetHack rumors file closed for renovation.";
|
|
switch (mechanism) {
|
|
case BY_ORACLE:
|
|
/* Oracle delivers the rumor */
|
|
pline("True to her word, the Oracle %ssays: ",
|
|
(!rn2(4) ? "offhandedly "
|
|
: (!rn2(3) ? "casually "
|
|
: (rn2(2) ? "nonchalantly " : ""))));
|
|
SetVoice((struct monst *) 0, 0, 80, voice_oracle);
|
|
verbalize1(line);
|
|
/* [WIS exercised by getrumor()] */
|
|
return;
|
|
case BY_COOKIE:
|
|
pline(fortune_msg);
|
|
/* FALLTHRU */
|
|
case BY_PAPER:
|
|
pline("It reads:");
|
|
break;
|
|
}
|
|
pline1(line);
|
|
}
|
|
|
|
static void
|
|
init_oracles(dlb *fp)
|
|
{
|
|
register int i;
|
|
char line[BUFSZ];
|
|
int cnt = 0;
|
|
|
|
/* this assumes we're only called once */
|
|
(void) dlb_fgets(line, sizeof line, fp); /* skip "don't edit" comment*/
|
|
(void) dlb_fgets(line, sizeof line, fp);
|
|
if (sscanf(line, "%5d\n", &cnt) == 1 && cnt > 0) {
|
|
go.oracle_cnt = (unsigned) cnt;
|
|
go.oracle_loc = (unsigned long *) alloc((unsigned) cnt * sizeof(long));
|
|
for (i = 0; i < cnt; i++) {
|
|
(void) dlb_fgets(line, sizeof line, fp);
|
|
(void) sscanf(line, "%5lx\n", &go.oracle_loc[i]);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
void
|
|
save_oracles(NHFILE *nhfp)
|
|
{
|
|
if (perform_bwrite(nhfp)) {
|
|
if (nhfp->structlevel)
|
|
bwrite(nhfp->fd, (genericptr_t) &go.oracle_cnt,
|
|
sizeof go.oracle_cnt);
|
|
if (go.oracle_cnt) {
|
|
if (nhfp->structlevel) {
|
|
bwrite(nhfp->fd, (genericptr_t) go.oracle_loc,
|
|
go.oracle_cnt * sizeof (long));
|
|
}
|
|
}
|
|
}
|
|
if (release_data(nhfp)) {
|
|
if (go.oracle_cnt) {
|
|
free((genericptr_t) go.oracle_loc);
|
|
go.oracle_loc = 0, go.oracle_cnt = 0, go.oracle_flg = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
restore_oracles(NHFILE *nhfp)
|
|
{
|
|
if (nhfp->structlevel)
|
|
mread(nhfp->fd, (genericptr_t) &go.oracle_cnt, sizeof go.oracle_cnt);
|
|
|
|
if (go.oracle_cnt) {
|
|
go.oracle_loc = (unsigned long *) alloc(go.oracle_cnt * sizeof(long));
|
|
if (nhfp->structlevel) {
|
|
mread(nhfp->fd, (genericptr_t) go.oracle_loc,
|
|
go.oracle_cnt * sizeof (long));
|
|
}
|
|
go.oracle_flg = 1; /* no need to call init_oracles() */
|
|
}
|
|
}
|
|
|
|
void
|
|
outoracle(boolean special, boolean delphi)
|
|
{
|
|
winid tmpwin;
|
|
dlb *oracles;
|
|
int oracle_idx;
|
|
char *endp, line[COLNO], xbuf[BUFSZ];
|
|
|
|
/* early return if we couldn't open ORACLEFILE on previous attempt,
|
|
or if all the oracularities are already exhausted */
|
|
if (go.oracle_flg < 0 || (go.oracle_flg > 0 && go.oracle_cnt == 0))
|
|
return;
|
|
|
|
oracles = dlb_fopen(ORACLEFILE, "r");
|
|
|
|
if (oracles) {
|
|
if (go.oracle_flg == 0) { /* if this is the first outoracle() */
|
|
init_oracles(oracles);
|
|
go.oracle_flg = 1;
|
|
if (go.oracle_cnt == 0)
|
|
goto close_oracles;
|
|
}
|
|
/* oracle_loc[0] is the special oracle;
|
|
oracle_loc[1..oracle_cnt-1] are normal ones */
|
|
if (go.oracle_cnt <= 1 && !special)
|
|
goto close_oracles; /*(shouldn't happen)*/
|
|
oracle_idx = special ? 0 : rnd((int) go.oracle_cnt - 1);
|
|
(void) dlb_fseek(oracles, (long) go.oracle_loc[oracle_idx], SEEK_SET);
|
|
if (!special) /* move offset of very last one into this slot */
|
|
go.oracle_loc[oracle_idx] = go.oracle_loc[--go.oracle_cnt];
|
|
|
|
tmpwin = create_nhwindow(NHW_TEXT);
|
|
if (delphi)
|
|
putstr(tmpwin, 0,
|
|
special
|
|
? "The Oracle scornfully takes all your gold and says:"
|
|
: "The Oracle meditates for a moment and then intones:");
|
|
else
|
|
putstr(tmpwin, 0, "The message reads:");
|
|
putstr(tmpwin, 0, "");
|
|
|
|
while (dlb_fgets(line, COLNO, oracles) && strcmp(line, "---\n")) {
|
|
if ((endp = strchr(line, '\n')) != 0)
|
|
*endp = 0;
|
|
putstr(tmpwin, 0, xcrypt(line, xbuf));
|
|
}
|
|
display_nhwindow(tmpwin, TRUE);
|
|
destroy_nhwindow(tmpwin);
|
|
close_oracles:
|
|
(void) dlb_fclose(oracles);
|
|
} else {
|
|
couldnt_open_file(ORACLEFILE);
|
|
go.oracle_flg = -1; /* don't try to open it again */
|
|
}
|
|
}
|
|
|
|
int
|
|
doconsult(struct monst *oracl)
|
|
{
|
|
long umoney;
|
|
int u_pay, minor_cost = 50, major_cost = 500 + 50 * u.ulevel;
|
|
int add_xpts;
|
|
char qbuf[QBUFSZ];
|
|
|
|
gm.multi = 0;
|
|
umoney = money_cnt(gi.invent);
|
|
|
|
if (!oracl) {
|
|
There("is no one here to consult.");
|
|
return ECMD_OK;
|
|
} else if (!oracl->mpeaceful) {
|
|
pline("%s is in no mood for consultations.", Monnam(oracl));
|
|
return ECMD_OK;
|
|
} else if (!umoney) {
|
|
You("have no gold.");
|
|
return ECMD_OK;
|
|
}
|
|
|
|
Sprintf(qbuf, "\"Wilt thou settle for a minor consultation?\" (%d %s)",
|
|
minor_cost, currency((long) minor_cost));
|
|
switch (ynq(qbuf)) {
|
|
default:
|
|
case 'q':
|
|
return ECMD_OK;
|
|
case 'y':
|
|
if (umoney < (long) minor_cost) {
|
|
You("don't even have enough gold for that!");
|
|
return ECMD_OK;
|
|
}
|
|
u_pay = minor_cost;
|
|
break;
|
|
case 'n':
|
|
if (umoney <= (long) minor_cost /* don't even ask */
|
|
|| (go.oracle_cnt == 1 || go.oracle_flg < 0))
|
|
return ECMD_OK;
|
|
Sprintf(qbuf, "\"Then dost thou desire a major one?\" (%d %s)",
|
|
major_cost, currency((long) major_cost));
|
|
if (y_n(qbuf) != 'y')
|
|
return ECMD_OK;
|
|
u_pay = (umoney < (long) major_cost) ? (int) umoney : major_cost;
|
|
break;
|
|
}
|
|
money2mon(oracl, (long) u_pay);
|
|
gc.context.botl = 1;
|
|
if (!u.uevent.major_oracle && !u.uevent.minor_oracle)
|
|
record_achievement(ACH_ORCL);
|
|
add_xpts = 0; /* first oracle of each type gives experience points */
|
|
if (u_pay == minor_cost) {
|
|
outrumor(1, BY_ORACLE);
|
|
if (!u.uevent.minor_oracle)
|
|
add_xpts = u_pay / (u.uevent.major_oracle ? 25 : 10);
|
|
/* 5 pts if very 1st, or 2 pts if major already done */
|
|
u.uevent.minor_oracle = TRUE;
|
|
} else {
|
|
boolean cheapskate = u_pay < major_cost;
|
|
|
|
outoracle(cheapskate, TRUE);
|
|
if (!cheapskate && !u.uevent.major_oracle)
|
|
add_xpts = u_pay / (u.uevent.minor_oracle ? 25 : 10);
|
|
/* ~100 pts if very 1st, ~40 pts if minor already done */
|
|
u.uevent.major_oracle = TRUE;
|
|
exercise(A_WIS, !cheapskate);
|
|
}
|
|
if (add_xpts) {
|
|
more_experienced(add_xpts, u_pay / 50);
|
|
newexplevel();
|
|
}
|
|
return ECMD_TIME;
|
|
}
|
|
|
|
static void
|
|
couldnt_open_file(const char *filename)
|
|
{
|
|
int save_something = gp.program_state.something_worth_saving;
|
|
|
|
/* most likely the file is missing, so suppress impossible()'s
|
|
"saving and restoring might fix this" (unless the fuzzer,
|
|
which escalates impossible to panic, is running) */
|
|
if (!iflags.debug_fuzzer)
|
|
gp.program_state.something_worth_saving = 0;
|
|
|
|
impossible("Can't open '%s' file.", filename);
|
|
gp.program_state.something_worth_saving = save_something;
|
|
}
|
|
|
|
/* is 'word' a capitalized monster name that should be preceded by "the"?
|
|
(non-unique monster like Mordor Orc, or capitalized title like Norn
|
|
rather than a name); used by the() on a string without any context;
|
|
this sets up a list of names rather than scan all of mons[] every time
|
|
the decision is needed (resulting list currently contains 27 monster
|
|
entries and 20 hallucination entries) */
|
|
boolean
|
|
CapitalMon(
|
|
const char *word) /* potential monster name; a name might be followed by
|
|
* something like " corpse" */
|
|
{
|
|
const char *nam;
|
|
unsigned i, wln, nln;
|
|
|
|
if (!word || !*word || *word == lowc(*word))
|
|
return FALSE; /* 'word' is not a capitalized monster name */
|
|
|
|
if (!CapMons)
|
|
init_CapMons();
|
|
|
|
wln = (unsigned) strlen(word);
|
|
for (i = 0; i < CapMonSiz - 1; ++i) {
|
|
nam = CapMons[i];
|
|
nln = (unsigned) strlen(nam);
|
|
if (wln < nln)
|
|
continue;
|
|
/*
|
|
* Unlike name_to_mon(), we don't need to find the longest match
|
|
* or return the gender or a pointer to trailing stuff. We do
|
|
* check full words though: "Foo" matches "Foo" and "Foo bar" and
|
|
* "Foo's bar" but not "Foobar". We use case-sensitive matching.
|
|
*/
|
|
if (!strncmp(nam, word, nln)
|
|
&& (!word[nln] || word[nln] == ' ' || word[nln] == '\''))
|
|
return TRUE; /* 'word' is a capitalized monster name */
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/* one-time initialization of CapMons[], a list of non-unique monsters
|
|
having a capitalized type name like Green-elf or Archon, plus unique
|
|
monsters whose "name" is a title rather than a personal name, plus
|
|
hallucinatory monster names that fall into either of those categories */
|
|
static void
|
|
init_CapMons(void)
|
|
{
|
|
unsigned pass;
|
|
dlb *bogonfile = dlb_fopen(BOGUSMONFILE, "r");
|
|
|
|
if (CapMons) /* sanity precaution */
|
|
free_CapMons();
|
|
|
|
/* first pass: count the number of relevant monster names, then
|
|
allocate memory for CapMons[]; second pass: populate CapMons[] */
|
|
for (pass = 1; pass <= 2; ++pass) {
|
|
struct permonst *mptr;
|
|
const char *nam;
|
|
unsigned mndx, mgend;
|
|
|
|
/* the first CapMonstCnt entries come from mons[].pmnames[] and
|
|
the next CapBogonCnt entries from from the 'bogusmons' file;
|
|
there is an extra entry for Null at the end, but that is only
|
|
useful to force non-zero array size in case both mons[] and
|
|
bogusmons get modified to have no applicable monster names */
|
|
CapMonstCnt = CapBogonCnt = 0;
|
|
|
|
/* gather applicable actual monsters */
|
|
for (mndx = LOW_PM; mndx < NUMMONS; ++mndx) {
|
|
mptr = &mons[mndx];
|
|
if ((mptr->geno & G_UNIQ) != 0 && !the_unique_pm(mptr))
|
|
continue;
|
|
for (mgend = MALE; mgend < NUM_MGENDERS; ++mgend) {
|
|
nam = mptr->pmnames[mgend];
|
|
if (nam && *nam != lowc(*nam)) {
|
|
if (pass == 2)
|
|
CapMons[CapMonstCnt] = nam;
|
|
++CapMonstCnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* now gather applicable hallucinatory monsters */
|
|
if (bogonfile) {
|
|
char hline[BUFSZ], xbuf[BUFSZ], *endp, *startp, code;
|
|
|
|
/* rewind; effectively a no-op for pass 1; essential for pass 2 */
|
|
(void) dlb_fseek(bogonfile, 0L, SEEK_SET);
|
|
/* skip "don't edit" comment (first line of file) */
|
|
(void) dlb_fgets(hline, sizeof hline, bogonfile);
|
|
|
|
/* one monster name per line in rudimentary encrypted format;
|
|
some are prefixed by a classification code to indicate
|
|
gender and/or to distinguish an individual from a type
|
|
(code is a single punctuation character when present) */
|
|
while (dlb_fgets(hline, sizeof hline, bogonfile)) {
|
|
if ((endp = strchr(hline, '\n')) != 0)
|
|
*endp = '\0'; /* strip newline */
|
|
(void) xcrypt(hline, xbuf);
|
|
unpadline(xbuf);
|
|
|
|
if (!xbuf[0] || !strchr(bogon_codes, xbuf[0]))
|
|
code = '\0', startp = &xbuf[0]; /* ordinary */
|
|
else
|
|
code = xbuf[0], startp = &xbuf[1]; /* special */
|
|
|
|
if (*startp != lowc(*startp) && !bogon_is_pname(code)) {
|
|
if (pass == 2)
|
|
CapMons[CapMonstCnt + CapBogonCnt] = dupstr(startp);
|
|
++CapBogonCnt;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* finish the current pass */
|
|
if (pass == 1) {
|
|
CapMonSiz = CapMonstCnt + CapBogonCnt + 1; /* +1: terminator */
|
|
CapMons = (const char **) alloc(CapMonSiz * sizeof *CapMons);
|
|
} else { /* pass == 2 */
|
|
/* terminator; not strictly needed */
|
|
CapMons[CapMonSiz - 1] = (const char *) 0;
|
|
|
|
if (bogonfile)
|
|
(void) dlb_fclose(bogonfile), bogonfile = (dlb *) 0;
|
|
}
|
|
}
|
|
#ifdef DEBUG
|
|
/*
|
|
* CapMons[] init doesn't kick in until needed. To force this name
|
|
* dump, set DEBUGFILES to "CapMons" in your environment (or in
|
|
* sysconf) prior to starting nethack, wish for a statue of an Archon
|
|
* and drop it if held, then step away and apply a stethoscope towards
|
|
* it to trigger a message that passes "Archon" to the() which will
|
|
* then call CapitalMon() which in turn will call init_CapMons().
|
|
*/
|
|
if (wizard && explicitdebug("CapMons")) {
|
|
char buf[BUFSZ];
|
|
unsigned i;
|
|
winid tmpwin = create_nhwindow(NHW_TEXT);
|
|
|
|
putstr(tmpwin, 0,
|
|
"Capitalized monster type names normally preceded by \"the\":");
|
|
for (i = 0; i < CapMonSiz - 1; ++i) {
|
|
Sprintf(buf, " %.77s", CapMons[i]);
|
|
putstr(tmpwin, 0, buf);
|
|
}
|
|
display_nhwindow(tmpwin, TRUE);
|
|
destroy_nhwindow(tmpwin);
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
/* release memory allocated for the list of capitalized monster type names */
|
|
void
|
|
free_CapMons(void)
|
|
{
|
|
/* note: some elements of CapMons[] are string literals from
|
|
mons[].pmnames[] and should not be freed, others are dynamically
|
|
allocated copies of hallucinatory monster names and should be freed */
|
|
if (CapMons) {
|
|
unsigned idx;
|
|
|
|
/* skip 0..MonstCnt-1, free MonstCnt..(MonstCnt+BogonCnt-1) */
|
|
for (idx = CapMonstCnt; idx < CapMonSiz - 1; ++idx)
|
|
free((genericptr_t) CapMons[idx]); /* cast: discard 'const' */
|
|
free((genericptr_t) CapMons), CapMons = (const char **) 0;
|
|
}
|
|
CapMonSiz = 0;
|
|
}
|
|
|
|
/*rumors.c*/
|