/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* 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 3 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, see .
*
*/
#include "common/system.h"
#include "common/random.h"
#include "common/debug-channels.h"
#include "common/config-manager.h"
#include "common/memstream.h"
#include "common/compression/installshield_cab.h"
#include "common/serializer.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/cif.h"
#include "engines/nancy/iff.h"
#include "engines/nancy/input.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/console.h"
#include "engines/nancy/util.h"
#include "engines/nancy/action/conversation.h"
#include "engines/nancy/state/logo.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/state/help.h"
#include "engines/nancy/state/map.h"
#include "engines/nancy/state/credits.h"
#include "engines/nancy/state/mainmenu.h"
#include "engines/nancy/state/setupmenu.h"
#include "engines/nancy/state/loadsave.h"
#include "engines/nancy/state/savedialog.h"
namespace Nancy {
NancyEngine *g_nancy;
NancyEngine::NancyEngine(OSystem *syst, const NancyGameDescription *gd) :
Engine(syst),
_gameDescription(gd),
_system(syst),
_datFileMajorVersion(1),
_datFileMinorVersion(0),
_false(gd->gameType <= kGameTypeNancy2 ? 1 : 0),
_true(gd->gameType <= kGameTypeNancy2 ? 2 : 1) {
g_nancy = this;
_randomSource = new Common::RandomSource("Nancy");
_randomSource->setSeed(Common::RandomSource::generateNewSeed());
_input = new InputManager();
_sound = new SoundManager();
_graphics = new GraphicsManager();
_cursor = new CursorManager();
_resource = new ResourceManager();
_hasJustSaved = false;
}
NancyEngine::~NancyEngine() {
destroyState(NancyState::kLogo);
destroyState(NancyState::kCredits);
destroyState(NancyState::kMap);
destroyState(NancyState::kHelp);
destroyState(NancyState::kScene);
destroyState(NancyState::kMainMenu);
destroyState(NancyState::kSetup);
destroyState(NancyState::kLoadSave);
destroyState(NancyState::kSaveDialog);
delete _randomSource;
delete _graphics;
delete _cursor;
delete _input;
delete _sound;
delete _resource;
for (auto &data : _engineData) {
delete data._value;
}
}
NancyEngine *NancyEngine::create(GameType type, OSystem *syst, const NancyGameDescription *gd) {
if (type >= kGameTypeVampire && type <= kGameTypeNancy11) {
return new NancyEngine(syst, gd);
}
error("Unknown GameType");
}
Common::Error NancyEngine::loadGameStream(Common::SeekableReadStream *stream) {
Common::Serializer ser(stream, nullptr);
return synchronize(ser);
}
Common::Error NancyEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) {
Common::Serializer ser(nullptr, stream);
return synchronize(ser);
}
bool NancyEngine::canLoadGameStateCurrently(Common::U32String *msg) {
return canSaveGameStateCurrently();
}
bool NancyEngine::canSaveGameStateCurrently(Common::U32String *msg) {
return State::Scene::hasInstance() &&
NancySceneState._state == State::Scene::kRun &&
NancySceneState.getActiveConversation() == nullptr &&
NancySceneState.getActiveMovie() == nullptr &&
!NancySceneState.isRunningAd();
}
void NancyEngine::secondChance() {
uint secondChanceSlot = getMetaEngine()->getMaximumSaveSlot();
saveGameState(secondChanceSlot, "SECOND CHANCE", true);
}
void NancyEngine::errorString(const char *buf_input, char *buf_output, int buf_output_size) {
if (State::Scene::hasInstance()) {
if (NancySceneState._state == State::Scene::kLoad) {
// Error while loading scene
snprintf(buf_output, buf_output_size, "While loading scene S%u, frame %u, action record %u:\n%s",
NancySceneState._sceneState.currentScene.sceneID,
NancySceneState._sceneState.currentScene.frameID,
NancySceneState._actionManager.getActionRecords().size(),
buf_input);
} else {
// Error while running
snprintf(buf_output, buf_output_size, "In current scene S%u, frame %u:\n%s",
NancySceneState._sceneState.currentScene.sceneID,
NancySceneState._sceneState.currentScene.frameID,
buf_input);
}
} else {
strncpy(buf_output, buf_input, buf_output_size);
if (buf_output_size > 0)
buf_output[buf_output_size - 1] = '\0';
}
}
bool NancyEngine::hasFeature(EngineFeature f) const {
return (f == kSupportsReturnToLauncher) ||
(f == kSupportsLoadingDuringRuntime) ||
(f == kSupportsSavingDuringRuntime) ||
(f == kSupportsChangingOptionsDuringRuntime) ||
(f == kSupportsSubtitleOptions);
}
const char *NancyEngine::getCopyrightString() const {
return "Copyright 1989-1997 David P Gray, All Rights Reserved.";
}
uint32 NancyEngine::getGameFlags() const {
return _gameDescription->desc.flags;
}
const char *NancyEngine::getGameId() const {
return _gameDescription->desc.gameId;
}
GameType NancyEngine::getGameType() const {
return _gameDescription->gameType;
}
Common::Language NancyEngine::getGameLanguage() const {
return _gameDescription->desc.language;
}
Common::Platform NancyEngine::getPlatform() const {
return _gameDescription->desc.platform;
}
const StaticData &NancyEngine::getStaticData() const {
return _staticData;
}
const EngineData *NancyEngine::getEngineData(const Common::String &name) const {
if (_engineData.contains(name)) {
return _engineData[name];
}
return nullptr;
}
void NancyEngine::setState(NancyState::NancyState state, NancyState::NancyState overridePrevious) {
// Handle special cases first
switch (state) {
case NancyState::kBoot:
bootGameEngine();
setState(NancyState::kLogo);
return;
case NancyState::kMainMenu: {
if (!ConfMan.hasKey("original_menus") || ConfMan.getBool("original_menus")) {
break;
}
// Do not use the original engine's menus, call the GMM instead
openMainMenuDialog();
if (shouldQuit()) {
return;
}
_input->forceCleanInput();
return;
}
default:
break;
}
if (overridePrevious != NancyState::kNone) {
_gameFlow.prevState = overridePrevious;
} else {
_gameFlow.prevState = _gameFlow.curState;
}
_gameFlow.nextState = state;
_gameFlow.changingState = true;
}
void NancyEngine::setToPreviousState() {
setState(_gameFlow.prevState);
}
void NancyEngine::setMouseEnabled(bool enabled) {
_cursor->showCursor(enabled); _input->setMouseInputEnabled(enabled);
}
void NancyEngine::addDeferredLoader(Common::SharedPtr &loaderPtr) {
_deferredLoaderObjects.push_back(Common::WeakPtr(loaderPtr));
}
Common::Error NancyEngine::run() {
setDebugger(new NancyConsole());
// Set the default number of saves for earlier games
if (!ConfMan.hasKey("nancy_max_saves", ConfMan.getActiveDomainName())) {
if (getGameType() <= kGameTypeNancy7) {
ConfMan.setInt("nancy_max_saves", 8, ConfMan.getActiveDomainName());
}
}
// Boot the engine
setState(NancyState::kBoot);
// Check if we need to load a save state from the launcher
if (ConfMan.hasKey("save_slot")) {
int saveSlot = ConfMan.getInt("save_slot");
if (saveSlot >= 0 && saveSlot <= getMetaEngine()->getMaximumSaveSlot()) {
// Set to Scene but do not do the loading yet
setState(NancyState::kScene);
}
}
bool graphicsWereSuppressed = false;
// Main loop
while (true) {
_input->processEvents();
if (shouldQuit()) {
break;
}
uint32 frameEndTime = _system->getMillis() + 16;
if (!graphicsWereSuppressed) {
_cursor->setCursorType(CursorManager::kNormalArrow);
}
State::State *s;
if (_gameFlow.changingState) {
_gameFlow.curState = _gameFlow.nextState;
_gameFlow.nextState = NancyState::kNone;
s = getStateObject(_gameFlow.curState);
if (s) {
s->onStateEnter(_gameFlow.curState);
}
_gameFlow.changingState = false;
}
s = getStateObject(_gameFlow.curState);
if (s) {
s->process();
}
graphicsWereSuppressed = _graphics->_isSuppressed;
_graphics->draw();
if (_gameFlow.changingState) {
_graphics->clearObjects();
s = getStateObject(_gameFlow.curState);
if (s) {
if (s->onStateExit(_gameFlow.nextState)) {
destroyState(_gameFlow.curState);
}
}
}
_system->updateScreen();
// In cases where the graphics were not drawn for a frame, we want to make sure the next
// frame is processed as fast as possible. Thus, we skip deferred loaders and the time
// delay that normally maintains 60fps
if (!graphicsWereSuppressed) {
// Use the spare time until the next frame to load larger data objects
// Some loading is guaranteed to happen even with no time left, to ensure
// slower systems won't be stuck waiting forever
if (_deferredLoaderObjects.size()) {
uint i = _deferredLoaderObjects.size() - 1;
int32 timePerObj = (frameEndTime - g_system->getMillis()) / _deferredLoaderObjects.size();
if (timePerObj < 0) {
timePerObj = 0;
}
for (auto *iter = _deferredLoaderObjects.begin(); iter < _deferredLoaderObjects.end(); ++iter) {
if (iter->expired()) {
iter = _deferredLoaderObjects.erase(iter);
} else {
auto objectPtr = iter->lock();
if (objectPtr) {
if (objectPtr->load(frameEndTime - (i * timePerObj))) {
iter = _deferredLoaderObjects.erase(iter);
}
--i;
}
if (_system->getMillis() > frameEndTime) {
break;
}
}
}
}
uint32 frameFinishTime = _system->getMillis();
if (frameFinishTime < frameEndTime) {
_system->delayMillis(frameEndTime - frameFinishTime);
}
}
}
return Common::kNoError;
}
void NancyEngine::pauseEngineIntern(bool pause) {
State::State *s = getStateObject(_gameFlow.curState);
if (s) {
if (pause) {
s->onStateExit(NancyState::kPause);
} else {
s->onStateEnter(NancyState::kPause);
}
}
Engine::pauseEngineIntern(pause);
}
void NancyEngine::bootGameEngine() {
// Load paths
const Common::FSNode gameDataDir(ConfMan.getPath("path"));
SearchMan.addSubDirectoryMatching(gameDataDir, "game");
SearchMan.addSubDirectoryMatching(gameDataDir, "datafiles");
SearchMan.addSubDirectoryMatching(gameDataDir, "ciftree");
SearchMan.addSubDirectoryMatching(gameDataDir, "hdsound");
SearchMan.addSubDirectoryMatching(gameDataDir, "cdsound");
SearchMan.addSubDirectoryMatching(gameDataDir, "hdvideo");
SearchMan.addSubDirectoryMatching(gameDataDir, "cdvideo");
SearchMan.addSubDirectoryMatching(gameDataDir, "iff");
SearchMan.addSubDirectoryMatching(gameDataDir, "art");
SearchMan.addSubDirectoryMatching(gameDataDir, "font");
// Load archive if running a compressed variant
if (isCompressed()) {
Common::Archive *cabinet = Common::makeInstallShieldArchive("data");
if (cabinet) {
SearchMan.add("data1.cab", cabinet);
}
}
_resource->readCifTree("ciftree", "dat", 1);
_resource->readCifTree("promotree", "dat", 1);
// Read nancy.dat
readDatFile();
// Setup mixer
syncSoundSettings();
if (getGameType() >= kGameTypeNancy10) {
error("Game not supported; Use console to inspect game data");
}
IFF *iff = _resource->loadIFF("boot");
if (!iff)
error("Failed to load boot script");
// Load BOOT chunks data
Common::SeekableReadStream *chunkStream = nullptr;
#define LOAD_BOOT_L(t, s) if (chunkStream = iff->getChunkStream(s), chunkStream) { \
_engineData.setVal(s, new t(chunkStream)); \
delete chunkStream; \
}
#define LOAD_BOOT(t) LOAD_BOOT_L(t, #t)
LOAD_BOOT_L(ImageChunk, "OB0")
LOAD_BOOT_L(ImageChunk, "FR0")
LOAD_BOOT_L(ImageChunk, "LG0")
// One weird version of nancy3 has a partner logo implemented the same way as the other image chunks
LOAD_BOOT_L(ImageChunk, "PLG0")
// For all other games (starting with nancy4) the partner logo is a larger struct,
// containing video and sound data as well. Those go unused, however, so we still
// treat is as a simple image. Note the O instead of the 0 above.
LOAD_BOOT_L(ImageChunk, "PLGO")
LOAD_BOOT(BSUM) // This checks for PLG0, do NOT reorder
LOAD_BOOT(VIEW)
LOAD_BOOT(PCAL)
LOAD_BOOT(INV)
LOAD_BOOT(TBOX)
LOAD_BOOT(HELP)
LOAD_BOOT(CRED)
LOAD_BOOT(MENU)
LOAD_BOOT(LOAD)
LOAD_BOOT(SET)
LOAD_BOOT(SDLG)
LOAD_BOOT(MAP)
LOAD_BOOT(HINT)
LOAD_BOOT(SPUZ)
LOAD_BOOT(CLOK)
LOAD_BOOT(SPEC)
LOAD_BOOT(RCPR)
LOAD_BOOT(RCLB)
LOAD_BOOT(TABL)
LOAD_BOOT(MARK)
_cursor->init(iff->getChunkStream("CURS"));
_graphics->init();
_graphics->loadFonts(iff->getChunkStream("FONT"));
preloadCals();
_sound->initSoundChannels();
_sound->loadCommonSounds(iff);
delete iff;
// Load convo texts and autotext
auto *bsum = GetEngineData(BSUM);
if (bsum && !bsum->conversationTextsFilename.empty() && !bsum->autotextFilename.empty()) {
iff = _resource->loadIFF(bsum->conversationTextsFilename);
if (!iff) {
error("Could not load CONVO IFF");
}
if (chunkStream = iff->getChunkStream("CVTX"), chunkStream) {
_engineData.setVal("CONVO", new CVTX(chunkStream));
delete chunkStream;
}
delete iff;
iff = _resource->loadIFF(bsum->autotextFilename);
if (!iff) {
error("Could not load AUTOTEXT IFF");
}
if (chunkStream = iff->getChunkStream("CVTX"), chunkStream) {
_engineData.setVal("AUTOTEXT", new CVTX(chunkStream));
delete chunkStream;
}
delete iff;
}
#undef LOAD_BOOT_L
#undef LOAD_BOOT
}
State::State *NancyEngine::getStateObject(NancyState::NancyState state) const {
switch (state) {
case NancyState::kLogo:
return &State::Logo::instance();
case NancyState::kCredits:
return &State::Credits::instance();
case NancyState::kMap:
return &State::Map::instance();
case NancyState::kSetup:
return &State::SetupMenu::instance();
case NancyState::kHelp:
return &State::Help::instance();
case NancyState::kScene:
return &State::Scene::instance();
case NancyState::kMainMenu:
return &State::MainMenu::instance();
case NancyState::kLoadSave:
return &State::LoadSaveMenu::instance();
case NancyState::kSaveDialog:
return &State::SaveDialog::instance();
default:
return nullptr;
}
}
void NancyEngine::destroyState(NancyState::NancyState state) const {
switch (state) {
case NancyState::kLogo:
if (State::Logo::hasInstance()) {
State::Logo::instance().destroy();
}
break;
case NancyState::kCredits:
if (State::Credits::hasInstance()) {
State::Credits::instance().destroy();
}
break;
case NancyState::kMap:
if (State::Map::hasInstance()) {
State::Map::instance().destroy();
}
break;
case NancyState::kHelp:
if (State::Help::hasInstance()) {
State::Help::instance().destroy();
}
break;
case NancyState::kScene:
if (State::Scene::hasInstance()) {
State::Scene::instance().destroy();
}
break;
case NancyState::kMainMenu:
if (State::MainMenu::hasInstance()) {
State::MainMenu::instance().destroy();
}
break;
case NancyState::kSetup:
if (State::SetupMenu::hasInstance()) {
State::SetupMenu::instance().destroy();
}
break;
case NancyState::kLoadSave:
if (State::LoadSaveMenu::hasInstance()) {
State::LoadSaveMenu::instance().destroy();
}
break;
case NancyState::kSaveDialog:
if (State::SaveDialog::hasInstance()) {
State::SaveDialog::instance().destroy();
}
break;
default:
break;
}
}
void NancyEngine::preloadCals() {
auto *pcal = GetEngineData(PCAL);
if (!pcal) {
// CALs only appeared in nancy2 so a PCAL chunk may not exist
return;
}
for (const Common::String &name : pcal->calNames) {
if (!_resource->readCifTree(name, "cal", 2)) {
error("Failed to preload CAL '%s'", name.c_str());
}
}
}
void NancyEngine::readDatFile() {
Common::SeekableReadStream *datFile = SearchMan.createReadStreamForMember("nancy.dat");
if (!datFile) {
error("Unable to find nancy.dat");
}
if (datFile->readUint32BE() != MKTAG('N', 'N', 'C', 'Y')) {
error("nancy.dat is invalid");
}
int8 major = datFile->readSByte();
int8 minor = datFile->readSByte();
if (major != _datFileMajorVersion) {
error("Incorrect nancy.dat version. Expected '%d.%d', found %d.%d",
_datFileMajorVersion, _datFileMinorVersion, major, minor);
} else {
if (minor < _datFileMinorVersion) {
warning("Incorrect nancy.dat version. Expected at least '%d.%d', found %d.%d. Game may still work, but expect bugs",
_datFileMajorVersion, _datFileMinorVersion, major, minor);
}
}
uint16 numGames = datFile->readUint16LE();
uint16 gameType = getGameType();
if (gameType > numGames) {
// Fallback for when no data is present for the current game:
// throw a warning and use the last available game data
warning("Data for game type %d is not in nancy.dat", getGameType());
gameType = numGames;
}
// Seek to offset containing current game
datFile->skip((gameType - 1) * 4);
uint32 thisGameOffset = datFile->readUint32LE();
uint32 nextGameOffset = gameType == numGames ? datFile->size() : datFile->readUint32LE();
datFile->seek(thisGameOffset);
_staticData.readData(*datFile, _gameDescription->desc.language, nextGameOffset, major, minor);
delete datFile;
}
Common::Error NancyEngine::synchronize(Common::Serializer &ser) {
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
// Sync boot summary header, which includes full game title
ser.syncVersion(kSavegameVersion);
ser.matchBytes((const char *)bootSummary->header, 90);
// Sync scene and action records
NancySceneState.synchronize(ser);
NancySceneState._actionManager.synchronize(ser);
return Common::kNoError;
}
bool NancyEngine::isCompressed() {
return getGameFlags() & GF_COMPRESSED;
}
} // End of namespace Nancy