gcc has recognized various "magic comments" for white-listing
occurrences of implicit fallthrough in switch statements for
a long time:
The range and shape of "falls through" comments accepted are
contingent upon the level of the warning. (The default level is =3.)
-Wimplicit-fallthrough=0 disables the warning altogether.
-Wimplicit-fallthrough=1 treats any kind of comment as a "falls through" comment.
-Wimplicit-fallthrough=2 essentially accepts any comment that contains something
that matches (case insensitively) "falls?[ \t-]*thr(ough|u)" regular expression.
-Wimplicit-fallthrough=3 case sensitively matches a wide range of regular
expressions, listed in the GCC manual. E.g., all of these are accepted:
/* Falls through. */
/* fall-thru */
/* Else falls through. */
/* FALLTHRU */
/* ... falls through ... */
etc.
-Wimplicit-fallthrough=4 also, case sensitively matches a range of regular
expressions but is much more strict than level =3.
-Wimplicit-fallthrough=5 doesn't recognize any comments.
Plenty of other compilers did not recognize the gcc comment convention,
and up until now the compiler warning for detecting unintended
fallthrough had to be suppressed on other compilers. That's because the code
in NetHack has been relying on the gcc approach, and only the gcc approach.
The C23 standard introduces an attribute [[fallthrough]] for the
functionality, when implicit fallthrough warnings have been enabled.
Several popular compilers already support that, or a very similar attribute
style approach, today, even ahead of their C23 support:
C compiler whitelist approach
--------------------------- -------------------------------------
C23 conforming compilers [[fallthrough]]
clang versions supporting
standards prior to
C23 __attribute__((__fallthrough__))
Microsoft Visual Studio
since VS 2022 17.4.
The warning C5262 controls
whether the implict
fallthrough is detected and
warned about with
/std:clatest. [[fallthrough]]
This adds support to NetHack for the attribute approach by inserting a
macro FALLTHROUGH to the existing cases that require white-listing, so
other compilers can analyze things too.
The definition of the FALLTHROUGH macro is controlled in include/tradstdc.h.
The gcc comment approach has also been left in place at this time.
1484 lines
47 KiB
C
1484 lines
47 KiB
C
/* NetHack 3.7 topten.c $NHDT-Date: 1606009004 2020/11/22 01:36:44 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.74 $ */
|
|
/* 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"
|
|
|
|
#if defined(VMS) && !defined(UPDATE_RECORD_IN_PLACE)
|
|
/* We don't want to rewrite the whole file, because that entails
|
|
creating a new version which requires that the old one be deletable.
|
|
[Write and Delete are separate permissions on VMS. 'record' should
|
|
be writable but not deletable there.] */
|
|
#define UPDATE_RECORD_IN_PLACE
|
|
#endif
|
|
|
|
/*
|
|
* Updating in place can leave junk at the end of the file in some
|
|
* circumstances (if it shrinks and the O.S. doesn't have a straightforward
|
|
* way to truncate it). The trailing junk is harmless and the code
|
|
* which reads the scores will ignore it.
|
|
*/
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
static long final_fpos; /* [note: do not move this to the 'g' struct] */
|
|
#endif
|
|
|
|
#define done_stopprint program_state.stopprint
|
|
|
|
#define newttentry() (struct toptenentry *) alloc(sizeof (struct toptenentry))
|
|
#define dealloc_ttentry(ttent) free((genericptr_t) (ttent))
|
|
#ifndef NAMSZ
|
|
/* Changing NAMSZ can break your existing record/logfile */
|
|
#define NAMSZ 10
|
|
#endif
|
|
#define DTHSZ 100
|
|
#define ROLESZ 3
|
|
|
|
struct toptenentry {
|
|
struct toptenentry *tt_next;
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
long fpos;
|
|
#endif
|
|
long points;
|
|
int deathdnum, deathlev;
|
|
int maxlvl, hp, maxhp, deaths;
|
|
int ver_major, ver_minor, patchlevel;
|
|
long deathdate, birthdate;
|
|
int uid;
|
|
char plrole[ROLESZ + 1];
|
|
char plrace[ROLESZ + 1];
|
|
char plgend[ROLESZ + 1];
|
|
char plalign[ROLESZ + 1];
|
|
char name[NAMSZ + 1];
|
|
char death[DTHSZ + 1];
|
|
};
|
|
static struct toptenentry *tt_head;
|
|
/* size big enough to read in all the string fields at once; includes
|
|
room for separating space or trailing newline plus string terminator */
|
|
#define SCANBUFSZ (4 * (ROLESZ + 1) + (NAMSZ + 1) + (DTHSZ + 1) + 1)
|
|
|
|
static struct toptenentry zerott;
|
|
|
|
staticfn void topten_print(const char *);
|
|
staticfn void topten_print_bold(const char *);
|
|
staticfn void outheader(void);
|
|
staticfn void outentry(int, struct toptenentry *, boolean);
|
|
staticfn void discardexcess(FILE *);
|
|
staticfn void readentry(FILE *, struct toptenentry *);
|
|
staticfn void writeentry(FILE *, struct toptenentry *);
|
|
#ifdef XLOGFILE
|
|
staticfn void writexlentry(FILE *, struct toptenentry *, int);
|
|
staticfn long encodexlogflags(void);
|
|
staticfn long encodeconduct(void);
|
|
staticfn long encodeachieve(boolean);
|
|
staticfn void add_achieveX(char *, const char *, boolean);
|
|
staticfn char *encode_extended_achievements(char *);
|
|
staticfn char *encode_extended_conducts(char *);
|
|
#endif
|
|
staticfn void free_ttlist(struct toptenentry *);
|
|
staticfn int classmon(char *);
|
|
staticfn int score_wanted(boolean, int, struct toptenentry *, int,
|
|
const char **, int);
|
|
#ifdef NO_SCAN_BRACK
|
|
staticfn void nsb_mung_line(char *);
|
|
staticfn void nsb_unmung_line(char *);
|
|
#endif
|
|
|
|
/* "killed by",&c ["an"] 'svk.killer.name' */
|
|
void
|
|
formatkiller(
|
|
char *buf,
|
|
unsigned siz,
|
|
int how,
|
|
boolean incl_helpless)
|
|
{
|
|
static NEARDATA const char *const killed_by_prefix[] = {
|
|
/* DIED, CHOKING, POISONING, STARVING, */
|
|
"killed by ", "choked on ", "poisoned by ", "died of ",
|
|
/* DROWNING, BURNING, DISSOLVED, CRUSHING, */
|
|
"drowned in ", "burned by ", "dissolved in ", "crushed to death by ",
|
|
/* STONING, TURNED_SLIME, GENOCIDED, */
|
|
"petrified by ", "turned to slime by ", "killed by ",
|
|
/* PANICKED, TRICKED, QUIT, ESCAPED, ASCENDED */
|
|
"", "", "", "", ""
|
|
};
|
|
unsigned l;
|
|
char c, *kname = svk.killer.name;
|
|
|
|
buf[0] = '\0'; /* lint suppression */
|
|
switch (svk.killer.format) {
|
|
default:
|
|
impossible("bad killer format? (%d)", svk.killer.format);
|
|
FALLTHROUGH;
|
|
/*FALLTHRU*/
|
|
case NO_KILLER_PREFIX:
|
|
break;
|
|
case KILLED_BY_AN:
|
|
kname = an(kname);
|
|
FALLTHROUGH;
|
|
/*FALLTHRU*/
|
|
case KILLED_BY:
|
|
(void) strncat(buf, killed_by_prefix[how], siz - 1);
|
|
l = Strlen(buf);
|
|
buf += l, siz -= l;
|
|
break;
|
|
}
|
|
/* Copy kname into buf[].
|
|
* Object names and named fruit have already been sanitized, but
|
|
* monsters can have "called 'arbitrary text'" attached to them,
|
|
* so make sure that that text can't confuse field splitting when
|
|
* record, logfile, or xlogfile is re-read at some later point.
|
|
*/
|
|
while (--siz > 0) {
|
|
c = *kname++;
|
|
if (!c)
|
|
break;
|
|
else if (c == ',')
|
|
c = ';';
|
|
/* 'xlogfile' doesn't really need protection for '=', but
|
|
fixrecord.awk for corrupted 3.6.0 'record' does (only
|
|
if using xlogfile rather than logfile to repair record) */
|
|
else if (c == '=')
|
|
c = '_';
|
|
/* tab is not possible due to use of mungspaces() when naming;
|
|
it would disrupt xlogfile parsing if it were present */
|
|
else if (c == '\t')
|
|
c = ' ';
|
|
*buf++ = c;
|
|
}
|
|
*buf = '\0';
|
|
|
|
if (incl_helpless && gm.multi < 0) {
|
|
/* X <= siz: 'sizeof "string"' includes 1 for '\0' terminator */
|
|
if (gm.multi_reason
|
|
&& strlen(gm.multi_reason) + sizeof ", while " <= siz)
|
|
Sprintf(buf, ", while %s", gm.multi_reason);
|
|
/* either gm.multi_reason wasn't specified or wouldn't fit */
|
|
else if (sizeof ", while helpless" <= siz)
|
|
Strcpy(buf, ", while helpless");
|
|
/* else extra death info won't fit, so leave it out */
|
|
}
|
|
}
|
|
|
|
staticfn void
|
|
topten_print(const char *x)
|
|
{
|
|
if (gt.toptenwin == WIN_ERR)
|
|
raw_print(x);
|
|
else
|
|
putstr(gt.toptenwin, ATR_NONE, x);
|
|
}
|
|
|
|
staticfn void
|
|
topten_print_bold(const char *x)
|
|
{
|
|
if (gt.toptenwin == WIN_ERR)
|
|
raw_print_bold(x);
|
|
else
|
|
putstr(gt.toptenwin, ATR_BOLD, x);
|
|
}
|
|
|
|
int
|
|
observable_depth(d_level *lev)
|
|
{
|
|
#if 0
|
|
/* if we ever randomize the order of the elemental planes, we
|
|
must use a constant external representation in the record file */
|
|
if (In_endgame(lev)) {
|
|
if (Is_astralevel(lev))
|
|
return -5;
|
|
else if (Is_waterlevel(lev))
|
|
return -4;
|
|
else if (Is_firelevel(lev))
|
|
return -3;
|
|
else if (Is_airlevel(lev))
|
|
return -2;
|
|
else if (Is_earthlevel(lev))
|
|
return -1;
|
|
else
|
|
return 0; /* ? */
|
|
} else
|
|
#endif
|
|
return depth(lev);
|
|
}
|
|
|
|
/* throw away characters until current record has been entirely consumed */
|
|
staticfn void
|
|
discardexcess(FILE *rfile)
|
|
{
|
|
int c;
|
|
|
|
do {
|
|
c = fgetc(rfile);
|
|
} while (c != '\n' && c != EOF);
|
|
}
|
|
|
|
DISABLE_WARNING_FORMAT_NONLITERAL
|
|
|
|
staticfn void
|
|
readentry(FILE *rfile, struct toptenentry *tt)
|
|
{
|
|
char inbuf[SCANBUFSZ], s1[SCANBUFSZ], s2[SCANBUFSZ], s3[SCANBUFSZ],
|
|
s4[SCANBUFSZ], s5[SCANBUFSZ], s6[SCANBUFSZ];
|
|
|
|
#ifdef NO_SCAN_BRACK /* Version_ Pts DgnLevs_ Hp___ Died__Born id */
|
|
static const char fmt[] = "%d %d %d %ld %d %d %d %d %d %d %ld %ld %d%*c";
|
|
static const char fmt32[] = "%c%c %s %s%*c";
|
|
static const char fmt33[] = "%s %s %s %s %s %s%*c";
|
|
#else
|
|
static const char fmt[] = "%d.%d.%d %ld %d %d %d %d %d %d %ld %ld %d ";
|
|
static const char fmt32[] = "%c%c %[^,],%[^\n]%*c";
|
|
static const char fmt33[] = "%s %s %s %s %[^,],%[^\n]%*c";
|
|
#endif
|
|
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
/* note: input below must read the record's terminating newline */
|
|
final_fpos = tt->fpos = ftell(rfile);
|
|
#endif
|
|
#define TTFIELDS 13
|
|
if (fscanf(rfile, fmt, &tt->ver_major, &tt->ver_minor, &tt->patchlevel,
|
|
&tt->points, &tt->deathdnum, &tt->deathlev, &tt->maxlvl,
|
|
&tt->hp, &tt->maxhp, &tt->deaths, &tt->deathdate,
|
|
&tt->birthdate, &tt->uid) != TTFIELDS) {
|
|
#undef TTFIELDS
|
|
tt->points = 0;
|
|
discardexcess(rfile);
|
|
} else {
|
|
/* load remainder of record into a local buffer;
|
|
this imposes an implicit length limit of SCANBUFSZ
|
|
on every string field extracted from the buffer */
|
|
if (!fgets(inbuf, sizeof inbuf, rfile)) {
|
|
/* sscanf will fail and tt->points will be set to 0 */
|
|
*inbuf = '\0';
|
|
} else if (!strchr(inbuf, '\n')) {
|
|
Strcpy(&inbuf[sizeof inbuf - 2], "\n");
|
|
discardexcess(rfile);
|
|
}
|
|
/* Check for backwards compatibility */
|
|
if (tt->ver_major < 3 || (tt->ver_major == 3 && tt->ver_minor < 3)) {
|
|
int i;
|
|
|
|
if (sscanf(inbuf, fmt32, tt->plrole, tt->plgend, s1, s2) == 4) {
|
|
tt->plrole[1] = tt->plgend[1] = '\0'; /* read via %c */
|
|
copynchars(tt->name, s1, (int) (sizeof tt->name) - 1);
|
|
copynchars(tt->death, s2, (int) (sizeof tt->death) - 1);
|
|
} else
|
|
tt->points = 0;
|
|
tt->plrole[1] = '\0';
|
|
if ((i = str2role(tt->plrole)) >= 0)
|
|
Strcpy(tt->plrole, roles[i].filecode);
|
|
Strcpy(tt->plrace, "?");
|
|
Strcpy(tt->plgend, (tt->plgend[0] == 'M') ? "Mal" : "Fem");
|
|
Strcpy(tt->plalign, "?");
|
|
} else if (sscanf(inbuf, fmt33, s1, s2, s3, s4, s5, s6) == 6) {
|
|
copynchars(tt->plrole, s1, (int) (sizeof tt->plrole) - 1);
|
|
copynchars(tt->plrace, s2, (int) (sizeof tt->plrace) - 1);
|
|
copynchars(tt->plgend, s3, (int) (sizeof tt->plgend) - 1);
|
|
copynchars(tt->plalign, s4, (int) (sizeof tt->plalign) - 1);
|
|
copynchars(tt->name, s5, (int) (sizeof tt->name) - 1);
|
|
copynchars(tt->death, s6, (int) (sizeof tt->death) - 1);
|
|
} else
|
|
tt->points = 0;
|
|
#ifdef NO_SCAN_BRACK
|
|
if (tt->points > 0) {
|
|
nsb_unmung_line(tt->name);
|
|
nsb_unmung_line(tt->death);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* check old score entries for Y2K problem and fix whenever found */
|
|
if (tt->points > 0) {
|
|
if (tt->birthdate < 19000000L)
|
|
tt->birthdate += 19000000L;
|
|
if (tt->deathdate < 19000000L)
|
|
tt->deathdate += 19000000L;
|
|
}
|
|
}
|
|
|
|
staticfn void
|
|
writeentry(FILE *rfile, struct toptenentry *tt)
|
|
{
|
|
static const char fmt32[] = "%c%c "; /* role,gender */
|
|
static const char fmt33[] = "%s %s %s %s "; /* role,race,gndr,algn */
|
|
#ifndef NO_SCAN_BRACK
|
|
static const char fmt0[] = "%d.%d.%d %ld %d %d %d %d %d %d %ld %ld %d ";
|
|
static const char fmtX[] = "%s,%s\n";
|
|
#else /* NO_SCAN_BRACK */
|
|
static const char fmt0[] = "%d %d %d %ld %d %d %d %d %d %d %ld %ld %d ";
|
|
static const char fmtX[] = "%s %s\n";
|
|
|
|
nsb_mung_line(tt->name);
|
|
nsb_mung_line(tt->death);
|
|
#endif
|
|
|
|
(void) fprintf(rfile, fmt0, tt->ver_major, tt->ver_minor, tt->patchlevel,
|
|
tt->points, tt->deathdnum, tt->deathlev, tt->maxlvl,
|
|
tt->hp, tt->maxhp, tt->deaths, tt->deathdate,
|
|
tt->birthdate, tt->uid);
|
|
if (tt->ver_major < 3 || (tt->ver_major == 3 && tt->ver_minor < 3))
|
|
(void) fprintf(rfile, fmt32, tt->plrole[0], tt->plgend[0]);
|
|
else
|
|
(void) fprintf(rfile, fmt33, tt->plrole, tt->plrace, tt->plgend,
|
|
tt->plalign);
|
|
(void) fprintf(rfile, fmtX, onlyspace(tt->name) ? "_" : tt->name,
|
|
tt->death);
|
|
|
|
#ifdef NO_SCAN_BRACK
|
|
nsb_unmung_line(tt->name);
|
|
nsb_unmung_line(tt->death);
|
|
#endif
|
|
}
|
|
|
|
RESTORE_WARNING_FORMAT_NONLITERAL
|
|
|
|
#ifdef XLOGFILE
|
|
|
|
/* as tab is never used in eg. svp.plname or death, no need to mangle those. */
|
|
staticfn void
|
|
writexlentry(FILE *rfile, struct toptenentry *tt, int how)
|
|
{
|
|
#define Fprintf (void) fprintf
|
|
#define XLOG_SEP '\t' /* xlogfile field separator. */
|
|
char buf[BUFSZ], tmpbuf[DTHSZ + 1];
|
|
char achbuf[N_ACH * 40];
|
|
|
|
Sprintf(buf, "version=%d.%d.%d", tt->ver_major, tt->ver_minor,
|
|
tt->patchlevel);
|
|
Sprintf(eos(buf), "%cpoints=%ld%cdeathdnum=%d%cdeathlev=%d", XLOG_SEP,
|
|
tt->points, XLOG_SEP, tt->deathdnum, XLOG_SEP, tt->deathlev);
|
|
Sprintf(eos(buf), "%cmaxlvl=%d%chp=%d%cmaxhp=%d", XLOG_SEP, tt->maxlvl,
|
|
XLOG_SEP, tt->hp, XLOG_SEP, tt->maxhp);
|
|
Sprintf(eos(buf), "%cdeaths=%d%cdeathdate=%ld%cbirthdate=%ld%cuid=%d",
|
|
XLOG_SEP, tt->deaths, XLOG_SEP, tt->deathdate, XLOG_SEP,
|
|
tt->birthdate, XLOG_SEP, tt->uid);
|
|
Fprintf(rfile, "%s", buf);
|
|
Sprintf(buf, "%crole=%s%crace=%s%cgender=%s%calign=%s", XLOG_SEP,
|
|
tt->plrole, XLOG_SEP, tt->plrace, XLOG_SEP, tt->plgend, XLOG_SEP,
|
|
tt->plalign);
|
|
/* make a copy of death reason that doesn't include ", while helpless" */
|
|
formatkiller(tmpbuf, sizeof tmpbuf, how, FALSE);
|
|
Fprintf(rfile, "%s%cname=%s%cdeath=%s",
|
|
buf, /* (already includes separator) */
|
|
XLOG_SEP, svp.plname, XLOG_SEP, tmpbuf);
|
|
if (gm.multi < 0)
|
|
Fprintf(rfile, "%cwhile=%s", XLOG_SEP,
|
|
gm.multi_reason ? gm.multi_reason : "helpless");
|
|
Fprintf(rfile, "%cconduct=0x%lx%cturns=%ld%cachieve=0x%lx", XLOG_SEP,
|
|
encodeconduct(), XLOG_SEP, svm.moves, XLOG_SEP,
|
|
encodeachieve(FALSE));
|
|
Fprintf(rfile, "%cachieveX=%s", XLOG_SEP,
|
|
encode_extended_achievements(achbuf));
|
|
Fprintf(rfile, "%cconductX=%s", XLOG_SEP,
|
|
encode_extended_conducts(buf)); /* reuse 'buf[]' */
|
|
Fprintf(rfile, "%crealtime=%ld%cstarttime=%ld%cendtime=%ld", XLOG_SEP,
|
|
urealtime.realtime, XLOG_SEP,
|
|
timet_to_seconds(ubirthday), XLOG_SEP,
|
|
timet_to_seconds(urealtime.finish_time));
|
|
Fprintf(rfile, "%cgender0=%s%calign0=%s", XLOG_SEP,
|
|
genders[flags.initgend].filecode, XLOG_SEP,
|
|
aligns[1 - u.ualignbase[A_ORIGINAL]].filecode);
|
|
Fprintf(rfile, "%cflags=0x%lx", XLOG_SEP, encodexlogflags());
|
|
Fprintf(rfile, "%cgold=%ld", XLOG_SEP,
|
|
money_cnt(gi.invent) + hidden_gold(TRUE));
|
|
Fprintf(rfile, "%cwish_cnt=%ld", XLOG_SEP, u.uconduct.wishes);
|
|
Fprintf(rfile, "%carti_wish_cnt=%ld", XLOG_SEP, u.uconduct.wisharti);
|
|
Fprintf(rfile, "%cbones=%ld", XLOG_SEP, u.uroleplay.numbones);
|
|
Fprintf(rfile, "\n");
|
|
#undef XLOG_SEP
|
|
}
|
|
|
|
staticfn long
|
|
encodexlogflags(void)
|
|
{
|
|
long e = 0L;
|
|
|
|
if (wizard)
|
|
e |= 1L << 0;
|
|
if (discover)
|
|
e |= 1L << 1;
|
|
if (!u.uroleplay.numbones)
|
|
e |= 1L << 2;
|
|
|
|
return e;
|
|
}
|
|
|
|
staticfn long
|
|
encodeconduct(void)
|
|
{
|
|
long e = 0L;
|
|
|
|
if (!u.uconduct.food)
|
|
e |= 1L << 0;
|
|
if (!u.uconduct.unvegan)
|
|
e |= 1L << 1;
|
|
if (!u.uconduct.unvegetarian)
|
|
e |= 1L << 2;
|
|
if (!u.uconduct.gnostic)
|
|
e |= 1L << 3;
|
|
if (!u.uconduct.weaphit)
|
|
e |= 1L << 4;
|
|
if (!u.uconduct.killer)
|
|
e |= 1L << 5;
|
|
if (!u.uconduct.literate)
|
|
e |= 1L << 6;
|
|
if (!u.uconduct.polypiles)
|
|
e |= 1L << 7;
|
|
if (!u.uconduct.polyselfs)
|
|
e |= 1L << 8;
|
|
if (!u.uconduct.wishes)
|
|
e |= 1L << 9;
|
|
if (!u.uconduct.wisharti)
|
|
e |= 1L << 10;
|
|
if (!num_genocides())
|
|
e |= 1L << 11;
|
|
/* one bit isn't really adequate for sokoban conduct:
|
|
reporting "obeyed sokoban rules" is misleading if sokoban wasn't
|
|
completed or at least attempted; however, suppressing that when
|
|
sokoban was never entered, as we do here, risks reporting
|
|
"violated sokoban rules" when no such thing occurred; this can
|
|
be disambiguated in xlogfile post-processors by testing the
|
|
entered-sokoban bit in the 'achieve' field */
|
|
if (!u.uconduct.sokocheat && sokoban_in_play())
|
|
e |= 1L << 12;
|
|
if (!u.uconduct.pets)
|
|
e |= 1L << 13;
|
|
|
|
return e;
|
|
}
|
|
|
|
staticfn long
|
|
encodeachieve(
|
|
boolean secondlong) /* False: handle achievements 1..31, True: 32..62 */
|
|
{
|
|
int i, achidx, offset;
|
|
long r = 0L;
|
|
|
|
/*
|
|
* 32: portable limit for 'long'.
|
|
* Force 32 even on configurations that are using 64 bit longs.
|
|
*
|
|
* We use signed long and limit ourselves to 31 bits since tools
|
|
* that post-process xlogfile might not be able to cope with
|
|
* 'unsigned long'.
|
|
*/
|
|
offset = secondlong ? (32 - 1) : 0;
|
|
for (i = 0; u.uachieved[i]; ++i) {
|
|
achidx = u.uachieved[i] - offset;
|
|
if (achidx > 0 && achidx < 32) /* value 1..31 sets bit 0..30 */
|
|
r |= 1L << (achidx - 1);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/* add the achievement or conduct comma-separated to string */
|
|
staticfn void
|
|
add_achieveX(char *buf, const char *achievement, boolean condition)
|
|
{
|
|
if (condition) {
|
|
if (buf[0] != '\0') {
|
|
Strcat(buf, ",");
|
|
}
|
|
Strcat(buf, achievement);
|
|
}
|
|
}
|
|
|
|
staticfn char *
|
|
encode_extended_achievements(char *buf)
|
|
{
|
|
char rnkbuf[40];
|
|
const char *achievement = NULL;
|
|
int i, achidx, absidx;
|
|
|
|
buf[0] = '\0';
|
|
for (i = 0; u.uachieved[i]; i++) {
|
|
achidx = u.uachieved[i];
|
|
absidx = abs(achidx);
|
|
switch (absidx) {
|
|
case ACH_UWIN:
|
|
achievement = "ascended";
|
|
break;
|
|
case ACH_ASTR:
|
|
achievement = "entered_astral_plane";
|
|
break;
|
|
case ACH_ENDG:
|
|
achievement = "entered_elemental_planes";
|
|
break;
|
|
case ACH_AMUL:
|
|
achievement = "obtained_the_amulet_of_yendor";
|
|
break;
|
|
case ACH_INVK:
|
|
achievement = "performed_the_invocation_ritual";
|
|
break;
|
|
case ACH_BOOK:
|
|
achievement = "obtained_the_book_of_the_dead";
|
|
break;
|
|
case ACH_BELL:
|
|
achievement = "obtained_the_bell_of_opening";
|
|
break;
|
|
case ACH_CNDL:
|
|
achievement = "obtained_the_candelabrum_of_invocation";
|
|
break;
|
|
case ACH_HELL:
|
|
achievement = "entered_gehennom";
|
|
break;
|
|
case ACH_MEDU:
|
|
achievement = "defeated_medusa";
|
|
break;
|
|
case ACH_MINE_PRIZE:
|
|
achievement = "obtained_the_luckstone_from_the_mines";
|
|
break;
|
|
case ACH_SOKO_PRIZE:
|
|
achievement = "obtained_the_sokoban_prize";
|
|
break;
|
|
case ACH_ORCL:
|
|
achievement = "consulted_the_oracle";
|
|
break;
|
|
case ACH_NOVL:
|
|
achievement = "read_a_discworld_novel";
|
|
break;
|
|
case ACH_MINE:
|
|
achievement = "entered_the_gnomish_mines";
|
|
break;
|
|
case ACH_TOWN:
|
|
achievement = "entered_mine_town";
|
|
break;
|
|
case ACH_SHOP:
|
|
achievement = "entered_a_shop";
|
|
break;
|
|
case ACH_TMPL:
|
|
achievement = "entered_a_temple";
|
|
break;
|
|
case ACH_SOKO:
|
|
achievement = "entered_sokoban";
|
|
break;
|
|
case ACH_BGRM:
|
|
achievement = "entered_bigroom";
|
|
break;
|
|
case ACH_TUNE:
|
|
achievement = "learned_castle_drawbridge_tune";
|
|
break;
|
|
/* rank 0 is the starting condition, not an achievement; 8 is Xp 30 */
|
|
case ACH_RNK1: case ACH_RNK2: case ACH_RNK3: case ACH_RNK4:
|
|
case ACH_RNK5: case ACH_RNK6: case ACH_RNK7: case ACH_RNK8:
|
|
Sprintf(rnkbuf, "attained_the_rank_of_%s",
|
|
rank_of(rank_to_xlev(absidx - (ACH_RNK1 - 1)),
|
|
Role_switch, (achidx < 0) ? TRUE : FALSE));
|
|
strNsubst(rnkbuf, " ", "_", 0); /* replace every ' ' with '_' */
|
|
achievement = lcase(rnkbuf);
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
add_achieveX(buf, achievement, TRUE);
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
staticfn char *
|
|
encode_extended_conducts(char *buf)
|
|
{
|
|
buf[0] = '\0';
|
|
add_achieveX(buf, "foodless", !u.uconduct.food);
|
|
add_achieveX(buf, "vegan", !u.uconduct.unvegan);
|
|
add_achieveX(buf, "vegetarian", !u.uconduct.unvegetarian);
|
|
add_achieveX(buf, "atheist", !u.uconduct.gnostic);
|
|
add_achieveX(buf, "weaponless", !u.uconduct.weaphit);
|
|
add_achieveX(buf, "pacifist", !u.uconduct.killer);
|
|
add_achieveX(buf, "illiterate", !u.uconduct.literate);
|
|
add_achieveX(buf, "polyless", !u.uconduct.polypiles);
|
|
add_achieveX(buf, "polyselfless", !u.uconduct.polyselfs);
|
|
add_achieveX(buf, "wishless", !u.uconduct.wishes);
|
|
add_achieveX(buf, "artiwishless", !u.uconduct.wisharti);
|
|
add_achieveX(buf, "genocideless", !num_genocides());
|
|
if (sokoban_in_play())
|
|
add_achieveX(buf, "sokoban", !u.uconduct.sokocheat);
|
|
add_achieveX(buf, "blind", u.uroleplay.blind);
|
|
add_achieveX(buf, "deaf", u.uroleplay.deaf);
|
|
add_achieveX(buf, "nudist", u.uroleplay.nudist);
|
|
add_achieveX(buf, "pauper", u.uroleplay.pauper);
|
|
add_achieveX(buf, "bonesless", !flags.bones);
|
|
add_achieveX(buf, "petless", !u.uconduct.pets);
|
|
|
|
return buf;
|
|
}
|
|
|
|
#endif /* XLOGFILE */
|
|
|
|
staticfn void
|
|
free_ttlist(struct toptenentry *tt)
|
|
{
|
|
struct toptenentry *ttnext;
|
|
|
|
while (tt->points > 0) {
|
|
ttnext = tt->tt_next;
|
|
dealloc_ttentry(tt);
|
|
tt = ttnext;
|
|
}
|
|
dealloc_ttentry(tt);
|
|
}
|
|
|
|
void
|
|
topten(int how, time_t when)
|
|
{
|
|
struct toptenentry *t0, *tprev;
|
|
struct toptenentry *t1;
|
|
FILE *rfile;
|
|
#ifdef LOGFILE
|
|
FILE *lfile;
|
|
#endif
|
|
#ifdef XLOGFILE
|
|
FILE *xlfile;
|
|
#endif
|
|
int uid = getuid();
|
|
int rank, rank0 = -1, rank1 = 0;
|
|
int occ_cnt = sysopt.persmax;
|
|
int flg = 0;
|
|
boolean t0_used, skip_scores;
|
|
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
final_fpos = 0L;
|
|
#endif
|
|
/* If we are in the midst of a panic, cut out topten entirely.
|
|
* topten uses alloc() several times, which will lead to
|
|
* problems if the panic was the result of an alloc() failure.
|
|
*/
|
|
if (program_state.panicking)
|
|
return;
|
|
|
|
if (iflags.toptenwin) {
|
|
gt.toptenwin = create_nhwindow(NHW_TEXT);
|
|
}
|
|
|
|
#if defined(HANGUPHANDLING)
|
|
#define HUP if (!program_state.done_hup)
|
|
#else
|
|
#define HUP
|
|
#endif
|
|
|
|
#ifdef TOS
|
|
restore_colors(); /* make sure the screen is black on white */
|
|
#endif
|
|
/* create a new 'topten' entry */
|
|
t0_used = FALSE;
|
|
t0 = newttentry();
|
|
*t0 = zerott;
|
|
t0->ver_major = VERSION_MAJOR;
|
|
t0->ver_minor = VERSION_MINOR;
|
|
t0->patchlevel = PATCHLEVEL;
|
|
t0->points = u.urexp;
|
|
t0->deathdnum = u.uz.dnum;
|
|
/* deepest_lev_reached() is in terms of depth(), and reporting the
|
|
* deepest level reached in the dungeon death occurred in doesn't
|
|
* seem right, so we have to report the death level in depth() terms
|
|
* as well (which also seems reasonable since that's all the player
|
|
* sees on the screen anyway)
|
|
*/
|
|
t0->deathlev = observable_depth(&u.uz);
|
|
t0->maxlvl = deepest_lev_reached(TRUE);
|
|
t0->hp = u.uhp;
|
|
t0->maxhp = u.uhpmax;
|
|
t0->deaths = u.umortality;
|
|
t0->uid = uid;
|
|
copynchars(t0->plrole, gu.urole.filecode, ROLESZ);
|
|
copynchars(t0->plrace, gu.urace.filecode, ROLESZ);
|
|
copynchars(t0->plgend, genders[flags.female].filecode, ROLESZ);
|
|
copynchars(t0->plalign, aligns[1 - u.ualign.type].filecode, ROLESZ);
|
|
copynchars(t0->name, svp.plname, NAMSZ);
|
|
formatkiller(t0->death, sizeof t0->death, how, TRUE);
|
|
t0->birthdate = yyyymmdd(ubirthday);
|
|
t0->deathdate = yyyymmdd(when);
|
|
t0->tt_next = 0;
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
t0->fpos = -1L;
|
|
#endif
|
|
|
|
#ifdef LOGFILE /* used for debugging (who dies of what, where) */
|
|
if (lock_file(LOGFILE, SCOREPREFIX, 10)) {
|
|
if (!(lfile = fopen_datafile(LOGFILE, "a", SCOREPREFIX))) {
|
|
HUP raw_print("Cannot open log file!");
|
|
} else {
|
|
writeentry(lfile, t0);
|
|
(void) fclose(lfile);
|
|
}
|
|
unlock_file(LOGFILE);
|
|
}
|
|
#endif /* LOGFILE */
|
|
#ifdef XLOGFILE
|
|
if (lock_file(XLOGFILE, SCOREPREFIX, 10)) {
|
|
if (!(xlfile = fopen_datafile(XLOGFILE, "a", SCOREPREFIX))) {
|
|
HUP raw_print("Cannot open extended log file!");
|
|
} else {
|
|
writexlentry(xlfile, t0, how);
|
|
(void) fclose(xlfile);
|
|
}
|
|
unlock_file(XLOGFILE);
|
|
}
|
|
#endif /* XLOGFILE */
|
|
|
|
if (wizard || discover) {
|
|
if (how != PANICKED)
|
|
HUP {
|
|
char pbuf[BUFSZ];
|
|
|
|
topten_print("");
|
|
Sprintf(pbuf,
|
|
"Since you were in %s mode, the score list will not be checked.",
|
|
wizard ? "wizard" : "discover");
|
|
topten_print(pbuf);
|
|
}
|
|
goto showwin;
|
|
}
|
|
|
|
if (!lock_file(RECORD, SCOREPREFIX, 60))
|
|
goto destroywin;
|
|
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
rfile = fopen_datafile(RECORD, "r+", SCOREPREFIX);
|
|
#else
|
|
rfile = fopen_datafile(RECORD, "r", SCOREPREFIX);
|
|
#endif
|
|
|
|
if (!rfile) {
|
|
HUP raw_print("Cannot open record file!");
|
|
unlock_file(RECORD);
|
|
goto destroywin;
|
|
}
|
|
|
|
HUP topten_print("");
|
|
|
|
/* assure minimum number of points */
|
|
if (t0->points < sysopt.pointsmin)
|
|
t0->points = 0;
|
|
|
|
t1 = tt_head = newttentry();
|
|
tprev = 0;
|
|
/* rank0: -1 undefined, 0 not_on_list, n n_th on list */
|
|
for (rank = 1; ; ) {
|
|
readentry(rfile, t1);
|
|
if (t1->points < sysopt.pointsmin)
|
|
t1->points = 0;
|
|
if (rank0 < 0 && t1->points < t0->points) {
|
|
rank0 = rank++;
|
|
if (tprev == 0)
|
|
tt_head = t0;
|
|
else
|
|
tprev->tt_next = t0;
|
|
t0->tt_next = t1;
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
t0->fpos = t1->fpos; /* insert here */
|
|
#endif
|
|
t0_used = TRUE;
|
|
occ_cnt--;
|
|
flg++; /* ask for a rewrite */
|
|
} else
|
|
tprev = t1;
|
|
|
|
if (t1->points == 0)
|
|
break;
|
|
if ((sysopt.pers_is_uid ? t1->uid == t0->uid
|
|
: strncmp(t1->name, t0->name, NAMSZ) == 0)
|
|
&& !strncmp(t1->plrole, t0->plrole, ROLESZ) && --occ_cnt <= 0) {
|
|
if (rank0 < 0) {
|
|
rank0 = 0;
|
|
rank1 = rank;
|
|
HUP {
|
|
char pbuf[BUFSZ];
|
|
|
|
Sprintf(pbuf,
|
|
"You didn't beat your previous score of %ld points.",
|
|
t1->points);
|
|
topten_print(pbuf);
|
|
topten_print("");
|
|
}
|
|
}
|
|
if (occ_cnt < 0) {
|
|
flg++;
|
|
continue;
|
|
}
|
|
}
|
|
if (rank <= sysopt.entrymax) {
|
|
t1->tt_next = newttentry();
|
|
t1 = t1->tt_next;
|
|
rank++;
|
|
}
|
|
if (rank > sysopt.entrymax) {
|
|
t1->points = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (flg) { /* rewrite record file */
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
(void) fseek(rfile, (t0->fpos >= 0) ? t0->fpos : final_fpos, SEEK_SET);
|
|
#else
|
|
(void) fclose(rfile);
|
|
if (!(rfile = fopen_datafile(RECORD, "w", SCOREPREFIX))) {
|
|
HUP raw_print("Cannot write record file");
|
|
unlock_file(RECORD);
|
|
free_ttlist(tt_head);
|
|
goto destroywin;
|
|
}
|
|
#endif /* UPDATE_RECORD_IN_PLACE */
|
|
if (!done_stopprint)
|
|
if (rank0 > 0) {
|
|
if (rank0 <= 10) {
|
|
topten_print("You made the top ten list!");
|
|
} else {
|
|
char pbuf[BUFSZ];
|
|
|
|
Sprintf(pbuf,
|
|
"You reached the %d%s place on the top %d list.",
|
|
rank0, ordin(rank0), sysopt.entrymax);
|
|
topten_print(pbuf);
|
|
}
|
|
topten_print("");
|
|
}
|
|
}
|
|
skip_scores = !flags.end_top && !flags.end_around && !flags.end_own;
|
|
if (rank0 == 0)
|
|
rank0 = rank1;
|
|
if (rank0 <= 0)
|
|
rank0 = rank;
|
|
if (!skip_scores && !done_stopprint)
|
|
outheader();
|
|
for (t1 = tt_head, rank = 1; t1->points != 0; t1 = t1->tt_next, ++rank) {
|
|
if (flg
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
&& rank >= rank0
|
|
#endif
|
|
)
|
|
writeentry(rfile, t1);
|
|
if (skip_scores || done_stopprint)
|
|
continue;
|
|
if (rank <= flags.end_top
|
|
|| (rank >= rank0 - flags.end_around
|
|
&& rank <= rank0 + flags.end_around)
|
|
|| (flags.end_own && (sysopt.pers_is_uid
|
|
? t1->uid == t0->uid
|
|
: !strncmp(t1->name, t0->name, NAMSZ)))) {
|
|
if (rank == rank0 - flags.end_around
|
|
&& rank0 > flags.end_top + flags.end_around + 1
|
|
&& !flags.end_own)
|
|
topten_print("");
|
|
|
|
if (rank != rank0) {
|
|
outentry(rank, t1, FALSE);
|
|
} else if (!rank1) {
|
|
outentry(rank, t1, TRUE);
|
|
} else {
|
|
outentry(rank, t1, TRUE);
|
|
outentry(0, t0, TRUE);
|
|
}
|
|
}
|
|
}
|
|
if (rank0 >= rank)
|
|
if (!skip_scores && !done_stopprint)
|
|
outentry(0, t0, TRUE);
|
|
#ifdef UPDATE_RECORD_IN_PLACE
|
|
if (flg) {
|
|
#ifdef TRUNCATE_FILE
|
|
/* if a reasonable way to truncate a file exists, use it */
|
|
truncate_file(rfile);
|
|
#else
|
|
/* use sentinel record rather than relying on truncation */
|
|
*t1 = zerott;
|
|
t1->points = 0L; /* [redundant] terminates file when read back in */
|
|
t1->plrole[0] = t1->plrace[0] = t1->plgend[0] = t1->plalign[0] = '-';
|
|
t1->birthdate = t1->deathdate = yyyymmdd((time_t) 0L);
|
|
Strcpy(t1->name, "@");
|
|
Strcpy(t1->death, "<eod>\n"); /* end of data */
|
|
writeentry(rfile, t1);
|
|
/* note: there might be junk (if file has shrunk due to shorter
|
|
entries supplanting longer ones) after this dummy entry, but
|
|
reading and/or updating will ignore it */
|
|
(void) fflush(rfile);
|
|
#endif /* TRUNCATE_FILE */
|
|
}
|
|
#endif /* UPDATE_RECORD_IN_PLACE */
|
|
(void) fclose(rfile);
|
|
unlock_file(RECORD);
|
|
free_ttlist(tt_head);
|
|
|
|
showwin:
|
|
if (!done_stopprint) {
|
|
if (iflags.toptenwin) {
|
|
display_nhwindow(gt.toptenwin, TRUE);
|
|
} else {
|
|
/* when not a window, we need something comparable to more()
|
|
but can't use it directly because we aren't dealing with
|
|
the message window */
|
|
;
|
|
}
|
|
}
|
|
destroywin:
|
|
if (!t0_used)
|
|
dealloc_ttentry(t0);
|
|
if (iflags.toptenwin) {
|
|
destroy_nhwindow(gt.toptenwin);
|
|
gt.toptenwin = WIN_ERR;
|
|
}
|
|
}
|
|
|
|
staticfn void
|
|
outheader(void)
|
|
{
|
|
char linebuf[BUFSZ];
|
|
char *bp;
|
|
|
|
Strcpy(linebuf, " No Points Name");
|
|
bp = eos(linebuf);
|
|
while (bp < linebuf + COLNO - 9)
|
|
*bp++ = ' ';
|
|
Strcpy(bp, "Hp [max]");
|
|
topten_print(linebuf);
|
|
}
|
|
|
|
DISABLE_WARNING_FORMAT_NONLITERAL
|
|
|
|
/* so>0: standout line; so=0: ordinary line */
|
|
staticfn void
|
|
outentry(int rank, struct toptenentry *t1, boolean so)
|
|
{
|
|
boolean second_line = TRUE;
|
|
char linebuf[BUFSZ];
|
|
char *bp, hpbuf[24], linebuf3[BUFSZ];
|
|
int hppos, lngr;
|
|
|
|
linebuf[0] = '\0';
|
|
if (rank)
|
|
Sprintf(eos(linebuf), "%3d", rank);
|
|
else
|
|
Strcat(linebuf, " ");
|
|
|
|
Sprintf(eos(linebuf), " %10ld %.10s", t1->points ? t1->points : u.urexp,
|
|
t1->name);
|
|
Sprintf(eos(linebuf), "-%s", t1->plrole);
|
|
if (t1->plrace[0] != '?')
|
|
Sprintf(eos(linebuf), "-%s", t1->plrace);
|
|
/* Printing of gender and alignment is intentional. It has been
|
|
* part of the NetHack Geek Code, and illustrates a proper way to
|
|
* specify a character from the command line.
|
|
*/
|
|
Sprintf(eos(linebuf), "-%s", t1->plgend);
|
|
if (t1->plalign[0] != '?')
|
|
Sprintf(eos(linebuf), "-%s ", t1->plalign);
|
|
else
|
|
Strcat(linebuf, " ");
|
|
if (!strncmp("escaped", t1->death, 7)) {
|
|
Sprintf(eos(linebuf), "escaped the dungeon %s[max level %d]",
|
|
!strncmp(" (", t1->death + 7, 2) ? t1->death + 7 + 2 : "",
|
|
t1->maxlvl);
|
|
/* fixup for closing paren in "escaped... with...Amulet)[max..." */
|
|
if ((bp = strchr(linebuf, ')')) != 0)
|
|
*bp = (t1->deathdnum == astral_level.dnum) ? '\0' : ' ';
|
|
second_line = FALSE;
|
|
} else if (!strncmp("ascended", t1->death, 8)) {
|
|
Sprintf(eos(linebuf), "ascended to demigod%s-hood",
|
|
(t1->plgend[0] == 'F') ? "dess" : "");
|
|
second_line = FALSE;
|
|
} else {
|
|
if (!strncmp(t1->death, "quit", 4)) {
|
|
Strcat(linebuf, "quit");
|
|
second_line = FALSE;
|
|
} else if (!strncmp(t1->death, "died of st", 10)) {
|
|
Strcat(linebuf, "starved to death");
|
|
second_line = FALSE;
|
|
} else if (!strncmp(t1->death, "choked", 6)) {
|
|
Sprintf(eos(linebuf), "choked on h%s food",
|
|
(t1->plgend[0] == 'F') ? "er" : "is");
|
|
} else if (!strncmp(t1->death, "poisoned", 8)) {
|
|
Strcat(linebuf, "was poisoned");
|
|
} else if (!strncmp(t1->death, "crushed", 7)) {
|
|
Strcat(linebuf, "was crushed to death");
|
|
} else if (!strncmp(t1->death, "petrified by ", 13)) {
|
|
Strcat(linebuf, "turned to stone");
|
|
} else
|
|
Strcat(linebuf, "died");
|
|
|
|
if (t1->deathdnum == astral_level.dnum) {
|
|
const char *arg, *fmt = " on the Plane of %s";
|
|
|
|
switch (t1->deathlev) {
|
|
case -5:
|
|
fmt = " on the %s Plane";
|
|
arg = "Astral";
|
|
break;
|
|
case -4:
|
|
arg = "Water";
|
|
break;
|
|
case -3:
|
|
arg = "Fire";
|
|
break;
|
|
case -2:
|
|
arg = "Air";
|
|
break;
|
|
case -1:
|
|
arg = "Earth";
|
|
break;
|
|
default:
|
|
arg = "Void";
|
|
break;
|
|
}
|
|
Sprintf(eos(linebuf), fmt, arg);
|
|
} else {
|
|
Sprintf(eos(linebuf), " in %s", svd.dungeons[t1->deathdnum].dname);
|
|
if (t1->deathdnum != knox_level.dnum)
|
|
Sprintf(eos(linebuf), " on level %d", t1->deathlev);
|
|
if (t1->deathlev != t1->maxlvl)
|
|
Sprintf(eos(linebuf), " [max %d]", t1->maxlvl);
|
|
}
|
|
|
|
/* kludge for "quit while already on Charon's boat" */
|
|
if (!strncmp(t1->death, "quit ", 5))
|
|
Strcat(linebuf, t1->death + 4);
|
|
}
|
|
Strcat(linebuf, ".");
|
|
|
|
/* Quit, starved, ascended, and escaped contain no second line */
|
|
if (second_line) {
|
|
bp = eos(linebuf);
|
|
Sprintf(bp, " %c%s.", highc(*(t1->death)), t1->death + 1);
|
|
/* fix up "Killed by Mr. Asidonhopo; the shopkeeper"; that starts
|
|
with a comma but has it changed to semi-colon to keep the comma
|
|
out of 'record'; change it back for display */
|
|
(void) strsubst(bp, "; the ", ", the ");
|
|
}
|
|
|
|
lngr = (int) strlen(linebuf);
|
|
if (t1->hp <= 0)
|
|
hpbuf[0] = '-', hpbuf[1] = '\0';
|
|
else
|
|
Sprintf(hpbuf, "%d", t1->hp);
|
|
/* beginning of hp column after padding (not actually padded yet) */
|
|
hppos = COLNO - (int) (sizeof " Hp [max]" - sizeof "");
|
|
while (lngr >= hppos) {
|
|
for (bp = eos(linebuf); !(*bp == ' ' && bp - linebuf < hppos); bp--)
|
|
;
|
|
/* special case: word is too long, wrap in the middle */
|
|
if (linebuf + 15 >= bp)
|
|
bp = linebuf + hppos - 1;
|
|
/* special case: if about to wrap in the middle of maximum
|
|
dungeon depth reached, wrap in front of it instead */
|
|
if (bp > linebuf + 5 && !strncmp(bp - 5, " [max", 5))
|
|
bp -= 5;
|
|
if (*bp != ' ')
|
|
Strcpy(linebuf3, bp);
|
|
else
|
|
Strcpy(linebuf3, bp + 1);
|
|
*bp = '\0';
|
|
if (so) {
|
|
while (bp < linebuf + (COLNO - 1))
|
|
*bp++ = ' ';
|
|
*bp = '\0';
|
|
topten_print_bold(linebuf);
|
|
} else
|
|
topten_print(linebuf);
|
|
Snprintf(linebuf, sizeof(linebuf), "%15s %s", "", linebuf3);
|
|
lngr = Strlen(linebuf);
|
|
}
|
|
/* beginning of hp column not including padding */
|
|
hppos = COLNO - 7 - (int) strlen(hpbuf);
|
|
bp = eos(linebuf);
|
|
|
|
if (bp <= linebuf + hppos) {
|
|
/* pad any necessary blanks to the hit point entry */
|
|
while (bp < linebuf + hppos)
|
|
*bp++ = ' ';
|
|
Strcpy(bp, hpbuf);
|
|
Sprintf(eos(bp), " %s[%d]",
|
|
(t1->maxhp < 10) ? " " : (t1->maxhp < 100) ? " " : "",
|
|
t1->maxhp);
|
|
}
|
|
|
|
if (so) {
|
|
bp = eos(linebuf);
|
|
while (bp < linebuf + (COLNO - 1))
|
|
*bp++ = ' ';
|
|
*bp = '\0';
|
|
topten_print_bold(linebuf);
|
|
} else
|
|
topten_print(linebuf);
|
|
}
|
|
|
|
RESTORE_WARNING_FORMAT_NONLITERAL
|
|
|
|
staticfn int
|
|
score_wanted(
|
|
boolean current_ver,
|
|
int rank,
|
|
struct toptenentry *t1,
|
|
int playerct,
|
|
const char **players,
|
|
int uid)
|
|
{
|
|
const char *arg, *nxt;
|
|
int i;
|
|
|
|
if (current_ver && (t1->ver_major != VERSION_MAJOR
|
|
|| t1->ver_minor != VERSION_MINOR
|
|
|| t1->patchlevel != PATCHLEVEL))
|
|
return 0;
|
|
|
|
if (sysopt.pers_is_uid && !playerct && t1->uid == uid)
|
|
return 1;
|
|
|
|
/*
|
|
* FIXME:
|
|
* This selection produces a union (OR) of criteria rather than
|
|
* an intersection (AND). So
|
|
* nethack -s -u igor -p Cav -r Hum
|
|
* will list all entries for name igor regardless of role or race
|
|
* plus all entries for cave dwellers regardless of name or race
|
|
* plus all entries for humans regardless of name or role.
|
|
*
|
|
* It would be more useful if it only chose human cave dwellers
|
|
* named igor. That would be pretty straightforward if only one
|
|
* instance of each of the criteria were possible, but
|
|
* nethack -s -u igor -u ayn -p Cav -p Pri -r Hum -r Dwa
|
|
* should list human cave dwellers named igor and human cave
|
|
* dwellers named ayn plus dwarven cave dwellers named igor and
|
|
* dwarven cave dwellers named ayn plus human priest[esse]s named
|
|
* igor and human priest[esse]s named ayn (the combination of
|
|
* dwarven priest[esse]s doesn't occur but the selection can test
|
|
* entries without being aware of such; it just won't find any
|
|
* matches for that). An extra initial pass of the command line
|
|
* to collect all criteria before testing any entry is needed to
|
|
* accomplish this. And we might need to drop support for
|
|
* pre-3.3.0 entries (old elf role) depending on how the criteria
|
|
* matching is performed.
|
|
*
|
|
* It also ought to extended to handle
|
|
* nethack -s -u igor-Cav-Hum
|
|
* Alignment and gender could be useful too but no one has ever
|
|
* clamored for them. Presumably if they care they postprocess
|
|
* with some custom tool.
|
|
*/
|
|
|
|
for (i = 0; i < playerct; i++) {
|
|
arg = players[i];
|
|
if (arg[0] == '-' && arg[1] == 'u' && arg[2] != '\0')
|
|
arg += 2; /* handle '-uname' */
|
|
|
|
if (arg[0] == '-' && strchr("pru", arg[1]) && !arg[2]
|
|
&& i + 1 < playerct) {
|
|
nxt = players[i + 1];
|
|
if ((arg[1] == 'p' && str2role(nxt) == str2role(t1->plrole))
|
|
|| (arg[1] == 'r' && str2race(nxt) == str2race(t1->plrace))
|
|
/* handle '-u name' */
|
|
|| (arg[1] == 'u' && (!strcmp(nxt, "all")
|
|
|| !strncmp(t1->name, nxt, NAMSZ))))
|
|
return 1;
|
|
i++;
|
|
} else if (!strcmp(arg, "all")
|
|
|| !strncmp(t1->name, arg, NAMSZ)
|
|
|| (arg[0] == '-' && arg[1] == t1->plrole[0] && !arg[2])
|
|
|| (digit(arg[0]) && rank <= atoi(arg)))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* print selected parts of score list.
|
|
* argc >= 2, with argv[0] untrustworthy (directory names, et al.),
|
|
* and argv[1] starting with "-s".
|
|
* caveat: some shells might allow argv elements to be arbitrarily long.
|
|
*/
|
|
void
|
|
prscore(int argc, char **argv)
|
|
{
|
|
const char **players, *player0;
|
|
int i, playerct, rank;
|
|
struct toptenentry *t1;
|
|
FILE *rfile;
|
|
char pbuf[BUFSZ], *p;
|
|
unsigned ln;
|
|
int uid = -1;
|
|
boolean current_ver = TRUE, init_done = FALSE, match_found = FALSE;
|
|
|
|
/* expect "-s" or "--scores"; "-s<anything> is accepted */
|
|
ln = (argc < 2) ? 0U
|
|
: ((p = strchr(argv[1], ' ')) != 0) ? (unsigned) (p - argv[1])
|
|
: Strlen(argv[1]);
|
|
if (ln < 2 || (strncmp(argv[1], "-s", 2)
|
|
&& strcmp(argv[1], "--scores"))) {
|
|
raw_printf("prscore: bad arguments (%d)", argc);
|
|
return;
|
|
}
|
|
|
|
rfile = fopen_datafile(RECORD, "r", SCOREPREFIX);
|
|
if (!rfile) {
|
|
raw_print("Cannot open record file!");
|
|
return;
|
|
}
|
|
|
|
#ifdef AMIGA
|
|
{
|
|
extern winid amii_rawprwin;
|
|
|
|
init_nhwindows(&argc, argv);
|
|
amii_rawprwin = create_nhwindow(NHW_TEXT);
|
|
}
|
|
#endif
|
|
|
|
/* If the score list isn't after a game, we never went through
|
|
* initialization. */
|
|
if (wiz1_level.dlevel == 0) {
|
|
dlb_init();
|
|
init_dungeons();
|
|
init_done = TRUE;
|
|
}
|
|
|
|
/* to get here, argv[1] either starts with "-s" or is "--scores" without
|
|
trailing stuff; for "-s<anything>" treat <anything> as separate arg */
|
|
if (argv[1][1] == '-' || !argv[1][2]) {
|
|
argc--;
|
|
argv++;
|
|
} else { /* concatenated arg string; use up "-s" but keep argc,argv */
|
|
argv[1] += 2;
|
|
}
|
|
/* -v doesn't take a version number arg; it means 'all versions present
|
|
in the file' instead of the default of only the current version;
|
|
unlike -s, we don't accept "-v<anything>" for non-empty <anything> */
|
|
if (argc > 1 && !strcmp(argv[1], "-v")) {
|
|
current_ver = FALSE;
|
|
argc--;
|
|
argv++;
|
|
}
|
|
|
|
if (argc <= 1) {
|
|
if (sysopt.pers_is_uid) {
|
|
uid = getuid();
|
|
playerct = 0;
|
|
players = (const char **) 0;
|
|
} else {
|
|
player0 = svp.plname;
|
|
if (!*player0)
|
|
player0 = "all"; /* if no plname[], show all scores
|
|
* (possibly filtered by '-v') */
|
|
playerct = 1;
|
|
players = &player0;
|
|
}
|
|
} else {
|
|
playerct = --argc;
|
|
players = (const char **) ++argv;
|
|
}
|
|
raw_print("");
|
|
|
|
t1 = tt_head = newttentry();
|
|
for (rank = 1; ; rank++) {
|
|
readentry(rfile, t1);
|
|
if (t1->points == 0)
|
|
break;
|
|
if (!match_found
|
|
&& score_wanted(current_ver, rank, t1, playerct, players, uid))
|
|
match_found = TRUE;
|
|
t1->tt_next = newttentry();
|
|
t1 = t1->tt_next;
|
|
}
|
|
|
|
(void) fclose(rfile);
|
|
if (init_done) {
|
|
free_dungeons();
|
|
dlb_cleanup();
|
|
}
|
|
|
|
if (match_found) {
|
|
outheader();
|
|
t1 = tt_head;
|
|
for (rank = 1; t1->points != 0; rank++, t1 = t1->tt_next) {
|
|
if (score_wanted(current_ver, rank, t1, playerct, players, uid))
|
|
(void) outentry(rank, t1, FALSE);
|
|
}
|
|
} else {
|
|
Sprintf(pbuf, "Cannot find any %sentries for ",
|
|
current_ver ? "current " : "");
|
|
if (playerct < 1) {
|
|
Strcat(pbuf, "you");
|
|
} else {
|
|
/* minor bug: 'nethack -s -u ziggy' will say "any of"
|
|
even though the '-u' doesn't indicate multiple names */
|
|
if (playerct > 1)
|
|
Strcat(pbuf, "any of ");
|
|
for (i = 0; i < playerct; i++) {
|
|
/* accept '-u name' and '-uname' as well as just 'name'
|
|
so skip '-u' for the none-found feedback */
|
|
if (!strncmp(players[i], "-u", 2)) {
|
|
if (!players[i][2])
|
|
continue;
|
|
players[i] += 2;
|
|
}
|
|
/* stop printing players if there are too many to fit */
|
|
if (strlen(pbuf) + strlen(players[i]) + 2 >= BUFSZ) {
|
|
if (strlen(pbuf) < BUFSZ - 4)
|
|
Strcat(pbuf, "...");
|
|
else
|
|
Strcpy(pbuf + strlen(pbuf) - 4, "...");
|
|
break;
|
|
}
|
|
Strcat(pbuf, players[i]);
|
|
if (i < playerct - 1) {
|
|
if (players[i][0] == '-' && strchr("pr", players[i][1])
|
|
&& players[i][2] == 0)
|
|
Strcat(pbuf, " ");
|
|
else
|
|
Strcat(pbuf, ":");
|
|
}
|
|
}
|
|
}
|
|
/* append end-of-sentence punctuation if there is room */
|
|
if (strlen(pbuf) < BUFSZ - 1)
|
|
Strcat(pbuf, ".");
|
|
raw_print(pbuf);
|
|
raw_printf("Usage: %s -s [-v] <playertypes> [maxrank] [playernames]",
|
|
gh.hname);
|
|
raw_printf("Player types are: [-p role] [-r race]");
|
|
}
|
|
free_ttlist(tt_head);
|
|
#ifdef AMIGA
|
|
{
|
|
extern winid amii_rawprwin;
|
|
|
|
display_nhwindow(amii_rawprwin, 1);
|
|
destroy_nhwindow(amii_rawprwin);
|
|
amii_rawprwin = WIN_ERR;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
staticfn int
|
|
classmon(char *plch)
|
|
{
|
|
int i;
|
|
|
|
/* Look for this role in the role table */
|
|
for (i = 0; roles[i].name.m; i++) {
|
|
if (!strncmp(plch, roles[i].filecode, ROLESZ)) {
|
|
if (roles[i].mnum != NON_PM)
|
|
return roles[i].mnum;
|
|
else
|
|
return PM_HUMAN;
|
|
}
|
|
}
|
|
/* this might be from a 3.2.x score for former Elf class */
|
|
if (!strcmp(plch, "E"))
|
|
return PM_RANGER;
|
|
|
|
impossible("What weird role is this? (%s)", plch);
|
|
return PM_HUMAN_MUMMY;
|
|
}
|
|
|
|
/*
|
|
* Get a random player name and class from the high score list,
|
|
*/
|
|
struct toptenentry *
|
|
get_rnd_toptenentry(void)
|
|
{
|
|
int rank, i;
|
|
FILE *rfile;
|
|
struct toptenentry *tt;
|
|
static struct toptenentry tt_buf;
|
|
|
|
rfile = fopen_datafile(RECORD, "r", SCOREPREFIX);
|
|
if (!rfile) {
|
|
impossible("Cannot open record file!");
|
|
return NULL;
|
|
}
|
|
|
|
tt = &tt_buf;
|
|
rank = rnd(sysopt.tt_oname_maxrank);
|
|
pickentry:
|
|
for (i = rank; i; i--) {
|
|
readentry(rfile, tt);
|
|
if (tt->points == 0)
|
|
break;
|
|
}
|
|
|
|
if (tt->points == 0) {
|
|
if (rank > 1) {
|
|
rank = 1;
|
|
rewind(rfile);
|
|
goto pickentry;
|
|
}
|
|
tt = NULL;
|
|
}
|
|
|
|
(void) fclose(rfile);
|
|
return tt;
|
|
}
|
|
|
|
|
|
/*
|
|
* Attach random player name and class from high score list
|
|
* to an object (for statues or morgue corpses).
|
|
*/
|
|
struct obj *
|
|
tt_oname(struct obj *otmp)
|
|
{
|
|
struct toptenentry *tt;
|
|
if (!otmp)
|
|
return (struct obj *) 0;
|
|
|
|
tt = get_rnd_toptenentry();
|
|
|
|
if (!tt)
|
|
return (struct obj *) 0;
|
|
|
|
set_corpsenm(otmp, classmon(tt->plrole));
|
|
if (tt->plgend[0] == 'F')
|
|
otmp->spe = CORPSTAT_FEMALE;
|
|
else if (tt->plgend[0] == 'M')
|
|
otmp->spe = CORPSTAT_MALE;
|
|
otmp = oname(otmp, tt->name, ONAME_NO_FLAGS);
|
|
|
|
return otmp;
|
|
}
|
|
|
|
/* Randomly select a topten entry to mimic */
|
|
int
|
|
tt_doppel(struct monst *mon) {
|
|
struct toptenentry *tt = rn2(13) ? get_rnd_toptenentry() : NULL;
|
|
int ret;
|
|
|
|
if (!tt)
|
|
ret = rn1(PM_WIZARD - PM_ARCHEOLOGIST + 1, PM_ARCHEOLOGIST);
|
|
else {
|
|
if (tt->plgend[0] == 'F')
|
|
mon->female = 1;
|
|
else if (tt->plgend[0] == 'M')
|
|
mon->female = 0;
|
|
ret = classmon(tt->plrole);
|
|
/* Only take on a name if the player can see
|
|
the doppelganger, otherwise we end up with
|
|
named monsters spoiling the fun - Kes */
|
|
if (canseemon(mon))
|
|
christen_monst(mon, tt->name);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
#ifdef NO_SCAN_BRACK
|
|
/* Lattice scanf isn't up to reading the scorefile. What */
|
|
/* follows deals with that; I admit it's ugly. (KL) */
|
|
/* Now generally available (KL) */
|
|
staticfn void
|
|
nsb_mung_line(p)
|
|
char *p;
|
|
{
|
|
while ((p = strchr(p, ' ')) != 0)
|
|
*p = '|';
|
|
}
|
|
|
|
staticfn void
|
|
nsb_unmung_line(p)
|
|
char *p;
|
|
{
|
|
while ((p = strchr(p, '|')) != 0)
|
|
*p = ' ';
|
|
}
|
|
#endif /* NO_SCAN_BRACK */
|
|
|
|
/*topten.c*/
|