/* 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/serializer.h" #include "common/config-manager.h" #include "engines/nancy/nancy.h" #include "engines/nancy/iff.h" #include "engines/nancy/input.h" #include "engines/nancy/sound.h" #include "engines/nancy/graphics.h" #include "engines/nancy/cursor.h" #include "engines/nancy/util.h" #include "engines/nancy/constants.h" #include "engines/nancy/state/scene.h" #include "engines/nancy/ui/button.h" namespace Common { DECLARE_SINGLETON(Nancy::State::Scene); } namespace Nancy { namespace State { void Scene::SceneSummary::read(Common::SeekableReadStream &stream) { char *buf = new char[0x32]; stream.seek(0); Common::Serializer ser(&stream, nullptr); ser.setVersion(g_nancy->getGameType()); ser.syncBytes((byte *)buf, 0x32); description = Common::String(buf); ser.syncBytes((byte *)buf, 10); buf[9] = 0; videoFile = Common::String(buf); // skip 2 unknown bytes ser.skip(2); videoFormat = stream.readUint16LE(); // Load the palette data in The Vampire Diaries ser.skip(4, kGameTypeVampire, kGameTypeVampire); if (ser.getVersion() == kGameTypeVampire) { palettes.resize(3); readFilename(stream, palettes[0]); readFilename(stream, palettes[1]); readFilename(stream, palettes[2]); } sound.read(stream, SoundDescription::kScene); ser.skip(6); ser.syncAsUint16LE(dontWrap); ser.syncAsUint16LE(soundWrapAroundPan); ser.syncAsUint16LE(soundPanPerFrame); ser.syncAsUint16LE(totalViewAngle); ser.syncAsUint16LE(horizontalScrollDelta); ser.syncAsUint16LE(verticalScrollDelta); ser.syncAsUint16LE(horizontalEdgeSize); ser.syncAsUint16LE(verticalEdgeSize); ser.syncAsUint16LE((uint32 &)slowMoveTimeDelta); ser.syncAsUint16LE((uint32 &)fastMoveTimeDelta); if (g_nancy->_overrideMovementTimeDeltas) { slowMoveTimeDelta = g_nancy->_slowMovementTimeDelta; fastMoveTimeDelta = g_nancy->_fastMovementTimeDelta; } delete[] buf; } Scene::Scene() : _state (kInit), _lastHint(-1), _gameStateRequested(NancyState::kNone), _frame(), _viewport(), _textbox(_frame), _inventoryBox(_frame), _menuButton(nullptr), _helpButton(nullptr), _actionManager(), _difficulty(0), _activePrimaryVideo(nullptr) {} Scene::~Scene() { delete _helpButton; delete _menuButton; } void Scene::process() { switch (_state) { case kInit: init(); if (_state != kLoad) { break; } // fall through case kLoad: load(); // fall through case kStartSound: _state = kRun; if (!_sceneState.doNotStartSound) { g_nancy->_sound->stopAndUnloadSpecificSounds(); g_nancy->_sound->loadSound(_sceneState.summary.sound); g_nancy->_sound->playSound(_sceneState.summary.sound); } run(); // Extra run() call to fix the single frame with a wrong palette in TVD // fall through case kRun: run(); break; } } void Scene::onStateEnter() { if (_state != kInit) { registerGraphics(); _actionManager.onPause(false); g_nancy->_graphicsManager->redrawAll(); if (getHeldItem() != -1) { g_nancy->_cursorManager->setCursorItemID(getHeldItem()); } // Run once to clear out the previous scene when coming from Map process(); g_nancy->setTotalPlayTime((uint32)_timers.pushedPlayTime); unpauseSceneSpecificSounds(); g_nancy->_sound->stopSound("MSND"); } } void Scene::onStateExit() { _timers.pushedPlayTime = g_nancy->getTotalPlayTime(); _actionManager.onPause(true); pauseSceneSpecificSounds(); _gameStateRequested = NancyState::kNone; } void Scene::changeScene(uint16 id, uint16 frame, uint16 verticalOffset, bool noSound) { if (id == 9999) { return; } _sceneState.nextScene.sceneID = id; _sceneState.nextScene.frameID = frame; _sceneState.nextScene.verticalOffset = verticalOffset; _sceneState.doNotStartSound = noSound; _state = kLoad; } void Scene::changeScene(const SceneChangeDescription &sceneDescription) { changeScene(sceneDescription.sceneID, sceneDescription.frameID, sceneDescription.verticalOffset, sceneDescription.doNotStartSound); } void Scene::pushScene() { _sceneState.pushedScene = _sceneState.currentScene; _sceneState.isScenePushed = true; } void Scene::popScene() { changeScene(_sceneState.pushedScene.sceneID, _sceneState.pushedScene.frameID, _sceneState.pushedScene.verticalOffset, true); _sceneState.isScenePushed = false; } void Scene::pauseSceneSpecificSounds() { // TODO missing if, same condition as the one in SoundManager::stopAndUnloadSpecificSounds for (uint i = 0; i < 10; ++i) { g_nancy->_sound->pauseSound(i, true); } } void Scene::unpauseSceneSpecificSounds() { for (uint i = 0; i < 10; ++i) { g_nancy->_sound->pauseSound(i, false); } } void Scene::addItemToInventory(uint16 id) { _flags.items[id] = kTrue; if (_flags.heldItem == id) { setHeldItem(-1); } _inventoryBox.addItem(id); } void Scene::removeItemFromInventory(uint16 id, bool pickUp) { _flags.items[id] = kFalse; if (pickUp) { setHeldItem(id); } _inventoryBox.removeItem(id); } void Scene::setHeldItem(int16 id) { _flags.heldItem = id; g_nancy->_cursorManager->setCursorItemID(id); } void Scene::setEventFlag(int16 label, NancyFlag flag) { if (label > -1 && (uint)label < g_nancy->getConstants().numEventFlags) { _flags.eventFlags[label] = flag; } } void Scene::setEventFlag(EventFlagDescription eventFlag) { setEventFlag(eventFlag.label, eventFlag.flag); } bool Scene::getEventFlag(int16 label, NancyFlag flag) const { if (label > -1 && (uint)label < g_nancy->getConstants().numEventFlags) { return _flags.eventFlags[label] == flag; } else { return false; } } bool Scene::getEventFlag(EventFlagDescription eventFlag) const { return getEventFlag(eventFlag.label, eventFlag.flag); } void Scene::setLogicCondition(int16 label, NancyFlag flag) { if (label > -1) { _flags.logicConditions[label].flag = flag; _flags.logicConditions[label].timestamp = g_nancy->getTotalPlayTime(); } } bool Scene::getLogicCondition(int16 label, NancyFlag flag) const { if (label > -1) { return _flags.logicConditions[label].flag == flag; } else { return false; } } void Scene::clearLogicConditions() { for (auto &cond : _flags.logicConditions) { cond.flag = kFalse; cond.timestamp = 0; } } void Scene::useHint(int hintID, int hintWeight) { if (_lastHint != hintID) { _hintsRemaining[_difficulty] += hintWeight; _lastHint = hintID; } } void Scene::registerGraphics() { _frame.registerGraphics(); _viewport.registerGraphics(); _textbox.registerGraphics(); _inventoryBox.registerGraphics(); _menuButton->registerGraphics(); _helpButton->registerGraphics(); _textbox.setVisible(!_shouldClearTextbox); _menuButton->setVisible(false); _helpButton->setVisible(false); } void Scene::synchronize(Common::Serializer &ser) { if (ser.isSaving()) { ser.syncAsUint16LE(_sceneState.currentScene.sceneID); ser.syncAsUint16LE(_sceneState.currentScene.frameID); ser.syncAsUint16LE(_sceneState.currentScene.verticalOffset); } else if (ser.isLoading()) { ser.syncAsUint16LE(_sceneState.nextScene.sceneID); ser.syncAsUint16LE(_sceneState.nextScene.frameID); ser.syncAsUint16LE(_sceneState.nextScene.verticalOffset); _sceneState.doNotStartSound = false; load(); } ser.syncAsUint16LE(_sceneState.pushedScene.sceneID); ser.syncAsUint16LE(_sceneState.pushedScene.frameID); ser.syncAsUint16LE(_sceneState.pushedScene.verticalOffset); ser.syncAsByte(_sceneState.isScenePushed); // hardcoded number of logic conditions, check if there can ever be more/less for (uint i = 0; i < 30; ++i) { ser.syncAsUint32LE(_flags.logicConditions[i].flag); } for (uint i = 0; i < 30; ++i) { ser.syncAsUint32LE((uint32 &)_flags.logicConditions[i].timestamp); } // TODO hardcoded inventory size auto &order = getInventoryBox()._order; uint prevSize = getInventoryBox()._order.size(); getInventoryBox()._order.resize(g_nancy->getConstants().numItems); if (ser.isSaving()) { for (uint i = prevSize; i < order.size(); ++i) { order[i] = -1; } } ser.syncArray(order.data(), g_nancy->getConstants().numItems, Common::Serializer::Sint16LE); while (order.size() && order.back() == -1) { order.pop_back(); } if (ser.isLoading()) { // Make sure the shades are open if we have items getInventoryBox().onReorder(); } // TODO hardcoded inventory size ser.syncArray(_flags.items.data(), g_nancy->getConstants().numItems, Common::Serializer::Byte); ser.syncAsSint16LE(_flags.heldItem); g_nancy->_cursorManager->setCursorItemID(_flags.heldItem); ser.syncAsUint32LE((uint32 &)_timers.lastTotalTime); ser.syncAsUint32LE((uint32 &)_timers.sceneTime); ser.syncAsUint32LE((uint32 &)_timers.playerTime); ser.syncAsUint32LE((uint32 &)_timers.pushedPlayTime); ser.syncAsUint32LE((uint32 &)_timers.timerTime); ser.syncAsByte(_timers.timerIsActive); ser.syncAsByte(_timers.timeOfDay); g_nancy->setTotalPlayTime((uint32)_timers.lastTotalTime); // TODO hardcoded number of event flags ser.syncArray(_flags.eventFlags.data(), g_nancy->getConstants().numEventFlags, Common::Serializer::Byte); ser.syncArray(_flags.sceneHitCount, (uint16)2001, Common::Serializer::Uint16LE); ser.syncAsUint16LE(_difficulty); ser.syncArray(_hintsRemaining.data(), _hintsRemaining.size(), Common::Serializer::Uint16LE); ser.syncAsSint16LE(_lastHint); // Synchronize SliderPuzzle static data ser.syncAsByte(_sliderPuzzleState.playerHasTriedPuzzle); byte x = 0, y = 0; if (ser.isSaving()) { y = _sliderPuzzleState.playerTileOrder.size(); if (y) { x = _sliderPuzzleState.playerTileOrder.back().size(); } else { x = 0; } } ser.syncAsByte(x); ser.syncAsByte(y); _sliderPuzzleState.playerTileOrder.resize(y); for (int i = 0; i < y; ++i) { _sliderPuzzleState.playerTileOrder[i].resize(x); ser.syncArray(_sliderPuzzleState.playerTileOrder[i].data(), x, Common::Serializer::Sint16LE); } } void Scene::init() { _flags.eventFlags = Common::Array(g_nancy->getConstants().numEventFlags, kFalse); // Does this ever get used? for (uint i = 0; i < 2001; ++i) { _flags.sceneHitCount[i] = 0; } _flags.items = Common::Array(g_nancy->getConstants().numItems, kFalse); _timers.lastTotalTime = 0; _timers.playerTime = g_nancy->_startTimeHours * 3600000; _timers.sceneTime = 0; _timers.timerTime = 0; _timers.timerIsActive = false; _timers.playerTimeNextMinute = 0; _timers.pushedPlayTime = 0; _timers.timeOfDay = Timers::kDay; changeScene(g_nancy->_firstScene); Common::SeekableReadStream *chunk = g_nancy->getBootChunkStream("HINT"); if (chunk) { chunk->seek(0); _hintsRemaining.clear(); _hintsRemaining.reserve(3); for (uint i = 0; i < 3; ++i) { _hintsRemaining.push_back(chunk->readByte()); } _lastHint = -1; } _sliderPuzzleState.playerHasTriedPuzzle = false; initStaticData(); if (ConfMan.hasKey("save_slot")) { // Load savefile directly from the launcher int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0 && saveSlot <= g_nancy->getMetaEngine()->getMaximumSaveSlot()) { g_nancy->loadGameState(saveSlot); } } else { // Normal boot, load default first scene _state = kLoad; } registerGraphics(); g_nancy->_graphicsManager->redrawAll(); } void Scene::setActivePrimaryVideo(Action::PlayPrimaryVideoChan0 *activeVideo) { _activePrimaryVideo = activeVideo; } Action::PlayPrimaryVideoChan0 *Scene::getActivePrimaryVideo() { return _activePrimaryVideo; } void Scene::load() { clearSceneData(); // Scene IDs are prefixed with S inside the cif tree; e.g 100 -> S100 Common::String sceneName = Common::String::format("S%u", _sceneState.nextScene.sceneID); IFF sceneIFF(sceneName); if (!sceneIFF.load()) { error("Faled to load IFF %s", sceneName.c_str()); } Common::SeekableReadStream *sceneSummaryChunk = sceneIFF.getChunkStream("SSUM"); if (!sceneSummaryChunk) { error("Invalid IFF Chunk SSUM"); } _sceneState.summary.read(*sceneSummaryChunk); debugC(0, kDebugScene, "Loading new scene %i: description \"%s\", frame %i, vertical scroll %i, doNotStartSound == %s", _sceneState.nextScene.sceneID, _sceneState.summary.description.c_str(), _sceneState.nextScene.frameID, _sceneState.nextScene.verticalOffset, _sceneState.doNotStartSound == true ? "true" : "false"); _sceneState.currentScene = _sceneState.nextScene; // Search for Action Records, maximum for a scene is 30 Common::SeekableReadStream *actionRecordChunk = nullptr; while (actionRecordChunk = sceneIFF.getChunkStream("ACT", _actionManager._records.size()), actionRecordChunk != nullptr) { if (_actionManager._records.size() >= 30) { error("Invalid number of Action Records"); } _actionManager.addNewActionRecord(*actionRecordChunk); } _viewport.loadVideo(_sceneState.summary.videoFile, _sceneState.currentScene.frameID, _sceneState.currentScene.verticalOffset, _sceneState.summary.dontWrap, _sceneState.summary.videoFormat, _sceneState.summary.palettes.size() ? _sceneState.summary.palettes[_sceneState.currentScene.paletteID] : Common::String()); if (_viewport.getFrameCount() <= 1) { _viewport.disableEdges(kLeft | kRight); } if (_sceneState.summary.videoFormat == 1) { // TODO } else if (_sceneState.summary.videoFormat == 2) { // always start from the bottom _sceneState.currentScene.verticalOffset = _viewport.getMaxScroll(); } else { error("Unrecognized Scene summary chunk video file format"); } if (_sceneState.summary.videoFormat == 1) { // TODO } else if (_sceneState.summary.videoFormat == 2) { if (_viewport.getMaxScroll() == 0) { _viewport.disableEdges(kUp | kDown); } } _timers.sceneTime = 0; _sceneState.nextScene.paletteID = 0; _state = kStartSound; } void Scene::run() { if (_isComingFromMenu) { // TODO } _isComingFromMenu = false; if (_gameStateRequested != NancyState::kNone) { g_nancy->setState(_gameStateRequested); return; } Time currentPlayTime = g_nancy->getTotalPlayTime(); Time deltaTime = currentPlayTime - _timers.lastTotalTime; _timers.lastTotalTime = currentPlayTime; if (_timers.timerIsActive) { _timers.timerTime += deltaTime; } _timers.sceneTime += deltaTime; // Calculate the in-game time (playerTime) if (currentPlayTime > _timers.playerTimeNextMinute) { _timers.playerTime += 60000; // Add a minute _timers.playerTimeNextMinute = currentPlayTime + g_nancy->_playerTimeMinuteLength; } // Set the time of day according to playerTime if (_timers.playerTime.getHours() >= 7 && _timers.playerTime.getHours() < 18) { _timers.timeOfDay = Timers::kDay; } else if (_timers.playerTime.getHours() >= 19 || _timers.playerTime.getHours() < 6) { _timers.timeOfDay = Timers::kNight; } else { _timers.timeOfDay = Timers::kDuskDawn; } // Update the UI elements and handle input NancyInput input = g_nancy->_input->getInput(); _viewport.handleInput(input); _sceneState.currentScene.verticalOffset = _viewport.getCurVerticalScroll(); if (_sceneState.currentScene.frameID != _viewport.getCurFrame()) { _sceneState.currentScene.frameID = _viewport.getCurFrame(); g_nancy->_sound->calculatePanForAllSounds(); } _actionManager.handleInput(input); _menuButton->handleInput(input); _helpButton->handleInput(input); _textbox.handleInput(input); _inventoryBox.handleInput(input); if (_menuButton->_isClicked) { _menuButton->_isClicked = false; g_nancy->_sound->playSound("GLOB"); requestStateChange(NancyState::kMainMenu); } if (_helpButton->_isClicked) { _helpButton->_isClicked = false; g_nancy->_sound->playSound("GLOB"); requestStateChange(NancyState::kHelp); } // Handle invisible map button for (uint i = 0; i < ARRAYSIZE(g_nancy->getConstants().mapAccessSceneIDs); ++i) { if (g_nancy->getConstants().mapAccessSceneIDs[i] == -1) { break; } if ((int)_sceneState.currentScene.sceneID == g_nancy->getConstants().mapAccessSceneIDs[i]) { if (_mapHotspot.contains(input.mousePos)) { g_nancy->_cursorManager->setCursorType(CursorManager::kHotspotArrow); if (input.input & NancyInput::kLeftMouseButtonUp) { requestStateChange(NancyState::kMap); } } } } _actionManager.processActionRecords(); } void Scene::initStaticData() { Common::SeekableReadStream *chunk = g_nancy->getBootChunkStream("MAP"); chunk->seek(0x8A); readRect(*chunk, _mapHotspot); chunk = g_nancy->getBootChunkStream("FR0"); chunk->seek(0); _frame.init(chunk->readString()); _viewport.init(); _textbox.init(); _inventoryBox.init(); // Init menu and help buttons chunk = g_nancy->getBootChunkStream("BSUM"); chunk->seek(0x184); Common::Rect menuSrc, helpSrc, menuDest, helpDest; readRect(*chunk, menuSrc); readRect(*chunk, helpSrc); readRect(*chunk, menuDest); readRect(*chunk, helpDest); _menuButton = new UI::Button(_frame, 5, g_nancy->_graphicsManager->_object0, menuSrc, menuDest); _helpButton = new UI::Button(_frame, 5, g_nancy->_graphicsManager->_object0, helpSrc, helpDest); _menuButton->init(); _helpButton->init(); g_nancy->_cursorManager->showCursor(true); _state = kLoad; } void Scene::clearSceneData() { // only clear select flags for (uint i = 0; i < ARRAYSIZE(g_nancy->getConstants().eventFlagsToClearOnSceneChange); ++i) { if (g_nancy->getConstants().eventFlagsToClearOnSceneChange[i] == -1) { break; } _flags.eventFlags[g_nancy->getConstants().eventFlagsToClearOnSceneChange[i]] = kFalse; } clearLogicConditions(); _actionManager.clearActionRecords(); } } // End of namespace State } // End of namespace Nancy