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