1 /* $NetBSD: scores.c,v 1.27 2025/12/16 12:37:09 nia Exp $ */ 2 3 /*- 4 * Copyright (c) 1992, 1993 5 * The Regents of the University of California. All rights reserved. 6 * 7 * This code is derived from software contributed to Berkeley by 8 * Chris Torek and Darren F. Provine. 9 * 10 * Redistribution and use in source and binary forms, with or without 11 * modification, are permitted provided that the following conditions 12 * are met: 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. 21 * 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 32 * SUCH DAMAGE. 33 * 34 * @(#)scores.c 8.1 (Berkeley) 5/31/93 35 */ 36 37 /* 38 * Score code for Tetris, by Darren Provine (kilroy (at) gboro.glassboro.edu) 39 * modified 22 January 1992, to limit the number of entries any one 40 * person has. 41 * 42 * Major whacks since then. 43 */ 44 #include <err.h> 45 #include <errno.h> 46 #include <endian.h> 47 #include <fcntl.h> 48 #include <pwd.h> 49 #include <stdio.h> 50 #include <stdlib.h> 51 #include <string.h> 52 #include <sys/stat.h> 53 #include <time.h> 54 #include <term.h> 55 #include <unistd.h> 56 57 #include "pathnames.h" 58 #include "screen.h" 59 #include "scores.h" 60 #include "tetris.h" 61 62 /* 63 * Allow updating the high scores unless we're built as part of /rescue. 64 */ 65 #ifndef RESCUEDIR 66 #define ALLOW_SCORE_UPDATES 67 #endif 68 69 /* 70 * Within this code, we can hang onto one extra "high score", leaving 71 * room for our current score (whether or not it is high). 72 * 73 * We also sometimes keep tabs on the "highest" score on each level. 74 * As long as the scores are kept sorted, this is simply the first one at 75 * that level. 76 */ 77 #define NUMSPOTS (MAXHISCORES + 1) 78 #define NLEVELS (MAXLEVEL + 1) 79 80 static time_t now; 81 static int nscores; 82 static int gotscores; 83 static struct highscore scores[NUMSPOTS]; 84 85 static int checkscores(struct highscore *, int); 86 static int cmpscores(const void *, const void *); 87 static void getscores(int *); 88 static void printem(int, int, struct highscore *, int, const char *); 89 static char *thisuser(void); 90 91 /* contents chosen to be a highly illegal username */ 92 static const char hsh_magic_val[HSH_MAGIC_SIZE] = "//:\0\0://"; 93 94 #define HSH_ENDIAN_NATIVE 0x12345678 95 #define HSH_ENDIAN_OPP 0x78563412 96 97 /* current file format version */ 98 #define HSH_VERSION 1 99 100 /* codes for scorefile_probe return */ 101 #define SCOREFILE_ERROR (-1) 102 #define SCOREFILE_CURRENT 0 /* 40-byte */ 103 #define SCOREFILE_CURRENT_OPP 1 /* 40-byte, opposite-endian */ 104 #define SCOREFILE_599 2 /* 36-byte */ 105 #define SCOREFILE_599_OPP 3 /* 36-byte, opposite-endian */ 106 #define SCOREFILE_50 4 /* 32-byte */ 107 #define SCOREFILE_50_OPP 5 /* 32-byte, opposite-endian */ 108 109 /* 110 * Check (or guess) what kind of score file contents we have. 111 */ 112 static int 113 scorefile_probe(int sd) 114 { 115 struct stat st; 116 int t1, t2, t3, tx; 117 ssize_t result; 118 uint32_t numbers[3], offset56, offset60, offset64; 119 120 if (fstat(sd, &st) < 0) { 121 warn("Score file %s: fstat", _PATH_SCOREFILE); 122 return -1; 123 } 124 125 t1 = st.st_size % sizeof(struct highscore_ondisk) == 0; 126 t2 = st.st_size % sizeof(struct highscore_ondisk_599) == 0; 127 t3 = st.st_size % sizeof(struct highscore_ondisk_50) == 0; 128 tx = t1 + t2 + t3; 129 if (tx == 1) { 130 /* Size matches exact number of one kind of records */ 131 if (t1) { 132 return SCOREFILE_CURRENT; 133 } else if (t2) { 134 return SCOREFILE_599; 135 } else { 136 return SCOREFILE_50; 137 } 138 } else if (tx == 0) { 139 /* Size matches nothing, pick most likely as default */ 140 goto wildguess; 141 } 142 143 /* 144 * File size is multiple of more than one structure size. 145 * (For example, 288 bytes could be 9*hso50 or 8*hso599.) 146 * Read the file and see if we can figure out what's going 147 * on. This is the layout of the first two records: 148 * 149 * offset hso / current hso_599 hso_50 150 * (40-byte) (36-byte) (32-byte) 151 * 152 * 0 name #0 name #0 name #0 153 * 4 : : : 154 * 8 : : : 155 * 12 : : : 156 * 16 : : : 157 * 20 score #0 score #0 score #0 158 * 24 level #0 level #0 level #0 159 * 28 (pad) time #0 time #0 160 * 32 time #0 name #1 161 * 36 name #1 : 162 * 40 name #1 : : 163 * 44 : : : 164 * 48 : : : 165 * 52 : : score #1 166 * 56 : score #1 level #1 167 * 60 score #1 level #1 time #1 168 * 64 level #1 time #1 name #2 169 * 68 (pad) : : 170 * 72 time #1 name #2 : 171 * 76 : : : 172 * 80 --- end --- 173 * 174 * There are a number of things we could check here, but the 175 * most effective test is based on the following restrictions: 176 * 177 * - The level must be between 1 and 9 (inclusive) 178 * - All times must be after 1985 and are before 2038, 179 * so the high word must be 0 and the low word may not be 180 * a small value. 181 * - Integer values of 0 or 1-9 cannot be the beginning of 182 * a login name string. 183 * - Values of 1-9 are probably not a score. 184 * 185 * So we read the three words at offsets 56, 60, and 64, and 186 * poke at the values to try to figure things... 187 */ 188 189 if (lseek(sd, 56, SEEK_SET) < 0) { 190 warn("Score file %s: lseek", _PATH_SCOREFILE); 191 return -1; 192 } 193 result = read(sd, &numbers, sizeof(numbers)); 194 if (result < 0) { 195 warn("Score file %s: read", _PATH_SCOREFILE); 196 return -1; 197 } 198 if ((size_t)result != sizeof(numbers)) { 199 /* 200 * The smallest file whose size divides by more than 201 * one of the sizes is substantially larger than 64, 202 * so this should *never* happen. 203 */ 204 warnx("Score file %s: Unexpected EOF", _PATH_SCOREFILE); 205 return -1; 206 } 207 208 offset56 = numbers[0]; 209 offset60 = numbers[1]; 210 offset64 = numbers[2]; 211 212 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) { 213 /* 40-byte structure */ 214 return SCOREFILE_CURRENT; 215 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) { 216 /* 36-byte structure */ 217 return SCOREFILE_599; 218 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) { 219 /* 32-byte structure */ 220 return SCOREFILE_50; 221 } 222 223 /* None was a valid level; try opposite endian */ 224 offset64 = bswap32(offset64); 225 offset60 = bswap32(offset60); 226 offset56 = bswap32(offset56); 227 228 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) { 229 /* 40-byte structure */ 230 return SCOREFILE_CURRENT_OPP; 231 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) { 232 /* 36-byte structure */ 233 return SCOREFILE_599_OPP; 234 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) { 235 /* 32-byte structure */ 236 return SCOREFILE_50_OPP; 237 } 238 239 /* That didn't work either, dunno what's going on */ 240 wildguess: 241 warnx("Score file %s is likely corrupt", _PATH_SCOREFILE); 242 if (sizeof(void *) == 8 && sizeof(time_t) == 8) { 243 return SCOREFILE_CURRENT; 244 } else if (sizeof(time_t) == 8) { 245 return SCOREFILE_599; 246 } else { 247 return SCOREFILE_50; 248 } 249 } 250 251 /* 252 * Copy a string safely, making sure it's null-terminated. 253 */ 254 static void 255 readname(char *to, size_t maxto, const char *from, size_t maxfrom) 256 { 257 size_t amt; 258 259 amt = maxto < maxfrom ? maxto : maxfrom; 260 memcpy(to, from, amt); 261 to[maxto-1] = '\0'; 262 } 263 264 /* 265 * Copy integers, byte-swapping if desired. 266 */ 267 static int32_t 268 read32(int32_t val, int doflip) 269 { 270 if (doflip) { 271 val = bswap32(val); 272 } 273 return val; 274 } 275 276 static int64_t 277 read64(int64_t val, int doflip) 278 { 279 if (doflip) { 280 val = bswap64(val); 281 } 282 return val; 283 } 284 285 /* 286 * Read up to MAXHISCORES scorefile_ondisk entries. 287 */ 288 static int 289 readscores(int sd, int doflip) 290 { 291 struct highscore_ondisk buf[MAXHISCORES]; 292 ssize_t result; 293 int i; 294 295 result = read(sd, buf, sizeof(buf)); 296 if (result < 0) { 297 warn("Score file %s: read", _PATH_SCOREFILE); 298 return -1; 299 } 300 nscores = result / sizeof(buf[0]); 301 302 for (i=0; i<nscores; i++) { 303 readname(scores[i].hs_name, sizeof(scores[i].hs_name), 304 buf[i].hso_name, sizeof(buf[i].hso_name)); 305 scores[i].hs_score = read32(buf[i].hso_score, doflip); 306 scores[i].hs_level = read32(buf[i].hso_level, doflip); 307 scores[i].hs_time = read64(buf[i].hso_time, doflip); 308 } 309 return 0; 310 } 311 312 /* 313 * Read up to MAXHISCORES scorefile_ondisk_599 entries. 314 */ 315 static int 316 readscores599(int sd, int doflip) 317 { 318 struct highscore_ondisk_599 buf[MAXHISCORES]; 319 ssize_t result; 320 int i; 321 322 result = read(sd, buf, sizeof(buf)); 323 if (result < 0) { 324 warn("Score file %s: read", _PATH_SCOREFILE); 325 return -1; 326 } 327 nscores = result / sizeof(buf[0]); 328 329 for (i=0; i<nscores; i++) { 330 readname(scores[i].hs_name, sizeof(scores[i].hs_name), 331 buf[i].hso599_name, sizeof(buf[i].hso599_name)); 332 scores[i].hs_score = read32(buf[i].hso599_score, doflip); 333 scores[i].hs_level = read32(buf[i].hso599_level, doflip); 334 /* 335 * Don't bother pasting the time together into a 336 * 64-bit value; just take whichever half is nonzero. 337 */ 338 scores[i].hs_time = 339 read32(buf[i].hso599_time[buf[i].hso599_time[0] == 0], 340 doflip); 341 } 342 return 0; 343 } 344 345 /* 346 * Read up to MAXHISCORES scorefile_ondisk_50 entries. 347 */ 348 static int 349 readscores50(int sd, int doflip) 350 { 351 struct highscore_ondisk_50 buf[MAXHISCORES]; 352 ssize_t result; 353 int i; 354 355 result = read(sd, buf, sizeof(buf)); 356 if (result < 0) { 357 warn("Score file %s: read", _PATH_SCOREFILE); 358 return -1; 359 } 360 nscores = result / sizeof(buf[0]); 361 362 for (i=0; i<nscores; i++) { 363 readname(scores[i].hs_name, sizeof(scores[i].hs_name), 364 buf[i].hso50_name, sizeof(buf[i].hso50_name)); 365 scores[i].hs_score = read32(buf[i].hso50_score, doflip); 366 scores[i].hs_level = read32(buf[i].hso50_level, doflip); 367 scores[i].hs_time = read32(buf[i].hso50_time, doflip); 368 } 369 return 0; 370 } 371 372 /* 373 * Read the score file. Can be called from savescore (before showscores) 374 * or showscores (if savescore will not be called). If the given pointer 375 * is not NULL, sets *fdp to an open file handle that corresponds to a 376 * read/write score file that is locked with LOCK_EX. Otherwise, the 377 * file is locked with LOCK_SH for the read and closed before return. 378 */ 379 static void 380 getscores(int *fdp) 381 { 382 struct highscore_header header; 383 int sd, mint, lck; 384 mode_t mask; 385 const char *human; 386 int doflip; 387 int serrno; 388 ssize_t result; 389 390 #ifdef ALLOW_SCORE_UPDATES 391 if (fdp != NULL) { 392 mint = O_RDWR | O_CREAT; 393 human = "read/write"; 394 lck = LOCK_EX; 395 } else 396 #endif 397 { 398 mint = O_RDONLY; 399 human = "reading"; 400 lck = LOCK_SH; 401 } 402 setegid(egid); 403 mask = umask(S_IWOTH); 404 sd = open(_PATH_SCOREFILE, mint, 0666); 405 serrno = errno; 406 (void)umask(mask); 407 setegid(gid); 408 if (sd < 0) { 409 /* 410 * If the file simply isn't there because nobody's 411 * played yet, and we aren't going to be trying to 412 * update it, don't warn. Even if we are going to be 413 * trying to write it, don't fail -- we can still show 414 * the player the score they got. 415 */ 416 errno = serrno; 417 if (fdp != NULL || errno != ENOENT) { 418 warn("Cannot open %s for %s", _PATH_SCOREFILE, human); 419 } 420 goto fail; 421 } 422 423 /* 424 * Grab a lock. 425 * XXX: failure here should probably be more fatal than this. 426 */ 427 if (flock(sd, lck)) 428 warn("warning: score file %s cannot be locked", 429 _PATH_SCOREFILE); 430 431 /* 432 * The current format (since -current of 20090525) is 433 * 434 * struct highscore_header 435 * up to MAXHIGHSCORES x struct highscore_ondisk 436 * 437 * Before this, there is no header, and the contents 438 * might be any of three formats: 439 * 440 * highscore_ondisk (64-bit machines with 64-bit time_t) 441 * highscore_ondisk_599 (32-bit machines with 64-bit time_t) 442 * highscore_ondisk_50 (32-bit machines with 32-bit time_t) 443 * 444 * The first two appear in 5.99 between the time_t change and 445 * 20090525, depending on whether the compiler inserts 446 * structure padding before an unaligned 64-bit time_t. The 447 * last appears in 5.0 and earlier. 448 * 449 * Any or all of these might also appear on other OSes where 450 * this code has been ported. 451 * 452 * Since the old file has no header, we will have to guess 453 * which of these formats it has. 454 */ 455 456 /* 457 * First, look for a header. 458 */ 459 result = read(sd, &header, sizeof(header)); 460 if (result < 0) { 461 warn("Score file %s: read", _PATH_SCOREFILE); 462 goto sdfail; 463 } 464 if (result != 0 && (size_t)result != sizeof(header)) { 465 warnx("Score file %s: read: unexpected EOF", _PATH_SCOREFILE); 466 /* 467 * File is hopelessly corrupt, might as well truncate it 468 * and start over with empty scores. 469 */ 470 if (lseek(sd, 0, SEEK_SET) < 0) { 471 /* ? */ 472 warn("Score file %s: lseek", _PATH_SCOREFILE); 473 goto sdfail; 474 } 475 if (ftruncate(sd, 0) == 0) { 476 result = 0; 477 } else { 478 goto sdfail; 479 } 480 } 481 482 if (result == 0) { 483 /* Empty file; that just means there are no scores. */ 484 nscores = 0; 485 } else { 486 /* 487 * Is what we read a header, or the first 16 bytes of 488 * a score entry? hsh_magic_val is chosen to be 489 * something that is extremely unlikely to appear in 490 * hs_name[]. 491 */ 492 if (!memcmp(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE)) { 493 /* Yes, we have a header. */ 494 495 if (header.hsh_endiantag == HSH_ENDIAN_NATIVE) { 496 /* native endian */ 497 doflip = 0; 498 } else if (header.hsh_endiantag == HSH_ENDIAN_OPP) { 499 doflip = 1; 500 } else { 501 warnx("Score file %s: Unknown endian tag %u", 502 _PATH_SCOREFILE, header.hsh_endiantag); 503 goto sdfail; 504 } 505 506 if (header.hsh_version != HSH_VERSION) { 507 warnx("Score file %s: Unknown version code %u", 508 _PATH_SCOREFILE, header.hsh_version); 509 goto sdfail; 510 } 511 512 if (readscores(sd, doflip) < 0) { 513 goto sdfail; 514 } 515 } else { 516 /* 517 * Ok, it wasn't a header. Try to figure out what 518 * size records we have. 519 */ 520 result = scorefile_probe(sd); 521 if (lseek(sd, 0, SEEK_SET) < 0) { 522 warn("Score file %s: lseek", _PATH_SCOREFILE); 523 goto sdfail; 524 } 525 switch (result) { 526 case SCOREFILE_CURRENT: 527 result = readscores(sd, 0 /* don't flip */); 528 break; 529 case SCOREFILE_CURRENT_OPP: 530 result = readscores(sd, 1 /* do flip */); 531 break; 532 case SCOREFILE_599: 533 result = readscores599(sd, 0 /* don't flip */); 534 break; 535 case SCOREFILE_599_OPP: 536 result = readscores599(sd, 1 /* do flip */); 537 break; 538 case SCOREFILE_50: 539 result = readscores50(sd, 0 /* don't flip */); 540 break; 541 case SCOREFILE_50_OPP: 542 result = readscores50(sd, 1 /* do flip */); 543 break; 544 default: 545 goto sdfail; 546 } 547 if (result < 0) { 548 goto sdfail; 549 } 550 } 551 } 552 553 554 if (fdp) 555 *fdp = sd; 556 else 557 close(sd); 558 559 return; 560 561 sdfail: 562 close(sd); 563 fail: 564 if (fdp != NULL) { 565 *fdp = -1; 566 } 567 nscores = 0; 568 } 569 570 #ifdef ALLOW_SCORE_UPDATES 571 /* 572 * Paranoid write wrapper; unlike fwrite() it preserves errno. 573 */ 574 static int 575 dowrite(int sd, const void *vbuf, size_t len) 576 { 577 const char *buf = vbuf; 578 ssize_t result; 579 size_t done = 0; 580 581 while (done < len) { 582 result = write(sd, buf+done, len-done); 583 if (result < 0) { 584 if (errno == EINTR) { 585 continue; 586 } 587 return -1; 588 } 589 done += result; 590 } 591 return 0; 592 } 593 #endif /* ALLOW_SCORE_UPDATES */ 594 595 /* 596 * Write the score file out. 597 */ 598 static void 599 putscores(int sd) 600 { 601 #ifdef ALLOW_SCORE_UPDATES 602 struct highscore_header header; 603 struct highscore_ondisk buf[MAXHISCORES] = {0}; 604 int i; 605 606 if (sd == -1) { 607 return; 608 } 609 610 memcpy(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE); 611 header.hsh_endiantag = HSH_ENDIAN_NATIVE; 612 header.hsh_version = HSH_VERSION; 613 614 for (i=0; i<nscores; i++) { 615 memcpy(buf[i].hso_name, scores[i].hs_name, 616 sizeof(buf[i].hso_name)); 617 buf[i].hso_score = scores[i].hs_score; 618 buf[i].hso_level = scores[i].hs_level; 619 buf[i].hso_pad = 0xbaadf00d; 620 buf[i].hso_time = scores[i].hs_time; 621 } 622 623 if (lseek(sd, 0, SEEK_SET) < 0) { 624 warn("Score file %s: lseek", _PATH_SCOREFILE); 625 goto fail; 626 } 627 if (dowrite(sd, &header, sizeof(header)) < 0 || 628 dowrite(sd, buf, sizeof(buf[0]) * nscores) < 0) { 629 warn("Score file %s: write", _PATH_SCOREFILE); 630 goto fail; 631 } 632 return; 633 fail: 634 warnx("high scores may be damaged"); 635 #else 636 (void)sd; 637 #endif /* ALLOW_SCORE_UPDATES */ 638 } 639 640 /* 641 * Close the score file. 642 */ 643 static void 644 closescores(int sd) 645 { 646 flock(sd, LOCK_UN); 647 close(sd); 648 } 649 650 /* 651 * Read and update the scores file with the current reults. 652 */ 653 void 654 savescore(int level) 655 { 656 struct highscore *sp; 657 int i; 658 int change; 659 int sd; 660 const char *me; 661 662 getscores(&sd); 663 gotscores = 1; 664 (void)time(&now); 665 666 /* 667 * Allow at most one score per person per level -- see if we 668 * can replace an existing score, or (easiest) do nothing. 669 * Otherwise add new score at end (there is always room). 670 */ 671 change = 0; 672 me = thisuser(); 673 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) { 674 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0) 675 continue; 676 if (score > sp->hs_score) { 677 (void)printf("%s bettered %s %d score of %d!\n", 678 "\nYou", "your old level", level, 679 sp->hs_score * sp->hs_level); 680 sp->hs_score = score; /* new score */ 681 sp->hs_time = now; /* and time */ 682 change = 1; 683 } else if (score == sp->hs_score) { 684 (void)printf("%s tied %s %d high score.\n", 685 "\nYou", "your old level", level); 686 sp->hs_time = now; /* renew it */ 687 change = 1; /* gotta rewrite, sigh */ 688 } /* else new score < old score: do nothing */ 689 break; 690 } 691 if (i >= nscores) { 692 strcpy(sp->hs_name, me); 693 sp->hs_level = level; 694 sp->hs_score = score; 695 sp->hs_time = now; 696 nscores++; 697 change = 1; 698 } 699 700 if (change) { 701 /* 702 * Sort & clean the scores, then rewrite. 703 */ 704 nscores = checkscores(scores, nscores); 705 putscores(sd); 706 } 707 closescores(sd); 708 } 709 710 /* 711 * Get login name, or if that fails, get something suitable. 712 * The result is always trimmed to fit in a score. 713 */ 714 static char * 715 thisuser(void) 716 { 717 const char *p; 718 struct passwd *pw; 719 size_t l; 720 static char u[sizeof(scores[0].hs_name)]; 721 722 if (u[0]) 723 return (u); 724 p = getlogin(); 725 if (p == NULL || *p == '\0') { 726 pw = getpwuid(getuid()); 727 if (pw != NULL) 728 p = pw->pw_name; 729 else 730 p = " ???"; 731 } 732 l = strlen(p); 733 if (l >= sizeof(u)) 734 l = sizeof(u) - 1; 735 memcpy(u, p, l); 736 u[l] = '\0'; 737 return (u); 738 } 739 740 /* 741 * Score comparison function for qsort. 742 * 743 * If two scores are equal, the person who had the score first is 744 * listed first in the highscore file. 745 */ 746 static int 747 cmpscores(const void *x, const void *y) 748 { 749 const struct highscore *a, *b; 750 long l; 751 752 a = x; 753 b = y; 754 l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score; 755 if (l < 0) 756 return (-1); 757 if (l > 0) 758 return (1); 759 if (a->hs_time < b->hs_time) 760 return (-1); 761 if (a->hs_time > b->hs_time) 762 return (1); 763 return (0); 764 } 765 766 /* 767 * If we've added a score to the file, we need to check the file and ensure 768 * that this player has only a few entries. The number of entries is 769 * controlled by MAXSCORES, and is to ensure that the highscore file is not 770 * monopolised by just a few people. People who no longer have accounts are 771 * only allowed the highest score. Scores older than EXPIRATION seconds are 772 * removed, unless they are someone's personal best. 773 * Caveat: the highest score on each level is always kept. 774 */ 775 static int 776 checkscores(struct highscore *hs, int num) 777 { 778 struct highscore *sp; 779 int i, j, k, numnames; 780 int levelfound[NLEVELS]; 781 struct peruser { 782 char *name; 783 int times; 784 } count[NUMSPOTS]; 785 struct peruser *pu; 786 787 /* 788 * Sort so that highest totals come first. 789 * 790 * levelfound[i] becomes set when the first high score for that 791 * level is encountered. By definition this is the highest score. 792 */ 793 qsort((void *)hs, nscores, sizeof(*hs), cmpscores); 794 for (i = MINLEVEL; i < NLEVELS; i++) 795 levelfound[i] = 0; 796 numnames = 0; 797 for (i = 0, sp = hs; i < num;) { 798 /* 799 * This is O(n^2), but do you think we care? 800 */ 801 for (j = 0, pu = count; j < numnames; j++, pu++) 802 if (strcmp(sp->hs_name, pu->name) == 0) 803 break; 804 if (j == numnames) { 805 /* 806 * Add new user, set per-user count to 1. 807 */ 808 pu->name = sp->hs_name; 809 pu->times = 1; 810 numnames++; 811 } else { 812 /* 813 * Two ways to keep this score: 814 * - Not too many (per user), still has acct, & 815 * score not dated; or 816 * - High score on this level. 817 */ 818 if ((pu->times < MAXSCORES && 819 getpwnam(sp->hs_name) != NULL && 820 sp->hs_time + EXPIRATION >= now) || 821 levelfound[sp->hs_level] == 0) 822 pu->times++; 823 else { 824 /* 825 * Delete this score, do not count it, 826 * do not pass go, do not collect $200. 827 */ 828 num--; 829 for (k = i; k < num; k++) 830 hs[k] = hs[k + 1]; 831 continue; 832 } 833 } 834 if (sp->hs_level < NLEVELS && sp->hs_level >= 0) 835 levelfound[sp->hs_level] = 1; 836 i++, sp++; 837 } 838 return (num > MAXHISCORES ? MAXHISCORES : num); 839 } 840 841 /* 842 * Show current scores. This must be called after savescore, if 843 * savescore is called at all, for two reasons: 844 * - Showscores munches the time field. 845 * - Even if that were not the case, a new score must be recorded 846 * before it can be shown anyway. 847 */ 848 void 849 showscores(int level) 850 { 851 struct highscore *sp; 852 int i, n, c; 853 const char *me; 854 int levelfound[NLEVELS]; 855 856 if (!gotscores) 857 getscores(NULL); 858 (void)printf("\n\t\t\t Tetris High Scores\n"); 859 860 /* 861 * If level == 0, the person has not played a game but just asked for 862 * the high scores; we do not need to check for printing in highlight 863 * mode. If SOstr is null, we can't do highlighting anyway. 864 */ 865 me = level && enter_standout_mode ? thisuser() : NULL; 866 867 /* 868 * Set times to 0 except for high score on each level. 869 */ 870 for (i = MINLEVEL; i < NLEVELS; i++) 871 levelfound[i] = 0; 872 for (i = 0, sp = scores; i < nscores; i++, sp++) { 873 if (sp->hs_level < NLEVELS && sp->hs_level >= 0) { 874 if (levelfound[sp->hs_level]) 875 sp->hs_time = 0; 876 else { 877 sp->hs_time = 1; 878 levelfound[sp->hs_level] = 1; 879 } 880 } 881 } 882 883 /* 884 * Page each screenful of scores. 885 */ 886 for (i = 0, sp = scores; i < nscores; sp += n) { 887 n = 40; 888 if (i + n > nscores) 889 n = nscores - i; 890 printem(level, i + 1, sp, n, me); 891 if ((i += n) < nscores) { 892 (void)printf("\nHit RETURN to continue."); 893 (void)fflush(stdout); 894 while ((c = getchar()) != '\n') 895 if (c == EOF) 896 break; 897 (void)printf("\n"); 898 } 899 } 900 } 901 902 static void 903 printem(int level, int offset, struct highscore *hs, int n, const char *me) 904 { 905 struct highscore *sp; 906 int nrows, row, col, item, i, highlight; 907 char buf[100]; 908 #define TITLE "Rank Score Name (points/level)" 909 910 /* 911 * This makes a nice two-column sort with headers, but it's a bit 912 * convoluted... 913 */ 914 printf("%s %s\n", TITLE, n > 1 ? TITLE : ""); 915 916 highlight = 0; 917 nrows = (n + 1) / 2; 918 919 for (row = 0; row < nrows; row++) { 920 for (col = 0; col < 2; col++) { 921 item = col * nrows + row; 922 if (item >= n) { 923 /* 924 * Can only occur on trailing columns. 925 */ 926 (void)putchar('\n'); 927 continue; 928 } 929 sp = &hs[item]; 930 (void)snprintf(buf, sizeof(buf), 931 "%3d%c %6d %-11s (%6d on %d)", 932 item + offset, sp->hs_time ? '*' : ' ', 933 sp->hs_score * sp->hs_level, 934 sp->hs_name, sp->hs_score, sp->hs_level); 935 /* 936 * Highlight if appropriate. This works because 937 * we only get one score per level. 938 */ 939 if (me != NULL && 940 sp->hs_level == level && 941 sp->hs_score == score && 942 strcmp(sp->hs_name, me) == 0) { 943 putpad(enter_standout_mode); 944 highlight = 1; 945 } 946 (void)printf("%s", buf); 947 if (highlight) { 948 putpad(exit_standout_mode); 949 highlight = 0; 950 } 951 952 /* fill in spaces so column 1 lines up */ 953 if (col == 0) 954 for (i = 40 - strlen(buf); --i >= 0;) 955 (void)putchar(' '); 956 else /* col == 1 */ 957 (void)putchar('\n'); 958 } 959 } 960 } 961