1 /*
2  * scoreboard.c
3  * Copyright (C) 2009-2020 Joachim de Groot <jdegroot@web.de>
4  *
5  * NLarn is free software: you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the
7  * Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * NLarn is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13  * See the GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include <zlib.h>
23 
24 #if (defined __unix) || (defined __unix__) || (defined __APPLE__)
25 # include <sys/file.h>
26 #endif
27 
28 #include "extdefs.h"
29 #include "scoreboard.h"
30 #include "cJSON.h"
31 
32 #if ((defined (__unix) || defined (__unix__)) && defined (SETGID))
33 /* file descriptor for the scoreboard file when running setgid */
34 int scoreboard_fd = -1;
35 #endif
36 
37 /* scoreboard version */
38 static const gint sb_ver = 1;
39 
scores_load()40 GList *scores_load()
41 {
42     /* linked list of all scores */
43     GList *gs = NULL;
44 
45     /* read the scoreboard file into memory */
46 #if ((defined (__unix) || defined (__unix__)) && defined (SETGID))
47     /* we'll need the file desciptor for saving, too, so duplicate it */
48     int fd = dup(scoreboard_fd);
49 
50     /*
51      * Lock the scoreboard file while updating the scoreboard.
52      * Wait until another process that holds the lock releases it again.
53      */
54     if (flock(fd, LOCK_EX) == -1)
55     {
56         perror("Could not lock the scoreboard file");
57     }
58 
59     gzFile file = gzdopen(fd, "rb");
60 #else
61     gzFile file = gzopen(nlarn_highscores, "rb");
62 #endif
63 
64     if (file == NULL)
65     {
66         return gs;
67     }
68 
69     /* size of buffer to store uncompressed scoreboard content */
70     const gint bufsize = 8192;
71 
72     /* allocate buffer space */
73     gchar *scores = g_malloc(bufsize);
74 
75     /* count of buffer allocations */
76     gint bufcount = 1;
77 
78     /* read the scoreboard file
79      * append subsequent blocks at the end of the previously read block */
80     while(gzread(file, scores + ((bufcount - 1) * bufsize), bufsize) == bufsize)
81     {
82         /* it seems the buffer space was insufficient -> increase it */
83         bufcount += 1;
84         scores = g_realloc(scores, (bufsize * bufcount));
85     }
86 
87 #if ((defined (__unix) || defined (__unix__)) && defined (SETGID))
88     /* reposition to the start otherwise writing would append */
89     gzrewind(file);
90 #endif
91     /* close save file */
92     gzclose(file);
93 
94     /* parsed scoreboard; scoreboard entry */
95     cJSON *pscores, *s_entry;
96 
97     /* parse the scores */
98     if ((pscores = cJSON_Parse(scores)) == NULL)
99     {
100         /* empty file, no entries */
101         return gs;
102     }
103 
104     /* version of scoreboard file */
105     gint version = cJSON_GetObjectItem(pscores, "version")->valueint;
106 
107     if (version < sb_ver)
108     {
109         /* TODO: when there are multiple versions, handle old versions here */
110     }
111 
112     /* point to the first entry of the scores array */
113     s_entry = cJSON_GetObjectItem(pscores, "scores")->child;
114 
115     while (s_entry != NULL)
116     {
117         /* create new score record */
118         score_t *nscore = g_malloc(sizeof(score_t));
119 
120         /* add record to array */
121         gs = g_list_append(gs, nscore);
122 
123         /* fill score record fields with data */
124         nscore->player_name = g_strdup(cJSON_GetObjectItem(s_entry, "player_name")->valuestring);
125         nscore->sex        = cJSON_GetObjectItem(s_entry, "sex")->valueint;
126         nscore->score      = cJSON_GetObjectItem(s_entry, "score")->valueint;
127         nscore->moves      = cJSON_GetObjectItem(s_entry, "moves")->valueint;
128         nscore->cod        = cJSON_GetObjectItem(s_entry, "cod")->valueint;
129         nscore->cause      = cJSON_GetObjectItem(s_entry, "cause")->valueint;
130         nscore->hp         = cJSON_GetObjectItem(s_entry, "hp")->valueint;
131         nscore->hp_max     = cJSON_GetObjectItem(s_entry, "hp_max")->valueint;
132         nscore->level      = cJSON_GetObjectItem(s_entry, "level")->valueint;
133         nscore->level_max  = cJSON_GetObjectItem(s_entry, "level_max")->valueint;
134         nscore->dlevel     = cJSON_GetObjectItem(s_entry, "dlevel")->valueint;
135         nscore->dlevel_max = cJSON_GetObjectItem(s_entry, "dlevel_max")->valueint;
136         nscore->difficulty = cJSON_GetObjectItem(s_entry, "difficulty")->valueint;
137         nscore->time_start = cJSON_GetObjectItem(s_entry, "time_start")->valueint;
138         nscore->time_end   = cJSON_GetObjectItem(s_entry, "time_end")->valueint;
139 
140         s_entry = s_entry->next;
141     }
142 
143     /* free memory  */
144     cJSON_Delete(pscores);
145 
146     /* free the memory allocated for gzread */
147     g_free(scores);
148 
149     return gs;
150 }
151 
scores_save(game * g,GList * gs)152 static void scores_save(game *g, GList *gs)
153 {
154     /* serialize the scores */
155     cJSON *sf = cJSON_CreateObject();
156     cJSON_AddNumberToObject(sf, "version", sb_ver);
157     cJSON *scores = cJSON_CreateArray();
158     cJSON_AddItemToObject(sf, "scores", scores);
159 
160     for (GList *iterator = gs; iterator; iterator = iterator->next)
161     {
162         score_t *score = iterator->data;
163 
164         /* create new object to store a single scoreboard entry */
165         cJSON *sc = cJSON_CreateObject();
166         cJSON_AddItemToArray(scores, sc);
167 
168         /* add all scoreboard entry values */
169         cJSON_AddStringToObject(sc, "player_name", score->player_name);
170         cJSON_AddNumberToObject(sc, "sex", score->sex);
171         cJSON_AddNumberToObject(sc, "score", score->score);
172         cJSON_AddNumberToObject(sc, "moves", score->moves);
173         cJSON_AddNumberToObject(sc, "cod", score->cod);
174         cJSON_AddNumberToObject(sc, "cause", score->cause);
175         cJSON_AddNumberToObject(sc, "hp", score->hp);
176         cJSON_AddNumberToObject(sc, "hp_max", score->hp_max);
177         cJSON_AddNumberToObject(sc, "level", score->level);
178         cJSON_AddNumberToObject(sc, "level_max", score->level_max);
179         cJSON_AddNumberToObject(sc, "dlevel", score->dlevel);
180         cJSON_AddNumberToObject(sc, "dlevel_max", score->dlevel_max);
181         cJSON_AddNumberToObject(sc, "difficulty", score->difficulty);
182         cJSON_AddNumberToObject(sc, "time_start", score->time_start);
183         cJSON_AddNumberToObject(sc, "time_end", score->time_end);
184     }
185 
186     /* export the cJSON structure to a string */
187     char *uscores = cJSON_PrintUnformatted(sf);
188     cJSON_Delete(sf);
189 
190     /* open the file for writing */
191 #if ((defined (__unix) || defined (__unix__)) && defined (SETGID))
192     gzFile sb = gzdopen(scoreboard_fd, "wb");
193 #else
194     gzFile sb = gzopen(nlarn_highscores, "wb");
195 #endif
196 
197     if (sb == NULL)
198     {
199         /* opening the file failed */
200         log_add_entry(g->log, "Error opening scoreboard file.");
201         free(uscores);
202         return;
203     }
204 
205     /* write to file */
206     if (gzputs(sb, uscores) != (int)strlen(uscores))
207     {
208         /* handle error */
209         int err;
210 
211         log_add_entry(g->log, "Error writing scoreboard file: %s",
212                       gzerror(sb, &err));
213 
214         free(uscores);
215         return;
216     }
217 
218     /*
219      * Close file.
220      * As this was the last reference to that file, this action
221      * unlocks the scoreboard file again.
222      */
223     gzclose(sb);
224 
225     /* return memory */
226     g_free(uscores);
227 }
228 
score_compare(const void * scr_a,const void * scr_b)229 static int score_compare(const void *scr_a, const void *scr_b)
230 {
231     score_t *a = (score_t *)scr_a;
232     score_t *b = (score_t *)scr_b;
233 
234     if (a->score > b->score)
235         return -1;
236 
237     if (b->score > a->score)
238         return 1;
239 
240     return 0;
241 }
242 
score_new(game * g,player_cod cod,int cause)243 score_t *score_new(game *g, player_cod cod, int cause)
244 {
245     score_t *score = g_malloc0(sizeof(score_t));
246 
247     score->player_name = g_strdup(g->p->name);
248     score->sex = g->p->sex;
249     score->score = player_calc_score(g->p, (cod == PD_WON) ? TRUE : FALSE);
250     score->moves = game_turn(g);
251     score->cod = cod;
252     score->cause = cause;
253     score->hp = g->p->hp;
254     score->hp_max = g->p->hp_max;
255     score->level = g->p->level;
256     score->level_max = g->p->stats.max_level;
257     score->dlevel = Z(g->p->pos);
258     score->dlevel_max = g->p->stats.deepest_level;
259     score->difficulty = game_difficulty(g);
260     score->time_start = g->time_start;
261     score->time_end = time(0);
262 
263     return score;
264 }
265 
score_add(game * g,score_t * score)266 GList *score_add(game *g, score_t *score)
267 {
268     g_assert (g != NULL && score != NULL);
269 
270     GList *gs = scores_load();
271 
272     /* add new score */
273     gs = g_list_append(gs, score);
274 
275     /* sort scoreboard entries */
276     gs = g_list_sort(gs, (GCompareFunc)score_compare);
277 
278     /* save new scoreboard */
279     scores_save(g, gs);
280 
281     return gs;
282 }
283 
score_death_description(score_t * score,int verbose)284 char *score_death_description(score_t *score, int verbose)
285 {
286     const char *desc;
287     GString *text;
288 
289     g_assert(score != NULL);
290 
291     switch (score->cod)
292     {
293     case PD_LASTLEVEL:
294         desc = "passed away";
295         break;
296 
297     case PD_STUCK:
298         desc = "got stuck in solid rock";
299         break;
300 
301     case PD_TOO_LATE:
302         desc = "returned with the potion too late";
303         break;
304 
305     case PD_WON:
306         desc = "returned in time with the cure";
307         break;
308 
309     case PD_LOST:
310         desc = "could not find the potion in time";
311         break;
312 
313     case PD_QUIT:
314         desc = "quit the game";
315         break;
316 
317     case PD_GENOCIDE:
318         desc = "genocided";
319         break;
320 
321     case PD_SPELL:
322         if (score->cause < SP_MAX)
323             desc = "blasted";
324         else
325             desc = "got killed";
326         break;
327 
328     default:
329         desc = "killed";
330     }
331 
332     text = g_string_new_len(NULL, 200);
333 
334     g_string_append_printf(text, "%s (%c), %s", score->player_name,
335                            (score->sex == PS_MALE) ? 'm' : 'f', desc);
336 
337 
338     if (score->cod == PD_GENOCIDE)
339     {
340         g_string_append_printf(text, " %sself",
341                                (score->sex == PS_MALE) ? "him" : "her");
342     }
343 
344     if (verbose)
345     {
346         g_string_append_printf(text, " on level %s", map_names[score->dlevel]);
347 
348         if (score->dlevel_max > score->dlevel)
349         {
350             g_string_append_printf(text, " (max. %s)", map_names[score->dlevel_max]);
351         }
352 
353         if (score->cod < PD_TOO_LATE)
354         {
355             g_string_append_printf(text, " with %d and a maximum of %d hp",
356                                    score->hp, score->hp_max);
357         }
358     }
359 
360     switch (score->cod)
361     {
362     case PD_EFFECT:
363         switch (score->cause)
364         {
365         case ET_DEC_STR:
366             g_string_append(text, " by enfeeblement.");
367             break;
368 
369         case ET_DEC_DEX:
370             g_string_append(text, " by clumsiness.");
371             break;
372 
373         case ET_POISON:
374             g_string_append(text, " by poison.");
375             break;
376         }
377         break;
378 
379     case PD_LASTLEVEL:
380         g_string_append_printf(text,". %s left %s body.",
381                                (score->sex == PS_MALE) ? "He" : "She",
382                                (score->sex == PS_MALE) ? "his" : "her");
383         break;
384 
385     case PD_MONSTER:
386         /* TODO: regard monster's invisibility */
387         /* TODO: while sleeping / doing sth. */
388         g_string_append_printf(text, " by %s %s.",
389                                a_an(monster_type_name(score->cause)),
390                                monster_type_name(score->cause));
391         break;
392 
393     case PD_SPHERE:
394         g_string_append(text, " by a sphere of destruction.");
395         break;
396 
397     case PD_TRAP:
398         g_string_append_printf(text, " by %s%s %s.",
399                                score->cause == TT_TRAPDOOR ? "falling through " : "",
400                                a_an(trap_description(score->cause)),
401                                trap_description(score->cause));
402         break;
403 
404     case PD_MAP:
405         g_string_append_printf(text, " by %s.", mt_get_desc(score->cause));
406         break;
407 
408     case PD_SPELL:
409         /* player spell */
410         g_string_append_printf(text, " %s away with the spell \"%s\".",
411                                (score->sex == PS_MALE) ? "himself" : "herself",
412                                spell_name_by_id(score->cause));
413         break;
414 
415     case PD_CURSE:
416         g_string_append_printf(text, " by a cursed %s.",
417                                item_name_sg(score->cause));
418         break;
419 
420     case PD_SOBJECT:
421         switch (score->cause)
422         {
423         case LS_FOUNTAIN:
424             g_string_append(text, " by toxic water from a fountain.");
425             break;
426         default:
427             g_string_append(text, " by falling down a staircase.");
428             break;
429         }
430         break;
431 
432     default:
433         /* no further description */
434         g_string_append_c(text, '.');
435         break;
436     }
437 
438     if (verbose)
439     {
440         g_string_append_printf(text, " %s has scored %" G_GINT64_FORMAT
441                                " points, with the difficulty set to %d.",
442                                (score->sex == PS_MALE) ? "He" : "She",
443                                score->score, score->difficulty);
444     }
445 
446     return g_string_free(text, FALSE);
447 }
448 
scores_to_string(GList * scores,score_t * score)449 char *scores_to_string(GList *scores, score_t *score)
450 {
451     /* no scoreboard entries? */
452     if (!scores) return NULL;
453 
454     GString *text = g_string_new(NULL);
455 
456     guint rank = 0;
457     GList *iterator = scores;
458 
459     /* show scores surrounding a specific score? */
460     if (score)
461     {
462         /* determine position of score in the score list */
463         rank = g_list_index(scores, score);
464 
465         /* get entry three entries up of current/top score in list */
466        iterator = g_list_nth(scores, max(rank - 3, 0));
467     }
468 
469     /* display up to 7 surronding entries or all when score wasn't specified */
470     for (int nrec = max(rank - 3, 0);
471          iterator && (score ? (nrec < (max(rank, 0) + 4)) : TRUE);
472          iterator = iterator->next, nrec++)
473     {
474         gchar *desc;
475 
476         score_t *cscore = (score_t *)iterator->data;
477 
478         desc = score_death_description(cscore, FALSE);
479         g_string_append_printf(text, "  %c%2d) %7" G_GINT64_FORMAT " %s\n",
480                                (cscore == score) ? '*' : ' ',
481                                nrec + 1, cscore->score, desc);
482 
483         g_string_append_printf(text, "               [exp. level %d, caverns lvl. %s, %d/%d hp, difficulty %d]\n",
484                                cscore->level, map_names[cscore->dlevel],
485                                cscore->hp, cscore->hp_max, cscore->difficulty);
486         g_free(desc);
487     }
488 
489     return g_string_free(text, FALSE);
490 }
491 
scores_destroy(GList * gs)492 void scores_destroy(GList *gs)
493 {
494     for (GList *iterator = gs; iterator; iterator = iterator->next)
495     {
496         score_t *score = iterator->data;
497         g_free(score->player_name);
498         g_free(score);
499     }
500 
501     g_list_free(gs);
502 }
503