]>
git.cameronkatri.com Git - bsdgames-darwin.git/blob - tetris/scores.c
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.
56 #include "pathnames.h"
61 #include <libkern/OSByteOrder.h>
64 * Allow updating the high scores unless we're built as part of /rescue.
67 #define ALLOW_SCORE_UPDATES
71 * Within this code, we can hang onto one extra "high score", leaving
72 * room for our current score (whether or not it is high).
74 * We also sometimes keep tabs on the "highest" score on each level.
75 * As long as the scores are kept sorted, this is simply the first one at
78 #define NUMSPOTS (MAXHISCORES + 1)
79 #define NLEVELS (MAXLEVEL + 1)
84 static struct highscore scores
[NUMSPOTS
];
86 static int checkscores(struct highscore
*, int);
87 static int cmpscores(const void *, const void *);
88 static void getscores(int *);
89 static void printem(int, int, struct highscore
*, int, const char *);
90 static char *thisuser(void);
92 /* contents chosen to be a highly illegal username */
93 static const char hsh_magic_val
[HSH_MAGIC_SIZE
] = "//:\0\0://";
95 #define HSH_ENDIAN_NATIVE 0x12345678
96 #define HSH_ENDIAN_OPP 0x78563412
98 /* current file format version */
101 /* codes for scorefile_probe return */
102 #define SCOREFILE_ERROR (-1)
103 #define SCOREFILE_CURRENT 0 /* 40-byte */
104 #define SCOREFILE_CURRENT_OPP 1 /* 40-byte, opposite-endian */
105 #define SCOREFILE_599 2 /* 36-byte */
106 #define SCOREFILE_599_OPP 3 /* 36-byte, opposite-endian */
107 #define SCOREFILE_50 4 /* 32-byte */
108 #define SCOREFILE_50_OPP 5 /* 32-byte, opposite-endian */
111 * Check (or guess) what kind of score file contents we have.
114 scorefile_probe(int sd
)
119 uint32_t numbers
[3], offset56
, offset60
, offset64
;
121 if (fstat(sd
, &st
) < 0) {
122 warn("Score file %s: fstat", _PATH_SCOREFILE
);
126 t1
= st
.st_size
% sizeof(struct highscore_ondisk
) == 0;
127 t2
= st
.st_size
% sizeof(struct highscore_ondisk_599
) == 0;
128 t3
= st
.st_size
% sizeof(struct highscore_ondisk_50
) == 0;
131 /* Size matches exact number of one kind of records */
133 return SCOREFILE_CURRENT
;
135 return SCOREFILE_599
;
139 } else if (tx
== 0) {
140 /* Size matches nothing, pick most likely as default */
145 * File size is multiple of more than one structure size.
146 * (For example, 288 bytes could be 9*hso50 or 8*hso599.)
147 * Read the file and see if we can figure out what's going
148 * on. This is the layout of the first two records:
150 * offset hso / current hso_599 hso_50
151 * (40-byte) (36-byte) (32-byte)
153 * 0 name #0 name #0 name #0
158 * 20 score #0 score #0 score #0
159 * 24 level #0 level #0 level #0
160 * 28 (pad) time #0 time #0
167 * 56 : score #1 level #1
168 * 60 score #1 level #1 time #1
169 * 64 level #1 time #1 name #2
171 * 72 time #1 name #2 :
175 * There are a number of things we could check here, but the
176 * most effective test is based on the following restrictions:
178 * - The level must be between 1 and 9 (inclusive)
179 * - All times must be after 1985 and are before 2038,
180 * so the high word must be 0 and the low word may not be
182 * - Integer values of 0 or 1-9 cannot be the beginning of
183 * a login name string.
184 * - Values of 1-9 are probably not a score.
186 * So we read the three words at offsets 56, 60, and 64, and
187 * poke at the values to try to figure things...
190 if (lseek(sd
, 56, SEEK_SET
) < 0) {
191 warn("Score file %s: lseek", _PATH_SCOREFILE
);
194 result
= read(sd
, &numbers
, sizeof(numbers
));
196 warn("Score file %s: read", _PATH_SCOREFILE
);
199 if ((size_t)result
!= sizeof(numbers
)) {
201 * The smallest file whose size divides by more than
202 * one of the sizes is substantially larger than 64,
203 * so this should *never* happen.
205 warnx("Score file %s: Unexpected EOF", _PATH_SCOREFILE
);
209 offset56
= numbers
[0];
210 offset60
= numbers
[1];
211 offset64
= numbers
[2];
213 if (offset64
>= MINLEVEL
&& offset64
<= MAXLEVEL
) {
214 /* 40-byte structure */
215 return SCOREFILE_CURRENT
;
216 } else if (offset60
>= MINLEVEL
&& offset60
<= MAXLEVEL
) {
217 /* 36-byte structure */
218 return SCOREFILE_599
;
219 } else if (offset56
>= MINLEVEL
&& offset56
<= MAXLEVEL
) {
220 /* 32-byte structure */
224 /* None was a valid level; try opposite endian */
225 offset64
= OSSwapInt32(offset64
);
226 offset60
= OSSwapInt32(offset60
);
227 offset56
= OSSwapInt32(offset56
);
229 if (offset64
>= MINLEVEL
&& offset64
<= MAXLEVEL
) {
230 /* 40-byte structure */
231 return SCOREFILE_CURRENT_OPP
;
232 } else if (offset60
>= MINLEVEL
&& offset60
<= MAXLEVEL
) {
233 /* 36-byte structure */
234 return SCOREFILE_599_OPP
;
235 } else if (offset56
>= MINLEVEL
&& offset56
<= MAXLEVEL
) {
236 /* 32-byte structure */
237 return SCOREFILE_50_OPP
;
240 /* That didn't work either, dunno what's going on */
242 warnx("Score file %s is likely corrupt", _PATH_SCOREFILE
);
243 if (sizeof(void *) == 8 && sizeof(time_t) == 8) {
244 return SCOREFILE_CURRENT
;
245 } else if (sizeof(time_t) == 8) {
246 return SCOREFILE_599
;
253 * Copy a string safely, making sure it's null-terminated.
256 readname(char *to
, size_t maxto
, const char *from
, size_t maxfrom
)
260 amt
= maxto
< maxfrom
? maxto
: maxfrom
;
261 memcpy(to
, from
, amt
);
266 * Copy integers, byte-swapping if desired.
269 read32(int32_t val
, int doflip
)
272 val
= OSSwapInt32(val
);
278 read64(int64_t val
, int doflip
)
281 val
= OSSwapInt64(val
);
287 * Read up to MAXHISCORES scorefile_ondisk entries.
290 readscores(int sd
, int doflip
)
292 struct highscore_ondisk buf
[MAXHISCORES
];
296 result
= read(sd
, buf
, sizeof(buf
));
298 warn("Score file %s: read", _PATH_SCOREFILE
);
301 nscores
= result
/ sizeof(buf
[0]);
303 for (i
=0; i
<nscores
; i
++) {
304 readname(scores
[i
].hs_name
, sizeof(scores
[i
].hs_name
),
305 buf
[i
].hso_name
, sizeof(buf
[i
].hso_name
));
306 scores
[i
].hs_score
= read32(buf
[i
].hso_score
, doflip
);
307 scores
[i
].hs_level
= read32(buf
[i
].hso_level
, doflip
);
308 scores
[i
].hs_time
= read64(buf
[i
].hso_time
, doflip
);
314 * Read up to MAXHISCORES scorefile_ondisk_599 entries.
317 readscores599(int sd
, int doflip
)
319 struct highscore_ondisk_599 buf
[MAXHISCORES
];
323 result
= read(sd
, buf
, sizeof(buf
));
325 warn("Score file %s: read", _PATH_SCOREFILE
);
328 nscores
= result
/ sizeof(buf
[0]);
330 for (i
=0; i
<nscores
; i
++) {
331 readname(scores
[i
].hs_name
, sizeof(scores
[i
].hs_name
),
332 buf
[i
].hso599_name
, sizeof(buf
[i
].hso599_name
));
333 scores
[i
].hs_score
= read32(buf
[i
].hso599_score
, doflip
);
334 scores
[i
].hs_level
= read32(buf
[i
].hso599_level
, doflip
);
336 * Don't bother pasting the time together into a
337 * 64-bit value; just take whichever half is nonzero.
340 read32(buf
[i
].hso599_time
[buf
[i
].hso599_time
[0] == 0],
347 * Read up to MAXHISCORES scorefile_ondisk_50 entries.
350 readscores50(int sd
, int doflip
)
352 struct highscore_ondisk_50 buf
[MAXHISCORES
];
356 result
= read(sd
, buf
, sizeof(buf
));
358 warn("Score file %s: read", _PATH_SCOREFILE
);
361 nscores
= result
/ sizeof(buf
[0]);
363 for (i
=0; i
<nscores
; i
++) {
364 readname(scores
[i
].hs_name
, sizeof(scores
[i
].hs_name
),
365 buf
[i
].hso50_name
, sizeof(buf
[i
].hso50_name
));
366 scores
[i
].hs_score
= read32(buf
[i
].hso50_score
, doflip
);
367 scores
[i
].hs_level
= read32(buf
[i
].hso50_level
, doflip
);
368 scores
[i
].hs_time
= read32(buf
[i
].hso50_time
, doflip
);
374 * Read the score file. Can be called from savescore (before showscores)
375 * or showscores (if savescore will not be called). If the given pointer
376 * is not NULL, sets *fdp to an open file handle that corresponds to a
377 * read/write score file that is locked with LOCK_EX. Otherwise, the
378 * file is locked with LOCK_SH for the read and closed before return.
383 struct highscore_header header
;
391 #ifdef ALLOW_SCORE_UPDATES
393 mint
= O_RDWR
| O_CREAT
;
394 human
= "read/write";
404 mask
= umask(S_IWOTH
);
405 sd
= open(_PATH_SCOREFILE
, mint
, 0666);
411 * If the file simply isn't there because nobody's
412 * played yet, and we aren't going to be trying to
413 * update it, don't warn. Even if we are going to be
414 * trying to write it, don't fail -- we can still show
415 * the player the score they got.
418 if (fdp
!= NULL
|| errno
!= ENOENT
) {
419 warn("Cannot open %s for %s", _PATH_SCOREFILE
, human
);
426 * XXX: failure here should probably be more fatal than this.
429 warn("warning: score file %s cannot be locked",
433 * The current format (since -current of 20090525) is
435 * struct highscore_header
436 * up to MAXHIGHSCORES x struct highscore_ondisk
438 * Before this, there is no header, and the contents
439 * might be any of three formats:
441 * highscore_ondisk (64-bit machines with 64-bit time_t)
442 * highscore_ondisk_599 (32-bit machines with 64-bit time_t)
443 * highscore_ondisk_50 (32-bit machines with 32-bit time_t)
445 * The first two appear in 5.99 between the time_t change and
446 * 20090525, depending on whether the compiler inserts
447 * structure padding before an unaligned 64-bit time_t. The
448 * last appears in 5.0 and earlier.
450 * Any or all of these might also appear on other OSes where
451 * this code has been ported.
453 * Since the old file has no header, we will have to guess
454 * which of these formats it has.
458 * First, look for a header.
460 result
= read(sd
, &header
, sizeof(header
));
462 warn("Score file %s: read", _PATH_SCOREFILE
);
465 if (result
!= 0 && (size_t)result
!= sizeof(header
)) {
466 warnx("Score file %s: read: unexpected EOF", _PATH_SCOREFILE
);
468 * File is hopelessly corrupt, might as well truncate it
469 * and start over with empty scores.
471 if (lseek(sd
, 0, SEEK_SET
) < 0) {
473 warn("Score file %s: lseek", _PATH_SCOREFILE
);
476 if (ftruncate(sd
, 0) == 0) {
484 /* Empty file; that just means there are no scores. */
488 * Is what we read a header, or the first 16 bytes of
489 * a score entry? hsh_magic_val is chosen to be
490 * something that is extremely unlikely to appear in
493 if (!memcmp(header
.hsh_magic
, hsh_magic_val
, HSH_MAGIC_SIZE
)) {
494 /* Yes, we have a header. */
496 if (header
.hsh_endiantag
== HSH_ENDIAN_NATIVE
) {
499 } else if (header
.hsh_endiantag
== HSH_ENDIAN_OPP
) {
502 warnx("Score file %s: Unknown endian tag %u",
503 _PATH_SCOREFILE
, header
.hsh_endiantag
);
507 if (header
.hsh_version
!= HSH_VERSION
) {
508 warnx("Score file %s: Unknown version code %u",
509 _PATH_SCOREFILE
, header
.hsh_version
);
513 if (readscores(sd
, doflip
) < 0) {
518 * Ok, it wasn't a header. Try to figure out what
519 * size records we have.
521 result
= scorefile_probe(sd
);
522 if (lseek(sd
, 0, SEEK_SET
) < 0) {
523 warn("Score file %s: lseek", _PATH_SCOREFILE
);
527 case SCOREFILE_CURRENT
:
528 result
= readscores(sd
, 0 /* don't flip */);
530 case SCOREFILE_CURRENT_OPP
:
531 result
= readscores(sd
, 1 /* do flip */);
534 result
= readscores599(sd
, 0 /* don't flip */);
536 case SCOREFILE_599_OPP
:
537 result
= readscores599(sd
, 1 /* do flip */);
540 result
= readscores50(sd
, 0 /* don't flip */);
542 case SCOREFILE_50_OPP
:
543 result
= readscores50(sd
, 1 /* do flip */);
571 #ifdef ALLOW_SCORE_UPDATES
573 * Paranoid write wrapper; unlike fwrite() it preserves errno.
576 dowrite(int sd
, const void *vbuf
, size_t len
)
578 const char *buf
= vbuf
;
583 result
= write(sd
, buf
+done
, len
-done
);
585 if (errno
== EINTR
) {
594 #endif /* ALLOW_SCORE_UPDATES */
597 * Write the score file out.
602 #ifdef ALLOW_SCORE_UPDATES
603 struct highscore_header header
;
604 struct highscore_ondisk buf
[MAXHISCORES
] = {0};
611 memcpy(header
.hsh_magic
, hsh_magic_val
, HSH_MAGIC_SIZE
);
612 header
.hsh_endiantag
= HSH_ENDIAN_NATIVE
;
613 header
.hsh_version
= HSH_VERSION
;
615 for (i
=0; i
<nscores
; i
++) {
616 memcpy(buf
[i
].hso_name
, scores
[i
].hs_name
,
617 sizeof(buf
[i
].hso_name
));
618 buf
[i
].hso_score
= scores
[i
].hs_score
;
619 buf
[i
].hso_level
= scores
[i
].hs_level
;
620 buf
[i
].hso_pad
= 0xbaadf00d;
621 buf
[i
].hso_time
= scores
[i
].hs_time
;
624 if (lseek(sd
, 0, SEEK_SET
) < 0) {
625 warn("Score file %s: lseek", _PATH_SCOREFILE
);
628 if (dowrite(sd
, &header
, sizeof(header
)) < 0 ||
629 dowrite(sd
, buf
, sizeof(buf
[0]) * nscores
) < 0) {
630 warn("Score file %s: write", _PATH_SCOREFILE
);
635 warnx("high scores may be damaged");
638 #endif /* ALLOW_SCORE_UPDATES */
642 * Close the score file.
652 * Read and update the scores file with the current reults.
657 struct highscore
*sp
;
668 * Allow at most one score per person per level -- see if we
669 * can replace an existing score, or (easiest) do nothing.
670 * Otherwise add new score at end (there is always room).
674 for (i
= 0, sp
= &scores
[0]; i
< nscores
; i
++, sp
++) {
675 if (sp
->hs_level
!= level
|| strcmp(sp
->hs_name
, me
) != 0)
677 if (score
> sp
->hs_score
) {
678 (void)printf("%s bettered %s %d score of %d!\n",
679 "\nYou", "your old level", level
,
680 sp
->hs_score
* sp
->hs_level
);
681 sp
->hs_score
= score
; /* new score */
682 sp
->hs_time
= now
; /* and time */
684 } else if (score
== sp
->hs_score
) {
685 (void)printf("%s tied %s %d high score.\n",
686 "\nYou", "your old level", level
);
687 sp
->hs_time
= now
; /* renew it */
688 change
= 1; /* gotta rewrite, sigh */
689 } /* else new score < old score: do nothing */
693 strcpy(sp
->hs_name
, me
);
694 sp
->hs_level
= level
;
695 sp
->hs_score
= score
;
703 * Sort & clean the scores, then rewrite.
705 nscores
= checkscores(scores
, nscores
);
712 * Get login name, or if that fails, get something suitable.
713 * The result is always trimmed to fit in a score.
721 static char u
[sizeof(scores
[0].hs_name
)];
726 if (p
== NULL
|| *p
== '\0') {
727 pw
= getpwuid(getuid());
742 * Score comparison function for qsort.
744 * If two scores are equal, the person who had the score first is
745 * listed first in the highscore file.
748 cmpscores(const void *x
, const void *y
)
750 const struct highscore
*a
, *b
;
755 l
= (long)b
->hs_level
* b
->hs_score
- (long)a
->hs_level
* a
->hs_score
;
760 if (a
->hs_time
< b
->hs_time
)
762 if (a
->hs_time
> b
->hs_time
)
768 * If we've added a score to the file, we need to check the file and ensure
769 * that this player has only a few entries. The number of entries is
770 * controlled by MAXSCORES, and is to ensure that the highscore file is not
771 * monopolised by just a few people. People who no longer have accounts are
772 * only allowed the highest score. Scores older than EXPIRATION seconds are
773 * removed, unless they are someone's personal best.
774 * Caveat: the highest score on each level is always kept.
777 checkscores(struct highscore
*hs
, int num
)
779 struct highscore
*sp
;
780 int i
, j
, k
, numnames
;
781 int levelfound
[NLEVELS
];
789 * Sort so that highest totals come first.
791 * levelfound[i] becomes set when the first high score for that
792 * level is encountered. By definition this is the highest score.
794 qsort((void *)hs
, nscores
, sizeof(*hs
), cmpscores
);
795 for (i
= MINLEVEL
; i
< NLEVELS
; i
++)
798 for (i
= 0, sp
= hs
; i
< num
;) {
800 * This is O(n^2), but do you think we care?
802 for (j
= 0, pu
= count
; j
< numnames
; j
++, pu
++)
803 if (strcmp(sp
->hs_name
, pu
->name
) == 0)
807 * Add new user, set per-user count to 1.
809 pu
->name
= sp
->hs_name
;
814 * Two ways to keep this score:
815 * - Not too many (per user), still has acct, &
816 * score not dated; or
817 * - High score on this level.
819 if ((pu
->times
< MAXSCORES
&&
820 getpwnam(sp
->hs_name
) != NULL
&&
821 sp
->hs_time
+ EXPIRATION
>= now
) ||
822 levelfound
[sp
->hs_level
] == 0)
826 * Delete this score, do not count it,
827 * do not pass go, do not collect $200.
830 for (k
= i
; k
< num
; k
++)
835 if (sp
->hs_level
< NLEVELS
&& sp
->hs_level
>= 0)
836 levelfound
[sp
->hs_level
] = 1;
839 return (num
> MAXHISCORES
? MAXHISCORES
: num
);
843 * Show current scores. This must be called after savescore, if
844 * savescore is called at all, for two reasons:
845 * - Showscores munches the time field.
846 * - Even if that were not the case, a new score must be recorded
847 * before it can be shown anyway.
850 showscores(int level
)
852 struct highscore
*sp
;
855 int levelfound
[NLEVELS
];
859 (void)printf("\n\t\t\t Tetris High Scores\n");
862 * If level == 0, the person has not played a game but just asked for
863 * the high scores; we do not need to check for printing in highlight
864 * mode. If SOstr is null, we can't do highlighting anyway.
866 me
= level
&& enter_standout_mode
? thisuser() : NULL
;
869 * Set times to 0 except for high score on each level.
871 for (i
= MINLEVEL
; i
< NLEVELS
; i
++)
873 for (i
= 0, sp
= scores
; i
< nscores
; i
++, sp
++) {
874 if (sp
->hs_level
< NLEVELS
&& sp
->hs_level
>= 0) {
875 if (levelfound
[sp
->hs_level
])
879 levelfound
[sp
->hs_level
] = 1;
885 * Page each screenful of scores.
887 for (i
= 0, sp
= scores
; i
< nscores
; sp
+= n
) {
891 printem(level
, i
+ 1, sp
, n
, me
);
892 if ((i
+= n
) < nscores
) {
893 (void)printf("\nHit RETURN to continue.");
894 (void)fflush(stdout
);
895 while ((c
= getchar()) != '\n')
904 printem(int level
, int offset
, struct highscore
*hs
, int n
, const char *me
)
906 struct highscore
*sp
;
907 int nrows
, row
, col
, item
, i
, highlight
;
909 #define TITLE "Rank Score Name (points/level)"
912 * This makes a nice two-column sort with headers, but it's a bit
915 printf("%s %s\n", TITLE
, n
> 1 ? TITLE
: "");
920 for (row
= 0; row
< nrows
; row
++) {
921 for (col
= 0; col
< 2; col
++) {
922 item
= col
* nrows
+ row
;
925 * Can only occur on trailing columns.
931 (void)snprintf(buf
, sizeof(buf
),
932 "%3d%c %6d %-11s (%6d on %d)",
933 item
+ offset
, sp
->hs_time
? '*' : ' ',
934 sp
->hs_score
* sp
->hs_level
,
935 sp
->hs_name
, sp
->hs_score
, sp
->hs_level
);
937 * Highlight if appropriate. This works because
938 * we only get one score per level.
941 sp
->hs_level
== level
&&
942 sp
->hs_score
== score
&&
943 strcmp(sp
->hs_name
, me
) == 0) {
944 putpad(enter_standout_mode
);
947 (void)printf("%s", buf
);
949 putpad(exit_standout_mode
);
953 /* fill in spaces so column 1 lines up */
955 for (i
= 40 - strlen(buf
); --i
>= 0;)