1 /*
2  C-Dogs SDL
3  A port of the legendary (and fun) action/arcade cdogs.
4 
5  Copyright (c) 2013-2016, 2019, 2021 Cong Xu
6  All rights reserved.
7 
8  Redistribution and use in source and binary forms, with or without
9  modification, are permitted provided that the following conditions are met:
10 
11  Redistributions of source code must retain the above copyright notice, this
12  list of conditions and the following disclaimer.
13  Redistributions in binary form must reproduce the above copyright notice,
14  this list of conditions and the following disclaimer in the documentation
15  and/or other materials provided with the distribution.
16 
17  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27  POSSIBILITY OF SUCH DAMAGE.
28  */
29 #include "autosave.h"
30 
31 #include <errno.h>
32 #include <stdio.h>
33 #include <string.h>
34 
35 #include <json/json.h>
36 
37 #include <cdogs/campaign_entry.h>
38 #include <cdogs/json_utils.h>
39 #include <cdogs/sys_specifics.h>
40 #include <cdogs/utils.h>
41 
42 #ifdef __EMSCRIPTEN__
43 #include <emscripten.h>
44 #endif
45 
46 #define VERSION 3
47 
48 Autosave gAutosave;
49 
PlayerSaveInit(PlayerSave * ps)50 static void PlayerSaveInit(PlayerSave *ps)
51 {
52 	memset(ps, 0, sizeof *ps);
53 	CArrayInit(&ps->ammo, sizeof(int));
54 }
PlayerSaveTerminate(PlayerSave * ps)55 static void PlayerSaveTerminate(PlayerSave *ps)
56 {
57 	for (int i = 0; i < MAX_WEAPONS; i++)
58 	{
59 		CFREE(ps->Guns[i]);
60 	}
61 	CArrayTerminate(&ps->ammo);
62 }
PlayerSavesTerminate(CArray * a)63 static void PlayerSavesTerminate(CArray *a)
64 {
65 	CA_FOREACH(PlayerSave, ps, *a)
66 	PlayerSaveTerminate(ps);
67 	CA_FOREACH_END()
68 	CArrayTerminate(a);
69 }
70 
CampaignSaveInit(CampaignSave * ms)71 void CampaignSaveInit(CampaignSave *ms)
72 {
73 	memset(ms, 0, sizeof *ms);
74 	ms->IsValid = true;
75 	CArrayInit(&ms->MissionsCompleted, sizeof(int));
76 	CArrayInit(&ms->Players, sizeof(PlayerSave));
77 }
78 
CampaignSaveIsValid(const CampaignSave * cs)79 bool CampaignSaveIsValid(const CampaignSave *cs)
80 {
81 	return cs != NULL && cs->IsValid && strlen(cs->Campaign.Path) > 0;
82 }
83 
AutosaveInit(Autosave * autosave)84 void AutosaveInit(Autosave *autosave)
85 {
86 	memset(autosave, 0, sizeof *autosave);
87 	autosave->LastCampaignIndex = -1;
88 	CArrayInit(&autosave->Campaigns, sizeof(CampaignSave));
89 }
AutosaveTerminate(Autosave * autosave)90 void AutosaveTerminate(Autosave *autosave)
91 {
92 	CA_FOREACH(CampaignSave, m, autosave->Campaigns)
93 	CampaignEntryTerminate(&m->Campaign);
94 	CArrayTerminate(&m->MissionsCompleted);
95 	PlayerSavesTerminate(&m->Players);
96 	CA_FOREACH_END()
97 	CArrayTerminate(&autosave->Campaigns);
98 }
99 
LoadCampaignNode(CampaignEntry * c,json_t * node)100 static void LoadCampaignNode(CampaignEntry *c, json_t *node)
101 {
102 	const char *path = json_find_first_label(node, "Path")->child->text;
103 	if (path != NULL)
104 	{
105 		c->Path = json_unescape(path);
106 	}
107 	c->Mode = GAME_MODE_NORMAL;
108 }
AddCampaignNode(CampaignEntry * c,json_t * root)109 static void AddCampaignNode(CampaignEntry *c, json_t *root)
110 {
111 	json_t *subConfig = json_new_object();
112 	// Save relative path so that save files are portable across installs
113 	char path[CDOGS_PATH_MAX] = "";
114 	RelPathFromCWD(path, c->Path);
115 	json_insert_pair_into_object(
116 		subConfig, "Path", json_new_string(json_escape(path)));
117 	json_insert_pair_into_object(root, "Campaign", subConfig);
118 }
119 
LoadPlayersNode(CArray * players,json_t * node)120 static void LoadPlayersNode(CArray *players, json_t *node)
121 {
122 	json_t *playersNode = json_find_first_label(node, "Players");
123 	if (playersNode == NULL)
124 	{
125 		return;
126 	}
127 	for (json_t *child = playersNode->child->child; child; child = child->next)
128 	{
129 		PlayerSave ps;
130 		PlayerSaveInit(&ps);
131 
132 		json_t *gunsNode = json_find_first_label(child, "Guns")->child;
133 		int i = 0;
134 		for (json_t *gunNode = gunsNode->child;
135 			 gunNode != NULL && i < MAX_WEAPONS; gunNode = gunNode->next, i++)
136 		{
137 			char *gun = json_unescape(gunNode->text);
138 			if (strlen(gun) > 0)
139 			{
140 				ps.Guns[i] = gun;
141 			}
142 			else
143 			{
144 				CFREE(gun);
145 			}
146 		}
147 
148 		LoadIntArray(&ps.ammo, child, "Ammo");
149 
150 		CArrayPushBack(players, &ps);
151 	}
152 }
AddPlayersNode(CArray * players,json_t * root)153 static void AddPlayersNode(CArray *players, json_t *root)
154 {
155 	json_t *playersNode = json_new_array();
156 
157 	CA_FOREACH(const PlayerSave, ps, *players)
158 	json_t *playerNode = json_new_object();
159 
160 	json_t *gunsNode = json_new_array();
161 	for (int i = 0; i < MAX_WEAPONS; i++)
162 	{
163 		json_insert_child(
164 			gunsNode, json_new_string(ps->Guns[i] != NULL ? ps->Guns[i] : ""));
165 	}
166 	json_insert_pair_into_object(playerNode, "Guns", gunsNode);
167 	AddIntArray(playerNode, "Ammo", &ps->ammo);
168 
169 	json_insert_child(playersNode, playerNode);
170 	CA_FOREACH_END()
171 
172 	json_insert_pair_into_object(root, "Players", playersNode);
173 }
174 
LoadMissionNode(CampaignSave * m,json_t * node,const int version)175 static void LoadMissionNode(CampaignSave *m, json_t *node, const int version)
176 {
177 	CampaignSaveInit(m);
178 	LoadCampaignNode(
179 		&m->Campaign, json_find_first_label(node, "Campaign")->child);
180 	LoadInt(&m->NextMission, node, "NextMission");
181 	if (version < 3)
182 	{
183 		int missionsCompleted = 0;
184 		LoadInt(&missionsCompleted, node, "MissionsCompleted");
185 		for (int i = 0; i < missionsCompleted; i++)
186 		{
187 			CArrayPushBack(&m->MissionsCompleted, &i);
188 		}
189 		m->NextMission = missionsCompleted;
190 	}
191 	else
192 	{
193 		LoadIntArray(&m->MissionsCompleted, node, "MissionsCompleted");
194 	}
195 	// Check that file exists
196 	char buf[CDOGS_PATH_MAX];
197 	GetDataFilePath(buf, m->Campaign.Path);
198 	m->IsValid = access(buf, F_OK | R_OK) != -1;
199 	LoadPlayersNode(&m->Players, node);
200 }
CreateMissionNode(CampaignSave * m)201 static json_t *CreateMissionNode(CampaignSave *m)
202 {
203 	json_t *subConfig = json_new_object();
204 	AddCampaignNode(&m->Campaign, subConfig);
205 	AddIntPair(subConfig, "NextMission", m->NextMission);
206 	AddIntArray(subConfig, "MissionsCompleted", &m->MissionsCompleted);
207 	AddPlayersNode(&m->Players, subConfig);
208 	return subConfig;
209 }
210 
LoadMissionNodes(Autosave * a,json_t * root,const char * nodeName,const int version)211 static void LoadMissionNodes(
212 	Autosave *a, json_t *root, const char *nodeName, const int version)
213 {
214 	json_t *child;
215 	if (json_find_first_label(root, nodeName) == NULL)
216 	{
217 		return;
218 	}
219 	child = json_find_first_label(root, nodeName)->child->child;
220 	while (child != NULL)
221 	{
222 		CampaignSave m;
223 		LoadMissionNode(&m, child, version);
224 		AutosaveAddCampaign(a, &m);
225 		child = child->next;
226 	}
227 }
AddMissionNodes(Autosave * a,json_t * root,const char * nodeName)228 static void AddMissionNodes(Autosave *a, json_t *root, const char *nodeName)
229 {
230 	json_t *missions = json_new_array();
231 	CA_FOREACH(CampaignSave, m, a->Campaigns)
232 	json_insert_child(missions, CreateMissionNode(m));
233 	CA_FOREACH_END()
234 	json_insert_pair_into_object(root, nodeName, missions);
235 }
236 
237 static CampaignSave *FindCampaign(
238 	Autosave *autosave, const char *path, int *missionIndex);
239 
AutosaveLoad(Autosave * autosave,const char * filename)240 void AutosaveLoad(Autosave *autosave, const char *filename)
241 {
242 	FILE *f = fopen(filename, "r");
243 	json_t *root = NULL;
244 
245 	if (f == NULL)
246 	{
247 		printf("Error loading autosave '%s'\n", filename);
248 		goto bail;
249 	}
250 
251 	if (json_stream_parse(f, &root) != JSON_OK)
252 	{
253 		printf("Error parsing autosave '%s'\n", filename);
254 		goto bail;
255 	}
256 	int version = 2;
257 	LoadInt(&version, root, "Version");
258 	LoadMissionNodes(autosave, root, "Missions", version);
259 	if (version < 3)
260 	{
261 		if (json_find_first_label(root, "LastMission"))
262 		{
263 			json_t *lastMission =
264 				json_find_first_label(root, "LastMission")->child;
265 			json_t *campaign =
266 				json_find_first_label(lastMission, "Campaign")->child;
267 			char *path = NULL;
268 			LoadStr(&path, campaign, "Path");
269 			if (path != NULL)
270 			{
271 				FindCampaign(autosave, path, &autosave->LastCampaignIndex);
272 				CFREE(path);
273 			}
274 		}
275 	}
276 	else
277 	{
278 		LoadInt(&autosave->LastCampaignIndex, root, "LastCampaignIndex");
279 	}
280 
281 bail:
282 	json_free_value(&root);
283 	if (f != NULL)
284 	{
285 		fclose(f);
286 	}
287 }
288 
AutosaveSave(Autosave * autosave,const char * filename)289 void AutosaveSave(Autosave *autosave, const char *filename)
290 {
291 	char *text = NULL;
292 	json_t *root;
293 
294 	root = json_new_object();
295 	AddIntPair(root, "Version", VERSION);
296 	AddIntPair(root, "LastCampaignIndex", autosave->LastCampaignIndex);
297 	AddMissionNodes(autosave, root, "Missions");
298 
299 	json_tree_to_string(root, &text);
300 	char *formatText = json_format_string(text);
301 
302 	FILE *f = fopen(filename, "w");
303 	if (f == NULL)
304 	{
305 		printf("Error saving autosave '%s'\n", filename);
306 		return;
307 	}
308 	fputs(formatText, f);
309 	fclose(f);
310 
311 	// clean up
312 	CFREE(formatText);
313 	CFREE(text);
314 	json_free_value(&root);
315 
316 #ifdef __EMSCRIPTEN__
317 	EM_ASM(
318 		// persist changes
319 		FS.syncfs(
320 			false, function(err) { assert(!err); }););
321 #endif
322 }
323 
FindCampaign(Autosave * autosave,const char * path,int * missionIndex)324 static CampaignSave *FindCampaign(
325 	Autosave *autosave, const char *path, int *missionIndex)
326 {
327 	if (path == NULL || strlen(path) == 0)
328 	{
329 		return NULL;
330 	}
331 
332 	// Turn the path into a relative one, since autosave paths are all stored
333 	// in this form
334 	char relPath[CDOGS_PATH_MAX] = "";
335 	RelPathFromCWD(relPath, path);
336 	CA_FOREACH(CampaignSave, m, autosave->Campaigns)
337 	const char *campaignPath = m->Campaign.Path;
338 	if (campaignPath != NULL && strcmp(campaignPath, relPath) == 0)
339 	{
340 		if (missionIndex != NULL)
341 		{
342 			*missionIndex = _ca_index;
343 		}
344 		return m;
345 	}
346 	CA_FOREACH_END()
347 	return NULL;
348 }
349 
AutosaveAdd(Autosave * a,const CampaignEntry * ce,const int missionIndex,const int nextMission,const CArray * playerDatas)350 void AutosaveAdd(
351 	Autosave *a, const CampaignEntry *ce, const int missionIndex,
352 	const int nextMission, const CArray *playerDatas)
353 {
354 	CampaignSave ms;
355 	CampaignSaveInit(&ms);
356 	CampaignEntryCopy(&ms.Campaign, ce);
357 	CArrayPushBack(&ms.MissionsCompleted, &missionIndex);
358 	ms.NextMission = nextMission;
359 	CA_FOREACH(const PlayerData, pd, *playerDatas)
360 	PlayerSave ps;
361 	PlayerSaveInit(&ps);
362 	for (int i = 0; i < MAX_WEAPONS; i++)
363 	{
364 		if (pd->guns[i])
365 		{
366 			CSTRDUP(ps.Guns[i], pd->guns[i]->name);
367 		}
368 	}
369 	CArrayCopy(&ps.ammo, &pd->ammo);
370 	CArrayPushBack(&ms.Players, &ps);
371 	CA_FOREACH_END()
372 	AutosaveAddCampaign(a, &ms);
373 }
374 
AutosaveAddCampaign(Autosave * autosave,CampaignSave * cs)375 void AutosaveAddCampaign(Autosave *autosave, CampaignSave *cs)
376 {
377 	CampaignSave *existing = FindCampaign(
378 		autosave, cs->Campaign.Path, &autosave->LastCampaignIndex);
379 	if (existing != NULL)
380 	{
381 		CampaignEntryTerminate(&existing->Campaign);
382 	}
383 	else
384 	{
385 		CArrayPushBack(&autosave->Campaigns, cs);
386 		autosave->LastCampaignIndex = (int)autosave->Campaigns.size - 1;
387 		existing =
388 			CArrayGet(&autosave->Campaigns, autosave->LastCampaignIndex);
389 		CampaignSaveInit(existing);
390 	}
391 
392 	existing->NextMission = cs->NextMission;
393 	// Update missions completed
394 	CA_FOREACH(const int, missionIndex, cs->MissionsCompleted)
395 	bool found = false;
396 	for (int i = 0; i < (int)existing->MissionsCompleted.size; i++)
397 	{
398 		if (*(int *)CArrayGet(&existing->MissionsCompleted, i) ==
399 			*missionIndex)
400 		{
401 			found = true;
402 			break;
403 		}
404 	}
405 	if (!found)
406 	{
407 		CArrayPushBack(&existing->MissionsCompleted, missionIndex);
408 	}
409 	CA_FOREACH_END()
410 	// Sort and remove duplicates
411 	qsort(
412 		existing->MissionsCompleted.data, existing->MissionsCompleted.size,
413 		existing->MissionsCompleted.elemSize, CompareIntsAsc);
414 	CArrayUnique(&existing->MissionsCompleted, IntsEqual);
415 	CArrayTerminate(&cs->MissionsCompleted);
416 
417 	PlayerSavesTerminate(&existing->Players);
418 	memcpy(&existing->Players, &cs->Players, sizeof cs->Players);
419 
420 	memcpy(&existing->Campaign, &cs->Campaign, sizeof existing->Campaign);
421 }
422 
AutosaveGetCampaign(Autosave * autosave,const char * path)423 const CampaignSave *AutosaveGetCampaign(Autosave *autosave, const char *path)
424 {
425 	return FindCampaign(autosave, path, NULL);
426 }
427 
AutosaveGetLastCampaign(const Autosave * a)428 const CampaignSave *AutosaveGetLastCampaign(const Autosave *a)
429 {
430 	if (a->LastCampaignIndex < 0)
431 	{
432 		return NULL;
433 	}
434 	return CArrayGet(&a->Campaigns, a->LastCampaignIndex);
435 }
436