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