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(&current_version);
279 		int cmp = taisei_version_compare(&current_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(&current_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