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