1 /*
2 * This software is licensed under the terms of the MIT License.
3 * See COPYING for further information.
4 * ---
5 * Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
6 * Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
7 */
8
9 #include "taisei.h"
10
11 #include <zlib.h>
12
13 #include "progress.h"
14 #include "stage.h"
15 #include "version.h"
16
17 /*
18
19 This module implements a persistent storage of a player's game progress, such as unlocked stages, high-scores etc.
20
21 Basic outline of the progress file structure (little-endian encoding):
22 [uint64 magic] [uint32 checksum] [array of commands]
23
24 Where a command is:
25 [uint8 command] [uint16 size] [array of {size} bytes, contents depend on {command}]
26
27 This way we can easily extend the structure to store whatever we want, without breaking compatibility.
28 This is forwards-compatible as well: any unrecognized command can be skipped when reading, since its size is known.
29
30 The checksum is calculated on the whole array of commands (see progress_checksum() for implementation).
31 If the checksum is incorrect, the whole progress file is deemed invalid and ignored.
32
33 All commands are optional and the array may be empty. In that case, the checksum may be omitted as well.
34
35 Currently implemented commands (see also the ProgfileCommand enum in progress.h):
36
37 - PCMD_UNLOCK_STAGES:
38 Unlocks one or more stages. Only works for stages with fixed difficulty.
39
40 - PCMD_UNLOCK_STAGES_WITH_DIFFICULTY:
41 Unlocks one or more stages, each on a specific difficulty
42
43 - PCMD_HISCORE
44 Sets the "Hi-Score" (highest score ever attained in one game session)
45
46 - PCMD_STAGE_PLAYINFO
47 Sets the times played and times cleared counters for a list of stage/difficulty combinations
48
49 - PCMD_ENDINGS
50 Sets the the number of times an ending was achieved for a list of endings
51
52 - PCMD_GAME_SETTINGS
53 Sets the last picked difficulty, character and shot mode
54
55 - PCMD_GAME_VERSION
56 Sets the game version this file was last written with
57
58 - PCMD_UNLOCK_BGMS
59 Unlocks BGMs in the music room
60
61 */
62
63 /*
64
65 Now in case you wonder why I decided to do it this way instead of stuffing everything in the config file, here are a couple of reasons:
66
67 - The config module, as of the time of writting, is messy enough and probably needs refactoring.
68
69 - I don't want to mix user preferences with things that are not supposed to be directly edited.
70
71 - I don't want someone to accidentally lose progress after deleting the config file because they wanted to restore the default settings.
72
73 - I want to discourage players from editing this file should they find it. This is why it's not textual and has a checksum.
74 Of course that doesn't actually prevent one from cheating it, but if you know how to do that, you might as well grab the game's source code and make it do whatever you please.
75 As long as this can stop a l33th4XxXxX0r1998 armed with notepad.exe or a hex editor, I'm happy.
76
77 */
78
79 GlobalProgress progress;
80
81 static uint8_t progress_magic_bytes[] = {
82 0x00, 0x67, 0x74, 0x66, 0x6f, 0xe3, 0x83, 0x84
83 };
84
progress_checksum(uint8_t * buf,size_t num)85 static uint32_t progress_checksum(uint8_t *buf, size_t num) {
86 return crc32(0xB16B00B5, buf, num);
87 }
88
89 typedef struct UnknownCmd {
90 LIST_INTERFACE(struct UnknownCmd);
91
92 uint8_t cmd;
93 uint16_t size;
94 uint8_t *data;
95 } UnknownCmd;
96
progress_read_verify_cmd_size(SDL_RWops * vfile,uint8_t cmd,uint16_t cmdsize,uint16_t expectsize)97 static bool progress_read_verify_cmd_size(SDL_RWops *vfile, uint8_t cmd, uint16_t cmdsize, uint16_t expectsize) {
98 if(cmdsize == expectsize) {
99 return true;
100 }
101
102 log_warn("Command %x with bad size %u ignored", cmd, cmdsize);
103
104 if(SDL_RWseek(vfile, cmdsize, RW_SEEK_CUR) < 0) {
105 log_sdl_error(LOG_WARN, "SDL_RWseek");
106 }
107
108 return false;
109 }
110
progress_read(SDL_RWops * file)111 static void progress_read(SDL_RWops *file) {
112 int64_t filesize = SDL_RWsize(file);
113
114 if(filesize < 0) {
115 log_sdl_error(LOG_ERROR, "SDL_RWseek");
116 return;
117 }
118
119 if(filesize > PROGRESS_MAXFILESIZE) {
120 log_error("Progress file is huge (%"PRIi64" bytes, %i max)", filesize, PROGRESS_MAXFILESIZE);
121 return;
122 }
123
124 for(int i = 0; i < sizeof(progress_magic_bytes); ++i) {
125 if(SDL_ReadU8(file) != progress_magic_bytes[i]) {
126 log_error("Invalid header");
127 return;
128 }
129 }
130
131 if(filesize - SDL_RWtell(file) < 4) {
132 return;
133 }
134
135 uint32_t checksum_fromfile;
136 // no byteswapping here
137 SDL_RWread(file, &checksum_fromfile, 4, 1);
138
139 size_t bufsize = filesize - sizeof(progress_magic_bytes) - 4;
140 uint8_t *buf = malloc(bufsize);
141
142 if(!SDL_RWread(file, buf, bufsize, 1)) {
143 log_sdl_error(LOG_ERROR, "SDL_RWread");
144 free(buf);
145 return;
146 }
147
148 SDL_RWops *vfile = SDL_RWFromMem(buf, bufsize);
149 uint32_t checksum = progress_checksum(buf, bufsize);
150
151 if(checksum != checksum_fromfile) {
152 log_error("Bad checksum: %x != %x", checksum, checksum_fromfile);
153 SDL_RWclose(vfile);
154 free(buf);
155 return;
156 }
157
158 TaiseiVersion version_info = { 0 };
159
160 while(SDL_RWtell(vfile) < bufsize) {
161 ProgfileCommand cmd = (int8_t)SDL_ReadU8(vfile);
162 uint16_t cur = 0;
163 uint16_t cmdsize = SDL_ReadLE16(vfile);
164
165 switch(cmd) {
166 case PCMD_UNLOCK_STAGES:
167 while(cur < cmdsize) {
168 StageProgress *p = stage_get_progress(SDL_ReadLE16(vfile), D_Any, true);
169 if(p) {
170 p->unlocked = true;
171 }
172 cur += 2;
173 }
174 break;
175
176 case PCMD_UNLOCK_STAGES_WITH_DIFFICULTY:
177 while(cur < cmdsize) {
178 uint16_t stg = SDL_ReadLE16(vfile);
179 uint8_t dflags = SDL_ReadU8(vfile);
180 StageInfo *info = stage_get(stg);
181
182 for(uint diff = D_Easy; diff <= D_Lunatic && info != NULL; ++diff) {
183 if(dflags & (1 << (diff - D_Easy))) {
184 StageProgress *p = stage_get_progress_from_info(info, diff, true);
185 if(p) {
186 p->unlocked = true;
187 }
188 }
189 }
190
191 cur += 3;
192 }
193 break;
194
195 case PCMD_HISCORE:
196 progress.hiscore = SDL_ReadLE32(vfile);
197 break;
198
199 case PCMD_STAGE_PLAYINFO:
200 while(cur < cmdsize) {
201 uint16_t stg = SDL_ReadLE16(vfile); cur += sizeof(uint16_t);
202 Difficulty diff = SDL_ReadU8(vfile); cur += sizeof(uint8_t);
203 StageProgress *p = stage_get_progress(stg, diff, true);
204
205 // assert(p != NULL);
206
207 uint32_t np = SDL_ReadLE32(vfile); cur += sizeof(uint32_t);
208 uint32_t nc = SDL_ReadLE32(vfile); cur += sizeof(uint32_t);
209
210 if(p) {
211 p->num_played = np;
212 p->num_cleared = nc;
213 } else {
214 log_warn("Invalid stage %X ignored", stg);
215 }
216 }
217 break;
218
219 case PCMD_ENDINGS:
220 while(cur < cmdsize) {
221 uint8_t ending = SDL_ReadU8(vfile); cur += sizeof(uint8_t);
222 uint32_t num_achieved = SDL_ReadLE32(vfile); cur += sizeof(uint32_t);
223
224 if(ending < NUM_ENDINGS) {
225 progress.achieved_endings[ending] = num_achieved;
226 } else {
227 log_warn("Invalid ending %u ignored", ending);
228 }
229 }
230 break;
231
232 case PCMD_GAME_SETTINGS:
233 if(progress_read_verify_cmd_size(vfile, cmd, cmdsize, sizeof(uint8_t) * 3)) {
234 progress.game_settings.difficulty = SDL_ReadU8(vfile);
235 progress.game_settings.character = SDL_ReadU8(vfile);
236 progress.game_settings.shotmode = SDL_ReadU8(vfile);
237 }
238 break;
239
240 case PCMD_GAME_VERSION:
241 if(progress_read_verify_cmd_size(vfile, cmd, cmdsize, TAISEI_VERSION_SIZE)) {
242 if(version_info.major > 0) {
243 log_warn("Multiple version information entries in progress file");
244 }
245
246 attr_unused size_t read = taisei_version_read(vfile, &version_info);
247 assert(read == TAISEI_VERSION_SIZE);
248 char *vstr = taisei_version_tostring(&version_info);
249 log_info("Progress file from Taisei v%s", vstr);
250 free(vstr);
251 }
252 break;
253
254 case PCMD_UNLOCK_BGMS:
255 if(progress_read_verify_cmd_size(vfile, cmd, cmdsize, sizeof(uint64_t))) {
256 progress.unlocked_bgms |= SDL_ReadLE64(vfile);
257 }
258 break;
259
260 default:
261 log_warn("Unknown command %x (%u bytes). Will preserve as-is and not interpret", cmd, cmdsize);
262
263 UnknownCmd *c = malloc(sizeof(UnknownCmd));
264 c->cmd = cmd;
265 c->size = cmdsize;
266 c->data = malloc(cmdsize);
267 SDL_RWread(vfile, c->data, c->size, 1);
268 list_append(&progress.unknown, c);
269
270 break;
271 }
272 }
273
274 if(version_info.major == 0) {
275 log_warn("No version information in progress file, it's probably just old (Taisei v1.1, or an early pre-v1.2 development build)");
276 } else {
277 TaiseiVersion current_version;
278 TAISEI_VERSION_GET_CURRENT(¤t_version);
279 int cmp = taisei_version_compare(¤t_version, &version_info, VCMP_TWEAK);
280
281 if(cmp != 0) {
282 char *v_prog = taisei_version_tostring(&version_info);
283 char *v_game = taisei_version_tostring(¤t_version);
284
285 if(cmp > 0) {
286 log_info("Progress file will be automatically upgraded from v%s to v%s upon saving", v_prog, v_game);
287 } else {
288 log_warn("Progress file will be automatically downgraded from v%s to v%s upon saving", v_prog, v_game);
289 }
290
291 free(v_prog);
292 free(v_game);
293 }
294 }
295
296 free(buf);
297 SDL_RWclose(vfile);
298 }
299
300 typedef void (*cmd_preparefunc_t)(size_t*, void**);
301 typedef void (*cmd_writefunc_t)(SDL_RWops*, void**);
302
303 typedef struct cmd_writer_t {
304 cmd_preparefunc_t prepare;
305 cmd_writefunc_t write;
306 void *data;
307 } cmd_writer_t;
308
309 #define CMD_HEADER_SIZE 3
310
311 //
312 // PCMD_UNLOCK_STAGES
313 //
314
progress_prepare_cmd_unlock_stages(size_t * bufsize,void ** arg)315 static void progress_prepare_cmd_unlock_stages(size_t *bufsize, void **arg) {
316 int num_unlocked = 0;
317
318 dynarray_foreach_elem(&stages, StageInfo *stg, {
319 StageProgress *p = stage_get_progress_from_info(stg, D_Any, false);
320 if(p && p->unlocked) {
321 ++num_unlocked;
322 }
323 });
324
325 if(num_unlocked) {
326 *bufsize += CMD_HEADER_SIZE;
327 *bufsize += num_unlocked * 2;
328 }
329
330 *arg = (void*)(intptr_t)num_unlocked;
331 }
332
progress_write_cmd_unlock_stages(SDL_RWops * vfile,void ** arg)333 static void progress_write_cmd_unlock_stages(SDL_RWops *vfile, void **arg) {
334 int num_unlocked = (intptr_t)*arg;
335
336 if(!num_unlocked) {
337 return;
338 }
339
340 SDL_WriteU8(vfile, PCMD_UNLOCK_STAGES);
341 SDL_WriteLE16(vfile, num_unlocked * 2);
342
343 dynarray_foreach_elem(&stages, StageInfo *stg, {
344 StageProgress *p = stage_get_progress_from_info(stg, D_Any, false);
345 if(p && p->unlocked) {
346 SDL_WriteLE16(vfile, stg->id);
347 }
348 });
349 }
350
351 //
352 // PCMD_UNLOCK_STAGES_WITH_DIFFICULTY
353 //
354
progress_prepare_cmd_unlock_stages_with_difficulties(size_t * bufsize,void ** arg)355 static void progress_prepare_cmd_unlock_stages_with_difficulties(size_t *bufsize, void **arg) {
356 int num_unlocked = 0;
357
358 dynarray_foreach_elem(&stages, StageInfo *stg, {
359 if(stg->difficulty) {
360 continue;
361 }
362
363 bool unlocked = false;
364
365 for(Difficulty diff = D_Easy; diff <= D_Lunatic; ++diff) {
366 StageProgress *p = stage_get_progress_from_info(stg, diff, false);
367 if(p && p->unlocked) {
368 unlocked = true;
369 }
370 }
371
372 if(unlocked) {
373 ++num_unlocked;
374 }
375 });
376
377 if(num_unlocked) {
378 *bufsize += CMD_HEADER_SIZE;
379 *bufsize += num_unlocked * 3;
380 }
381
382 *arg = (void*)(intptr_t)num_unlocked;
383 }
384
progress_write_cmd_unlock_stages_with_difficulties(SDL_RWops * vfile,void ** arg)385 static void progress_write_cmd_unlock_stages_with_difficulties(SDL_RWops *vfile, void **arg) {
386 int num_unlocked = (intptr_t)*arg;
387
388 if(!num_unlocked) {
389 return;
390 }
391
392 SDL_WriteU8(vfile, PCMD_UNLOCK_STAGES_WITH_DIFFICULTY);
393 SDL_WriteLE16(vfile, num_unlocked * 3);
394
395 dynarray_foreach_elem(&stages, StageInfo *stg, {
396 if(stg->difficulty) {
397 continue;
398 }
399
400 uint8_t dflags = 0;
401
402 for(Difficulty diff = D_Easy; diff <= D_Lunatic; ++diff) {
403 StageProgress *p = stage_get_progress_from_info(stg, diff, false);
404 if(p && p->unlocked) {
405 dflags |= (uint)pow(2, diff - D_Easy);
406 }
407 }
408
409 if(dflags) {
410 SDL_WriteLE16(vfile, stg->id);
411 SDL_WriteU8(vfile, dflags);
412 }
413 });
414 }
415
416 //
417 // PCMD_HISCORE
418 //
419
progress_prepare_cmd_hiscore(size_t * bufsize,void ** arg)420 static void progress_prepare_cmd_hiscore(size_t *bufsize, void **arg) {
421 *bufsize += CMD_HEADER_SIZE + sizeof(uint32_t);
422 }
423
progress_write_cmd_hiscore(SDL_RWops * vfile,void ** arg)424 static void progress_write_cmd_hiscore(SDL_RWops *vfile, void **arg) {
425 SDL_WriteU8(vfile, PCMD_HISCORE);
426 SDL_WriteLE16(vfile, sizeof(uint32_t));
427 SDL_WriteLE32(vfile, progress.hiscore);
428 }
429
430 //
431 // PCMD_STAGE_PLAYINFO
432 //
433
434 struct cmd_stage_playinfo_data_elem {
435 LIST_INTERFACE(struct cmd_stage_playinfo_data_elem);
436 uint16_t stage;
437 uint8_t diff;
438 uint32_t num_played;
439 uint32_t num_cleared;
440 };
441
442 struct cmd_stage_playinfo_data {
443 size_t size;
444 struct cmd_stage_playinfo_data_elem *elems;
445 };
446
progress_prepare_cmd_stage_playinfo(size_t * bufsize,void ** arg)447 static void progress_prepare_cmd_stage_playinfo(size_t *bufsize, void **arg) {
448 struct cmd_stage_playinfo_data *data = malloc(sizeof(struct cmd_stage_playinfo_data));
449 memset(data, 0, sizeof(struct cmd_stage_playinfo_data));
450
451 dynarray_foreach_elem(&stages, StageInfo *stg, {
452 Difficulty d_first, d_last;
453
454 if(stg->difficulty == D_Any) {
455 d_first = D_Easy;
456 d_last = D_Lunatic;
457 } else {
458 d_first = d_last = D_Any;
459 }
460
461 for(Difficulty d = d_first; d <= d_last; ++d) {
462 StageProgress *p = stage_get_progress_from_info(stg, d, false);
463
464 if(p && (p->num_played || p->num_cleared)) {
465 struct cmd_stage_playinfo_data_elem *e = malloc(sizeof(struct cmd_stage_playinfo_data_elem));
466
467 e->stage = stg->id; data->size += sizeof(uint16_t);
468 e->diff = d; data->size += sizeof(uint8_t);
469 e->num_played = p->num_played; data->size += sizeof(uint32_t);
470 e->num_cleared = p->num_cleared; data->size += sizeof(uint32_t);
471
472 (void)list_push(&data->elems, e);
473 }
474 }
475 });
476
477 *arg = data;
478
479 if(data->size) {
480 *bufsize += CMD_HEADER_SIZE + data->size;
481 }
482 }
483
progress_write_cmd_stage_playinfo(SDL_RWops * vfile,void ** arg)484 static void progress_write_cmd_stage_playinfo(SDL_RWops *vfile, void **arg) {
485 struct cmd_stage_playinfo_data *data = *arg;
486
487 if(!data->size) {
488 goto cleanup;
489 }
490
491 SDL_WriteU8(vfile, PCMD_STAGE_PLAYINFO);
492 SDL_WriteLE16(vfile, data->size);
493
494 for(struct cmd_stage_playinfo_data_elem *e = data->elems; e; e = e->next) {
495 SDL_WriteLE16(vfile, e->stage);
496 SDL_WriteU8(vfile, e->diff);
497 SDL_WriteLE32(vfile, e->num_played);
498 SDL_WriteLE32(vfile, e->num_cleared);
499 }
500
501 cleanup:
502 list_free_all(&data->elems);
503 free(data);
504 }
505
506 //
507 // PCMD_ENDINGS
508 //
509
progress_prepare_cmd_endings(size_t * bufsize,void ** arg)510 static void progress_prepare_cmd_endings(size_t *bufsize, void **arg) {
511 int n = 0;
512 *arg = 0;
513
514 for(int i = 0; i < NUM_ENDINGS; ++i) {
515 if(progress.achieved_endings[i]) {
516 ++n;
517 }
518 }
519
520 if(n) {
521 uint16_t sz = n * (sizeof(uint8_t) + sizeof(uint32_t));
522 *arg = (void*)(uintptr_t)sz;
523 *bufsize += CMD_HEADER_SIZE + sz;
524 }
525 }
526
progress_write_cmd_endings(SDL_RWops * vfile,void ** arg)527 static void progress_write_cmd_endings(SDL_RWops *vfile, void **arg) {
528 uint16_t sz = (uintptr_t)*arg;
529
530 if(!sz) {
531 return;
532 }
533
534 SDL_WriteU8(vfile, PCMD_ENDINGS);
535 SDL_WriteLE16(vfile, sz);
536
537 for(int i = 0; i < NUM_ENDINGS; ++i) {
538 if(progress.achieved_endings[i]) {
539 SDL_WriteU8(vfile, i);
540 SDL_WriteLE32(vfile, progress.achieved_endings[i]);
541 }
542 }
543 }
544
545 //
546 // PCMD_GAME_SETTINGS
547 //
548
progress_prepare_cmd_game_settings(size_t * bufsize,void ** arg)549 static void progress_prepare_cmd_game_settings(size_t *bufsize, void **arg) {
550 *bufsize += CMD_HEADER_SIZE + sizeof(uint8_t) * 3;
551 }
552
progress_write_cmd_game_settings(SDL_RWops * vfile,void ** arg)553 static void progress_write_cmd_game_settings(SDL_RWops *vfile, void **arg) {
554 SDL_WriteU8(vfile, PCMD_GAME_SETTINGS);
555 SDL_WriteLE16(vfile, sizeof(uint8_t) * 3);
556 SDL_WriteU8(vfile, progress.game_settings.difficulty);
557 SDL_WriteU8(vfile, progress.game_settings.character);
558 SDL_WriteU8(vfile, progress.game_settings.shotmode);
559 }
560
561 //
562 // PCMD_GAME_VERSION
563 //
564
progress_prepare_cmd_game_version(size_t * bufsize,void ** arg)565 static void progress_prepare_cmd_game_version(size_t *bufsize, void **arg) {
566 *bufsize += CMD_HEADER_SIZE + TAISEI_VERSION_SIZE;
567 }
568
progress_write_cmd_game_version(SDL_RWops * vfile,void ** arg)569 static void progress_write_cmd_game_version(SDL_RWops *vfile, void **arg) {
570 SDL_WriteU8(vfile, PCMD_GAME_VERSION);
571 SDL_WriteLE16(vfile, TAISEI_VERSION_SIZE);
572
573 TaiseiVersion v;
574 TAISEI_VERSION_GET_CURRENT(&v);
575
576 // the buffer consistency check should fail later if there are any errors here
577 taisei_version_write(vfile, &v);
578 }
579
580 //
581 // PCMD_UNLOCK_BGMS
582 //
583
progress_prepare_cmd_unlock_bgms(size_t * bufsize,void ** arg)584 static void progress_prepare_cmd_unlock_bgms(size_t *bufsize, void **arg) {
585 *bufsize += CMD_HEADER_SIZE + sizeof(uint64_t);
586 }
587
progress_write_cmd_unlock_bgms(SDL_RWops * vfile,void ** arg)588 static void progress_write_cmd_unlock_bgms(SDL_RWops *vfile, void **arg) {
589 SDL_WriteU8(vfile, PCMD_UNLOCK_BGMS);
590 SDL_WriteLE16(vfile, sizeof(uint64_t));
591 SDL_WriteLE64(vfile, progress.unlocked_bgms);
592 }
593
594 //
595 // Copy unhandled commands from the original file
596 //
597
progress_prepare_cmd_unknown(size_t * bufsize,void ** arg)598 static void progress_prepare_cmd_unknown(size_t *bufsize, void **arg) {
599 for(UnknownCmd *c = progress.unknown; c; c = c->next) {
600 *bufsize += CMD_HEADER_SIZE + c->size;
601 }
602 }
603
progress_write_cmd_unknown(SDL_RWops * vfile,void ** arg)604 static void progress_write_cmd_unknown(SDL_RWops *vfile, void **arg) {
605 for(UnknownCmd *c = progress.unknown; c; c = c->next) {
606 SDL_WriteU8(vfile, c->cmd);
607 SDL_WriteLE16(vfile, c->size);
608 SDL_RWwrite(vfile, c->data, c->size, 1);
609 }
610 }
611
612 //
613 // Test
614 //
615
progress_prepare_cmd_test(size_t * bufsize,void ** arg)616 attr_unused static void progress_prepare_cmd_test(size_t *bufsize, void **arg) {
617 *bufsize += CMD_HEADER_SIZE + sizeof(uint32_t);
618 }
619
progress_write_cmd_test(SDL_RWops * vfile,void ** arg)620 attr_unused static void progress_write_cmd_test(SDL_RWops *vfile, void **arg) {
621 SDL_WriteU8(vfile, 0x7f);
622 SDL_WriteLE16(vfile, sizeof(uint32_t));
623 SDL_WriteLE32(vfile, 0xdeadbeef);
624 }
625
progress_write(SDL_RWops * file)626 static void progress_write(SDL_RWops *file) {
627 size_t bufsize = 0;
628 SDL_RWwrite(file, progress_magic_bytes, 1, sizeof(progress_magic_bytes));
629
630 cmd_writer_t cmdtable[] = {
631 {progress_prepare_cmd_game_version, progress_write_cmd_game_version, NULL},
632 {progress_prepare_cmd_unlock_stages, progress_write_cmd_unlock_stages, NULL},
633 {progress_prepare_cmd_unlock_stages_with_difficulties, progress_write_cmd_unlock_stages_with_difficulties, NULL},
634 {progress_prepare_cmd_hiscore, progress_write_cmd_hiscore, NULL},
635 {progress_prepare_cmd_stage_playinfo, progress_write_cmd_stage_playinfo, NULL},
636 {progress_prepare_cmd_endings, progress_write_cmd_endings, NULL},
637 {progress_prepare_cmd_game_settings, progress_write_cmd_game_settings, NULL},
638 // {progress_prepare_cmd_test, progress_write_cmd_test, NULL},
639 {progress_prepare_cmd_unlock_bgms, progress_write_cmd_unlock_bgms, NULL},
640 {progress_prepare_cmd_unknown, progress_write_cmd_unknown, NULL},
641 {NULL}
642 };
643
644 for(cmd_writer_t *w = cmdtable; w->prepare; ++w) {
645 attr_unused size_t oldsize = bufsize;
646 w->prepare(&bufsize, &w->data);
647 log_debug("prepare %i: %i", (int)(w - cmdtable), (int)(bufsize - oldsize));
648 }
649
650 if(!bufsize) {
651 return;
652 }
653
654 uint8_t *buf = malloc(bufsize);
655 memset(buf, 0x7f, bufsize);
656 SDL_RWops *vfile = SDL_RWFromMem(buf, bufsize);
657
658 for(cmd_writer_t *w = cmdtable; w->prepare; ++w) {
659 attr_unused size_t oldpos = SDL_RWtell(vfile);
660 w->write(vfile, &w->data);
661 log_debug("write %i: %i", (int)(w - cmdtable), (int)(SDL_RWtell(vfile) - oldpos));
662 }
663
664 if(SDL_RWtell(vfile) != bufsize) {
665 free(buf);
666 log_fatal("Buffer is inconsistent");
667 return;
668 }
669
670 uint32_t cs = progress_checksum(buf, bufsize);
671 // no byteswapping here
672 SDL_RWwrite(file, &cs, 4, 1);
673
674 if(!SDL_RWwrite(file, buf, bufsize, 1)) {
675 log_error("SDL_RWwrite() failed: %s", SDL_GetError());
676 return;
677 }
678
679 free(buf);
680 SDL_RWclose(vfile);
681 }
682
683 #ifdef PROGRESS_UNLOCK_ALL
progress_unlock_all(void)684 static void progress_unlock_all(void) {
685 StageInfo *stg;
686
687 for(stg = stages; stg->procs; ++stg) {
688 for(Difficulty diff = D_Any; diff <= D_Lunatic; ++diff) {
689 StageProgress *p = stage_get_progress_from_info(stg, diff, true);
690 if(p) {
691 p->unlocked = true;
692 }
693 }
694 }
695
696 progress.unlocked_bgms = UINT64_MAX;
697 }
698 #endif
699
progress_load(void)700 void progress_load(void) {
701 memset(&progress, 0, sizeof(GlobalProgress));
702
703 #ifdef PROGRESS_UNLOCK_ALL
704 progress_unlock_all();
705 progress_save();
706 #endif
707
708 SDL_RWops *file = vfs_open(PROGRESS_FILE, VFS_MODE_READ);
709
710 if(!file) {
711 VFSInfo i = vfs_query(PROGRESS_FILE);
712
713 if(i.error) {
714 log_error("VFS error: %s", vfs_get_error());
715 } else if(i.exists) {
716 log_error("Progress file %s is not readable", PROGRESS_FILE);
717 }
718
719 return;
720 }
721
722 progress_read(file);
723 SDL_RWclose(file);
724 }
725
progress_save(void)726 void progress_save(void) {
727 SDL_RWops *file = vfs_open(PROGRESS_FILE, VFS_MODE_WRITE);
728
729 if(!file) {
730 log_error("Couldn't open the progress file for writing: %s", vfs_get_error());
731 return;
732 }
733
734 progress_write(file);
735 SDL_RWclose(file);
736 }
737
delete_unknown_cmd(List ** dest,List * elem,void * arg)738 static void* delete_unknown_cmd(List **dest, List *elem, void *arg) {
739 UnknownCmd *cmd = (UnknownCmd*)elem;
740 free(cmd->data);
741 free(list_unlink(dest, elem));
742 return NULL;
743 }
744
progress_unload(void)745 void progress_unload(void) {
746 list_foreach(&progress.unknown, delete_unknown_cmd, NULL);
747 }
748
progress_times_any_ending_achieved(void)749 uint32_t progress_times_any_ending_achieved(void) {
750 uint x = 0;
751
752 for(uint i = 0; i < NUM_ENDINGS; ++i) {
753 x += progress.achieved_endings[i];
754 }
755
756 return x;
757 }
758
progress_times_any_good_ending_achieved(void)759 uint32_t progress_times_any_good_ending_achieved(void) {
760 uint x = 0;
761
762 #define ENDING(e) x += progress.achieved_endings[e];
763 GOOD_ENDINGS
764 #undef ENDING
765
766 return x;
767 }
768
progress_bgm_id(const char * bgm)769 static ProgressBGMID progress_bgm_id(const char *bgm) {
770 static const char* map[] = {
771 [PBGM_MENU] = "menu",
772 [PBGM_STAGE1] = "stage1",
773 [PBGM_STAGE1_BOSS] = "stage1boss",
774 [PBGM_STAGE2] = "stage2",
775 [PBGM_STAGE2_BOSS] = "stage2boss",
776 [PBGM_STAGE3] = "stage3",
777 [PBGM_STAGE3_BOSS] = "stage3boss",
778 [PBGM_STAGE4] = "stage4",
779 [PBGM_STAGE4_BOSS] = "stage4boss",
780 [PBGM_STAGE5] = "stage5",
781 [PBGM_STAGE5_BOSS] = "stage5boss",
782 [PBGM_STAGE6] = "stage6",
783 [PBGM_STAGE6_BOSS1] = "stage6boss_phase1",
784 [PBGM_STAGE6_BOSS2] = "stage6boss_phase2",
785 [PBGM_STAGE6_BOSS3] = "stage6boss_phase3",
786 [PBGM_ENDING] = "ending",
787 [PBGM_CREDITS] = "credits",
788 [PBGM_BONUS0] = "bonus0",
789 [PBGM_BONUS1] = "scuttle",
790 };
791
792 for(int i = 0; i < ARRAY_SIZE(map); ++i) {
793 if(!strcmp(map[i], bgm)) {
794 return i;
795 }
796 }
797
798 UNREACHABLE;
799 }
800
progress_bgm_bit(ProgressBGMID id)801 static inline uint64_t progress_bgm_bit(ProgressBGMID id) {
802 return (UINT64_C(1) << id);
803 }
804
progress_is_bgm_unlocked(const char * name)805 bool progress_is_bgm_unlocked(const char *name) {
806 return progress.unlocked_bgms & progress_bgm_bit(progress_bgm_id(name));
807 }
808
progress_unlock_bgm(const char * name)809 void progress_unlock_bgm(const char *name) {
810 progress.unlocked_bgms |= progress_bgm_bit(progress_bgm_id(name));
811 }
812