/* List the Nethack record file and the average score. Nethack 3.4-3.6, 3.3, 3.2 and 3.1 RECORD file formats supported Output: stdout RECLIST [-f filename] [-u playername] [-p profession] [-r race] [-n number] [-ns score] [-np interval] [-d] [-ds date since] [-du date until] [-g version] Check out the comments in the #defines and #includes before compiling */ /* J. Lahtinen 8/1993 - 12/2015 internet: jslahtin@gmail.com Some parts of this program are copied from the Nethack 3.3.0 sources, the rest I wrote myself. Tell me if you find any bugs.. including the program telling the death reason differently from "nethack -s". When reporting bugs, please include the line in your record file that reclist doesn't treat properly (copy & paste is just fine). 2015-12-12 - confirmed compatibility to Nethack 3.6.0 - removed the warning of "too" modern game version 2015-02-21 confirmed the compatibility to the leaked Nethack 3.5.0 version 2003-01-20 changed the parameters -ds and -du from long to char* to allow comparisons of partial date (for example -ds 2003 to list games finished after the beginning of year 2003) 2002-03-29 v3.4.0 2001-04-07 increased the size of the line buffer to fit Lethe patch scores 2000-09-06 removed the level number from escaping scores 2000-08-21 removed the level number from ascensions anything in curly braces shown even if the "death" string isn't (for the move counter added with my moves patch) 2000-08-12 v3.3.1 2000-07-30 added option -ns to show only scores above some limit 2000-01-23 added option -a to select by alignment 2000-01-02 v3.3 updated to understand the file format of game version 3.3.0 removed old compile options NHPLUS and ENDGAME 980731 v3.2.2 removed false append of "with the Amulet" on escaped games 980108 options -ds and -du to delimit game ending dates 971130 v3.2.1 changed the endgame level names to those used by the game version 3.2.2 970525 don't split lines between [] (the game version 3.2 itself doesn't). give an error message from parameter -f without a filename.. 961124 v3.2, fixed the endgame level indexes changed from game ver.3.1 960628 option -g to select scores of a certain game version 960505 Both 3.1 and 3.2 game versions handled 960421 v3.0: File format changed for game version 3.2 950524 don't print "in the endgame" when the exact end level is shown I noticed the NhPlus class U missing from the helps, added it there.. 950523 2.3 fixed a difference between this and nethack -s in splitting the line when the death string is 55 characters made the more accurate endgame level names optional (#define ENDGAME) 950521 alternative definition of getch for unix users 950228 2.2c "Escaped with the Amulet" added 950117 a negative number in -npX stops display only at the end -npX always stops at the end 941117 option -npX pauses after about every X lines 940601 -l option reads the logfile. Option -help works like -? 940517 added the missing "." in the ascensions 940509 "Crushed" and "escaped" ends specified 940406 Make the difference between demigod and demigoddess 940322 2.2a Endgame- levels specified 940224 the parameter -d displays the date also with the scores on a single display line 940110 2.2 the parameter -fs may be used to read stdin 931203 2.1b conio.h headers removed for better operating system compatibility extra space on "quit" scores removed uid longer than 1 character caused some errors in unix, fixed */ #include #include #include #include #if defined(__MSDOS__)||defined(__OS2__) #include #else #define getch() getc(stdin) /* getch() reads one character without waiting for CR in MSDOS and OS/2 and it's currently the only thing in conio.h I use */ #endif /* The functions strnicmp and stricmp are not ANSI, according to the BorlandC help files. If your compiler doesn't understand them, you can replace them with the standard functions (and lose the case insensitivity..) with these following defines (or use suitable case-insensitive functions that your compiler understands): #define strnicmp strncmp #define stricmp strcmp */ /* NOTE: With unix you may prefer defining them as strncasecmp and strcasecmp as below.. */ #if defined(__unix__) #define RECORD "/usr/games/lib/nethackdir/record" #define LOGFILE "/usr/games/lib/nethackdir/logfile" #ifndef stricmp #define strnicmp strncasecmp #define stricmp strcasecmp #endif #else #define RECORD "record" #define LOGFILE "logfile" #endif /* Some defines taken from game sources (src/topten.c) */ #define NAMSZ 10 /* max length of player name */ #define DTHSZ 100 /* max length of "killer" string */ #define ROLESZ 3 /* role length */ #define BUFSZ 256 /* text buffer size */ char *eos(char *p); char *ver="3.6.0"; /* version number */ /* command line parameters */ int pause=0, /* pause interval */ plin=0, /* lines printed without pause */ pnum=0, /* how many scores to show */ day=0; /* !0 : show dying dates */ long low=0; /* minimum score to show */ char *begdate=NULL, /* date boundaries */ *enddate=NULL, *name=NULL, /* if only one username is to be listed */ *prole=NULL, /* character class */ *prace=NULL, *pver=NULL, /* if only scores of a certain version are to be listed */ *pgend=NULL, /* list only this gender */ *palign=NULL; /* only this alignment */ /* other globals used everywhere */ int num=0; /* number of scores listed */ int main(int argc, char *argv[]) { FILE *inp; int i; long sum=0, score=0; char line[BUFSZ], fver[6]; char *filename=RECORD; long chkline(char *line); void help(char *prog,char *par,char *explain); /* Command line parameters */ for (i=1; i'3') { sprintf(line,"-g %s",pver); help(argv[0],line,"Versions later than 3.x.x not supported yet"); } } else if (!(strnicmp(argv[i],"-np",3))) /* pause interval, MUST be */ if (strlen(argv[i])==3) { /* checked before -n */ i++; pause=atoi(argv[i]); } else pause=atoi(argv[i]+3); else if (!(strnicmp(argv[i],"-ns",3))) /* minimum score to list */ if (strlen(argv[i])==3) { /* MUST be checked before -n */ i++; low=atol(argv[i]); } else low=atol(argv[i]+3); else if (!(strnicmp(argv[i],"-n",2))) /* number */ if (strlen(argv[i])==2) { i++; pnum=atoi(argv[i]); } else pnum=atoi(argv[i]+2); else if (!(strnicmp(argv[i],"-ds",3))) /* dates since.. */ if (strlen(argv[i])==3) { /* MUST be checked before -d */ i++; begdate=argv[i]; } else begdate=argv[i]+3; else if (!(strnicmp(argv[i],"-du",3))) /* dates until.. */ if (strlen(argv[i])==3) { /* MUST be checked before -d */ i++; enddate=argv[i]; } else enddate=argv[i]+3; else if (argv[i][1]=='d') /* display the dates */ day=1; else if (!(strnicmp(argv[i],"-p",2))) /* role */ if (strlen(argv[i])==2) { i++; prole=argv[i]; } else prole=argv[i]+2; else if (!(strnicmp(argv[i],"-r",2))) { /* race */ if (strlen(argv[i])==2) { i++; prace=argv[i]; } else prace=argv[i]+2; } else if (!(strnicmp(argv[i],"-a",2))) { /* alignment */ if (strlen(argv[i])==2) { i++; palign=argv[i]; } else palign=argv[i]+2; if (strlen(palign)>3) { sprintf(line,"-a %s",palign); help(argv[0],line,"Alignment should be one of law/neu/cha/l/n/c"); } } } /* Command line ok, let's process the file */ if (!filename) inp=stdin; /* I don't know if this works on all compilers.. */ else if (!(inp=fopen(filename,"r"))) { printf ("File %s not found\n",filename); exit (2); } /* Actually I now think there's no point in this. * You'll probably see if the output looks funny anyway, * so you might consider the warning more annoying than helpful. 12.12.2015 fgets(line,sizeof(line),inp); sscanf(line,"%5s",fver); if (fver[1]=='.' && fver[3]=='.' && strncmp(fver,"3.6",3) > 0) { printf ("The file seems to be from a game version newer than me.\n"); printf ("The output may be incorrect.\n\n"); } else if (fver[1] != '.') { printf ("Not sure about which game version the file is from.\n"); printf ("Version 3.1 assumed for unknown lines.\n\n"); } */ printf("%-71s%s"," No Points Name","Hp [max]\n"); /* HERE is the main loop */ while ((!(feof(inp))) && ((!pnum) || (num0 && plin >= pause) { printf ("--more--\r"); if (tolower(getch()=='n')) pnum = num; printf (" \r"); plin = 0; } if (score == -9) { printf ("\nInvalid file format!\n"); return 1; } sum += score; fgets(line,sizeof(line),inp); } if (num) printf("\n%d scores. Average score %ld\n",num,sum/(long)num); else printf("\nNo games found.\n"); if (inp!=stdin) fclose (inp); if (pause) getch(); return 0; } /* Interpret one line of the file. Check for the game version. */ long chkline (char *line) { long line33 (char *line,int rank); long line32 (char *line); long line31 (char *line); int ver_major, ver_minor, patchlevel, n; static int rank=0; /* Which game version did this score come from? Are we interested in scores of that version? */ n = sscanf (line, "%d.%d.%d", &ver_major, &ver_minor, &patchlevel); if (!pver || !strncmp(pver,line,strlen(pver)) || !strncmp(pver,"3.1",3)) { rank++; if ((ver_major == 3 && ver_minor >= 3) || ver_major > 3) return line33(line,rank); else if (ver_major == 3 && ver_minor == 2) return line32(line); else return line31(line); } else if (pver) /* We didn't care about that version */ return 0; else return -9; /* I don't understand this version. Newer than 3.3 */ } /* Interpret a line of game version 3.3 format */ long line33 (char *line, int rank) { #define ENDGAMBR 7 #define EGMIN -5 /* last endgame level */ #define EGMAX -1 /* first endgame level */ #define DUNMIN 0 /* minimum dungeonbranch number */ #define DUNMAX 7 /* maximum dungeonbranch number */ long score; int dtype, dlev, maxlev, hp, maxhp; char bdate[BUFSZ], /* 9 should be enough in VALID files, but we don't */ ddate[BUFSZ], /* want to crash on invalid entries either.. */ role[ROLESZ+1], race[ROLESZ+1], gender[ROLESZ+1], align[ROLESZ+1], plname[NAMSZ+1], death[DTHSZ+1]; char linebuf[BUFSZ], prtbuf[81], hpbuf[6]; char *start, *end; int n, secline=1, dayshown=0, par=0, hppos; static char *eglev[] ={"Plane of Earth", /* -1 */ "Plane of Air", /* -2 */ "Plane of Fire", /* -3 */ "Plane of Water", /* -4 */ "Astral Plane"}; /* -5 */ static char *dungeon[] ={"The Dungeons of Doom", /* 0 */ "Gehennom", /* 1 */ "The Gnomish Mines", /* 2 */ "The Quest", /* 3 */ "Sokoban", /* 4 */ "Fort Ludios", /* 5 */ "Vlad's Tower", /* 6 */ "the endgame"}; /* 7 */ /* Fields in the line: version(3.3.n, ignored here), score, dungeon branch, dlevel, max dlevel, hp, max hp, number of deaths (ignored), birthdate, death date, uid (ignored), role, race, gender, alignment, name, reason */ n = sscanf(line, "%*d.%*d.%*d %ld %d %d %d %d %d %*d %s %s %*d %s %s %s %s %[^,],%[^\n\r]", &score,&dtype,&dlev,&maxlev,&hp,&maxhp,&ddate,&bdate, role, race, gender, align, plname, death); if (n<14) /* Invalid line, not all fields found */ return 0L; /* Are we interested in this playername, role, alignment and deathdate? */ if (role[0]<'A') return 0L; if ((prole) && (strnicmp(prole,role,strlen(prole)))) return 0L; if ((name) && (strcmp(name,plname))) return 0L; if ((prace) && (strnicmp(prace,race,strlen(prace)))) return 0L; if ((palign) && (strnicmp(palign,align,strlen(palign)))) return 0L; if ((begdate != NULL && strcmp(ddate,begdate) < 0) || (enddate != NULL && strcmp(ddate,enddate) > 0)) return 0L; if (score= EGMIN && dlev <= EGMAX) { sprintf(eos(linebuf)," on the %s",eglev[-dlev - 1]); } else { strcat(linebuf," on the Plane of Void"); } } } else if (strncmp(death,"escape",6)) { sprintf(eos(linebuf), " in %s on level %d", (dtype >= DUNMIN && dtype < DUNMAX)? dungeon[dtype]: "an unknown dungeon branch", dlev); if (dlev != maxlev) sprintf(eos(linebuf), " [max %d]", maxlev); } strcat(linebuf,"."); if (secline) sprintf(eos(linebuf)," %c%s.",toupper(*death),death+1); else if (start=strchr(death,'{')) /* Anyway, print what may be in curly braces. I have the game duration * appended there with my topten patch * (http://www.clinet.fi/~walker/nethack.html#moves) */ sprintf(eos(linebuf)," %s",start); start=linebuf; if (hp>0) sprintf(hpbuf,"%d",hp); else sprintf(hpbuf,"-"); hppos=70; while (strlen(start)>=hppos) { /* split the line as necessary */ for (end=start; end start && (*end != ' ' || par)) { if (*end == '[') par=0; else if (*end == ']') par=1; end--; } if (end == start) /* There was no good split position, shouldn't happen */ end=start+hppos; strncpy(prtbuf, start, end-start); prtbuf[end-start]=0; printf("%s\n",prtbuf); /* This display line */ plin++; if (day && !dayshown) { printf(" %-15s", ddate); dayshown=1; } else printf("%16s"," "); start=end+1; /* Start position of the NEXT line */ hppos= 57 - strlen(hpbuf); } end=eos(start); while (end < start+hppos && end < linebuf+sizeof(linebuf)) *end++=' '; sprintf(end,hpbuf); printf("%s",start); sprintf(hpbuf,"[%d]",maxhp); printf("%6s\n",hpbuf); plin++; if (day && !dayshown) { printf(" %-15s\n",ddate); plin++; } num++; return score; } /* Interpret a line of game version 3.2 format */ long line32 (char *line) { int n, pos, pos2, dtype, dlev, maxlev, hp, maxhp, par=0, dayshown=0; static int nro=0; long score; char pvm1[BUFSZ], pvm2[BUFSZ], chclass, sex, sver[10], death[150]={""}, ascend=0; char *reason; long line31(char *line); static char *dungeon[]={"The Dungeons of Doom",/* 0 */ "Gehennom", /* 1 */ "The Gnomish Mines", /* 2 */ "The Quest", /* 3 */ "Fort Ludios", /* 4 */ "Vlad's Tower", /* 5 */ "the endgame"}; /* 6 */ static char *eglev[]={"Plane of Earth", /* -1 */ "Plane of Air", /* -2 */ "Plane of Fire", /* -3 */ "Plane of Water", /* -4 */ "Astral Plane"}; /* -5 */ nro++; /* First there is the game version (3.2.something), then comes the actual score. Then the dungeon branch of death and dlevel, maxdlevel, hp, maxhp, number of deaths(ignored), dates when the game ended and started, userid(ignored, character class&sex, name and reason for game ending */ n = sscanf (line,"%9s %ld %d %d %d %d %d %*d %s %s %*d %c%c", &sver,&score,&dtype,&dlev,&maxlev,&hp,&maxhp, &pvm1,&pvm2,&chclass,&sex); if (chclass<'A') return (0L); /* The VMS version may have end line with 'class' < A */ if ((prole) && (prole[0]!=chclass)) return (0L); /* Some other class specified on the command line */ if (scorestrlen(line)) /* Never is in a valid file. But we don't want */ return (0L); /* to crash just if the file is invalid, do we.. */ pos += 3; for (pos2=0;(line[pos]!=',')&&(pos2 0)) return (0L); /* We're not intrested in dates this early /late */ printf("%3d%11ld ",nro,score); reason= &line[pos+1]; strcat(death,"-"); /* The name is already there in the beginnin of string */ pos=strlen(death); death[pos]=chclass; death[pos+1]=0; /* Here is the part where I'm not yet totally sure everything still works as it should.. but this worked on the 3.1 files */ if (!(strncmp(reason,"poisoned",8))) strcat(death," was poisoned"); else if (!(strncmp(reason,"petrified",9))) strcat(death," turned to stone"); else if (!(strncmp(reason,"starv",5))) strcat(death," starved to death"); else if (!(strncmp(reason,"quit",4))) strcat(death," quit"); else if (!(strncmp(reason,"crushed",7))) strcat(death," was crushed to death"); else if (!(strncmp(reason,"choked",6))) if (sex=='M') strcat(death," choked on his food"); else strcat(death," choked on her food"); else if (!(strncmp(reason,"ascended",8))) { if (sex=='F') strcat(death," ascended to demigoddess-hood."); else strcat(death," ascended to demigod-hood."); ascend=1; } else if (!(strncmp(reason,"escaped",7))) { sprintf (death+strlen(death)," escaped the dungeon [max level %d].",maxlev); ascend=1; /* Not an ascension, but also listed in a special way. */ } else strcat(death," died"); if (!ascend) { pos=strlen(death); if (dtype==6) /* The endgame */ { if (dlev < -5 || dlev > -1) { printf ("Invalid endgame level number %d in file!\n", dlev); return (0L); } sprintf (death+pos," on the %s. ",eglev[-1-dlev]); /* dlev is negative if you die in the endgame, from -1 to -5 */ } else { sprintf (death+pos," in %s",dungeon[dtype]); if (dtype!=6) sprintf(death+strlen(death)," on level %d",dlev); if (maxlev!=dlev) sprintf(death+strlen(death)," [max %d]. ",maxlev); else strcat(death,". "); } } if ((strncmp(reason,"quit",4)) && (strncmp(reason,"starvation",10)) && (!ascend)) { reason[0]=toupper(reason[0]); for (pos2=0; reason[pos2]>=' '; pos2++); reason[pos2++]='.'; reason[pos2]=0; pos=strlen(death); strcat(death,reason); } pos=strlen(death); if (pos>54) { /* split line at the last space before position 54 */ while (pos>54 || death[pos]!=' ' || (par>0 && pos>30)) { if(death[pos]==']') /* but we don't split between [] */ par++; if(death[pos]=='[') par--; pos--; } death[pos]=0; if (day && !dayshown) { printf ("%s\n %s\t\t",death,pvm1); dayshown=1; } else { printf("%s\n\t\t",death); } plin++; pos++; pos2=0; while (death[pos]) { death[pos2]=death[pos]; pos++; pos2++; } death[pos2]=0; } printf("%-54s",death); pos=0; if (hp>0) printf("%3d",hp); else printf(" -"); sprintf(death,"[%d]\n",maxhp); printf("%7s",death); plin++; if (day && !dayshown) { printf (" %s\n",pvm1); plin++; } num++; return (score); } /* Show some help about the arguments */ void help(char *prog, char *par, char *explain) { printf ("RECLIST, version %s\n",ver); printf ("List Nethack 3.1 - 3.5 record file and calculate the average score.\n"); printf("\nUsage: %s [-f file / -fs / -l] [-u name] [-p role] [-r race]\n", prog); printf(" [-a alignment] [-g ver] [-nX] [-nsX] [-npX] [-d]"); printf(" [-ds date] [-du date]\n\n"); printf("The options are:\n"); printf("\t-u name list only the scores of player 'name'\n"); printf("\t-p profession list only the scores for one character class\n"); printf("\t-r race race to be listed (since 3.3: hum/dwa/elf/gno/orc)\n"); printf("\t-a alignment list only this alignment (since 3.3: l/n/c)\n"); printf("\t-f filename read the scores from the file 'filename'\n"); printf("\t-fs read the scorelist from stdin\n"); printf("\t-l read the logfile from %s\n", LOGFILE); printf("\t-g ver list only scores of game version ver\n"); printf("\t-n X list only X scores\n"); printf("\t-np X pause after every X lines\n"); printf("\t-ns X don't list scores below X points\n"); printf("\t-d show also the dates\n"); printf("\t-ds date games ended at or after date\n"); printf("\t-du date games ended at or before date\n"); printf("\nYou may specify the options in any order.\n"); printf("The space after -f, -g, -u, -r, -a, -n, -ns or -np is optional.\n"); if (par) printf ("\nUnknown parameter %s\n",par); if (explain) printf("%s\n", explain); exit (1); } /* If it wasn't a 3.2 record file, we may still check if it makes sense in the 3.1 format */ long line31 (char *line) { int pos, pos2, dtype, dlev, maxlev, hp, maxhp, dayshown=0; static int nro=0, plin=1; long score, pvm; char chclass, sex, death[70]={""}, ascend=0; char *reason; static char *dungeon[]={"The Dungeons of Doom",/* 0 */ "Gehennom", /* 1 */ "The Gnomish Mines", /* 2 */ "The Quest", /* 3 */ "Fort Ludios", /* 4 */ "Vlad's Tower", /* 5 */ "the endgame"}; /* 6 */ static char *eglev[]={"Earth", /* 0 */ "Air", /* -1*/ "Fire", /* -2*/ "Water", /* -3*/ "Astral"}; /* -4*/ nro++; /* The date uses positions 0-5 (YYMMDD), uid (starts) in 7. After that, the dungeon branch of death and dlevel, maxdlevel, hp, maxhp, score, character class&sex, name and reason for game ending */ sscanf (line,"%ld %*d %d %d %d %d %d %ld %c%c", &pvm,&dtype,&dlev,&maxlev,&hp,&maxhp,&score,&chclass,&sex); if (chclass<'A') return (0); /* The VMS version may have end line with class < A */ if ((prole) && (prole[0]!=chclass)) return (0); if (scorestrlen(line)) /* Never is in a valid file. But we don't want */ return (0); /* to crash just if the file is invalid.. */ pos += 3; for (pos2=0;(line[pos]!=',')&&(pos254) { /* split the line before the death reason */ death[pos]=0; if (day && !dayshown) { printf ("%s\n %ld\t\t",death,pvm); plin++; dayshown=0; } else { printf("%s\n\t\t",death); plin++; } pos++; pos2=0; while (death[pos]) { death[pos2]=death[pos]; pos++; pos2++; } death[pos2]=0; } /* if (strlen(death)==55) death[54]=0; */ } printf("%-54s",death); if (hp>0) printf("%3d",hp); else printf(" -"); sprintf(death,"[%d]",maxhp); printf("%6s\n",death); plin++; if ((strncmp(reason,"quit",4)) && (strncmp(reason,"starvation",10)) && (!ascend)) { reason[0]=toupper(reason[0]); reason[strlen(reason)-1]=0; /* remove the linefeed at the end */ if (day && !dayshown) { printf(" %ld",pvm); dayshown=0; } printf("\t\t%s.\n",reason); plin++; } if (day && !dayshown) { printf (" %ld\n",pvm); plin++; } num++; if (pause>0 && plin >= pause) { printf ("--more--\r"); if (tolower(getch()=='n')) pnum = num; printf (" \r"); plin = 0; } return (score); } char *eos(char *p) { while (*p) p++; return (p); }