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