scummvm/engines/stark/stark.cpp
Bastien Bouclet 8b1beea68a STARK: Add savegame version handling
The save file header has a version attrubute, as well as each serialized
resource tree. The reason for this is that serialized resource trees
are not necessarily rehydrated into objects during the gameplay.
Thus the save version is not always consistent across the serialized resource
trees in a save file.
2017-05-30 19:07:42 +02:00

424 lines
13 KiB
C++

/* ResidualVM - A 3D game interpreter
*
* ResidualVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the AUTHORS
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "engines/stark/stark.h"
#include "engines/stark/console.h"
#include "engines/stark/debug.h"
#include "engines/stark/resources/level.h"
#include "engines/stark/resources/location.h"
#include "engines/stark/scene.h"
#include "engines/stark/services/userinterface.h"
#include "engines/stark/services/archiveloader.h"
#include "engines/stark/services/dialogplayer.h"
#include "engines/stark/services/diary.h"
#include "engines/stark/services/fontprovider.h"
#include "engines/stark/services/gameinterface.h"
#include "engines/stark/services/global.h"
#include "engines/stark/services/resourceprovider.h"
#include "engines/stark/services/services.h"
#include "engines/stark/services/stateprovider.h"
#include "engines/stark/services/staticprovider.h"
#include "engines/stark/gfx/driver.h"
#include "engines/stark/gfx/framelimiter.h"
#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/random.h"
#include "common/savefile.h"
#include "common/system.h"
#include "audio/mixer.h"
namespace Stark {
StarkEngine::StarkEngine(OSystem *syst, const ADGameDescription *gameDesc) :
Engine(syst),
_gameDescription(gameDesc),
_gfx(nullptr),
_frameLimiter(nullptr),
_scene(nullptr),
_console(nullptr),
_global(nullptr),
_gameInterface(nullptr),
_archiveLoader(nullptr),
_stateProvider(nullptr),
_staticProvider(nullptr),
_resourceProvider(nullptr),
_randomSource(nullptr),
_dialogPlayer(nullptr),
_diary(nullptr),
_userInterface(nullptr),
_fontProvider(nullptr),
_lastClickTime(0) {
_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, 127);
_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, ConfMan.getInt("sfx_volume"));
_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, ConfMan.getInt("speech_volume"));
_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, ConfMan.getInt("music_volume"));
// Add the available debug channels
DebugMan.addDebugChannel(kDebugArchive, "Archive", "Debug the archive loading");
DebugMan.addDebugChannel(kDebugXMG, "XMG", "Debug the loading of XMG images");
DebugMan.addDebugChannel(kDebugXRC, "XRC", "Debug the loading of XRC resource trees");
DebugMan.addDebugChannel(kDebugUnknown, "Unknown", "Debug unknown values on the data");
}
StarkEngine::~StarkEngine() {
delete _gameInterface;
delete _diary;
delete _dialogPlayer;
delete _randomSource;
delete _scene;
delete _console;
delete _gfx;
delete _frameLimiter;
delete _staticProvider;
delete _resourceProvider;
delete _global;
delete _stateProvider;
delete _archiveLoader;
delete _userInterface;
delete _fontProvider;
StarkServices::destroy();
}
Common::Error StarkEngine::run() {
_console = new Console();
_gfx = Gfx::Driver::create();
// Get the screen prepared
_gfx->init();
_frameLimiter = new Gfx::FrameLimiter(_system, ConfMan.getInt("engine_speed"));
_archiveLoader = new ArchiveLoader();
_stateProvider = new StateProvider();
_global = new Global();
_resourceProvider = new ResourceProvider(_archiveLoader, _stateProvider, _global);
_staticProvider = new StaticProvider(_archiveLoader);
_randomSource = new Common::RandomSource("stark");
_fontProvider = new FontProvider();
_scene = new Scene(_gfx);
_dialogPlayer = new DialogPlayer();
_diary = new Diary();
_gameInterface = new GameInterface();
_userInterface = new UserInterface(_gfx);
// Setup the public services
StarkServices &services = StarkServices::instance();
services.archiveLoader = _archiveLoader;
services.dialogPlayer = _dialogPlayer;
services.diary = _diary;
services.gfx = _gfx;
services.global = _global;
services.resourceProvider = _resourceProvider;
services.randomSource = _randomSource;
services.scene = _scene;
services.staticProvider = _staticProvider;
services.gameInterface = _gameInterface;
services.userInterface = _userInterface;
services.fontProvider = _fontProvider;
// Load global resources
_resourceProvider->initGlobal();
_staticProvider->init();
_fontProvider->initFonts();
// Initialize the UI
_userInterface->init();
if (ConfMan.hasKey("save_slot")) {
// Load game from specified slot, if any
loadGameState(ConfMan.getInt("save_slot"));
} else {
// Otherwise, set the startup location, ie the House of All Worlds by default
setStartupLocation();
}
// Start running
mainLoop();
_staticProvider->shutdown();
_resourceProvider->shutdown();
return Common::kNoError;
}
void StarkEngine::setStartupLocation() {
if (ConfMan.hasKey("startup_chapter")) {
_global->setCurrentChapter(ConfMan.getInt("startup_chapter"));
} else {
_global->setCurrentChapter(0);
}
if (ConfMan.hasKey("startup_level") && ConfMan.hasKey("startup_location")) {
uint levelIndex = strtol(ConfMan.get("startup_level").c_str(), nullptr, 16);
uint locationIndex = strtol(ConfMan.get("startup_location").c_str(), nullptr, 16);
_resourceProvider->requestLocationChange(levelIndex, locationIndex);
} else {
if (isDemo()) {
_resourceProvider->requestLocationChange(0x4f, 0x00);
} else {
// Start us up at the house of all worlds
_resourceProvider->requestLocationChange(0x45, 0x00);
}
}
}
void StarkEngine::mainLoop() {
while (!shouldQuit()) {
_frameLimiter->startFrame();
processEvents();
if (_userInterface->shouldExit()) {
quitGame();
break;
}
if (_resourceProvider->hasLocationChangeRequest()) {
_global->setNormalSpeed();
_resourceProvider->performLocationChange();
}
updateDisplayScene();
// Swap buffers
_frameLimiter->delayBeforeSwap();
_gfx->flipBuffer();
}
}
void StarkEngine::processEvents() {
Common::Event e;
while (g_system->getEventManager()->pollEvent(e)) {
// Handle any buttons, keys and joystick operations
if (e.type == Common::EVENT_KEYDOWN) {
if (e.kbd.keycode == Common::KEYCODE_d) {
if (e.kbd.flags & Common::KBD_CTRL) {
_console->attach();
_console->onFrame();
}
} else if (e.kbd.keycode == Common::KEYCODE_ESCAPE) {
// Quick-hack for now.
bool skipped = _gameInterface->skipCurrentSpeeches();
if (!skipped) {
skipped = _userInterface->skipFMV();
}
if (!skipped) {
_global->setFastForward();
}
}
} else if (e.type == Common::EVENT_LBUTTONUP) {
// Do nothing for now
} else if (e.type == Common::EVENT_MOUSEMOVE) {
_userInterface->handleMouseMove(e.mouse);
} else if (e.type == Common::EVENT_LBUTTONDOWN) {
_userInterface->handleClick();
if (_system->getMillis() - _lastClickTime < _doubleClickDelay) {
_userInterface->handleDoubleClick();
}
_lastClickTime = _system->getMillis();
} else if (e.type == Common::EVENT_RBUTTONDOWN) {
_userInterface->handleRightClick();
}
}
}
void StarkEngine::updateDisplayScene() {
if (_global->isFastForward()) {
// The original engine was frame limited to 30 fps.
// Set the frame duration to 1000 / 30 ms so that fast forward
// skips the same amount of simulated time as the original.
_global->setMillisecondsPerGameloop(33);
} else {
_global->setMillisecondsPerGameloop(_frameLimiter->getLastFrameDuration());
}
// Clear the screen
_gfx->clearScreen();
// Only update the world resources when on the game screen
if (_userInterface->isInGameScreen()) {
int frames = 0;
do {
// Update the game resources
_global->getLevel()->onGameLoop();
_global->getCurrent()->getLevel()->onGameLoop();
_global->getCurrent()->getLocation()->onGameLoop();
frames++;
// When the game is in fast forward mode, update
// the game resources for multiple frames,
// but render only once.
} while (_global->isFastForward() && frames < 100);
_global->setNormalSpeed();
}
// Render the current scene
// Update the UI state before displaying the scene
_userInterface->update();
// Tell the UI to render, and update implicitly, if this leads to new mouse-over events.
_userInterface->render();
}
bool StarkEngine::hasFeature(EngineFeature f) const {
return
(f == kSupportsLoadingDuringRuntime) ||
(f == kSupportsSavingDuringRuntime) ||
(f == kSupportsArbitraryResolutions) ||
(f == kSupportsRTL);
}
bool StarkEngine::canLoadGameStateCurrently() {
return true;
}
Common::Error StarkEngine::loadGameState(int slot) {
// Open the save file
Common::String filename = formatSaveName(_targetName.c_str(), slot);
Common::InSaveFile *save = _saveFileMan->openForLoading(filename);
if (!save) {
return _saveFileMan->getError();
}
StateReadStream *stream = new StateReadStream(save);
// Read the header
Common::String desc = stream->readString();
Common::String level = stream->readString();
uint levelIndex = strtol(level.c_str(), nullptr, 16);
Common::String location = stream->readString();
uint locationIndex = strtol(location.c_str(), nullptr, 16);
Common::String versionField = stream->readString();
if (!versionField.hasPrefix("Version:\t")) {
warning("The save file '%s' does not match the expected format", filename.c_str());
return Common::kReadingFailed;
}
uint version = atoi(&(versionField.c_str()[8]));
if (version < StateProvider::kMinSaveVersion || version > StateProvider::kSaveVersion) {
warning("The save file '%s' version (v%d) is not supported by this version of ResidualVM. Only versions v%d to v%d are allowed.",
filename.c_str(), version, StateProvider::kMinSaveVersion, StateProvider::kSaveVersion);
return Common::kReadingFailed;
}
// Reset the UI
_userInterface->skipFMV();
_userInterface->clearLocationDependentState();
_userInterface->setInteractive(true);
// Clear the previous world resources
_resourceProvider->shutdown();
// Read the resource trees state
_stateProvider->readStateFromStream(stream, version);
// Read the diary state
_diary->readStateFromStream(stream, version);
delete stream;
// Initialize the world resources with the loaded state
_resourceProvider->initGlobal();
_resourceProvider->setShouldRestoreCurrentState();
_resourceProvider->requestLocationChange(levelIndex, locationIndex);
return Common::kNoError;
}
bool StarkEngine::canSaveGameStateCurrently() {
return _userInterface->isInteractive();
}
Common::Error StarkEngine::saveGameState(int slot, const Common::String &desc) {
// Ensure the state store is up to date
_resourceProvider->commitActiveLocationsState();
// Open the save file
Common::String filename = formatSaveName(_targetName.c_str(), slot);
Common::OutSaveFile *save = _saveFileMan->openForSaving(filename);
if (!save) {
return _saveFileMan->getError();
}
// 1. Write the header
// Save description
save->writeUint32LE(desc.size());
save->writeString(desc);
// Level
Common::String level = _global->getCurrent()->getLevel()->getIndexAsString();
save->writeUint32LE(level.size());
save->writeString(level);
// Location
Common::String location = _global->getCurrent()->getLocation()->getIndexAsString();
save->writeUint32LE(location.size());
save->writeString(location);
// Version
Common::String version = Common::String::format("Version:\t%02d", StateProvider::kSaveVersion);
save->writeUint32LE(version.size());
save->writeString(version);
// 2. Write the resource trees state
_stateProvider->writeStateToStream(save);
// 3. Write the diary state
_diary->writeStateToStream(save);
//TODO: Write a screenshot and ResidualVM specific metadata
delete save;
return Common::kNoError;
}
bool StarkEngine::isDemo() {
return _gameDescription->flags & ADGF_DEMO;
}
Common::String StarkEngine::formatSaveName(const char *target, int slot) {
return Common::String::format("%s-%03d.tlj", target, slot);
}
void StarkEngine::pauseEngineIntern(bool pause) {
Engine::pauseEngineIntern(pause);
if (!_global || !_frameLimiter) {
// This function may be called when an error occurs before the engine is fully initialized
return;
}
_global->getLevel()->onEnginePause(pause);
_global->getCurrent()->getLevel()->onEnginePause(pause);
_global->getCurrent()->getLocation()->onEnginePause(pause);
_frameLimiter->pause(pause);
}
} // End of namespace Stark