1 /* highscore.c - maintain the highscore list
2 *
3 * Copyright 1999, 2000, 2001, 2006 Jochen Voss */
4
5 static const char rcsid[] = "$Id: highscore.c 6825 2006-03-19 19:18:39Z voss $";
6
7 #ifdef HAVE_CONFIG_H
8 #include <config.h>
9 #endif
10
11 #ifdef _XOPEN_SOURCE
12 #define _XOPEN_SOURCE_EXTENDED 1
13 #endif
14
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <fcntl.h>
19 #include <unistd.h>
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <pwd.h>
23 #include <assert.h>
24 #if HAVE_ERRNO_H
25 #include <errno.h>
26 #else
27 extern int errno;
28 #endif
29
30 #if defined(__hp9000s800)
31 #include <stdarg.h>
32 #endif
33
34 #include "moon-buggy.h"
35
36
37 struct mode *highscore_mode;
38
39
40 #define SCORE_FILE "mbscore"
41 #define MAX_NAME_CHARS 40
42 #define HIGHSCORE_SLOTS 100
43
44 #define qx(str) #str
45 #define quote(str) qx(str)
46
47
48 /* The highscore list, as read by `read_data'. */
49 struct score_entry {
50 int score, level;
51 time_t date;
52 char name [MAX_NAME_CHARS];
53 int new;
54 };
55 static struct score_entry highscore [HIGHSCORE_SLOTS];
56 static int highscore_changed;
57
58
59 static char *
compose_filename(const char * dir,const char * name)60 compose_filename (const char *dir, const char *name)
61 /* Concatenate the path DIR and the name NAME in a newly allocated string. */
62 {
63 char *res;
64
65 if (dir) {
66 res = xmalloc (strlen(dir) + 1 + strlen(name) + 1);
67 strcpy (res, dir);
68 strcat (res, "/");
69 strcat (res, name);
70 } else {
71 res = xstrdup (name);
72 }
73 return res;
74 }
75
76 static char *
local_score_file_name(void)77 local_score_file_name (void)
78 /* Return the local score file's name.
79 * The caller is reponsible for freeing the returned string via `free'. */
80 {
81 uid_t me = getuid ();
82 struct passwd *my_passwd = getpwuid (me);
83
84 if (my_passwd && my_passwd->pw_dir) {
85 return compose_filename (my_passwd->pw_dir, "." SCORE_FILE);
86 } else {
87 return compose_filename (NULL, "." SCORE_FILE);
88 }
89 }
90
91 static char *
global_score_file_name(void)92 global_score_file_name (void)
93 /* Return the global score file's name.
94 * The is reponsible for freeing the returned string via `free'. */
95 {
96 return compose_filename (score_dir, SCORE_FILE);
97 }
98
99
100 static int
compare_entries(const void * a,const void * b)101 compare_entries (const void *a, const void *b)
102 /* Compare two score values.
103 * This is a comparison function for the use with `qsort'. */
104 {
105 const struct score_entry *aa = a;
106 const struct score_entry *bb = b;
107 if (aa->score > bb->score) return -1;
108 if (aa->score < bb->score) return +1;
109 return 0;
110 }
111
112 static void
merge_entry(const struct score_entry * entry)113 merge_entry (const struct score_entry *entry)
114 /* Merge ENTRY into the highscore list `highscore'.
115 * Set `highscore_changed' to 1 if the list is changed. */
116 {
117 struct score_entry *last;
118
119 last = highscore+(HIGHSCORE_SLOTS-1);
120
121 if (entry->score > last->score) {
122 memcpy (last, entry, sizeof (struct score_entry));
123 qsort (highscore, HIGHSCORE_SLOTS, sizeof (struct score_entry),
124 compare_entries);
125 highscore_changed = 1;
126 }
127 }
128
129 static void
randomize_entry(int n)130 randomize_entry (int n)
131 /* Fill slot N of `highscore' with a random entry. */
132 {
133 static const char *names [13] = {
134 "Dwalin", "Balin", "Kili", "Fili", "Dori", "Nori", "Ori",
135 "Oin", "Gloin", "Bifur", "Bofur", "Bombur", "Thorin"
136 };
137
138 highscore[n].score = 10*(HIGHSCORE_SLOTS-n);
139 highscore[n].level = 1;
140 highscore[n].date = time (NULL);
141 strcpy (highscore[n].name, names[uniform_rnd(13)]);
142 highscore[n].new = 0;
143
144 highscore_changed = 1;
145 }
146
147 static time_t
expire_date(int rank,time_t date)148 expire_date (int rank, time_t date)
149 /* Calculate the expiration date for an entry, which is at position RANK
150 * and which was entered at DATE. */
151 {
152 /* linear interpolation: rank 3 expires after 180 days, the last one
153 * after 14 days. */
154 double day = 24*60*60;
155 double rate = (180-14)*day/(HIGHSCORE_SLOTS-4);
156
157 if (rank < 3) return time(NULL)+2000*day;
158 return date + 14*day + (HIGHSCORE_SLOTS-1-rank)*rate;
159 }
160
161 static void
refill_old_entries(void)162 refill_old_entries (void)
163 /* Replace old entries of `highscore' with new random ones. */
164 {
165 time_t now;
166 int i;
167
168 now = time (NULL);
169 for (i=3; i<HIGHSCORE_SLOTS; ++i) {
170 if (expire_date (i, highscore[i].date) < now) randomize_entry (i);
171 }
172 qsort (highscore, HIGHSCORE_SLOTS, sizeof (struct score_entry),
173 compare_entries);
174 }
175
176
177 static void
generate_data(void)178 generate_data (void)
179 /* Initialise `highscore' with random entries. */
180 {
181 int i;
182
183 for (i=0; i<HIGHSCORE_SLOTS; ++i) randomize_entry (i);
184 }
185
186
187 static int
read_version2_data(FILE * score_file)188 read_version2_data (FILE *score_file)
189 {
190 int err = 0;
191 int i, res;
192
193 generate_data ();
194 for (i=0; i<10 && ! err; ++i) {
195 int score, level;
196 int day, month, year;
197 char name [MAX_NAME_CHARS+1];
198 res = fscanf (score_file,
199 "|%d|%d|%d|%d|%d|%" quote(MAX_NAME_CHARS) "[^|]|\n",
200 &score, &level, &day, &month, &year, name);
201 if (res != 6) {
202 print_message ("Score file corrupted");
203 err = 1;
204 }
205 highscore[i].score = score;
206 highscore[i].level = level;
207 highscore[i].date = convert_old_date (day, month, year);
208 highscore[i].name[0] = '\0';
209 highscore[i].new = 0;
210 strncat (highscore[i].name, name, MAX_NAME_CHARS);
211 }
212 qsort (highscore, HIGHSCORE_SLOTS, sizeof (struct score_entry),
213 compare_entries);
214 refill_old_entries ();
215
216 return err;
217 }
218
219 static int
read_version3_data(FILE * score_file)220 read_version3_data (FILE *score_file)
221 /* Read the highscore list from SCORE_FILE into `highscore'.
222 * Return 1 on success and 0 on error. */
223 {
224 int err = 0;
225 int i, res;
226
227 for (i=0; i<HIGHSCORE_SLOTS && ! err; ++i) {
228 int score, level;
229 char date [MAX_DATE_CHARS];
230 char name [MAX_NAME_CHARS+1];
231 res = fscanf (score_file,
232 "|%d|%d|%" quote(MAX_DATE_CHARS) "[^|]"
233 "|%" quote(MAX_NAME_CHARS) "[^|]|\n",
234 &score, &level, date, name);
235 if (res != 4) {
236 print_message ("Score file corrupted");
237 err = 1;
238 }
239 highscore[i].score = score;
240 highscore[i].level = level;
241 highscore[i].date = parse_date (date);
242 highscore[i].name[0] = '\0';
243 highscore[i].new = 0;
244 strncat (highscore[i].name, name, MAX_NAME_CHARS);
245 }
246 if (ferror (score_file)) {
247 print_message ("Error while reading score file");
248 err = 1;
249 }
250
251 return err;
252 }
253
254 static int
read_data(FILE * score_file)255 read_data (FILE *score_file)
256 /* Read the highscore list from SCORE_FILE into `highscore'.
257 * Return 1 on success and 0 on error. */
258 {
259 int version;
260 int err = 0;
261 int res;
262
263 res = fscanf (score_file, "moon-buggy hiscore file (version %d)\n",
264 &version);
265 if (res != 1) {
266 print_message ("Score file corrupted");
267 return 0;
268 }
269 highscore_changed = 0;
270
271 switch (version) {
272 case 2:
273 err = read_version2_data (score_file);
274 break;
275 case 3:
276 err = read_version3_data (score_file);
277 break;
278 default:
279 {
280 char buffer [80];
281 sprintf (buffer, "Invalid score file version %d", version);
282 print_message (buffer);
283 }
284 err = 1;
285 break;
286 }
287 if (ferror (score_file)) {
288 print_message ("Error while reading score file");
289 err = 1;
290 }
291
292 return ! err;
293 }
294
295 static void
write_data(FILE * score_file)296 write_data (FILE *score_file)
297 /* Write out the current highscore list to stream SCORE_FILE. */
298 {
299 int i;
300 int res;
301
302 res = fprintf (score_file, "moon-buggy hiscore file (version 3)\n");
303 if (res < 0) fatal ("Score file write error (%s)", strerror (errno));
304
305 for (i=0; i<HIGHSCORE_SLOTS; ++i) {
306 char date [MAX_DATE_CHARS];
307
308 format_date (date, highscore[i].date);
309 res = fprintf (score_file,
310 "|%d|%d|%s|%." quote(MAX_NAME_CHARS) "s|\n",
311 highscore[i].score, highscore[i].level, date,
312 highscore[i].name);
313 if (res < 0) fatal ("Score file write error (%s)", strerror (errno));
314 }
315
316 #if HAVE_FTRUNCATE
317 {
318 int fd = fileno (score_file);
319 long int pos = ftell (score_file);
320 if (pos != -1) {
321 #if HAVE_FCLEAN
322 fclean (score_file);
323 #else
324 fflush (score_file);
325 #endif
326 ftruncate (fd, pos);
327 }
328 }
329 #endif
330 highscore_changed = 0;
331 }
332
333 static int
do_open(const char * name,int flags,int lock,int must_succeed)334 do_open (const char *name, int flags, int lock, int must_succeed)
335 /* Try to open the file NAME using open flags FLAGS.
336 * If LOCK ist 1, try to get a shared lock for the file.
337 * If LOCK ist 2, try to get a exclusive lock for the file.
338 * If MUST_SUCCEED is true, then abort on failure.
339 * Otherwise return -1 if the file cannot be accessed. */
340 {
341 int fd;
342 int lock_done;
343 mode_t mode, mask;
344
345 lock_done = 1;
346 if (lock == 1) {
347 #ifdef O_SHLOCK
348 flags |= O_SHLOCK;
349 #else
350 lock_done = 0;
351 #endif
352 }
353 if (lock == 2) {
354 #ifdef O_EXLOCK
355 flags |= O_EXLOCK;
356 #else
357 lock_done = 0;
358 #endif
359 }
360
361 mode = S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH;
362 if ( is_setgid () ) mode |= S_IWGRP;
363 mask = umask (0);
364 do {
365 fd = open (name, flags, mode);
366 } while (fd == -1 && errno == EINTR);
367 if (fd == -1 && (must_succeed || (errno != EACCES && errno != ENOENT))) {
368 fatal ("Cannot open score file \"%s\": %s", name, strerror (errno));
369 }
370 umask (mask);
371
372 if (fd != -1 && ! lock_done) {
373 struct flock l;
374 int res;
375
376 l.l_type = (lock == 1) ? F_RDLCK : F_WRLCK;
377 l.l_whence = SEEK_SET;
378 l.l_start = 0;
379 l.l_len = 0;
380
381 do {
382 res = fcntl (fd, F_SETLKW, &l);
383 } while (res == -1 && errno == EINTR);
384 if (res == -1) {
385 fatal ("Cannot lock score file \"%s\": %s", name, strerror (errno));
386 }
387 }
388
389 return fd;
390 }
391
392 static void
update_score_file(const struct score_entry * entry)393 update_score_file (const struct score_entry *entry)
394 /* Modify the highscore file, to contain the data from *ENTRY.
395 * This updates the array `highscores', too.
396 * If ENTRY is NULL, only `highscores' is updated. */
397 {
398 char *local_name, *global_name;
399 int in_fd, out_fd;
400 enum persona in_pers, out_pers;
401 FILE *f;
402 int res, method;
403
404 global_name = global_score_file_name ();
405 local_name = local_score_file_name ();
406
407 for (method=1; method<=4; ++method) {
408 in_fd = out_fd = -1;
409
410 switch (method) {
411 case 1:
412 /* method 1: try read/write access to global score file
413 * on success: in_fd == global, out_fd == global */
414 in_pers = out_pers = pers_GAME;
415 set_persona (pers_GAME);
416 in_fd = out_fd = do_open (global_name, O_RDWR, 2, 0);
417 break;
418 case 2:
419 /* method 2: try write access to global score file
420 * on success: in_fd == -1, out_fd == global */
421 out_pers = pers_GAME;
422 set_persona (pers_GAME);
423 out_fd = do_open (global_name, O_WRONLY|O_CREAT, 2, 0);
424 break;
425 case 3:
426 /* method 3: try read/write access to local score file
427 * on success: in_fd == local, out_fd == local */
428 in_pers = out_pers = pers_USER;
429 set_persona (pers_USER);
430 in_fd = out_fd = do_open (local_name, O_RDWR, 2, 0);
431 break;
432 case 4:
433 /* method 4: try write access to local score file and
434 * read access to global score file
435 * on success: in_fd == global or -1, out_fd == local */
436 out_pers = pers_USER;
437 set_persona (pers_USER);
438 out_fd = do_open (local_name, O_WRONLY|O_CREAT, 2, 1);
439 in_pers = pers_GAME;
440 set_persona (pers_GAME);
441 in_fd = do_open (global_name, O_RDONLY, 1, 0);
442 break;
443 }
444
445 if (in_fd != -1) {
446 int read_success;
447
448 set_persona (in_pers);
449 f = fdopen (in_fd, in_fd==out_fd ? "r+" : "r");
450 read_success = read_data (f);
451 if (read_success) {
452 if (in_fd != out_fd) {
453 res = fclose (f);
454 if (res == EOF) fatal ("Score file read error (%s)", strerror (errno));
455 } else {
456 res = fseek (f, 0, SEEK_SET);
457 if (res != 0) fatal ("Score file seek error (%s)", strerror (errno));
458 }
459 } else {
460 fclose (f);
461 if (out_fd == in_fd)
462 out_fd = -1;
463 in_fd = -1;
464 }
465 }
466
467 if (out_fd != -1) break;
468 }
469 if (in_fd == -1) generate_data ();
470
471 refill_old_entries ();
472 if (entry) merge_entry (entry);
473 refill_old_entries ();
474
475 assert (out_fd != -1);
476 if (in_fd != out_fd || highscore_changed) {
477 set_persona (out_pers);
478 if (in_fd != out_fd) f = fdopen (out_fd, "w");
479 write_data (f);
480 }
481 res = fclose (f);
482 if (res == EOF) fatal ("Score file write error (%s)", strerror (errno));
483
484 set_persona (pers_USER);
485 free (local_name);
486 free (global_name);
487 }
488
489 void
create_highscores(void)490 create_highscores (void)
491 /* Make sure, that a highscore file exists.
492 * This must be called on installation, to make setgid-usage work. */
493 {
494 block_all ();
495 update_score_file (NULL);
496 unblock ();
497 }
498
499 void
show_highscores(void)500 show_highscores (void)
501 /* Print the highscore list to stdout.
502 * Do not use curses. */
503 {
504 time_t now;
505 int i;
506
507 block_all ();
508 update_score_file (NULL);
509 unblock ();
510
511 now = time (NULL);
512 puts ("rank score lvl date expires name");
513 for (i=0; i<HIGHSCORE_SLOTS; ++i) {
514 char date [16];
515 char expire [16];
516 double dt;
517
518 format_display_date (date, highscore[i].date);
519 dt = difftime (expire_date (i, highscore[i].date), now);
520 format_relative_time (expire, dt);
521 printf ("%3d %8u %-3d %s %s %." quote(MAX_NAME_CHARS) "s\n",
522 i+1, highscore[i].score, highscore[i].level, date, expire,
523 highscore[i].name);
524 }
525 }
526
527 static int highscore_valid, last_score, last_level;
528 static int gap; /* number of omitted entries after pos 3 */
529 static int max_line;
530
531 static void
fix_gap(void)532 fix_gap (void)
533 /* Cast `gap' to valid value. */
534 {
535 int limit = HIGHSCORE_SLOTS-(max_line-3);
536 if (gap > limit) gap = limit;
537 if (gap < 2) gap = 0;
538 }
539
540 static void
center_new(void)541 center_new (void)
542 /* Prepare `gap' in order to view values around `last_score'. */
543 {
544 int limit;
545 int i, pos;
546
547 max_line = LINES-11;
548 if (max_line > 25) max_line = 25;
549 limit = HIGHSCORE_SLOTS-(max_line-3);
550
551 for (i=1; i<HIGHSCORE_SLOTS; ++i) {
552 if (highscore[i].score < last_score) break;
553 }
554 --i;
555
556 pos = 7+(max_line-6)/2;
557 gap = 1 + (i+4) - pos;
558 fix_gap ();
559 }
560
561 static void
print_scores(void)562 print_scores (void)
563 /* Print the highscore table to the screen. */
564 {
565 time_t now;
566 int i, line, my_rank;
567
568 now = time (NULL);
569
570 mvwaddstr (moon, 1, 5, "rank score lvl date expires name");
571 line = 3;
572 my_rank = -1;
573 for (i=0; i<HIGHSCORE_SLOTS; ++i) {
574 char date [16];
575 char expire [16];
576 double dt;
577
578 if (highscore[i].new) {
579 if (highscore[i].score == last_score) my_rank = i+1;
580 }
581
582 if ((i==3 && gap>0)
583 || (i<HIGHSCORE_SLOTS-1 && line==max_line)) {
584 mvwprintw (moon, line++, 5, " ...");
585 wclrtoeol (moon);
586 }
587 if ((i>=3 && i<3+gap) || line>max_line) continue;
588
589 format_display_date (date, highscore[i].date);
590 dt = difftime (expire_date (i, highscore[i].date), now);
591 format_relative_time (expire, dt);
592 if (highscore[i].new) wstandout (moon);
593 mvwprintw (moon, line++, 5,
594 "%3d %8u %-3d %s %s %." quote(MAX_NAME_CHARS) "s\n",
595 i+1, highscore[i].score, highscore[i].level, date, expire,
596 highscore[i].name);
597 if (highscore[i].new) wstandend (moon);
598 }
599 ++line;
600 if (last_score > 0)
601 mvwprintw (moon, line++, 17, "your score: %d", last_score);
602 if (my_rank > 0) mvwprintw (moon, line++, 17, "your rank: %d", my_rank);
603 wrefresh (moon);
604 }
605
606 void
score_set(int score,int level)607 score_set (int score, int level)
608 {
609 last_score = score;
610 last_level = level;
611 }
612
613 static void
enter_name_h(game_time t,void * client_data)614 enter_name_h (game_time t, void *client_data)
615 {
616 struct score_entry entry;
617 int res;
618
619 entry.score = last_score;
620 entry.level = last_level;
621 entry.date = time (NULL);
622 retry:
623 entry.name[0] = '\0';
624 res = get_real_user_name (entry.name, MAX_NAME_CHARS);
625 if (res == ERR || ! entry.name[0]) {
626 mode_redraw ();
627 goto retry;
628 }
629 entry.new = 1;
630
631 print_message ("writing score file ...");
632 doupdate ();
633 block_all ();
634 update_score_file (&entry);
635 unblock ();
636 mode_redraw ();
637 }
638
639 static void
highscore_enter(int seed)640 highscore_enter (int seed)
641 {
642 print_ground ();
643 print_buggy ();
644
645 print_message ("loading score file ...");
646 doupdate ();
647 block_all ();
648 update_score_file (NULL);
649 highscore_valid = 1;
650 unblock ();
651 center_new ();
652
653 if (last_score > highscore[HIGHSCORE_SLOTS-1].score) {
654 add_event (0.0, enter_name_h, NULL);
655 }
656 }
657
658 static void
highscore_leave(void)659 highscore_leave (void)
660 {
661 print_game_over (0);
662 last_score = last_level = 0;
663 }
664
665 void
highscore_redraw(void)666 highscore_redraw (void)
667 {
668 resize_meteors ();
669 resize_ground (0);
670
671 max_line = LINES-11;
672 if (max_line > 25) max_line = 25;
673 fix_gap ();
674
675 print_ground ();
676 adjust_score (0);
677 print_lives ();
678 print_buggy ();
679 if (last_level > 0) print_game_over (1);
680 if (highscore_valid) print_scores ();
681 }
682
683 static void
key_handler(game_time t,int val)684 key_handler (game_time t, int val)
685 {
686 switch (val) {
687 case 1:
688 mode_change (game_mode, 0);
689 break;
690 case 2:
691 quit_main_loop ();
692 break;
693 case 3:
694 if (highscore_valid) {
695 if (gap > 2) --gap; else gap = 0;
696 print_scores ();
697 }
698 break;
699 case 4:
700 if (highscore_valid) {
701 if (gap < 2) gap = 2; else ++gap;
702 fix_gap ();
703 print_scores ();
704 }
705 break;
706 case 5:
707 if (highscore_valid) {
708 gap -= max_line-7;
709 fix_gap ();
710 print_scores ();
711 }
712 break;
713 case 6:
714 if (highscore_valid) {
715 gap += max_line-7;
716 fix_gap ();
717 print_scores ();
718 }
719 break;
720 case 7:
721 print_message ("reloading score file ...");
722 doupdate ();
723 block_all ();
724 update_score_file (NULL);
725 highscore_valid = 1;
726 unblock ();
727 center_new ();
728 mode_redraw ();
729 break;
730 }
731 }
732
733 void
setup_highscore_mode(void)734 setup_highscore_mode (void)
735 {
736 highscore_mode = new_mode ();
737 highscore_mode->enter = highscore_enter;
738 highscore_mode->leave = highscore_leave;
739 highscore_mode->redraw = highscore_redraw;
740 highscore_mode->keypress = key_handler;
741 mode_add_key (highscore_mode, mbk_start, "new game", 1);
742 mode_add_key (highscore_mode, mbk_end, "quit", 2);
743 mode_add_key (highscore_mode, mbk_up, "up", 3);
744 mode_add_key (highscore_mode, mbk_down, "down", 4);
745 mode_add_key (highscore_mode, mbk_pageup, "pg up", 5);
746 mode_add_key (highscore_mode, mbk_pagedown, "pg down", 6);
747 mode_add_key (highscore_mode, mbk_scores, "reload", 7);
748 mode_complete (highscore_mode);
749 }
750