]> git.cameronkatri.com Git - bsdgames-darwin.git/blob - tetris/scores.c
Security improvements for games (largely from or inspired by OpenBSD).
[bsdgames-darwin.git] / tetris / scores.c
1 /* $NetBSD: scores.c,v 1.6 1999/09/12 09:02:23 jsm 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. All advertising materials mentioning features or use of this software
19 * must display the following acknowledgement:
20 * This product includes software developed by the University of
21 * California, Berkeley and its contributors.
22 * 4. Neither the name of the University nor the names of its contributors
23 * may be used to endorse or promote products derived from this software
24 * without specific prior written permission.
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
27 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
30 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
31 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
32 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
33 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
34 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
35 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
36 * SUCH DAMAGE.
37 *
38 * @(#)scores.c 8.1 (Berkeley) 5/31/93
39 */
40
41 /*
42 * Score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu)
43 * modified 22 January 1992, to limit the number of entries any one
44 * person has.
45 *
46 * Major whacks since then.
47 */
48 #include <errno.h>
49 #include <fcntl.h>
50 #include <pwd.h>
51 #include <stdio.h>
52 #include <stdlib.h>
53 #include <string.h>
54 #include <sys/stat.h>
55 #include <time.h>
56 #include <termcap.h>
57 #include <unistd.h>
58
59 #include "pathnames.h"
60 #include "screen.h"
61 #include "scores.h"
62 #include "tetris.h"
63
64 /*
65 * Within this code, we can hang onto one extra "high score", leaving
66 * room for our current score (whether or not it is high).
67 *
68 * We also sometimes keep tabs on the "highest" score on each level.
69 * As long as the scores are kept sorted, this is simply the first one at
70 * that level.
71 */
72 #define NUMSPOTS (MAXHISCORES + 1)
73 #define NLEVELS (MAXLEVEL + 1)
74
75 static time_t now;
76 static int nscores;
77 static int gotscores;
78 static struct highscore scores[NUMSPOTS];
79
80 static int checkscores __P((struct highscore *, int));
81 static int cmpscores __P((const void *, const void *));
82 static void getscores __P((FILE **));
83 static void printem __P((int, int, struct highscore *, int, const char *));
84 static char *thisuser __P((void));
85
86 /*
87 * Read the score file. Can be called from savescore (before showscores)
88 * or showscores (if savescore will not be called). If the given pointer
89 * is not NULL, sets *fpp to an open file pointer that corresponds to a
90 * read/write score file that is locked with LOCK_EX. Otherwise, the
91 * file is locked with LOCK_SH for the read and closed before return.
92 *
93 * Note, we assume closing the stdio file releases the lock.
94 */
95 static void
96 getscores(fpp)
97 FILE **fpp;
98 {
99 int sd, mint, lck;
100 mode_t mask;
101 const char *mstr, *human;
102 FILE *sf;
103
104 if (fpp != NULL) {
105 mint = O_RDWR | O_CREAT;
106 mstr = "r+";
107 human = "read/write";
108 lck = LOCK_EX;
109 } else {
110 mint = O_RDONLY;
111 mstr = "r";
112 human = "reading";
113 lck = LOCK_SH;
114 }
115 setegid(egid);
116 mask = umask(S_IWOTH);
117 sd = open(_PATH_SCOREFILE, mint, 0666);
118 (void)umask(mask);
119 if (sd < 0) {
120 if (fpp == NULL) {
121 nscores = 0;
122 setegid(gid);
123 return;
124 }
125 (void)fprintf(stderr, "tetris: cannot open %s for %s: %s\n",
126 _PATH_SCOREFILE, human, strerror(errno));
127 exit(1);
128 }
129 if ((sf = fdopen(sd, mstr)) == NULL) {
130 (void)fprintf(stderr, "tetris: cannot fdopen %s for %s: %s\n",
131 _PATH_SCOREFILE, human, strerror(errno));
132 exit(1);
133 }
134 setegid(gid);
135
136 /*
137 * Grab a lock.
138 */
139 if (flock(sd, lck))
140 (void)fprintf(stderr,
141 "tetris: warning: score file %s cannot be locked: %s\n",
142 _PATH_SCOREFILE, strerror(errno));
143
144 nscores = fread(scores, sizeof(scores[0]), MAXHISCORES, sf);
145 if (ferror(sf)) {
146 (void)fprintf(stderr, "tetris: error reading %s: %s\n",
147 _PATH_SCOREFILE, strerror(errno));
148 exit(1);
149 }
150
151 if (fpp)
152 *fpp = sf;
153 else
154 (void)fclose(sf);
155 }
156
157 void
158 savescore(level)
159 int level;
160 {
161 register struct highscore *sp;
162 register int i;
163 int change;
164 FILE *sf;
165 const char *me;
166
167 getscores(&sf);
168 gotscores = 1;
169 (void)time(&now);
170
171 /*
172 * Allow at most one score per person per level -- see if we
173 * can replace an existing score, or (easiest) do nothing.
174 * Otherwise add new score at end (there is always room).
175 */
176 change = 0;
177 me = thisuser();
178 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) {
179 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0)
180 continue;
181 if (score > sp->hs_score) {
182 (void)printf("%s bettered %s %d score of %d!\n",
183 "\nYou", "your old level", level,
184 sp->hs_score * sp->hs_level);
185 sp->hs_score = score; /* new score */
186 sp->hs_time = now; /* and time */
187 change = 1;
188 } else if (score == sp->hs_score) {
189 (void)printf("%s tied %s %d high score.\n",
190 "\nYou", "your old level", level);
191 sp->hs_time = now; /* renew it */
192 change = 1; /* gotta rewrite, sigh */
193 } /* else new score < old score: do nothing */
194 break;
195 }
196 if (i >= nscores) {
197 strcpy(sp->hs_name, me);
198 sp->hs_level = level;
199 sp->hs_score = score;
200 sp->hs_time = now;
201 nscores++;
202 change = 1;
203 }
204
205 if (change) {
206 /*
207 * Sort & clean the scores, then rewrite.
208 */
209 nscores = checkscores(scores, nscores);
210 rewind(sf);
211 if (fwrite(scores, sizeof(*sp), nscores, sf) != nscores ||
212 fflush(sf) == EOF)
213 (void)fprintf(stderr,
214 "tetris: error writing %s: %s -- %s\n",
215 _PATH_SCOREFILE, strerror(errno),
216 "high scores may be damaged");
217 }
218 (void)fclose(sf); /* releases lock */
219 }
220
221 /*
222 * Get login name, or if that fails, get something suitable.
223 * The result is always trimmed to fit in a score.
224 */
225 static char *
226 thisuser()
227 {
228 register const char *p;
229 register struct passwd *pw;
230 register size_t l;
231 static char u[sizeof(scores[0].hs_name)];
232
233 if (u[0])
234 return (u);
235 p = getlogin();
236 if (p == NULL || *p == '\0') {
237 pw = getpwuid(getuid());
238 if (pw != NULL)
239 p = pw->pw_name;
240 else
241 p = " ???";
242 }
243 l = strlen(p);
244 if (l >= sizeof(u))
245 l = sizeof(u) - 1;
246 memcpy(u, p, l);
247 u[l] = '\0';
248 return (u);
249 }
250
251 /*
252 * Score comparison function for qsort.
253 *
254 * If two scores are equal, the person who had the score first is
255 * listed first in the highscore file.
256 */
257 static int
258 cmpscores(x, y)
259 const void *x, *y;
260 {
261 register const struct highscore *a, *b;
262 register long l;
263
264 a = x;
265 b = y;
266 l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score;
267 if (l < 0)
268 return (-1);
269 if (l > 0)
270 return (1);
271 if (a->hs_time < b->hs_time)
272 return (-1);
273 if (a->hs_time > b->hs_time)
274 return (1);
275 return (0);
276 }
277
278 /*
279 * If we've added a score to the file, we need to check the file and ensure
280 * that this player has only a few entries. The number of entries is
281 * controlled by MAXSCORES, and is to ensure that the highscore file is not
282 * monopolised by just a few people. People who no longer have accounts are
283 * only allowed the highest score. Scores older than EXPIRATION seconds are
284 * removed, unless they are someone's personal best.
285 * Caveat: the highest score on each level is always kept.
286 */
287 static int
288 checkscores(hs, num)
289 register struct highscore *hs;
290 int num;
291 {
292 register struct highscore *sp;
293 register int i, j, k, numnames;
294 int levelfound[NLEVELS];
295 struct peruser {
296 char *name;
297 int times;
298 } count[NUMSPOTS];
299 register struct peruser *pu;
300
301 /*
302 * Sort so that highest totals come first.
303 *
304 * levelfound[i] becomes set when the first high score for that
305 * level is encountered. By definition this is the highest score.
306 */
307 qsort((void *)hs, nscores, sizeof(*hs), cmpscores);
308 for (i = MINLEVEL; i < NLEVELS; i++)
309 levelfound[i] = 0;
310 numnames = 0;
311 for (i = 0, sp = hs; i < num;) {
312 /*
313 * This is O(n^2), but do you think we care?
314 */
315 for (j = 0, pu = count; j < numnames; j++, pu++)
316 if (strcmp(sp->hs_name, pu->name) == 0)
317 break;
318 if (j == numnames) {
319 /*
320 * Add new user, set per-user count to 1.
321 */
322 pu->name = sp->hs_name;
323 pu->times = 1;
324 numnames++;
325 } else {
326 /*
327 * Two ways to keep this score:
328 * - Not too many (per user), still has acct, &
329 * score not dated; or
330 * - High score on this level.
331 */
332 if ((pu->times < MAXSCORES &&
333 getpwnam(sp->hs_name) != NULL &&
334 sp->hs_time + EXPIRATION >= now) ||
335 levelfound[sp->hs_level] == 0)
336 pu->times++;
337 else {
338 /*
339 * Delete this score, do not count it,
340 * do not pass go, do not collect $200.
341 */
342 num--;
343 for (k = i; k < num; k++)
344 hs[k] = hs[k + 1];
345 continue;
346 }
347 }
348 levelfound[sp->hs_level] = 1;
349 i++, sp++;
350 }
351 return (num > MAXHISCORES ? MAXHISCORES : num);
352 }
353
354 /*
355 * Show current scores. This must be called after savescore, if
356 * savescore is called at all, for two reasons:
357 * - Showscores munches the time field.
358 * - Even if that were not the case, a new score must be recorded
359 * before it can be shown anyway.
360 */
361 void
362 showscores(level)
363 int level;
364 {
365 register struct highscore *sp;
366 register int i, n, c;
367 const char *me;
368 int levelfound[NLEVELS];
369
370 if (!gotscores)
371 getscores((FILE **)NULL);
372 (void)printf("\n\t\t\t Tetris High Scores\n");
373
374 /*
375 * If level == 0, the person has not played a game but just asked for
376 * the high scores; we do not need to check for printing in highlight
377 * mode. If SOstr is null, we can't do highlighting anyway.
378 */
379 me = level && SOstr ? thisuser() : NULL;
380
381 /*
382 * Set times to 0 except for high score on each level.
383 */
384 for (i = MINLEVEL; i < NLEVELS; i++)
385 levelfound[i] = 0;
386 for (i = 0, sp = scores; i < nscores; i++, sp++) {
387 if (levelfound[sp->hs_level])
388 sp->hs_time = 0;
389 else {
390 sp->hs_time = 1;
391 levelfound[sp->hs_level] = 1;
392 }
393 }
394
395 /*
396 * Page each screenful of scores.
397 */
398 for (i = 0, sp = scores; i < nscores; sp += n) {
399 n = 40;
400 if (i + n > nscores)
401 n = nscores - i;
402 printem(level, i + 1, sp, n, me);
403 if ((i += n) < nscores) {
404 (void)printf("\nHit RETURN to continue.");
405 (void)fflush(stdout);
406 while ((c = getchar()) != '\n')
407 if (c == EOF)
408 break;
409 (void)printf("\n");
410 }
411 }
412 }
413
414 static void
415 printem(level, offset, hs, n, me)
416 int level, offset;
417 register struct highscore *hs;
418 register int n;
419 const char *me;
420 {
421 register struct highscore *sp;
422 int nrows, row, col, item, i, highlight;
423 char buf[100];
424 #define TITLE "Rank Score Name (points/level)"
425
426 /*
427 * This makes a nice two-column sort with headers, but it's a bit
428 * convoluted...
429 */
430 printf("%s %s\n", TITLE, n > 1 ? TITLE : "");
431
432 highlight = 0;
433 nrows = (n + 1) / 2;
434
435 for (row = 0; row < nrows; row++) {
436 for (col = 0; col < 2; col++) {
437 item = col * nrows + row;
438 if (item >= n) {
439 /*
440 * Can only occur on trailing columns.
441 */
442 (void)putchar('\n');
443 continue;
444 }
445 (void)printf(item + offset < 10 ? " " : " ");
446 sp = &hs[item];
447 (void)sprintf(buf,
448 "%d%c %6d %-11s (%d on %d)",
449 item + offset, sp->hs_time ? '*' : ' ',
450 sp->hs_score * sp->hs_level,
451 sp->hs_name, sp->hs_score, sp->hs_level);
452 /*
453 * Highlight if appropriate. This works because
454 * we only get one score per level.
455 */
456 if (me != NULL &&
457 sp->hs_level == level &&
458 sp->hs_score == score &&
459 strcmp(sp->hs_name, me) == 0) {
460 putpad(SOstr);
461 highlight = 1;
462 }
463 (void)printf("%s", buf);
464 if (highlight) {
465 putpad(SEstr);
466 highlight = 0;
467 }
468
469 /* fill in spaces so column 1 lines up */
470 if (col == 0)
471 for (i = 40 - strlen(buf); --i >= 0;)
472 (void)putchar(' ');
473 else /* col == 1 */
474 (void)putchar('\n');
475 }
476 }
477 }