/* $NetBSD: scores.c,v 1.22 2014/03/22 19:05:30 dholland Exp $ */ /*- * Copyright (c) 1992, 1993 * The Regents of the University of California. All rights reserved. * * This code is derived from software contributed to Berkeley by * Chris Torek and Darren F. Provine. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * @(#)scores.c 8.1 (Berkeley) 5/31/93 */ /* * Score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu) * modified 22 January 1992, to limit the number of entries any one * person has. * * Major whacks since then. */ #include #include #include #include #include #include #include #include #include #include #include #include "pathnames.h" #include "screen.h" #include "scores.h" #include "tetris.h" /* * Allow updating the high scores unless we're built as part of /rescue. */ #ifndef RESCUEDIR #define ALLOW_SCORE_UPDATES #endif /* * Within this code, we can hang onto one extra "high score", leaving * room for our current score (whether or not it is high). * * We also sometimes keep tabs on the "highest" score on each level. * As long as the scores are kept sorted, this is simply the first one at * that level. */ #define NUMSPOTS (MAXHISCORES + 1) #define NLEVELS (MAXLEVEL + 1) static time_t now; static int nscores; static int gotscores; static struct highscore scores[NUMSPOTS]; static int checkscores(struct highscore *, int); static int cmpscores(const void *, const void *); static void getscores(int *); static void printem(int, int, struct highscore *, int, const char *); static char *thisuser(void); /* contents chosen to be a highly illegal username */ static const char hsh_magic_val[HSH_MAGIC_SIZE] = "//:\0\0://"; #define HSH_ENDIAN_NATIVE 0x12345678 #define HSH_ENDIAN_OPP 0x78563412 /* current file format version */ #define HSH_VERSION 1 /* codes for scorefile_probe return */ #define SCOREFILE_ERROR (-1) #define SCOREFILE_CURRENT 0 /* 40-byte */ #define SCOREFILE_CURRENT_OPP 1 /* 40-byte, opposite-endian */ #define SCOREFILE_599 2 /* 36-byte */ #define SCOREFILE_599_OPP 3 /* 36-byte, opposite-endian */ #define SCOREFILE_50 4 /* 32-byte */ #define SCOREFILE_50_OPP 5 /* 32-byte, opposite-endian */ /* * Check (or guess) what kind of score file contents we have. */ static int scorefile_probe(int sd) { struct stat st; int t1, t2, t3, tx; ssize_t result; uint32_t numbers[3], offset56, offset60, offset64; if (fstat(sd, &st) < 0) { warn("Score file %s: fstat", _PATH_SCOREFILE); return -1; } t1 = st.st_size % sizeof(struct highscore_ondisk) == 0; t2 = st.st_size % sizeof(struct highscore_ondisk_599) == 0; t3 = st.st_size % sizeof(struct highscore_ondisk_50) == 0; tx = t1 + t2 + t3; if (tx == 1) { /* Size matches exact number of one kind of records */ if (t1) { return SCOREFILE_CURRENT; } else if (t2) { return SCOREFILE_599; } else { return SCOREFILE_50; } } else if (tx == 0) { /* Size matches nothing, pick most likely as default */ goto wildguess; } /* * File size is multiple of more than one structure size. * (For example, 288 bytes could be 9*hso50 or 8*hso599.) * Read the file and see if we can figure out what's going * on. This is the layout of the first two records: * * offset hso / current hso_599 hso_50 * (40-byte) (36-byte) (32-byte) * * 0 name #0 name #0 name #0 * 4 : : : * 8 : : : * 12 : : : * 16 : : : * 20 score #0 score #0 score #0 * 24 level #0 level #0 level #0 * 28 (pad) time #0 time #0 * 32 time #0 name #1 * 36 name #1 : * 40 name #1 : : * 44 : : : * 48 : : : * 52 : : score #1 * 56 : score #1 level #1 * 60 score #1 level #1 time #1 * 64 level #1 time #1 name #2 * 68 (pad) : : * 72 time #1 name #2 : * 76 : : : * 80 --- end --- * * There are a number of things we could check here, but the * most effective test is based on the following restrictions: * * - The level must be between 1 and 9 (inclusive) * - All times must be after 1985 and are before 2038, * so the high word must be 0 and the low word may not be * a small value. * - Integer values of 0 or 1-9 cannot be the beginning of * a login name string. * - Values of 1-9 are probably not a score. * * So we read the three words at offsets 56, 60, and 64, and * poke at the values to try to figure things... */ if (lseek(sd, 56, SEEK_SET) < 0) { warn("Score file %s: lseek", _PATH_SCOREFILE); return -1; } result = read(sd, &numbers, sizeof(numbers)); if (result < 0) { warn("Score file %s: read", _PATH_SCOREFILE); return -1; } if ((size_t)result != sizeof(numbers)) { /* * The smallest file whose size divides by more than * one of the sizes is substantially larger than 64, * so this should *never* happen. */ warnx("Score file %s: Unexpected EOF", _PATH_SCOREFILE); return -1; } offset56 = numbers[0]; offset60 = numbers[1]; offset64 = numbers[2]; if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) { /* 40-byte structure */ return SCOREFILE_CURRENT; } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) { /* 36-byte structure */ return SCOREFILE_599; } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) { /* 32-byte structure */ return SCOREFILE_50; } /* None was a valid level; try opposite endian */ offset64 = bswap32(offset64); offset60 = bswap32(offset60); offset56 = bswap32(offset56); if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) { /* 40-byte structure */ return SCOREFILE_CURRENT_OPP; } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) { /* 36-byte structure */ return SCOREFILE_599_OPP; } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) { /* 32-byte structure */ return SCOREFILE_50_OPP; } /* That didn't work either, dunno what's going on */ wildguess: warnx("Score file %s is likely corrupt", _PATH_SCOREFILE); if (sizeof(void *) == 8 && sizeof(time_t) == 8) { return SCOREFILE_CURRENT; } else if (sizeof(time_t) == 8) { return SCOREFILE_599; } else { return SCOREFILE_50; } } /* * Copy a string safely, making sure it's null-terminated. */ static void readname(char *to, size_t maxto, const char *from, size_t maxfrom) { size_t amt; amt = maxto < maxfrom ? maxto : maxfrom; memcpy(to, from, amt); to[maxto-1] = '\0'; } /* * Copy integers, byte-swapping if desired. */ static int32_t read32(int32_t val, int doflip) { if (doflip) { val = bswap32(val); } return val; } static int64_t read64(int64_t val, int doflip) { if (doflip) { val = bswap64(val); } return val; } /* * Read up to MAXHISCORES scorefile_ondisk entries. */ static int readscores(int sd, int doflip) { struct highscore_ondisk buf[MAXHISCORES]; ssize_t result; int i; result = read(sd, buf, sizeof(buf)); if (result < 0) { warn("Score file %s: read", _PATH_SCOREFILE); return -1; } nscores = result / sizeof(buf[0]); for (i=0; ihs_level != level || strcmp(sp->hs_name, me) != 0) continue; if (score > sp->hs_score) { (void)printf("%s bettered %s %d score of %d!\n", "\nYou", "your old level", level, sp->hs_score * sp->hs_level); sp->hs_score = score; /* new score */ sp->hs_time = now; /* and time */ change = 1; } else if (score == sp->hs_score) { (void)printf("%s tied %s %d high score.\n", "\nYou", "your old level", level); sp->hs_time = now; /* renew it */ change = 1; /* gotta rewrite, sigh */ } /* else new score < old score: do nothing */ break; } if (i >= nscores) { strcpy(sp->hs_name, me); sp->hs_level = level; sp->hs_score = score; sp->hs_time = now; nscores++; change = 1; } if (change) { /* * Sort & clean the scores, then rewrite. */ nscores = checkscores(scores, nscores); putscores(sd); } closescores(sd); } /* * Get login name, or if that fails, get something suitable. * The result is always trimmed to fit in a score. */ static char * thisuser(void) { const char *p; struct passwd *pw; size_t l; static char u[sizeof(scores[0].hs_name)]; if (u[0]) return (u); p = getlogin(); if (p == NULL || *p == '\0') { pw = getpwuid(getuid()); if (pw != NULL) p = pw->pw_name; else p = " ???"; } l = strlen(p); if (l >= sizeof(u)) l = sizeof(u) - 1; memcpy(u, p, l); u[l] = '\0'; return (u); } /* * Score comparison function for qsort. * * If two scores are equal, the person who had the score first is * listed first in the highscore file. */ static int cmpscores(const void *x, const void *y) { const struct highscore *a, *b; long l; a = x; b = y; l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score; if (l < 0) return (-1); if (l > 0) return (1); if (a->hs_time < b->hs_time) return (-1); if (a->hs_time > b->hs_time) return (1); return (0); } /* * If we've added a score to the file, we need to check the file and ensure * that this player has only a few entries. The number of entries is * controlled by MAXSCORES, and is to ensure that the highscore file is not * monopolised by just a few people. People who no longer have accounts are * only allowed the highest score. Scores older than EXPIRATION seconds are * removed, unless they are someone's personal best. * Caveat: the highest score on each level is always kept. */ static int checkscores(struct highscore *hs, int num) { struct highscore *sp; int i, j, k, numnames; int levelfound[NLEVELS]; struct peruser { char *name; int times; } count[NUMSPOTS]; struct peruser *pu; /* * Sort so that highest totals come first. * * levelfound[i] becomes set when the first high score for that * level is encountered. By definition this is the highest score. */ qsort((void *)hs, nscores, sizeof(*hs), cmpscores); for (i = MINLEVEL; i < NLEVELS; i++) levelfound[i] = 0; numnames = 0; for (i = 0, sp = hs; i < num;) { /* * This is O(n^2), but do you think we care? */ for (j = 0, pu = count; j < numnames; j++, pu++) if (strcmp(sp->hs_name, pu->name) == 0) break; if (j == numnames) { /* * Add new user, set per-user count to 1. */ pu->name = sp->hs_name; pu->times = 1; numnames++; } else { /* * Two ways to keep this score: * - Not too many (per user), still has acct, & * score not dated; or * - High score on this level. */ if ((pu->times < MAXSCORES && getpwnam(sp->hs_name) != NULL && sp->hs_time + EXPIRATION >= now) || levelfound[sp->hs_level] == 0) pu->times++; else { /* * Delete this score, do not count it, * do not pass go, do not collect $200. */ num--; for (k = i; k < num; k++) hs[k] = hs[k + 1]; continue; } } if (sp->hs_level < NLEVELS && sp->hs_level >= 0) levelfound[sp->hs_level] = 1; i++, sp++; } return (num > MAXHISCORES ? MAXHISCORES : num); } /* * Show current scores. This must be called after savescore, if * savescore is called at all, for two reasons: * - Showscores munches the time field. * - Even if that were not the case, a new score must be recorded * before it can be shown anyway. */ void showscores(int level) { struct highscore *sp; int i, n, c; const char *me; int levelfound[NLEVELS]; if (!gotscores) getscores(NULL); (void)printf("\n\t\t\t Tetris High Scores\n"); /* * If level == 0, the person has not played a game but just asked for * the high scores; we do not need to check for printing in highlight * mode. If SOstr is null, we can't do highlighting anyway. */ me = level && enter_standout_mode ? thisuser() : NULL; /* * Set times to 0 except for high score on each level. */ for (i = MINLEVEL; i < NLEVELS; i++) levelfound[i] = 0; for (i = 0, sp = scores; i < nscores; i++, sp++) { if (sp->hs_level < NLEVELS && sp->hs_level >= 0) { if (levelfound[sp->hs_level]) sp->hs_time = 0; else { sp->hs_time = 1; levelfound[sp->hs_level] = 1; } } } /* * Page each screenful of scores. */ for (i = 0, sp = scores; i < nscores; sp += n) { n = 40; if (i + n > nscores) n = nscores - i; printem(level, i + 1, sp, n, me); if ((i += n) < nscores) { (void)printf("\nHit RETURN to continue."); (void)fflush(stdout); while ((c = getchar()) != '\n') if (c == EOF) break; (void)printf("\n"); } } } static void printem(int level, int offset, struct highscore *hs, int n, const char *me) { struct highscore *sp; int nrows, row, col, item, i, highlight; char buf[100]; #define TITLE "Rank Score Name (points/level)" /* * This makes a nice two-column sort with headers, but it's a bit * convoluted... */ printf("%s %s\n", TITLE, n > 1 ? TITLE : ""); highlight = 0; nrows = (n + 1) / 2; for (row = 0; row < nrows; row++) { for (col = 0; col < 2; col++) { item = col * nrows + row; if (item >= n) { /* * Can only occur on trailing columns. */ (void)putchar('\n'); continue; } sp = &hs[item]; (void)snprintf(buf, sizeof(buf), "%3d%c %6d %-11s (%6d on %d)", item + offset, sp->hs_time ? '*' : ' ', sp->hs_score * sp->hs_level, sp->hs_name, sp->hs_score, sp->hs_level); /* * Highlight if appropriate. This works because * we only get one score per level. */ if (me != NULL && sp->hs_level == level && sp->hs_score == score && strcmp(sp->hs_name, me) == 0) { putpad(enter_standout_mode); highlight = 1; } (void)printf("%s", buf); if (highlight) { putpad(exit_standout_mode); highlight = 0; } /* fill in spaces so column 1 lines up */ if (col == 0) for (i = 40 - strlen(buf); --i >= 0;) (void)putchar(' '); else /* col == 1 */ (void)putchar('\n'); } } }