1 /* $NetBSD: scores.c,v 1.26 2021/05/02 12:50:46 rillig Exp $ */
4 * Copyright (c) 1992, 1993
5 * The Regents of the University of California. All rights reserved.
7 * This code is derived from software contributed to Berkeley by
8 * Chris Torek and Darren F. Provine.
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 * 3. Neither the name of the University nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
22 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34 * @(#)scores.c 8.1 (Berkeley) 5/31/93
38 * Score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu)
39 * modified 22 January 1992, to limit the number of entries any one
42 * Major whacks since then.
57 # include <sys/endian.h>
58 # define swap32 bswap32
59 # define swap64 bswap64
60 #elif defined(__linux__)
61 # include <sys/file.h>
62 # define swap32 __builtin_bswap32
63 # define swap64 __builtin_bswap64
66 #include "pathnames.h"
72 * Allow updating the high scores unless we're built as part of /rescue.
75 #define ALLOW_SCORE_UPDATES
79 * Within this code, we can hang onto one extra "high score", leaving
80 * room for our current score (whether or not it is high).
82 * We also sometimes keep tabs on the "highest" score on each level.
83 * As long as the scores are kept sorted, this is simply the first one at
86 #define NUMSPOTS (MAXHISCORES + 1)
87 #define NLEVELS (MAXLEVEL + 1)
92 static struct highscore scores[NUMSPOTS];
94 static int checkscores(struct highscore *, int);
95 static int cmpscores(const void *, const void *);
96 static void getscores(int *);
97 static void printem(int, int, struct highscore *, int, const char *);
98 static char *thisuser(void);
100 /* contents chosen to be a highly illegal username */
101 static const char hsh_magic_val[HSH_MAGIC_SIZE] = "//:\0\0://";
103 #define HSH_ENDIAN_NATIVE 0x12345678
104 #define HSH_ENDIAN_OPP 0x78563412
106 /* current file format version */
107 #define HSH_VERSION 1
109 /* codes for scorefile_probe return */
110 #define SCOREFILE_ERROR (-1)
111 #define SCOREFILE_CURRENT 0 /* 40-byte */
112 #define SCOREFILE_CURRENT_OPP 1 /* 40-byte, opposite-endian */
113 #define SCOREFILE_599 2 /* 36-byte */
114 #define SCOREFILE_599_OPP 3 /* 36-byte, opposite-endian */
115 #define SCOREFILE_50 4 /* 32-byte */
116 #define SCOREFILE_50_OPP 5 /* 32-byte, opposite-endian */
119 * Check (or guess) what kind of score file contents we have.
122 scorefile_probe(int sd)
127 uint32_t numbers[3], offset56, offset60, offset64;
129 if (fstat(sd, &st) < 0) {
130 warn("Score file %s: fstat", _PATH_SCOREFILE);
134 t1 = st.st_size % sizeof(struct highscore_ondisk) == 0;
135 t2 = st.st_size % sizeof(struct highscore_ondisk_599) == 0;
136 t3 = st.st_size % sizeof(struct highscore_ondisk_50) == 0;
139 /* Size matches exact number of one kind of records */
141 return SCOREFILE_CURRENT;
143 return SCOREFILE_599;
147 } else if (tx == 0) {
148 /* Size matches nothing, pick most likely as default */
153 * File size is multiple of more than one structure size.
154 * (For example, 288 bytes could be 9*hso50 or 8*hso599.)
155 * Read the file and see if we can figure out what's going
156 * on. This is the layout of the first two records:
158 * offset hso / current hso_599 hso_50
159 * (40-byte) (36-byte) (32-byte)
161 * 0 name #0 name #0 name #0
166 * 20 score #0 score #0 score #0
167 * 24 level #0 level #0 level #0
168 * 28 (pad) time #0 time #0
175 * 56 : score #1 level #1
176 * 60 score #1 level #1 time #1
177 * 64 level #1 time #1 name #2
179 * 72 time #1 name #2 :
183 * There are a number of things we could check here, but the
184 * most effective test is based on the following restrictions:
186 * - The level must be between 1 and 9 (inclusive)
187 * - All times must be after 1985 and are before 2038,
188 * so the high word must be 0 and the low word may not be
190 * - Integer values of 0 or 1-9 cannot be the beginning of
191 * a login name string.
192 * - Values of 1-9 are probably not a score.
194 * So we read the three words at offsets 56, 60, and 64, and
195 * poke at the values to try to figure things...
198 if (lseek(sd, 56, SEEK_SET) < 0) {
199 warn("Score file %s: lseek", _PATH_SCOREFILE);
202 result = read(sd, &numbers, sizeof(numbers));
204 warn("Score file %s: read", _PATH_SCOREFILE);
207 if ((size_t)result != sizeof(numbers)) {
209 * The smallest file whose size divides by more than
210 * one of the sizes is substantially larger than 64,
211 * so this should *never* happen.
213 warnx("Score file %s: Unexpected EOF", _PATH_SCOREFILE);
217 offset56 = numbers[0];
218 offset60 = numbers[1];
219 offset64 = numbers[2];
221 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) {
222 /* 40-byte structure */
223 return SCOREFILE_CURRENT;
224 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) {
225 /* 36-byte structure */
226 return SCOREFILE_599;
227 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) {
228 /* 32-byte structure */
232 /* None was a valid level; try opposite endian */
233 offset64 = swap32(offset64);
234 offset60 = swap32(offset60);
235 offset56 = swap32(offset56);
237 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) {
238 /* 40-byte structure */
239 return SCOREFILE_CURRENT_OPP;
240 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) {
241 /* 36-byte structure */
242 return SCOREFILE_599_OPP;
243 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) {
244 /* 32-byte structure */
245 return SCOREFILE_50_OPP;
248 /* That didn't work either, dunno what's going on */
250 warnx("Score file %s is likely corrupt", _PATH_SCOREFILE);
251 if (sizeof(void *) == 8 && sizeof(time_t) == 8) {
252 return SCOREFILE_CURRENT;
253 } else if (sizeof(time_t) == 8) {
254 return SCOREFILE_599;
261 * Copy a string safely, making sure it's null-terminated.
264 readname(char *to, size_t maxto, const char *from, size_t maxfrom)
268 amt = maxto < maxfrom ? maxto : maxfrom;
269 memcpy(to, from, amt);
274 * Copy integers, byte-swapping if desired.
277 read32(int32_t val, int doflip)
286 read64(int64_t val, int doflip)
295 * Read up to MAXHISCORES scorefile_ondisk entries.
298 readscores(int sd, int doflip)
300 struct highscore_ondisk buf[MAXHISCORES];
304 result = read(sd, buf, sizeof(buf));
306 warn("Score file %s: read", _PATH_SCOREFILE);
309 nscores = result / sizeof(buf[0]);
311 for (i=0; i<nscores; i++) {
312 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
313 buf[i].hso_name, sizeof(buf[i].hso_name));
314 scores[i].hs_score = read32(buf[i].hso_score, doflip);
315 scores[i].hs_level = read32(buf[i].hso_level, doflip);
316 scores[i].hs_time = read64(buf[i].hso_time, doflip);
322 * Read up to MAXHISCORES scorefile_ondisk_599 entries.
325 readscores599(int sd, int doflip)
327 struct highscore_ondisk_599 buf[MAXHISCORES];
331 result = read(sd, buf, sizeof(buf));
333 warn("Score file %s: read", _PATH_SCOREFILE);
336 nscores = result / sizeof(buf[0]);
338 for (i=0; i<nscores; i++) {
339 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
340 buf[i].hso599_name, sizeof(buf[i].hso599_name));
341 scores[i].hs_score = read32(buf[i].hso599_score, doflip);
342 scores[i].hs_level = read32(buf[i].hso599_level, doflip);
344 * Don't bother pasting the time together into a
345 * 64-bit value; just take whichever half is nonzero.
348 read32(buf[i].hso599_time[buf[i].hso599_time[0] == 0],
355 * Read up to MAXHISCORES scorefile_ondisk_50 entries.
358 readscores50(int sd, int doflip)
360 struct highscore_ondisk_50 buf[MAXHISCORES];
364 result = read(sd, buf, sizeof(buf));
366 warn("Score file %s: read", _PATH_SCOREFILE);
369 nscores = result / sizeof(buf[0]);
371 for (i=0; i<nscores; i++) {
372 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
373 buf[i].hso50_name, sizeof(buf[i].hso50_name));
374 scores[i].hs_score = read32(buf[i].hso50_score, doflip);
375 scores[i].hs_level = read32(buf[i].hso50_level, doflip);
376 scores[i].hs_time = read32(buf[i].hso50_time, doflip);
382 * Read the score file. Can be called from savescore (before showscores)
383 * or showscores (if savescore will not be called). If the given pointer
384 * is not NULL, sets *fdp to an open file handle that corresponds to a
385 * read/write score file that is locked with LOCK_EX. Otherwise, the
386 * file is locked with LOCK_SH for the read and closed before return.
391 struct highscore_header header;
399 #ifdef ALLOW_SCORE_UPDATES
401 mint = O_RDWR | O_CREAT;
402 human = "read/write";
412 mask = umask(S_IWOTH);
413 sd = open(_PATH_SCOREFILE, mint, 0666);
419 * If the file simply isn't there because nobody's
420 * played yet, and we aren't going to be trying to
421 * update it, don't warn. Even if we are going to be
422 * trying to write it, don't fail -- we can still show
423 * the player the score they got.
426 if (fdp != NULL || errno != ENOENT) {
427 warn("Cannot open %s for %s", _PATH_SCOREFILE, human);
434 * XXX: failure here should probably be more fatal than this.
437 warn("warning: score file %s cannot be locked",
441 * The current format (since -current of 20090525) is
443 * struct highscore_header
444 * up to MAXHIGHSCORES x struct highscore_ondisk
446 * Before this, there is no header, and the contents
447 * might be any of three formats:
449 * highscore_ondisk (64-bit machines with 64-bit time_t)
450 * highscore_ondisk_599 (32-bit machines with 64-bit time_t)
451 * highscore_ondisk_50 (32-bit machines with 32-bit time_t)
453 * The first two appear in 5.99 between the time_t change and
454 * 20090525, depending on whether the compiler inserts
455 * structure padding before an unaligned 64-bit time_t. The
456 * last appears in 5.0 and earlier.
458 * Any or all of these might also appear on other OSes where
459 * this code has been ported.
461 * Since the old file has no header, we will have to guess
462 * which of these formats it has.
466 * First, look for a header.
468 result = read(sd, &header, sizeof(header));
470 warn("Score file %s: read", _PATH_SCOREFILE);
473 if (result != 0 && (size_t)result != sizeof(header)) {
474 warnx("Score file %s: read: unexpected EOF", _PATH_SCOREFILE);
476 * File is hopelessly corrupt, might as well truncate it
477 * and start over with empty scores.
479 if (lseek(sd, 0, SEEK_SET) < 0) {
481 warn("Score file %s: lseek", _PATH_SCOREFILE);
484 if (ftruncate(sd, 0) == 0) {
492 /* Empty file; that just means there are no scores. */
496 * Is what we read a header, or the first 16 bytes of
497 * a score entry? hsh_magic_val is chosen to be
498 * something that is extremely unlikely to appear in
501 if (!memcmp(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE)) {
502 /* Yes, we have a header. */
504 if (header.hsh_endiantag == HSH_ENDIAN_NATIVE) {
507 } else if (header.hsh_endiantag == HSH_ENDIAN_OPP) {
510 warnx("Score file %s: Unknown endian tag %u",
511 _PATH_SCOREFILE, header.hsh_endiantag);
515 if (header.hsh_version != HSH_VERSION) {
516 warnx("Score file %s: Unknown version code %u",
517 _PATH_SCOREFILE, header.hsh_version);
521 if (readscores(sd, doflip) < 0) {
526 * Ok, it wasn't a header. Try to figure out what
527 * size records we have.
529 result = scorefile_probe(sd);
530 if (lseek(sd, 0, SEEK_SET) < 0) {
531 warn("Score file %s: lseek", _PATH_SCOREFILE);
535 case SCOREFILE_CURRENT:
536 result = readscores(sd, 0 /* don't flip */);
538 case SCOREFILE_CURRENT_OPP:
539 result = readscores(sd, 1 /* do flip */);
542 result = readscores599(sd, 0 /* don't flip */);
544 case SCOREFILE_599_OPP:
545 result = readscores599(sd, 1 /* do flip */);
548 result = readscores50(sd, 0 /* don't flip */);
550 case SCOREFILE_50_OPP:
551 result = readscores50(sd, 1 /* do flip */);
579 #ifdef ALLOW_SCORE_UPDATES
581 * Paranoid write wrapper; unlike fwrite() it preserves errno.
584 dowrite(int sd, const void *vbuf, size_t len)
586 const char *buf = vbuf;
591 result = write(sd, buf+done, len-done);
593 if (errno == EINTR) {
602 #endif /* ALLOW_SCORE_UPDATES */
605 * Write the score file out.
610 #ifdef ALLOW_SCORE_UPDATES
611 struct highscore_header header;
612 struct highscore_ondisk buf[MAXHISCORES] = {0};
619 memcpy(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE);
620 header.hsh_endiantag = HSH_ENDIAN_NATIVE;
621 header.hsh_version = HSH_VERSION;
623 for (i=0; i<nscores; i++) {
624 memcpy(buf[i].hso_name, scores[i].hs_name,
625 sizeof(buf[i].hso_name));
626 buf[i].hso_score = scores[i].hs_score;
627 buf[i].hso_level = scores[i].hs_level;
628 buf[i].hso_pad = 0xbaadf00d;
629 buf[i].hso_time = scores[i].hs_time;
632 if (lseek(sd, 0, SEEK_SET) < 0) {
633 warn("Score file %s: lseek", _PATH_SCOREFILE);
636 if (dowrite(sd, &header, sizeof(header)) < 0 ||
637 dowrite(sd, buf, sizeof(buf[0]) * nscores) < 0) {
638 warn("Score file %s: write", _PATH_SCOREFILE);
643 warnx("high scores may be damaged");
646 #endif /* ALLOW_SCORE_UPDATES */
650 * Close the score file.
660 * Read and update the scores file with the current reults.
665 struct highscore *sp;
676 * Allow at most one score per person per level -- see if we
677 * can replace an existing score, or (easiest) do nothing.
678 * Otherwise add new score at end (there is always room).
682 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) {
683 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0)
685 if (score > sp->hs_score) {
686 (void)printf("%s bettered %s %d score of %d!\n",
687 "\nYou", "your old level", level,
688 sp->hs_score * sp->hs_level);
689 sp->hs_score = score; /* new score */
690 sp->hs_time = now; /* and time */
692 } else if (score == sp->hs_score) {
693 (void)printf("%s tied %s %d high score.\n",
694 "\nYou", "your old level", level);
695 sp->hs_time = now; /* renew it */
696 change = 1; /* gotta rewrite, sigh */
697 } /* else new score < old score: do nothing */
701 strcpy(sp->hs_name, me);
702 sp->hs_level = level;
703 sp->hs_score = score;
711 * Sort & clean the scores, then rewrite.
713 nscores = checkscores(scores, nscores);
720 * Get login name, or if that fails, get something suitable.
721 * The result is always trimmed to fit in a score.
729 static char u[sizeof(scores[0].hs_name)];
734 if (p == NULL || *p == '\0') {
735 pw = getpwuid(getuid());
750 * Score comparison function for qsort.
752 * If two scores are equal, the person who had the score first is
753 * listed first in the highscore file.
756 cmpscores(const void *x, const void *y)
758 const struct highscore *a, *b;
763 l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score;
768 if (a->hs_time < b->hs_time)
770 if (a->hs_time > b->hs_time)
776 * If we've added a score to the file, we need to check the file and ensure
777 * that this player has only a few entries. The number of entries is
778 * controlled by MAXSCORES, and is to ensure that the highscore file is not
779 * monopolised by just a few people. People who no longer have accounts are
780 * only allowed the highest score. Scores older than EXPIRATION seconds are
781 * removed, unless they are someone's personal best.
782 * Caveat: the highest score on each level is always kept.
785 checkscores(struct highscore *hs, int num)
787 struct highscore *sp;
788 int i, j, k, numnames;
789 int levelfound[NLEVELS];
797 * Sort so that highest totals come first.
799 * levelfound[i] becomes set when the first high score for that
800 * level is encountered. By definition this is the highest score.
802 qsort((void *)hs, nscores, sizeof(*hs), cmpscores);
803 for (i = MINLEVEL; i < NLEVELS; i++)
806 for (i = 0, sp = hs; i < num;) {
808 * This is O(n^2), but do you think we care?
810 for (j = 0, pu = count; j < numnames; j++, pu++)
811 if (strcmp(sp->hs_name, pu->name) == 0)
815 * Add new user, set per-user count to 1.
817 pu->name = sp->hs_name;
822 * Two ways to keep this score:
823 * - Not too many (per user), still has acct, &
824 * score not dated; or
825 * - High score on this level.
827 if ((pu->times < MAXSCORES &&
828 getpwnam(sp->hs_name) != NULL &&
829 sp->hs_time + EXPIRATION >= now) ||
830 levelfound[sp->hs_level] == 0)
834 * Delete this score, do not count it,
835 * do not pass go, do not collect $200.
838 for (k = i; k < num; k++)
843 if (sp->hs_level < NLEVELS && sp->hs_level >= 0)
844 levelfound[sp->hs_level] = 1;
847 return (num > MAXHISCORES ? MAXHISCORES : num);
851 * Show current scores. This must be called after savescore, if
852 * savescore is called at all, for two reasons:
853 * - Showscores munches the time field.
854 * - Even if that were not the case, a new score must be recorded
855 * before it can be shown anyway.
858 showscores(int level)
860 struct highscore *sp;
863 int levelfound[NLEVELS];
867 (void)printf("\n\t\t\t Tetris High Scores\n");
870 * If level == 0, the person has not played a game but just asked for
871 * the high scores; we do not need to check for printing in highlight
872 * mode. If SOstr is null, we can't do highlighting anyway.
874 me = level && enter_standout_mode ? thisuser() : NULL;
877 * Set times to 0 except for high score on each level.
879 for (i = MINLEVEL; i < NLEVELS; i++)
881 for (i = 0, sp = scores; i < nscores; i++, sp++) {
882 if (sp->hs_level < NLEVELS && sp->hs_level >= 0) {
883 if (levelfound[sp->hs_level])
887 levelfound[sp->hs_level] = 1;
893 * Page each screenful of scores.
895 for (i = 0, sp = scores; i < nscores; sp += n) {
899 printem(level, i + 1, sp, n, me);
900 if ((i += n) < nscores) {
901 (void)printf("\nHit RETURN to continue.");
902 (void)fflush(stdout);
903 while ((c = getchar()) != '\n')
912 printem(int level, int offset, struct highscore *hs, int n, const char *me)
914 struct highscore *sp;
915 int nrows, row, col, item, i, highlight;
917 #define TITLE "Rank Score Name (points/level)"
920 * This makes a nice two-column sort with headers, but it's a bit
923 printf("%s %s\n", TITLE, n > 1 ? TITLE : "");
928 for (row = 0; row < nrows; row++) {
929 for (col = 0; col < 2; col++) {
930 item = col * nrows + row;
933 * Can only occur on trailing columns.
939 (void)snprintf(buf, sizeof(buf),
940 "%3d%c %6d %-11s (%6d on %d)",
941 item + offset, sp->hs_time ? '*' : ' ',
942 sp->hs_score * sp->hs_level,
943 sp->hs_name, sp->hs_score, sp->hs_level);
945 * Highlight if appropriate. This works because
946 * we only get one score per level.
949 sp->hs_level == level &&
950 sp->hs_score == score &&
951 strcmp(sp->hs_name, me) == 0) {
952 putpad(enter_standout_mode);
955 (void)printf("%s", buf);
957 putpad(exit_standout_mode);
961 /* fill in spaces so column 1 lines up */
963 for (i = 40 - strlen(buf); --i >= 0;)