1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "gui/saveload.h"
24 
25 #include "graphics/thumbnail.h"
26 
27 #include "common/file.h"
28 #include "common/savefile.h"
29 #include "common/serializer.h"
30 #include "common/translation.h"
31 
32 #include "startrek/resource.h"
33 #include "startrek/room.h"
34 #include "startrek/startrek.h"
35 
36 namespace StarTrek {
37 
showSaveMenu()38 bool StarTrekEngine::showSaveMenu() {
39 	GUI::SaveLoadChooser *dialog;
40 	Common::String desc;
41 	int slot;
42 
43 	dialog = new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true);
44 
45 	slot = dialog->runModalWithCurrentTarget();
46 	desc = dialog->getResultString();
47 
48 	if (desc.empty()) {
49 		// create our own description for the saved game, the user didnt enter it
50 		desc = dialog->createDefaultSaveDescription(slot);
51 	}
52 
53 	if (desc.size() > 28)
54 		desc = Common::String(desc.c_str(), 28);
55 
56 	delete dialog;
57 
58 	if (slot < 0)
59 		return true;
60 
61 	return saveGame(slot, desc);
62 }
63 
showLoadMenu()64 bool StarTrekEngine::showLoadMenu() {
65 	GUI::SaveLoadChooser *dialog;
66 	int slot;
67 
68 	dialog = new GUI::SaveLoadChooser(_("Restore game:"), _("Restore"), false);
69 	slot = dialog->runModalWithCurrentTarget();
70 
71 	delete dialog;
72 
73 	if (slot < 0)
74 		return true;
75 
76 	return loadGame(slot);
77 }
78 
79 const uint32 CURRENT_SAVEGAME_VERSION = 1;
80 
saveGame(int slot,Common::String desc)81 bool StarTrekEngine::saveGame(int slot, Common::String desc) {
82 	Common::String filename = getSavegameFilename(slot);
83 	Common::OutSaveFile *out;
84 
85 	if (!(out = _saveFileMan->openForSaving(filename))) {
86 		warning("Can't create file '%s', game not saved", filename.c_str());
87 		return false;
88 	} else {
89 		debug(3, "Successfully opened %s for writing", filename.c_str());
90 	}
91 
92 	SavegameMetadata meta;
93 	meta.version = CURRENT_SAVEGAME_VERSION;
94 	memset(meta.description, 0, sizeof(meta.description));
95 	strncpy(meta.description, desc.c_str(), SAVEGAME_DESCRIPTION_LEN);
96 
97 	TimeDate curTime;
98 	_system->getTimeAndDate(curTime);
99 	meta.setSaveTimeAndDate(curTime);
100 	meta.playTime = g_engine->getTotalPlayTime();
101 
102 	if (!saveOrLoadMetadata(nullptr, out, &meta)) {
103 		delete out;
104 		return false;
105 	}
106 	if (!saveOrLoadGameData(nullptr, out, &meta)) {
107 		delete out;
108 		return false;
109 	}
110 
111 	out->finalize();
112 	delete out;
113 	return true;
114 }
115 
loadGame(int slot)116 bool StarTrekEngine::loadGame(int slot) {
117 	Common::String filename = getSavegameFilename(slot);
118 	Common::InSaveFile *in;
119 
120 	if (!(in = _saveFileMan->openForLoading(filename))) {
121 		warning("Can't open file '%s', game not loaded", filename.c_str());
122 		return false;
123 	} else {
124 		debug(3, "Successfully opened %s for loading", filename.c_str());
125 	}
126 
127 	SavegameMetadata meta;
128 	if (!saveOrLoadMetadata(in, nullptr, &meta)) {
129 		delete in;
130 		return false;
131 	}
132 
133 	if (meta.version > CURRENT_SAVEGAME_VERSION) {
134 		delete in;
135 		error("Savegame version (%u) is newer than current version (%u). A newer version of ScummVM is needed", meta.version, CURRENT_SAVEGAME_VERSION);
136 	}
137 
138 	if (!saveOrLoadGameData(in, nullptr, &meta)) {
139 		delete in;
140 		return false;
141 	}
142 
143 	delete in;
144 
145 	_lastGameMode = _gameMode;
146 
147 	if (_gameMode == GAMEMODE_AWAYMISSION) {
148 		for (int i = 0; i < NUM_ACTORS; i++) {
149 			Actor *a = &_actorList[i];
150 			if (a->spriteDrawn) {
151 				if (a->animType != 1)
152 					a->animFile = SharedPtr<Common::MemoryReadStreamEndian>(_resource->loadFile(a->animFilename + ".anm"));
153 				_gfx->addSprite(&a->sprite);
154 				a->sprite.setBitmap(loadAnimationFrame(a->bitmapFilename, a->scale));
155 			}
156 		}
157 	} else if (_gameMode == -1) {
158 		initBridge(true);
159 		_lastGameMode = GAMEMODE_BRIDGE;
160 		// TODO: mode change
161 	} else {
162 		_resource->setTxtFileName(_missionToLoad);
163 		initBridge(false);
164 		// TODO: mode change
165 	}
166 
167 	return true;
168 }
169 
saveOrLoadGameData(Common::SeekableReadStream * in,Common::WriteStream * out,SavegameMetadata * meta)170 bool StarTrekEngine::saveOrLoadGameData(Common::SeekableReadStream *in, Common::WriteStream *out, SavegameMetadata *meta) {
171 	Common::Serializer ser(in, out);
172 
173 	if (ser.isLoading()) {
174 		if (_lastGameMode == GAMEMODE_BRIDGE)
175 			cleanupBridge();
176 		else // Assume GAMEMODE_AWAYMISSION
177 			unloadRoom();
178 	}
179 
180 	ser.syncAsUint16LE(_gameMode);
181 	// TODO: sub_1d8eb (save) / sub_1d958 (load) (probably bridge / space combat state)
182 
183 	Common::String midiFilename = _sound->_loadedMidiFilename;
184 	ser.syncString(midiFilename);
185 	ser.syncAsSint16LE(_sound->_loopingMidiTrack);
186 
187 	if (ser.isLoading()) {
188 		if (midiFilename.empty())
189 			_sound->clearAllMidiSlots();
190 		else {
191 			_sound->loadMusicFile(midiFilename);
192 			_sound->playMidiMusicTracks(_sound->_loopingMidiTrack, _sound->_loopingMidiTrack);
193 		}
194 	}
195 
196 	ser.syncAsUint16LE(_frameIndex);
197 	ser.syncAsUint16LE(_mouseControllingShip);
198 	if (meta->version >= 1) {
199 		ser.syncAsSint16LE(_enterpriseState.inOrbit);
200 		ser.syncAsSint16LE(_enterpriseState.underAttack);
201 		ser.syncAsSint16LE(_randomEncounterType);
202 
203 		int16 unkFlag1 = 0; // TODO: word_5082e (either 0 or 1)
204 		ser.syncAsSint16LE(unkFlag1);
205 
206 		int unkVar1 = 0;	// TODO: dword_519b0
207 		ser.syncAsSint32LE(unkVar1);
208 
209 		ser.syncAsSint16LE(_currentPlanet);
210 		ser.syncAsSint16LE(_targetPlanet);
211 
212 		int16 unkFlag2 = 0; // TODO: word_45ab8 (either 0 or -1)
213 		ser.syncAsSint16LE(unkFlag2);
214 	}
215 
216 	ser.syncString(_missionToLoad);
217 
218 	if (meta->version >= 1) {
219 		int16 unkFlag3 = 0; // TODO: word_4b032 (either 0 or 1)
220 		ser.syncAsSint16LE(unkFlag3);
221 
222 		ser.syncAsUint16LE(_hailedTarget);
223 		ser.syncAsSint16LE(_lastMissionId);
224 		for (int i = 0; i < 7; i++) {
225 			ser.syncAsUint16LE(_missionPoints[i]);
226 		}
227 	}
228 
229 	ser.syncString(_sound->_loopingAudioName);
230 
231 	if (ser.isLoading()) {
232 		if (!_sound->_loopingAudioName.empty())
233 			_sound->playVoc(_sound->_loopingAudioName);
234 	}
235 
236 	if (meta->version >= 1) {
237 		ser.syncAsSint16LE(_bridgeSequenceToLoad);
238 	}
239 
240 	for (int i = 0; i < NUM_OBJECTS; i++) {
241 		ser.syncAsByte(_itemList[i].have);
242 	}
243 
244 	if (_gameMode == GAMEMODE_AWAYMISSION) {
245 		ser.syncString(_missionName);
246 		ser.syncAsSint16LE(_roomIndex);
247 
248 		if (ser.isLoading()) {
249 			_gfx->fadeoutScreen();
250 			_resource->setTxtFileName("ground");
251 
252 			// This must be done before loading the actor variables, since this clears
253 			// them.
254 			loadRoom(_missionName, _roomIndex);
255 		}
256 
257 		ser.syncAsUint32LE(_roomFrameCounter);
258 		ser.syncAsUint32LE(_frameIndex); // FIXME: redundant
259 
260 		byte filler = 0;
261 
262 		// Serialize the "actor" class
263 		for (int i = 0; i < NUM_ACTORS; i++) {
264 			Actor *a = &_actorList[i];
265 			ser.syncAsUint16LE(a->spriteDrawn);
266 			ser.syncString(a->animFilename);
267 			if (a->animFilename.size() < 15) {
268 				filler = 0;
269 				for (uint j = 0; j < 16 - a->animFilename.size() - 1; ++j)
270 					ser.syncAsByte(filler);	// make sure that exactly 16 bytes are synced
271 			}
272 			a->animFilename.trim();
273 
274 			ser.syncAsUint16LE(a->animType);
275 
276 			a->sprite.saveLoadWithSerializer(ser);
277 
278 			ser.syncString(a->bitmapFilename);
279 			if (a->bitmapFilename.size() < 9) {
280 				filler = 0;
281 				for (uint j = 0; j < 10 - a->bitmapFilename.size() - 1; ++j)
282 					ser.syncAsByte(filler);	// make sure that exactly 10 bytes are synced
283 			}
284 			a->bitmapFilename.trim();
285 
286 			a->scale.saveLoadWithSerializer(ser);
287 			// Can't save "animFile" (will be reloaded)
288 			ser.syncAsUint16LE(a->numAnimFrames);
289 			ser.syncAsUint16LE(a->animFrame);
290 			ser.syncAsUint32LE(a->frameToStartNextAnim);
291 			ser.syncAsSint16LE(a->pos.x);
292 			ser.syncAsSint16LE(a->pos.y);
293 			ser.syncAsUint16LE(a->field60);
294 			ser.syncAsUint16LE(a->field62);
295 			ser.syncAsUint16LE(a->triggerActionWhenAnimFinished);
296 			ser.syncAsUint16LE(a->finishedAnimActionParam);
297 			ser.syncString(a->animationString2);
298 			if (a->animationString2.size() < 7) {
299 				filler = 0;
300 				for (uint j = 0; j < 8 - a->animationString2.size() - 1; ++j)
301 					ser.syncAsByte(filler);	// make sure that exactly 8 bytes are synced
302 			}
303 			ser.syncAsUint16LE(a->field70);
304 			ser.syncAsUint16LE(a->field72);
305 			ser.syncAsUint16LE(a->field74);
306 			ser.syncAsUint16LE(a->field76);
307 			ser.syncAsSint16LE(a->iwSrcPosition);
308 			ser.syncAsSint16LE(a->iwDestPosition);
309 			a->granularPosX.saveLoadWithSerializer(ser);
310 			a->granularPosY.saveLoadWithSerializer(ser);
311 			a->speedX.saveLoadWithSerializer(ser);
312 			a->speedY.saveLoadWithSerializer(ser);
313 			ser.syncAsSint16LE(a->dest.x);
314 			ser.syncAsSint16LE(a->dest.y);
315 			ser.syncAsUint16LE(a->field90);
316 			ser.syncAsByte(a->field92);
317 			ser.syncAsByte(a->direction);
318 			ser.syncAsUint16LE(a->field94);
319 			ser.syncAsUint16LE(a->field96);
320 			ser.syncString(a->animationString);
321 			if (a->animationString.size() < 9) {
322 				filler = 0;
323 				for (uint j = 0; j < 10 - a->animationString.size() - 1; ++j)
324 					ser.syncAsByte(filler);	// make sure that exactly 10 bytes are synced
325 			}
326 			ser.syncAsUint16LE(a->fielda2);
327 			ser.syncAsUint16LE(a->fielda4);
328 			ser.syncAsUint16LE(a->fielda6);
329 		}
330 
331 		Common::String unused = getScreenName();
332 		ser.syncString(unused);
333 
334 		// Away mission struct
335 		for (int i = 0; i < 8; i++)
336 			ser.syncAsSint16LE(_awayMission.timers[i]);
337 		ser.syncAsSint16LE(_awayMission.mouseX);
338 		ser.syncAsSint16LE(_awayMission.mouseY);
339 		for (int i = 0; i < 4; i++)
340 			ser.syncAsSint16LE(_awayMission.crewGetupTimers[i]);
341 		ser.syncAsByte(_awayMission.disableWalking);
342 		ser.syncAsByte(_awayMission.disableInput);
343 		ser.syncAsByte(_awayMission.redshirtDead);
344 		ser.syncAsByte(_awayMission.activeAction);
345 		ser.syncAsByte(_awayMission.activeObject);
346 		ser.syncAsByte(_awayMission.passiveObject);
347 		ser.syncAsByte(_awayMission.rdfStillDoDefaultAction);
348 		ser.syncAsByte(_awayMission.crewDownBitset);
349 		for (int i = 0; i < 4; i++)
350 			ser.syncAsByte(_awayMission.crewDirectionsAfterWalk[i]);
351 
352 		if (_missionName == "DEMON") {
353 			_awayMission.demon.saveLoadWithSerializer(ser);
354 			_room->_roomVar.demon.saveLoadWithSerializer(ser);
355 		} else if (_missionName == "TUG") {
356 			_awayMission.tug.saveLoadWithSerializer(ser);
357 			_room->_roomVar.tug.saveLoadWithSerializer(ser);
358 		} else if (_missionName == "LOVE") {
359 			_awayMission.love.saveLoadWithSerializer(ser);
360 			_room->_roomVar.love.saveLoadWithSerializer(ser);
361 		} else if (_missionName == "MUDD") {
362 			_awayMission.mudd.saveLoadWithSerializer(ser);
363 			_room->_roomVar.mudd.saveLoadWithSerializer(ser);
364 		} else if (_missionName == "FEATHER") {
365 			_awayMission.feather.saveLoadWithSerializer(ser);
366 			_room->_roomVar.feather.saveLoadWithSerializer(ser);
367 		} else if (_missionName == "TRIAL") {
368 			_awayMission.trial.saveLoadWithSerializer(ser);
369 			_room->_roomVar.trial.saveLoadWithSerializer(ser);
370 		} else if (_missionName == "SINS") {
371 			_awayMission.sins.saveLoadWithSerializer(ser);
372 			_room->_roomVar.sins.saveLoadWithSerializer(ser);
373 		} else if (_missionName == "VENG") {
374 			_awayMission.veng.saveLoadWithSerializer(ser);
375 			_room->_roomVar.veng.saveLoadWithSerializer(ser);
376 		}
377 
378 		// The action queue
379 		if (ser.isLoading()) {
380 			_actionQueue = Common::Queue<Action>();
381 			int16 n = 0;
382 			ser.syncAsSint16LE(n);
383 			for (int i = 0; i < n; i++) {
384 				Action a;
385 				a.saveLoadWithSerializer(ser);
386 				_actionQueue.push(a);
387 			}
388 		} else { // Saving
389 			int16 n = _actionQueue.size();
390 			ser.syncAsSint16LE(n);
391 			for (int i = 0; i < n; i++) {
392 				Action a = _actionQueue.pop();
393 				a.saveLoadWithSerializer(ser);
394 				_actionQueue.push(a);
395 			}
396 		}
397 
398 		// Original game located changes in RDF files and saved them. Since RDF files
399 		// aren't modified directly here, that's skipped.
400 
401 		ser.syncAsSint16LE(_objectHasWalkPosition);
402 		ser.syncAsSint16LE(_objectWalkPosition.x);
403 		ser.syncAsSint16LE(_objectWalkPosition.y);
404 
405 		for (int i = 0; i < MAX_BUFFERED_WALK_ACTIONS; i++) {
406 			_actionOnWalkCompletion[i].saveLoadWithSerializer(ser);
407 			ser.syncAsByte(_actionOnWalkCompletionInUse[i]);
408 		}
409 
410 		ser.syncAsSint16LE(_warpHotspotsActive);
411 	}
412 
413 	return true;
414 }
415 
getSavegameFilename(int slotId) const416 Common::String StarTrekEngine::getSavegameFilename(int slotId) const {
417 	Common::String saveLoadSlot = _targetName;
418 	saveLoadSlot += Common::String::format(".%.3d", slotId);
419 	return saveLoadSlot;
420 }
421 
422 
423 // Static function (reused in detection.cpp)
saveOrLoadMetadata(Common::SeekableReadStream * in,Common::WriteStream * out,SavegameMetadata * meta)424 bool saveOrLoadMetadata(Common::SeekableReadStream *in, Common::WriteStream *out, SavegameMetadata *meta) {
425 	Common::Serializer ser(in, out);
426 
427 	ser.syncAsUint32LE(meta->version);
428 	ser.syncBytes((byte *)meta->description, SAVEGAME_DESCRIPTION_LEN + 1);
429 
430 	// Thumbnail
431 	if (ser.isLoading()) {
432 		if (!::Graphics::loadThumbnail(*in, meta->thumbnail))
433 			meta->thumbnail = nullptr;
434 	} else
435 		::Graphics::saveThumbnail(*out);
436 
437 	// Creation date/time
438 	ser.syncAsUint32LE(meta->saveDate);
439 	debugC(5, kDebugSavegame, "Save date: %d", meta->saveDate);
440 	ser.syncAsUint16LE(meta->saveTime);
441 	debugC(5, kDebugSavegame, "Save time: %d", meta->saveTime);
442 	ser.syncAsByte(meta->saveTimeSecs); // write seconds of save time as well
443 	ser.syncAsUint32LE(meta->playTime);
444 	debugC(5, kDebugSavegame, "Play time: %d", meta->playTime);
445 
446 	return true;
447 }
448 
449 } // End of namespace StarTrek
450