1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /*
21 * loadsave.c
22 * load and save Popup screens.
23 *
24 * these don't actually do any loading or saving, but just
25 * return a filename to use for the ops.
26 */
27
28 #include <physfs.h>
29 #include "lib/framework/physfs_ext.h"
30 #include <ctime>
31
32 #include "lib/framework/frame.h"
33 #include "lib/framework/input.h"
34 #include "lib/framework/stdio_ext.h"
35 #include "lib/framework/wztime.h"
36 #include "lib/widget/button.h"
37 #include "lib/widget/editbox.h"
38 #include "lib/widget/widget.h"
39 #include "lib/ivis_opengl/piepalette.h" // for predefined colours.
40 #include "lib/ivis_opengl/bitimage.h"
41 #include "lib/ivis_opengl/pieblitfunc.h" // for boxfill
42 #include "hci.h"
43 #include "loadsave.h"
44 #include "multiplay.h"
45 #include "game.h"
46 #include "lib/sound/audio_id.h"
47 #include "lib/sound/audio.h"
48 #include "frontend.h"
49 #include "main.h"
50 #include "display.h"
51 #include "lib/netplay/netplay.h"
52 #include "loop.h"
53 #include "intdisplay.h"
54 #include "mission.h"
55 #include "lib/gamelib/gtime.h"
56 #include "console.h"
57 #include "keybind.h"
58 #include "keymap.h"
59 #include "qtscript.h"
60 #include "clparse.h"
61 #include "ingameop.h"
62
63 #define totalslots 36 // saves slots
64 #define slotsInColumn 12 // # of slots in a column
65 #define totalslotspace 64 // guessing 64 max chars for filename.
66
67 #define LOADSAVE_X D_W + 16
68 #define LOADSAVE_Y D_H + 5
69 #define LOADSAVE_W 610
70 #define LOADSAVE_H 215
71
72 #define MAX_SAVE_NAME 60
73
74 #define LOADSAVE_HGAP 9
75 #define LOADSAVE_VGAP 9
76 #define LOADSAVE_BANNER_DEPTH 40 //top banner which displays either load or save
77
78 #define LOADENTRY_W ((LOADSAVE_W / 3 )-(3 * LOADSAVE_HGAP))
79 #define LOADENTRY_H (LOADSAVE_H -(5 * LOADSAVE_VGAP )- (LOADSAVE_BANNER_DEPTH+LOADSAVE_VGAP) ) /5
80
81 #define ID_LOADSAVE 21000
82 #define LOADSAVE_FORM ID_LOADSAVE+1 // back form.
83 #define LOADSAVE_CANCEL ID_LOADSAVE+2 // cancel but.
84 #define LOADSAVE_LABEL ID_LOADSAVE+3 // load/save
85 #define LOADSAVE_BANNER ID_LOADSAVE+4 // banner.
86
87 #define LOADENTRY_START ID_LOADSAVE+10 // each of the buttons.
88 #define LOADENTRY_END ID_LOADSAVE+10 +totalslots // must have unique ID hmm -Q
89
90 #define SAVEENTRY_EDIT ID_LOADSAVE + totalslots + totalslots // save edit box. must be highest value possible I guess. -Q
91 #define AUTOSAVE_CAM_DIR "savegames/campaign/auto"
92 #define AUTOSAVE_SKI_DIR "savegames/skirmish/auto"
93
94 // ////////////////////////////////////////////////////////////////////////////
95 static void displayLoadBanner(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
96 static void displayLoadSlot(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset);
97
98 struct LoadSaveDisplayLoadSlotCache {
99 std::string fullText;
100 WzText wzText;
101 };
102
103 static std::shared_ptr<W_SCREEN> psRequestScreen = nullptr; // Widget screen for requester
104 static bool mode;
105 static UDWORD chosenSlotId;
106
107 bool bLoadSaveUp = false; // true when interface is up and should be run.
108 char saveGameName[256]; //the name of the save game to load from the front end
109 char sRequestResult[PATH_MAX]; // filename returned;
110 bool bRequestLoad = false;
111 bool autosaveEnabled = true;
112 LOADSAVE_MODE bLoadSaveMode;
113 static const char *savedTitle;
114 static const char *sSaveGameExtension = ".gam";
115
116 // ////////////////////////////////////////////////////////////////////////////
117 // return whether the specified filename looks like a saved game file, i.e. ends with .gam
isASavedGamefile(const char * filename)118 bool isASavedGamefile(const char* filename)
119 {
120 static const size_t saveGameExtensionLength = strlen(sSaveGameExtension);
121
122 if (nullptr == filename)
123 {
124 return false;
125 }
126
127 size_t filenameLength = strlen(filename);
128 if (filenameLength <= saveGameExtensionLength)
129 {
130 // reject filename of insufficient length to contain "<anything>.gam"
131 return false;
132 }
133 return 0 == strcmp(filename + filenameLength - saveGameExtensionLength, sSaveGameExtension);
134 }
135
136
137 // ////////////////////////////////////////////////////////////////////////////
138 // return whether the save screen was displayed in the mission results screen
saveInMissionRes()139 bool saveInMissionRes()
140 {
141 return bLoadSaveMode == SAVE_MISSIONEND;
142 }
143
144 // ////////////////////////////////////////////////////////////////////////////
145 // return whether the save screen was displayed in the middle of a mission
saveMidMission()146 bool saveMidMission()
147 {
148 return bLoadSaveMode == SAVE_INGAME_MISSION;
149 }
150
loadSaveScreenSizeDidChange(unsigned int oldWidth,unsigned int oldHeight,unsigned int newWidth,unsigned int newHeight)151 void loadSaveScreenSizeDidChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
152 {
153 if (psRequestScreen == nullptr) return;
154 psRequestScreen->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
155 }
156
157 // ////////////////////////////////////////////////////////////////////////////
addLoadSave(LOADSAVE_MODE savemode,const char * title)158 bool addLoadSave(LOADSAVE_MODE savemode, const char *title)
159 {
160 bool bLoad = true;
161 char NewSaveGamePath[PATH_MAX] = {'\0'};
162 bLoadSaveMode = savemode;
163 savedTitle = title;
164 UDWORD slotCount;
165
166 // Static as these are assigned to the widget buttons by reference
167 static char sSlotCaps[totalslots][totalslotspace];
168 static char sSlotTips[totalslots][totalslotspace];
169
170 switch (savemode)
171 {
172 case LOAD_FRONTEND_MISSION:
173 case LOAD_INGAME_MISSION:
174 case LOAD_MISSIONEND:
175 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "campaign");
176 break;
177 case LOAD_FRONTEND_SKIRMISH:
178 case LOAD_INGAME_SKIRMISH:
179 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "skirmish");
180 break;
181 case LOAD_FRONTEND_MISSION_AUTO:
182 case LOAD_INGAME_MISSION_AUTO:
183 case LOAD_MISSIONEND_AUTO:
184 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "campaign/auto");
185 break;
186 case LOAD_FRONTEND_SKIRMISH_AUTO:
187 case LOAD_INGAME_SKIRMISH_AUTO:
188 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "skirmish/auto");
189 break;
190 case SAVE_MISSIONEND:
191 case SAVE_INGAME_MISSION:
192 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "campaign");
193 bLoad = false;
194 break;
195 case SAVE_INGAME_SKIRMISH:
196 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "skirmish");
197 bLoad = false;
198 break;
199 default:
200 ASSERT("Invalid load/save mode!", "Invalid load/save mode!");
201 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "campaign");
202 break;
203 }
204
205 mode = bLoad;
206 debug(LOG_SAVE, "called (%d, %s)", bLoad, title);
207
208 if ((bLoadSaveMode == LOAD_INGAME_MISSION) || (bLoadSaveMode == SAVE_INGAME_MISSION)
209 || (bLoadSaveMode == LOAD_INGAME_SKIRMISH) || (bLoadSaveMode == SAVE_INGAME_SKIRMISH)
210 || (bLoadSaveMode == LOAD_INGAME_MISSION_AUTO) || (bLoadSaveMode == LOAD_INGAME_SKIRMISH_AUTO))
211 {
212 if (!bMultiPlayer || (NetPlay.bComms == 0))
213 {
214 gameTimeStop();
215 if (GetGameMode() == GS_NORMAL)
216 {
217 // just display the 3d, no interface
218 displayWorld();
219 }
220
221 setGamePauseStatus(true);
222 setAllPauseStates(true);
223 }
224
225 forceHidePowerBar();
226 intRemoveReticule();
227 }
228
229 psRequestScreen = W_SCREEN::make();
230
231 auto const &parent = psRequestScreen->psForm;
232
233 /* add a form to place the tabbed form on */
234 // we need the form to be long enough for all resolutions, so we take the total number of items * height
235 // and * the gaps, add the banner, and finally, the fudge factor ;)
236 auto loadSaveForm = std::make_shared<IntFormAnimated>();
237 parent->attach(loadSaveForm);
238 loadSaveForm->id = LOADSAVE_FORM;
239 loadSaveForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
240 psWidget->setGeometry(LOADSAVE_X, LOADSAVE_Y, LOADSAVE_W, slotsInColumn * (LOADENTRY_H + LOADSAVE_HGAP) + LOADSAVE_BANNER_DEPTH + 20);
241 }));
242
243 // Add Banner
244 W_FORMINIT sFormInit;
245 sFormInit.formID = LOADSAVE_FORM;
246 sFormInit.id = LOADSAVE_BANNER;
247 sFormInit.x = LOADSAVE_HGAP;
248 sFormInit.y = LOADSAVE_VGAP;
249 sFormInit.width = LOADSAVE_W - (2 * LOADSAVE_HGAP);
250 sFormInit.height = LOADSAVE_BANNER_DEPTH;
251 sFormInit.pDisplay = displayLoadBanner;
252 sFormInit.UserData = bLoad;
253 widgAddForm(psRequestScreen, &sFormInit);
254
255 // add cancel.
256 W_BUTINIT sButInit;
257 sButInit.formID = LOADSAVE_BANNER;
258 sButInit.x = 8;
259 sButInit.y = 10;
260 sButInit.width = iV_GetImageWidth(IntImages, IMAGE_NRUTER);
261 sButInit.height = iV_GetImageHeight(IntImages, IMAGE_NRUTER);
262 sButInit.UserData = PACKDWORD_TRI(0, IMAGE_NRUTER , IMAGE_NRUTER);
263
264 sButInit.id = LOADSAVE_CANCEL;
265 sButInit.style = WBUT_PLAIN;
266 sButInit.pTip = _("Close");
267 sButInit.pDisplay = intDisplayImageHilight;
268 widgAddButton(psRequestScreen, &sButInit);
269
270 // Add Banner Label
271 W_LABINIT sLabInit;
272 sLabInit.formID = LOADSAVE_BANNER;
273 sLabInit.FontID = font_large;
274 sLabInit.id = LOADSAVE_LABEL;
275 sLabInit.style = WLAB_ALIGNCENTRE;
276 sLabInit.x = 0;
277 sLabInit.y = 0;
278 sLabInit.width = LOADSAVE_W - (2 * LOADSAVE_HGAP); //LOADSAVE_W;
279 sLabInit.height = LOADSAVE_BANNER_DEPTH; //This looks right -Q
280 sLabInit.pText = WzString::fromUtf8(title);
281 widgAddLabel(psRequestScreen, &sLabInit);
282
283 // add slots
284 sButInit = W_BUTINIT();
285 sButInit.formID = LOADSAVE_FORM;
286 sButInit.style = WBUT_PLAIN;
287 sButInit.width = LOADENTRY_W;
288 sButInit.height = LOADENTRY_H;
289 sButInit.pDisplay = displayLoadSlot;
290 sButInit.initPUserDataFunc = []() -> void * { return new LoadSaveDisplayLoadSlotCache(); };
291 sButInit.onDelete = [](WIDGET *psWidget) {
292 assert(psWidget->pUserData != nullptr);
293 delete static_cast<LoadSaveDisplayLoadSlotCache *>(psWidget->pUserData);
294 psWidget->pUserData = nullptr;
295 };
296
297 for (slotCount = 0; slotCount < totalslots; slotCount++)
298 {
299 sButInit.id = slotCount + LOADENTRY_START;
300
301 if (slotCount < slotsInColumn)
302 {
303 sButInit.x = 22 + LOADSAVE_HGAP;
304 sButInit.y = (SWORD)((LOADSAVE_BANNER_DEPTH + (2 * LOADSAVE_VGAP)) + (
305 slotCount * (LOADSAVE_VGAP + LOADENTRY_H)));
306 }
307 else if (slotCount >= slotsInColumn && (slotCount < (slotsInColumn * 2)))
308 {
309 sButInit.x = 22 + (2 * LOADSAVE_HGAP + LOADENTRY_W);
310 sButInit.y = (SWORD)((LOADSAVE_BANNER_DEPTH + (2 * LOADSAVE_VGAP)) + (
311 (slotCount % slotsInColumn) * (LOADSAVE_VGAP + LOADENTRY_H)));
312 }
313 else
314 {
315 sButInit.x = 22 + (3 * LOADSAVE_HGAP + (2 * LOADENTRY_W));
316 sButInit.y = (SWORD)((LOADSAVE_BANNER_DEPTH + (2 * LOADSAVE_VGAP)) + (
317 (slotCount % slotsInColumn) * (LOADSAVE_VGAP + LOADENTRY_H)));
318 }
319 widgAddButton(psRequestScreen, &sButInit);
320 }
321
322 // fill slots.
323 slotCount = 0;
324
325 // The first slot is for [auto] or [..]
326 {
327 W_BUTTON *button;
328
329 button = (W_BUTTON *)widgGetFromID(psRequestScreen, LOADENTRY_START + slotCount);
330 if (bLoadSaveMode >= LOAD_FRONTEND_MISSION_AUTO)
331 {
332 button->pTip = _("Parent directory");
333 button->pText = "[..]";
334 }
335 else
336 {
337 button->pTip = mode? _("Autosave directory") : _("Autosave directory (not allowed for saving)");
338 button->pText = "[auto]";
339 }
340 slotCount++;
341 }
342
343 debug(LOG_SAVE, "Searching \"%s\" for savegames", NewSaveGamePath);
344
345 // add savegame filenames minus extensions to buttons
346
347 struct SaveGameNamesAndTimes
348 {
349 std::string name;
350 time_t savetime;
351
352 SaveGameNamesAndTimes(std::string name, time_t savetime)
353 : name(std::move(name))
354 , savetime(savetime)
355 { }
356 };
357
358 std::vector<SaveGameNamesAndTimes> saveGameNamesAndTimes;
359
360 WZ_PHYSFS_enumerateFiles(NewSaveGamePath, [&NewSaveGamePath, &saveGameNamesAndTimes](char *i) -> bool {
361 char savefile[256];
362 time_t savetime;
363
364 if (!isASavedGamefile(i))
365 {
366 // If it doesn't, move on to the next filename
367 return true;
368 }
369
370 debug(LOG_SAVE, "We found [%s]", i);
371
372 /* Figure save-time */
373 snprintf(savefile, sizeof(savefile), "%s/%s", NewSaveGamePath, i);
374 savetime = WZ_PHYSFS_getLastModTime(savefile);
375
376 (i)[strlen(i) - 4] = '\0'; // remove .gam extension
377
378 saveGameNamesAndTimes.emplace_back(i, savetime);
379 return true;
380 });
381
382 // Sort the save games so that the most recent one appears first
383 std::sort(saveGameNamesAndTimes.begin(),
384 saveGameNamesAndTimes.end(),
385 [](SaveGameNamesAndTimes& a, SaveGameNamesAndTimes& b) { return a.savetime > b.savetime; });
386
387 // Now store the sorted save game names to the buttons
388 slotCount = 1;
389 (void)std::all_of(saveGameNamesAndTimes.begin(), saveGameNamesAndTimes.end(), [&](SaveGameNamesAndTimes& saveGameNameAndTime)
390 {
391 /* Set the button-text and tip text (the save time) into static storage */
392 sstrcpy(sSlotCaps[slotCount], saveGameNameAndTime.name.c_str());
393 auto newtime = getLocalTime(saveGameNameAndTime.savetime);
394 strftime(sSlotTips[slotCount], sizeof(sSlotTips[slotCount]), "%F %H:%M:%S", &newtime);
395
396 /* Add a button that references the static strings */
397 W_BUTTON* button = (W_BUTTON*)widgGetFromID(psRequestScreen, LOADENTRY_START + slotCount);
398 button->pTip = sSlotTips[slotCount];
399 button->pText = WzString::fromUtf8(sSlotCaps[slotCount]);
400 slotCount++;
401
402 return (slotCount < totalslots);
403 }
404 );
405
406 bLoadSaveUp = true;
407 return true;
408 }
409
410 // ////////////////////////////////////////////////////////////////////////////
closeLoadSave(bool goBack)411 bool closeLoadSave(bool goBack)
412 {
413 bLoadSaveUp = false;
414
415 if ((bLoadSaveMode == LOAD_INGAME_MISSION) || (bLoadSaveMode == SAVE_INGAME_MISSION)
416 || (bLoadSaveMode == LOAD_INGAME_SKIRMISH) || (bLoadSaveMode == SAVE_INGAME_SKIRMISH)
417 || (bLoadSaveMode == LOAD_INGAME_MISSION_AUTO) || (bLoadSaveMode == LOAD_INGAME_SKIRMISH_AUTO))
418 {
419 if (goBack)
420 {
421 intReopenMenuWithoutUnPausing();
422 }
423 if (!bMultiPlayer || (NetPlay.bComms == 0))
424 {
425 setGameUpdatePause(false);
426 if (!goBack)
427 {
428 gameTimeStart();
429 setGamePauseStatus(false);
430 setAllPauseStates(false);
431 }
432 }
433
434 intAddReticule();
435 intShowPowerBar();
436 }
437
438 psRequestScreen = nullptr;
439 // need to "eat" up the return key so it don't pass back to game.
440 inputLoseFocus();
441 return true;
442 }
443
444 /***************************************************************************
445 Delete a savegame. fileName should be a .gam extension save game
446 filename reference. We delete this file, any .es file with the same
447 name, and any files in the directory with the same name.
448 ***************************************************************************/
deleteSaveGame(char * fileName)449 void deleteSaveGame(char *fileName)
450 {
451 ASSERT(strlen(fileName) < MAX_STR_LENGTH, "deleteSaveGame; save game name too long");
452
453 PHYSFS_delete(fileName);
454 fileName[strlen(fileName) - 4] = '\0'; // strip extension
455
456 strcat(fileName, ".es"); // remove script data if it exists.
457 PHYSFS_delete(fileName);
458 fileName[strlen(fileName) - 3] = '\0'; // strip extension
459
460 // check for a directory and remove that too.
461 WZ_PHYSFS_enumerateFiles(fileName, [fileName](const char *i) -> bool {
462 char del_file[PATH_MAX];
463
464 // Construct the full path to the file by appending the
465 // filename to the directory it is in.
466 snprintf(del_file, sizeof(del_file), "%s/%s", fileName, i);
467
468 debug(LOG_SAVE, "Deleting [%s].", del_file);
469
470 // Delete the file
471 if (!PHYSFS_delete(del_file))
472 {
473 debug(LOG_ERROR, "Warning [%s] could not be deleted due to PhysicsFS error: %s", del_file, WZ_PHYSFS_getLastError());
474 }
475 return true; // continue
476 });
477
478 if (!PHYSFS_delete(fileName)) // now (should be)empty directory
479 {
480 debug(LOG_ERROR, "Warning directory[%s] could not be deleted because %s", fileName, WZ_PHYSFS_getLastError());
481 }
482 }
483
484 char lastSavePath[PATH_MAX];
485 bool lastSaveMP;
486 static time_t lastSaveTime;
487
findLastSaveFrom(const char * path)488 static bool findLastSaveFrom(const char *path)
489 {
490 bool found = false;
491
492 WZ_PHYSFS_enumerateFiles(path, [&path, &found](const char *i) -> bool {
493 char savefile[PATH_MAX];
494 time_t savetime;
495
496 if (!isASavedGamefile(i))
497 {
498 // If it doesn't, move on to the next filename
499 return true;
500 }
501 /* Figure save-time */
502 snprintf(savefile, sizeof(savefile), "%s/%s", path, i);
503 savetime = WZ_PHYSFS_getLastModTime(savefile);
504 if (difftime(savetime, lastSaveTime) > 0.0)
505 {
506 lastSaveTime = savetime;
507 strcpy(lastSavePath, savefile);
508 found = true;
509 }
510 return true;
511 });
512 return found;
513 }
514
findLastSave()515 bool findLastSave()
516 {
517 char NewSaveGamePath[PATH_MAX] = {'\0'};
518 bool foundMP, foundCAM;
519
520 lastSaveTime = 0;
521 lastSaveMP = false;
522 lastSavePath[0] = '\0';
523 ssprintf(NewSaveGamePath, "%scampaign/", SaveGamePath);
524 foundCAM = findLastSaveFrom(NewSaveGamePath);
525 ssprintf(NewSaveGamePath, "%scampaign/auto/", SaveGamePath);
526 foundCAM |= findLastSaveFrom(NewSaveGamePath);
527 ssprintf(NewSaveGamePath, "%sskirmish/", SaveGamePath);
528 foundMP = findLastSaveFrom(NewSaveGamePath);
529 ssprintf(NewSaveGamePath, "%sskirmish/auto/", SaveGamePath);
530 foundMP |= findLastSaveFrom(NewSaveGamePath);
531 if (foundMP)
532 {
533 lastSaveMP = true;
534 }
535 return foundMP | foundCAM;
536 }
537
suggestSaveName(const char * NewSaveGamePath)538 static WzString suggestSaveName(const char *NewSaveGamePath)
539 {
540 const WzString levelName = getLevelName();
541 const std::string cheatedSuffix = Cheated ? _("cheated") : "";
542 char saveNamePartial[64] = "\0";
543
544 if (bLoadSaveMode == SAVE_MISSIONEND || bLoadSaveMode == SAVE_INGAME_MISSION)
545 {
546 std::string campaignName;
547 if (levelName.startsWith("CAM_1") || levelName.startsWith("SUB_1"))
548 {
549 campaignName = "Alpha";
550 }
551 else if (levelName.startsWith("CAM_2") || levelName.startsWith("SUB_2"))
552 {
553 campaignName = "Beta";
554 }
555 else if (levelName.startsWith("CAM_3") || levelName.startsWith("SUB_3"))
556 {
557 campaignName = "Gamma";
558 }
559 ssprintf(saveNamePartial, "%s %s %s", campaignName.c_str(), levelName.toStdString().c_str(),
560 cheatedSuffix.c_str());
561 }
562 else if (bLoadSaveMode == SAVE_INGAME_SKIRMISH)
563 {
564 int humanPlayers = 0;
565 for (int i = 0; i < MAX_PLAYERS; i++)
566 {
567 if (isHumanPlayer(i))
568 {
569 humanPlayers++;
570 }
571 }
572
573 ssprintf(saveNamePartial, "%s %dp %s", levelName.toStdString().c_str(), humanPlayers, cheatedSuffix.c_str());
574 }
575
576 WzString saveName = WzString(saveNamePartial).trimmed();
577 int similarSaveGames = 0;
578 WZ_PHYSFS_enumerateFiles(NewSaveGamePath, [&similarSaveGames, &saveName](const char *fileName) -> bool {
579 if (isASavedGamefile(fileName) && WzString(fileName).startsWith(saveName))
580 {
581 similarSaveGames++;
582 }
583 return true;
584 });
585
586
587 if (similarSaveGames > 0)
588 {
589 saveName += " " + WzString::number(similarSaveGames + 1);
590 }
591
592 return saveName;
593 }
594
runLoadSaveCleanup(bool resetWidgets,bool goBack)595 static void runLoadSaveCleanup(bool resetWidgets, bool goBack)
596 {
597 closeLoadSave(goBack);
598 bRequestLoad = false;
599 if (!goBack && resetWidgets && widgGetFromID(psWScreen, IDMISSIONRES_FORM) == nullptr)
600 {
601 resetMissionWidgets();
602 }
603 }
604
runLoadCleanup()605 static void runLoadCleanup()
606 {
607 int campaign = getCampaign(sRequestResult);
608 setCampaignNumber(campaign);
609 debug(LOG_WZ, "Set campaign for %s to %u", sRequestResult, campaign);
610 closeLoadSave();
611 bRequestLoad = true;
612 }
613
614 // ////////////////////////////////////////////////////////////////////////////
615 // Returns true if cancel pressed or a valid game slot was selected.
616 // if when returning true strlen(sRequestResult) != 0 then a valid game slot was selected
617 // otherwise cancel was selected..
runLoadSave(bool bResetMissionWidgets)618 bool runLoadSave(bool bResetMissionWidgets)
619 {
620 static char sDelete[PATH_MAX];
621 char NewSaveGamePath[PATH_MAX] = {'\0'};
622
623 WidgetTriggers const &triggers = widgRunScreen(psRequestScreen);
624 unsigned id = triggers.empty() ? 0 : triggers.front().widget->id; // Just use first click here, since the next click could be on another menu.
625
626 sstrcpy(sRequestResult, ""); // set returned filename to null;
627
628 if (id == LOADSAVE_CANCEL || CancelPressed())
629 {
630 runLoadSaveCleanup(bResetMissionWidgets, true);
631 return true;
632 }
633 if (bMultiPlayer)
634 {
635 if (bLoadSaveMode >= LOAD_FRONTEND_MISSION_AUTO)
636 {
637 ssprintf(NewSaveGamePath, "%s%s/auto/", SaveGamePath, "skirmish");
638 }
639 else
640 {
641 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "skirmish");
642 }
643 }
644 else
645 {
646 if (bLoadSaveMode >= LOAD_FRONTEND_MISSION_AUTO)
647 {
648 ssprintf(NewSaveGamePath, "%s%s/auto/", SaveGamePath, "campaign");
649 }
650 else
651 {
652 ssprintf(NewSaveGamePath, "%s%s/", SaveGamePath, "campaign");
653 }
654 }
655 if (id == LOADENTRY_START && mode) // [auto] or [..], ignore click for saves
656 {
657 int iLoadSaveMode = (int)bLoadSaveMode; // for evil integer arithmetics
658 bLoadSaveMode = (enum LOADSAVE_MODE)(iLoadSaveMode ^ 16); // toggle _AUTO bit
659 closeLoadSave(); // decrement game time / pause counters
660 addLoadSave(bLoadSaveMode, savedTitle); // restart in other directory
661 return false;
662 }
663 // clicked a load entry
664 else if (id > LOADENTRY_START && id <= LOADENTRY_END)
665 {
666 W_BUTTON *slotButton = (W_BUTTON *)widgGetFromID(psRequestScreen, id);
667
668 if (mode) // Loading, return that entry.
669 {
670 if (!slotButton->pText.isEmpty())
671 {
672 ssprintf(sRequestResult, "%s%s%s", NewSaveGamePath, ((W_BUTTON *)widgGetFromID(psRequestScreen, id))->pText.toUtf8().c_str(), sSaveGameExtension);
673 }
674 else
675 {
676 return false; // clicked on an empty box
677 }
678
679 runLoadCleanup();
680 return true;
681 }
682 else // SAVING!add edit box at that position.
683 {
684
685 if (! widgGetFromID(psRequestScreen, SAVEENTRY_EDIT))
686 {
687 WIDGET *parent = widgGetFromID(psRequestScreen, LOADSAVE_FORM);
688
689 // add blank box.
690 auto saveEntryEdit = std::make_shared<W_EDITBOX>();
691 parent->attach(saveEntryEdit);
692 saveEntryEdit->id = SAVEENTRY_EDIT;
693 saveEntryEdit->setGeometry(slotButton->geometry());
694 saveEntryEdit->setString(slotButton->getString());
695 saveEntryEdit->setBoxColours(WZCOL_MENU_LOAD_BORDER, WZCOL_MENU_LOAD_BORDER, WZCOL_MENU_BACKGROUND);
696
697 if (!slotButton->pText.isEmpty())
698 {
699 ssprintf(sDelete, "%s%s%s", NewSaveGamePath, slotButton->pText.toUtf8().c_str(), sSaveGameExtension);
700 }
701 else
702 {
703 WzString suggestedSaveName = suggestSaveName(NewSaveGamePath);
704 saveEntryEdit->setString(suggestedSaveName);
705 sstrcpy(sDelete, "");
706 }
707
708 slotButton->hide(); // hide the old button
709 chosenSlotId = id;
710
711 // auto click in the edit box we just made.
712 W_CONTEXT context = W_CONTEXT::ZeroContext();
713 context.mx = mouseX();
714 context.my = mouseY();
715 saveEntryEdit->clicked(&context);
716 }
717 }
718 }
719
720 // finished entering a name.
721 if (id == SAVEENTRY_EDIT)
722 {
723 char sTemp[MAX_STR_LENGTH];
724
725 if (!keyPressed(KEY_RETURN) && !keyPressed(KEY_KPENTER)) // enter was not pushed, so not a vaild entry.
726 {
727 widgDelete(psRequestScreen, SAVEENTRY_EDIT); //unselect this box, and go back ..
728 widgReveal(psRequestScreen, chosenSlotId);
729 return true;
730 }
731
732
733 // scan to see if that game exists in another slot, if so then fail.
734 sstrcpy(sTemp, widgGetString(psRequestScreen, id));
735
736 for (int i = LOADENTRY_START; i < LOADENTRY_END; i++)
737 {
738 if (i != chosenSlotId)
739 {
740
741 if (!((W_BUTTON *)widgGetFromID(psRequestScreen, i))->pText.isEmpty()
742 && strcmp(sTemp, ((W_BUTTON *)widgGetFromID(psRequestScreen, i))->pText.toUtf8().c_str()) == 0)
743 {
744 widgDelete(psRequestScreen, SAVEENTRY_EDIT); //unselect this box, and go back ..
745 widgReveal(psRequestScreen, chosenSlotId);
746 audio_PlayTrack(ID_SOUND_BUILD_FAIL);
747 return true;
748 }
749 }
750 }
751
752
753 // return with this name, as we've edited it.
754 if (strlen(widgGetString(psRequestScreen, id)))
755 {
756 sstrcpy(sTemp, widgGetString(psRequestScreen, id));
757 removeWildcards(sTemp);
758 snprintf(sRequestResult, sizeof(sRequestResult), "%s%s%s", NewSaveGamePath, sTemp, sSaveGameExtension);
759 if (strlen(sDelete) != 0)
760 {
761 deleteSaveGame(sDelete); //only delete game if a new game fills the slot
762 }
763 }
764
765 runLoadSaveCleanup(bResetMissionWidgets, false);
766 return true;
767 }
768
769 return false;
770 }
771
772 // ////////////////////////////////////////////////////////////////////////////
773 // should be done when drawing the other widgets.
displayLoadSave()774 bool displayLoadSave()
775 {
776 widgDisplayScreen(psRequestScreen); // display widgets.
777 return true;
778 }
779
780
781 // ////////////////////////////////////////////////////////////////////////////
782 // char HANDLER, replaces dos wildcards in a string with harmless chars.
removeWildcards(char * pStr)783 void removeWildcards(char *pStr)
784 {
785 UDWORD i;
786
787 // Remember never to allow: < > : " / \ | ? *
788
789 // Whitelist: Get rid of any characters except:
790 // a-z A-Z 0-9 - + ! , = ^ @ # $ % & ' ( ) [ ] (and space and unicode characters ≥ 0x80)
791 for (i = 0; i < strlen(pStr); i++)
792 {
793 if (!isalnum(pStr[i])
794 && (pStr[i] != ' ' || i == 0 || pStr[i - 1] == ' ')
795 // We allow spaces as long as they aren't the first char, or two spaces in a row
796 && pStr[i] != '-'
797 && pStr[i] != '+'
798 && pStr[i] != '[' && pStr[i] != ']'
799 && (pStr[i] & 0x80) != 0x80 // á é í ó ú α β γ δ ε
800 )
801 {
802 pStr[i] = '_';
803 }
804 }
805
806 if (strlen(pStr) >= MAX_SAVE_NAME)
807 {
808 pStr[MAX_SAVE_NAME - 1] = 0;
809 }
810 else if (strlen(pStr) == 0)
811 {
812 pStr[0] = '!';
813 pStr[1] = 0;
814 return;
815 }
816 // Trim trailing spaces
817 for (i = strlen(pStr); i > 0 && pStr[i - 1] == ' '; --i)
818 {}
819 pStr[i] = 0;
820
821 // If that leaves us with a blank string, replace with '!'
822 if (pStr[0] == 0)
823 {
824 pStr[0] = '!';
825 pStr[1] = 0;
826 }
827 }
828
829 // ////////////////////////////////////////////////////////////////////////////
830 // ////////////////////////////////////////////////////////////////////////////
831 // ////////////////////////////////////////////////////////////////////////////
832 // DISPLAY FUNCTIONS
833
displayLoadBanner(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)834 static void displayLoadBanner(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
835 {
836 PIELIGHT col;
837 int x = xOffset + psWidget->x();
838 int y = yOffset + psWidget->y();
839
840 if (psWidget->pUserData)
841 {
842 col = WZCOL_GREEN;
843 }
844 else
845 {
846 col = WZCOL_MENU_LOAD_BORDER;
847 }
848
849 pie_BoxFill(x, y, x + psWidget->width(), y + psWidget->height(), col);
850 pie_BoxFill(x + 2, y + 2, x + psWidget->width() - 2, y + psWidget->height() - 2, WZCOL_MENU_BACKGROUND);
851 }
852
853 // ////////////////////////////////////////////////////////////////////////////
displayLoadSlot(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)854 static void displayLoadSlot(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
855 {
856 // Any widget using displayLoadSlot must have its pUserData initialized to a (LoadSaveDisplayLoadSlotCache*)
857 assert(psWidget->pUserData != nullptr);
858 LoadSaveDisplayLoadSlotCache& cache = *static_cast<LoadSaveDisplayLoadSlotCache *>(psWidget->pUserData);
859
860 int x = xOffset + psWidget->x();
861 int y = yOffset + psWidget->y();
862 char butString[64];
863
864 drawBlueBox(x, y, psWidget->width(), psWidget->height()); //draw box
865
866 if (!((W_BUTTON *)psWidget)->pText.isEmpty())
867 {
868 sstrcpy(butString, ((W_BUTTON *)psWidget)->pText.toUtf8().c_str());
869
870 if (cache.fullText != butString)
871 {
872 // Update cache
873 while (iV_GetTextWidth(butString, font_regular) > psWidget->width())
874 {
875 butString[strlen(butString) - 1] = '\0';
876 }
877 cache.wzText.setText(butString, font_regular);
878 cache.fullText = butString;
879 }
880
881 cache.wzText.render(x + 4, y + 17, WZCOL_FORM_TEXT);
882 }
883 }
884
drawBlueBoxInset(UDWORD x,UDWORD y,UDWORD w,UDWORD h)885 void drawBlueBoxInset(UDWORD x, UDWORD y, UDWORD w, UDWORD h)
886 {
887 pie_BoxFill(x, y, x + w, y + h, WZCOL_MENU_BORDER);
888 pie_BoxFill(x + 1, y + 1, x + w - 1, y + h - 1, WZCOL_MENU_BACKGROUND);
889 }
890
891 /**
892 * Same as drawBlueBoxInset, but the rectangle is overflown by one pixel.
893 */
drawBlueBox(UDWORD x,UDWORD y,UDWORD w,UDWORD h)894 void drawBlueBox(UDWORD x, UDWORD y, UDWORD w, UDWORD h)
895 {
896 drawBlueBoxInset(x - 1, y - 1, w + 2, h + 2);
897 }
898
freeAutoSaveSlot(const char * path)899 static void freeAutoSaveSlot(const char *path)
900 {
901 char **i, **files;
902 files = PHYSFS_enumerateFiles(path);
903 ASSERT_OR_RETURN(, files, "PHYSFS_enumerateFiles(\"%s\") failed: %s", path, WZ_PHYSFS_getLastError());
904 int nfiles = 0;
905 for (i = files; *i != nullptr; ++i)
906 {
907 if (!isASavedGamefile(*i))
908 {
909 // If it doesn't, move on to the next filename
910 continue;
911 }
912 nfiles++;
913 }
914 if (nfiles < totalslots)
915 {
916 PHYSFS_freeList(files);
917 return;
918 }
919
920 // too many autosaves, let's delete the oldest
921 char oldestSavePath[PATH_MAX];
922 time_t oldestSaveTime = time(nullptr);
923 for (i = files; *i != nullptr; ++i)
924 {
925 char savefile[PATH_MAX];
926
927 if (!isASavedGamefile(*i))
928 {
929 // If it doesn't, move on to the next filename
930 continue;
931 }
932 /* Figure save-time */
933 snprintf(savefile, sizeof(savefile), "%s/%s", path, *i);
934 time_t savetime = WZ_PHYSFS_getLastModTime(savefile);
935 if (difftime(savetime, oldestSaveTime) < 0.0)
936 {
937 oldestSaveTime = savetime;
938 strcpy(oldestSavePath, savefile);
939 }
940 }
941 PHYSFS_freeList(files);
942 deleteSaveGame(oldestSavePath);
943 }
944
autoSave()945 bool autoSave()
946 {
947 // Bail out if we're running a _true_ multiplayer game or are playing a tutorial/debug/cheating/autogames
948 if (!autosaveEnabled || runningMultiplayer() || bInTutorial || getDebugMappingStatus() || Cheated || autogame_enabled())
949 {
950 return false;
951 }
952 const char *dir = bMultiPlayer? AUTOSAVE_SKI_DIR : AUTOSAVE_CAM_DIR;
953 freeAutoSaveSlot(dir);
954
955 time_t now = time(nullptr);
956 struct tm timeinfo = getLocalTime(now);
957 char savedate[PATH_MAX];
958 strftime(savedate, sizeof(savedate), "%F_%H%M%S", &timeinfo);
959
960 std::string withoutTechlevel = mapNameWithoutTechlevel(getLevelName());
961 char savefile[PATH_MAX];
962 snprintf(savefile, sizeof(savefile), "%s/%s_%s.gam", dir, withoutTechlevel.c_str(), savedate);
963 if (saveGame(savefile, GTYPE_SAVE_MIDMISSION))
964 {
965 console("AutoSave %s", savefile);
966 return true;
967 }
968 else
969 {
970 console("AutoSave %s failed", savefile);
971 return false;
972 }
973 }
974