Files
2026-02-02 04:50:13 +01:00

1287 lines
37 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "common/serializer.h"
#include "common/config-manager.h"
#include "common/func.h"
#include "common/random.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/resource.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/state/map.h"
#include "engines/nancy/ui/button.h"
#include "engines/nancy/ui/ornaments.h"
#include "engines/nancy/ui/clock.h"
#include "engines/nancy/misc/lightning.h"
#include "engines/nancy/misc/specialeffect.h"
namespace Common {
DECLARE_SINGLETON(Nancy::State::Scene);
}
namespace Nancy {
namespace State {
void Scene::SceneSummary::read(Common::SeekableReadStream &stream) {
char *buf = new char[0x32];
int32 x = 0;
int32 y = 0;
int32 z = 0;
stream.seek(0);
Common::Serializer ser(&stream, nullptr);
ser.setVersion(g_nancy->getGameType());
ser.syncBytes((byte *)buf, 0x32);
description = Common::String(buf);
readFilename(stream, videoFile);
// skip 2 unknown bytes
ser.skip(2);
videoFormat = stream.readUint16LE();
// Load the palette data in The Vampire Diaries
ser.skip(4, kGameTypeVampire, kGameTypeVampire);
readFilenameArray(ser, palettes, 3, kGameTypeVampire, kGameTypeVampire);
sound.readScene(stream);
ser.syncAsUint16LE(panningType);
ser.syncAsUint16LE(numberOfVideoFrames, kGameTypeVampire, kGameTypeNancy2);
ser.syncAsUint16LE(degreesPerRotation);
ser.syncAsUint16LE(totalViewAngle, kGameTypeVampire, kGameTypeNancy2);
ser.syncAsUint32LE(x, kGameTypeNancy3);
ser.syncAsUint32LE(y, kGameTypeNancy3);
ser.syncAsUint32LE(z, kGameTypeNancy3);
listenerPosition.set(x, y, z);
ser.syncAsUint16LE(horizontalScrollDelta);
ser.syncAsUint16LE(verticalScrollDelta);
ser.syncAsUint16LE(horizontalEdgeSize);
ser.syncAsUint16LE(verticalEdgeSize);
ser.syncAsUint16LE((uint32 &)slowMoveTimeDelta);
ser.syncAsUint16LE((uint32 &)fastMoveTimeDelta);
ser.skip(1); // CD required for scene
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
if (bootSummary->overrideMovementTimeDeltas) {
slowMoveTimeDelta = bootSummary->slowMovementTimeDelta;
fastMoveTimeDelta = bootSummary->fastMovementTimeDelta;
}
delete[] buf;
}
void Scene::SceneSummary::readTerse(Common::SeekableReadStream &stream) {
char buf[0x32];
stream.read(buf, 0x32);
description = buf;
readFilename(stream, videoFile);
sound.readTerse(stream);
}
Scene::Scene() :
_state (kInit),
_lastHintCharacter(-1),
_lastHintID(-1),
_gameStateRequested(NancyState::kNone),
_frame(),
_viewport(),
_textbox(),
_inventoryBox(),
_menuButton(nullptr),
_helpButton(nullptr),
_viewportOrnaments(nullptr),
_textboxOrnaments(nullptr),
_inventoryBoxOrnaments(nullptr),
_clock(nullptr),
_actionManager(),
_difficulty(0),
_activeMovie(nullptr),
_activeConversation(nullptr),
_lightning(nullptr),
_destroyOnExit(false),
_isRunningAd(false),
_hotspotDebug(50) {}
Scene::~Scene() {
delete _helpButton;
delete _menuButton;
delete _viewportOrnaments;
delete _textboxOrnaments;
delete _inventoryBoxOrnaments;
delete _clock;
delete _lightning;
clearPuzzleData();
}
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.currentScene.continueSceneSound == kLoadSceneSound) {
g_nancy->_sound->stopAndUnloadSceneSpecificSounds();
g_nancy->_sound->loadSound(_sceneState.summary.sound);
g_nancy->_sound->playSound(_sceneState.summary.sound);
}
// fall through
case kRun:
run();
break;
}
}
void Scene::onStateEnter(const NancyState::NancyState prevState) {
if (_state != kInit) {
registerGraphics();
if (prevState != NancyState::kPause) {
g_nancy->setTotalPlayTime((uint32)_timers.pushedPlayTime);
}
_actionManager.onPause(false);
g_nancy->_graphics->redrawAll();
if (getHeldItem() != -1) {
g_nancy->_cursor->setCursorItemID(getHeldItem());
}
if (prevState == NancyState::kPause) {
g_nancy->_sound->pauseAllSounds(false);
} else {
g_nancy->_sound->pauseSceneSpecificSounds(false);
}
g_nancy->_sound->stopSound("MSND");
}
g_nancy->_hasJustSaved = false;
}
bool Scene::onStateExit(const NancyState::NancyState nextState) {
if (_state == kRun) {
// Exiting the state outside the kRun state means we've encountered an error
g_nancy->_graphics->screenshotScreen(_lastScreenshot);
}
if (nextState != NancyState::kPause) {
_timers.pushedPlayTime = g_nancy->getTotalPlayTime();
}
_actionManager.onPause(true);
if (nextState == NancyState::kPause) {
g_nancy->_sound->pauseAllSounds(true);
} else {
g_nancy->_sound->pauseSceneSpecificSounds(true);
}
_gameStateRequested = NancyState::kNone;
// Re-register the clock so the open/close animation can continue playing inside Map
if (nextState == NancyState::kMap && g_nancy->getGameType() == kGameTypeVampire) {
_clock->registerGraphics();
}
return _destroyOnExit;
}
void Scene::changeScene(const SceneChangeDescription &sceneDescription) {
if (sceneDescription.sceneID == kNoScene || _state == kLoad) {
return;
}
// HACK: Nancy 9 tries to reload the same scene when changing
// angle/power in scene 5651 (stuck bottle in rocks). This ends up
// resetting the scene flags, which makes the angle/power buttons
// unresponsive. We avoid reloading the scene in this case, if the
// new scene is the same as the current one. This has the negative
// side-effect that the button arrows are not updated, but at least
// it makes them usable.
// TODO: find a better solution for this.
if (sceneDescription.sceneID == _sceneState.currentScene.sceneID &&
g_nancy->getGameType() == kGameTypeNancy9 && sceneDescription.sceneID == 5651) {
return;
}
_sceneState.nextScene = sceneDescription;
_state = kLoad;
}
void Scene::pushScene(int16 itemID) {
if (itemID == -1) {
_sceneState.pushedScene = _sceneState.currentScene;
_sceneState.isScenePushed = true;
} else {
if (_sceneState.isInvScenePushed) {
// Re-add current pushed item
addItemToInventory(_sceneState.pushedInvItemID);
} else {
// Only set this when another item hasn't been pushed, otherwise
// the player will never be able to exit
_sceneState.pushedInvScene = _sceneState.currentScene;
}
_sceneState.isInvScenePushed = true;
_sceneState.pushedInvItemID = itemID;
}
}
void Scene::popScene(bool inventory) {
if (!inventory || _sceneState.pushedInvItemID == -1) {
_sceneState.pushedScene.continueSceneSound = true;
changeScene(_sceneState.pushedScene);
_sceneState.isScenePushed = false;
} else {
_sceneState.pushedInvScene.continueSceneSound = true;
changeScene(_sceneState.pushedInvScene);
_sceneState.isInvScenePushed = false;
addItemToInventory(_sceneState.pushedInvItemID);
_sceneState.pushedInvItemID = kEvNoEvent;
_sceneState.pushedInvScene.sceneID = kNoScene;
}
}
void Scene::setPlayerTime(Time time, byte relative) {
if (relative == kRelativeClockBump) {
// Relative, add the specified time to current playerTime
_timers.playerTime += time;
} else {
// Absolute, maintain days but replace hours and minutes
_timers.playerTime = _timers.playerTime.getDays() * 86400000 + time;
}
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
_timers.playerTimeNextMinute = g_nancy->getTotalPlayTime() + bootSummary->playerTimeMinuteLength;
}
byte Scene::getPlayerTOD() const {
if (g_nancy->getGameType() <= kGameTypeNancy1) {
if (_timers.playerTime.getHours() >= 7 && _timers.playerTime.getHours() < 18) {
return kPlayerDay;
} else if (_timers.playerTime.getHours() >= 19 || _timers.playerTime.getHours() < 6) {
return kPlayerNight;
} else {
return kPlayerDuskDawn;
}
} else if (g_nancy->getGameType() <= kGameTypeNancy5) {
// nancy2 and up removed dusk/dawn
if (_timers.playerTime.getHours() >= 6 && _timers.playerTime.getHours() < 18) {
return kPlayerDay;
} else {
return kPlayerNight;
}
} else {
// nancy6 added the day start/end times (in minutes) to BSUM
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
uint16 minutes = _timers.playerTime.getHours() * 60 + _timers.playerTime.getMinutes();
if (minutes >= bootSummary->dayStartMinutes && minutes < bootSummary->dayEndMinutes) {
return kPlayerDay;
} else {
return kPlayerNight;
}
}
}
void Scene::addItemToInventory(int16 id) {
if (id == -1) {
return;
}
if (_flags.items[id] == g_nancy->_false) {
_flags.items[id] = g_nancy->_true;
if (_flags.heldItem == id) {
setHeldItem(-1);
}
g_nancy->_sound->playSound("BUOK");
_inventoryBox.addItem(id);
}
}
void Scene::removeItemFromInventory(int16 id, bool pickUp) {
if (id == -1) {
return;
}
if (_flags.items[id] == g_nancy->_true || getHeldItem() == id) {
_flags.items[id] = g_nancy->_false;
if (pickUp) {
setHeldItem(id);
} else if (getHeldItem() == id) {
setHeldItem(-1);
}
g_nancy->_sound->playSound("BUOK");
_inventoryBox.removeItem(id);
}
}
void Scene::setHeldItem(int16 id) {
_flags.heldItem = id; g_nancy->_cursor->setCursorItemID(id);
}
void Scene::setNoHeldItem() {
if (getHeldItem() != -1) {
addItemToInventory(getHeldItem());
}
}
byte Scene::hasItem(int16 id) const {
if (getHeldItem() == id) {
return g_nancy->_true;
} else {
return _flags.items[id];
}
}
void Scene::installInventorySoundOverride(byte command, const SoundDescription &sound, const Common::String &caption, uint16 itemID) {
InventorySoundOverride newOverride;
switch (command) {
case kInvSoundOverrideCommandNoSound :
// Make the sound silent
newOverride.sound = sound;
newOverride.sound.name = "NO SOUND";
newOverride.caption = caption; // Assumes the caption will be empty
_inventorySoundOverrides.setVal(itemID, newOverride);
break;
case kInvSoundOverrideCommandNewSound :
newOverride.sound = sound;
newOverride.caption = caption;
_inventorySoundOverrides.setVal(itemID, newOverride);
break;
case kInvSoundOverrideCommandICant :
// Make the sound the default "I can't use that here"
newOverride.isDefault = true;
_inventorySoundOverrides.setVal(itemID, newOverride);
break;
case kInvSoundOverrideCommandTurnOff :
// Remove any previous override
_inventorySoundOverrides.erase(itemID);
break;
default :
return;
}
}
void Scene::playItemCantSound(int16 itemID, bool notHoldingSound) {
if (ConfMan.getBool("subtitles") && g_nancy->getGameType() >= kGameTypeNancy2) {
_textbox.clear();
}
// Improvement: nancy2 never shows the caption text, even though it exists in the data; we show it
auto *inventoryData = GetEngineData(INV);
assert(inventoryData);
if (itemID < 0) {
if (inventoryData->cantSound.name.size()) {
// Play default "can't" inside inventory data (if present)
g_nancy->_sound->loadSound(inventoryData->cantSound);
g_nancy->_sound->playSound(inventoryData->cantSound);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(inventoryData->cantText, inventoryData->captionAutoClearTime);
}
} else {
// TVD and nancy1 contain no sound data in INV, and have no captions
g_nancy->_sound->playSound("CANT");
}
} else if ((uint)itemID < _flags.items.size()) {
if (_inventorySoundOverrides.contains(itemID)) {
// We have an override installed
InventorySoundOverride &override = _inventorySoundOverrides[itemID];
if (!override.isDefault) {
// Not set to the default sound, play the override
g_nancy->_sound->loadSound(override.sound);
g_nancy->_sound->playSound(override.sound);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(override.caption, inventoryData->captionAutoClearTime);
}
return;
} else {
// Play the default "I can't" sound
const INV::ItemDescription item = inventoryData->itemDescriptions[itemID];
if (notHoldingSound && item.cantSoundNotHolding.name.size()) {
// This field only exists in nancy2
g_nancy->_sound->loadSound(item.cantSoundNotHolding);
g_nancy->_sound->playSound(item.cantSoundNotHolding);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(item.cantTextNotHolding, inventoryData->captionAutoClearTime);
}
} else if (inventoryData->cantSound.name.size()) {
g_nancy->_sound->loadSound(inventoryData->cantSound);
g_nancy->_sound->playSound(inventoryData->cantSound);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(inventoryData->cantText, inventoryData->captionAutoClearTime);
}
} else {
// Should be unreachable
g_nancy->_sound->playSound("CANT");
}
}
}
// No override installed
const INV::ItemDescription item = inventoryData->itemDescriptions[itemID];
if (item.cantSound.name.size()) {
// The inventory data contains a custom "can't" sound for this item
SoundDescription cantSound = item.cantSound;
Common::String cantText = item.cantText;
// For Nancy9 and newer, we have multiple sounds to choose from,
// so randomly select one, if available
if (g_nancy->getGameType() >= kGameTypeNancy9 && !item.cantSounds[1].name.empty()) {
const uint maxSound = item.cantSounds[2].name.empty() ? 1 : 2;
uint soundIndex = g_nancy->_randomSource->getRandomNumber(maxSound);
cantSound = item.cantSounds[soundIndex];
cantText = item.cantTexts[soundIndex];
}
g_nancy->_sound->loadSound(cantSound);
g_nancy->_sound->playSound(cantSound);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(cantText, inventoryData->captionAutoClearTime);
}
} else if (inventoryData->cantSound.name.size()) {
// No custom sound, play default "can't" inside inventory data. Should (?) be unreachable
g_nancy->_sound->loadSound(inventoryData->cantSound);
g_nancy->_sound->playSound(inventoryData->cantSound);
if (ConfMan.getBool("subtitles")) {
_textbox.addTextLine(inventoryData->cantText, inventoryData->captionAutoClearTime);
}
} else {
// TVD and nancy1 contain no sound data in INV, and have no captions
g_nancy->_sound->playSound("CANT");
}
}
}
void Scene::setEventFlag(int16 label, byte flag) {
if (label >= 1000) {
// In nancy3 and onwards flags begin from 1000
label -= 1000;
}
if (label > kEvNoEvent && (uint)label < g_nancy->getStaticData().numEventFlags) {
_flags.eventFlags[label] = flag;
}
}
void Scene::setEventFlag(FlagDescription eventFlag) {
setEventFlag(eventFlag.label, eventFlag.flag);
}
bool Scene::getEventFlag(int16 label, byte flag) const {
if (label >= 1000) {
// In nancy3 and onwards flags begin from 1000
label -= 1000;
}
if (label > kEvNoEvent && (uint)label < g_nancy->getStaticData().numEventFlags) {
return _flags.eventFlags[label] == flag;
} else {
return false;
}
}
bool Scene::getEventFlag(FlagDescription eventFlag) const {
return getEventFlag(eventFlag.label, eventFlag.flag);
}
void Scene::setLogicCondition(int16 label, byte flag) {
if (label > kEvNoEvent) {
if (label >= 2000) {
// In nancy3 and onwards logic conditions begin from 2000
label -= 2000;
}
if (label > kEvNoEvent && (uint)label < 30) {
_flags.logicConditions[label].flag = flag;
_flags.logicConditions[label].timestamp = g_nancy->getTotalPlayTime();
}
}
}
bool Scene::getLogicCondition(int16 label, byte flag) const {
if (label > kEvNoEvent) {
return _flags.logicConditions[label].flag == flag;
} else {
return false;
}
}
void Scene::clearLogicConditions() {
for (auto &cond : _flags.logicConditions) {
cond.flag = g_nancy->_false;
cond.timestamp = 0;
}
}
void Scene::useHint(uint16 characterID, uint16 hintID) {
if (_lastHintID != hintID || _lastHintCharacter != characterID) {
_hintsRemaining[_difficulty] += g_nancy->getStaticData().hints[characterID][hintID].hintWeight;
_lastHintCharacter = characterID;
_lastHintID = hintID;
}
}
void Scene::registerGraphics() {
_frame.registerGraphics();
_viewport.registerGraphics();
_textbox.registerGraphics();
_inventoryBox.registerGraphics();
_hotspotDebug.registerGraphics();
if (_menuButton) {
_menuButton->registerGraphics();
_menuButton->setVisible(false);
}
if (_helpButton) {
_helpButton->registerGraphics();
_helpButton->setVisible(false);
}
if (_viewportOrnaments) {
_viewportOrnaments->registerGraphics();
_viewportOrnaments->setVisible(true);
}
if (_textboxOrnaments) {
_textboxOrnaments->registerGraphics();
_textboxOrnaments->setVisible(true);
}
if (_inventoryBoxOrnaments) {
_inventoryBoxOrnaments->registerGraphics();
_inventoryBoxOrnaments->setVisible(true);
}
if (_clock) {
_clock->registerGraphics();
}
}
void Scene::synchronize(Common::Serializer &ser) {
ser.syncAsUint16LE(_sceneState.currentScene.sceneID);
ser.syncAsUint16LE(_sceneState.currentScene.frameID);
ser.syncAsUint16LE(_sceneState.currentScene.verticalOffset);
if (g_nancy->getGameType() >= kGameTypeNancy3) {
ser.syncAsUint16LE(_sceneState.currentScene.frontVectorFrameID);
for (uint i = 0; i < 3; ++i) {
ser.syncAsFloatLE(_sceneState.currentScene.listenerFrontVector.getData()[i]);
}
}
if (ser.isLoading()) {
_sceneState.currentScene.continueSceneSound = kLoadSceneSound;
_sceneState.nextScene = _sceneState.currentScene;
g_nancy->_sound->stopAllSounds();
load(true);
}
ser.syncAsUint16LE(_sceneState.pushedScene.sceneID);
ser.syncAsUint16LE(_sceneState.pushedScene.frameID);
ser.syncAsUint16LE(_sceneState.pushedScene.verticalOffset);
ser.syncAsByte(_sceneState.isScenePushed);
// Inventory scene "stack" was introduced in nancy7
if (g_nancy->getGameType() >= kGameTypeNancy7) {
ser.syncAsUint16LE(_sceneState.pushedInvScene.sceneID);
ser.syncAsUint16LE(_sceneState.pushedInvScene.frameID);
ser.syncAsUint16LE(_sceneState.pushedInvScene.verticalOffset);
ser.syncAsByte(_sceneState.isInvScenePushed);
ser.syncAsUint16LE(_sceneState.pushedInvItemID);
}
// 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);
}
auto &order = getInventoryBox()._order;
uint prevSize = getInventoryBox()._order.size();
getInventoryBox()._order.resize(g_nancy->getStaticData().numItems);
if (ser.isSaving()) {
for (uint i = prevSize; i < order.size(); ++i) {
order[i] = -1;
}
}
ser.syncArray(order.data(), g_nancy->getStaticData().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();
}
ser.syncArray(_flags.items.data(), g_nancy->getStaticData().numItems, Common::Serializer::Byte);
ser.syncAsSint16LE(_flags.heldItem);
g_nancy->_cursor->setCursorItemID(_flags.heldItem);
if (g_nancy->getGameType() >= kGameTypeNancy7) {
ser.syncArray(_flags.disabledItems.data(), g_nancy->getStaticData().numItems, Common::Serializer::Byte);
}
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.skip(1, 0, 2);
g_nancy->setTotalPlayTime((uint32)_timers.lastTotalTime);
ser.syncArray(_flags.eventFlags.data(), g_nancy->getStaticData().numEventFlags, Common::Serializer::Byte);
// Clear generic flags
for (uint16 id : g_nancy->getStaticData().genericEventFlags) {
_flags.eventFlags[id] = g_nancy->_false;
}
// Skip empty sceneCount array
ser.skip(2001 * 2, 0, 2);
uint numSceneCounts = _flags.sceneCounts.size();
ser.syncAsUint16LE(numSceneCounts);
if (ser.isSaving()) {
uint16 key;
for (auto &entry : _flags.sceneCounts) {
key = entry._key;
ser.syncAsUint16LE(key);
ser.syncAsUint16LE(entry._value);
}
} else {
uint16 key = 0;
uint16 val = 0;
for (uint i = 0; i < numSceneCounts; ++i) {
ser.syncAsUint16LE(key);
ser.syncAsUint16LE(val);
_flags.sceneCounts.setVal(key, val);
}
}
ser.syncAsUint16LE(_difficulty);
ser.syncArray<uint16>(_hintsRemaining.data(), _hintsRemaining.size(), Common::Serializer::Uint16LE);
ser.syncAsSint16LE(_lastHintCharacter);
ser.syncAsSint16LE(_lastHintID);
// Sync game-specific puzzle data
// Support for older savefiles
if (ser.getVersion() < 3 && g_nancy->getGameType() <= kGameTypeNancy1) {
PuzzleData *pd = getPuzzleData(SliderPuzzleData::getTag());
if (pd) {
pd->synchronize(ser);
}
return;
}
byte numPuzzleData = _puzzleData.size();
ser.syncAsByte(numPuzzleData);
if (ser.isSaving()) {
for (auto &pd : _puzzleData) {
uint32 tag = pd._key;
ser.syncAsUint32LE(tag);
pd._value->synchronize(ser);
}
} else {
clearPuzzleData();
uint32 tag = 0;
for (uint i = 0; i < numPuzzleData; ++i) {
ser.syncAsUint32LE(tag);
PuzzleData *pd = getPuzzleData(tag);
if (pd) {
pd->synchronize(ser);
}
}
}
_isRunningAd = false;
ConfMan.removeKey("restore_after_ad", Common::ConfigManager::kTransientDomain);
g_nancy->_graphics->suppressNextDraw();
}
UI::Clock *Scene::getClock() {
auto *clok = GetEngineData(CLOK);
if (!clok || clok->clockIsDisabled || clok->clockIsDay) {
return nullptr;
} else {
return (UI::Clock *)_clock;
}
}
void Scene::init() {
auto *bootSummary = GetEngineData(BSUM);
auto *hintData = GetEngineData(HINT);
assert(bootSummary);
_flags.eventFlags.resize(g_nancy->getStaticData().numEventFlags, g_nancy->_false);
_flags.sceneCounts.clear();
_flags.items.resize(g_nancy->getStaticData().numItems, g_nancy->_false);
_flags.disabledItems.resize(_flags.items.size(), 0);
_timers.lastTotalTime = 0;
_timers.playerTime = bootSummary->startTimeHours * 3600000;
_timers.sceneTime = 0;
_timers.timerTime = 0;
_timers.timerIsActive = false;
_timers.playerTimeNextMinute = 0;
_timers.pushedPlayTime = 0;
if (ConfMan.hasKey("load_ad", Common::ConfigManager::kTransientDomain)) {
changeScene(bootSummary->adScene);
ConfMan.removeKey("load_ad", Common::ConfigManager::kTransientDomain);
_isRunningAd = true;
} else {
changeScene(bootSummary->firstScene);
}
if (hintData) {
_hintsRemaining.clear();
_hintsRemaining = hintData->numHints;
_lastHintCharacter = _lastHintID = -1;
}
initStaticData();
if (!_isRunningAd && ConfMan.hasKey("save_slot", Common::ConfigManager::kTransientDomain)) {
// Load savefile directly from the launcher
int saveSlot = ConfMan.getInt("save_slot", Common::ConfigManager::kTransientDomain);
if (saveSlot >= 0 && saveSlot <= g_nancy->getMetaEngine()->getMaximumSaveSlot()) {
g_nancy->loadGameState(saveSlot);
}
// Remove key so clicking on "New Game" in start menu doesn't just reload the save
ConfMan.removeKey("save_slot", Common::ConfigManager::kTransientDomain);
// Retain the last slot used so the nancy8+ save menu shows its name on top
ConfMan.setInt("display_slot", saveSlot, Common::ConfigManager::kTransientDomain);
} else {
// Normal boot, load default first scene
_state = kLoad;
// Make sure the nancy8+ save menu doesn't display a save name on new game
ConfMan.removeKey("display_slot", Common::ConfigManager::kTransientDomain);
}
// Set relevant event flag when player has won the game at least once
if (ConfMan.get("PlayerWonTheGame", ConfMan.getActiveDomainName()) == "AcedTheGame") {
setEventFlag(g_nancy->getStaticData().wonGameFlagID, g_nancy->_true);
}
if (g_nancy->getGameType() == kGameTypeVampire) {
_lightning = new Misc::Lightning();
}
Common::Rect vpPos = _viewport.getScreenPosition();
_hotspotDebug._drawSurface.create(vpPos.width(), vpPos.height(), g_nancy->_graphics->getScreenPixelFormat());
_hotspotDebug.moveTo(vpPos);
_hotspotDebug.setTransparent(true);
registerGraphics();
g_nancy->_graphics->redrawAll();
}
void Scene::setActiveMovie(Action::PlaySecondaryMovie *activeMovie) {
_activeMovie = activeMovie;
}
Action::PlaySecondaryMovie *Scene::getActiveMovie() {
return _activeMovie;
}
void Scene::setActiveConversation(Action::ConversationSound *activeConversation) {
_activeConversation = activeConversation;
}
Action::ConversationSound *Scene::getActiveConversation() {
return _activeConversation;
}
void Scene::beginLightning(int16 distance, uint16 pulseTime, int16 rgbPercent) {
if (_lightning) {
_lightning->beginLightning(distance, pulseTime, rgbPercent);
}
}
void Scene::specialEffect(byte type, uint16 fadeToBlackTime, uint16 frameTime) {
_specialEffects.push(Misc::SpecialEffect(type, fadeToBlackTime, frameTime));
_specialEffects.back().init();
}
void Scene::specialEffect(byte type, uint16 totalTime, uint16 fadeToBlackTime, Common::Rect rect) {
_specialEffects.push(Misc::SpecialEffect(type, totalTime, fadeToBlackTime, rect));
_specialEffects.back().init();
}
PuzzleData *Scene::getPuzzleData(const uint32 tag) {
// Lazy initialization ensures both init() and synchronize() will not need
// to care about which puzzles a specific game has
if (_puzzleData.contains(tag)) {
return _puzzleData[tag];
} else {
PuzzleData *newData = makePuzzleData(tag);
if (newData) {
_puzzleData.setVal(tag, newData);
}
return newData;
}
}
void Scene::load(bool fromSaveFile) {
if (_specialEffects.size()) {
_specialEffects.front().onSceneChange();
}
clearSceneData();
g_nancy->_graphics->suppressNextDraw();
// Scene IDs are prefixed with S inside the cif tree; e.g 100 -> S100
Common::Path sceneName(Common::String::format("S%u", _sceneState.nextScene.sceneID));
IFF *sceneIFF = g_nancy->_resource->loadIFF(sceneName);
if (!sceneIFF) {
error("Faled to load IFF %s", sceneName.toString().c_str());
}
Common::SeekableReadStream *sceneSummaryChunk = sceneIFF->getChunkStream("SSUM");
if (sceneSummaryChunk) {
_sceneState.summary.read(*sceneSummaryChunk);
} else {
sceneSummaryChunk = sceneIFF->getChunkStream("TSUM");
if (sceneSummaryChunk) {
_sceneState.summary.readTerse(*sceneSummaryChunk);
}
}
if (!sceneSummaryChunk) {
error("Invalid IFF Chunk SSUM");
}
delete sceneSummaryChunk;
debugC(0, kDebugScene, "Loading new scene %i: description \"%s\", frame %i, vertical scroll %i, %s",
_sceneState.nextScene.sceneID,
_sceneState.summary.description.c_str(),
_sceneState.nextScene.frameID,
_sceneState.nextScene.verticalOffset,
_sceneState.currentScene.continueSceneSound == kContinueSceneSound ? "kContinueSceneSound" : "kLoadSceneSound");
SceneChangeDescription lastScene = _sceneState.currentScene;
_sceneState.currentScene = _sceneState.nextScene;
// Make sure to discard invalid front vectors and reuse the last one
if (_sceneState.currentScene.listenerFrontVector.isZero()) {
_sceneState.currentScene.listenerFrontVector = lastScene.listenerFrontVector;
}
// Search for Action Records, maximum for a scene is 30
Common::SeekableReadStream *actionRecordChunk = nullptr;
uint numRecords = 0;
while (actionRecordChunk = sceneIFF->getChunkStream("ACT", numRecords), actionRecordChunk != nullptr) {
_actionManager.addNewActionRecord(*actionRecordChunk);
delete actionRecordChunk;
++numRecords;
}
if (_sceneState.currentScene.paletteID == -1) {
_sceneState.currentScene.paletteID = 0;
}
_viewport.loadVideo(_sceneState.summary.videoFile,
_sceneState.currentScene.frameID,
_sceneState.currentScene.verticalOffset,
_sceneState.summary.panningType,
_sceneState.summary.videoFormat,
_sceneState.summary.palettes.size() ? _sceneState.summary.palettes[(byte)_sceneState.currentScene.paletteID] : Common::Path());
if (_viewport.getFrameCount() <= 1) {
_viewport.disableEdges(kLeft | kRight);
}
if (_sceneState.summary.videoFormat == kSmallVideoFormat) {
// TODO
} else if (_sceneState.summary.videoFormat == kLargeVideoFormat) {
// always start from the bottom
_sceneState.currentScene.verticalOffset = _viewport.getMaxScroll();
} else {
error("Unrecognized Scene summary chunk video file format");
}
if (_sceneState.summary.videoFormat == kSmallVideoFormat) {
// TODO
} else if (_sceneState.summary.videoFormat == kLargeVideoFormat) {
if (_viewport.getMaxScroll() == 0) {
_viewport.disableEdges(kUp | kDown);
}
}
for (auto &override : _inventorySoundOverrides) {
g_nancy->_sound->stopSound(override._value.sound);
}
_inventorySoundOverrides.clear();
_timers.sceneTime = 0;
g_nancy->_sound->recalculateSoundEffects();
// Increment the number of times we've visited this scene, unless we're
// loading from a save
if (!fromSaveFile) {
_flags.sceneCounts.getOrCreateVal(_sceneState.currentScene.sceneID)++;
}
delete sceneIFF;
_state = kStartSound;
}
void Scene::run() {
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) {
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
_timers.playerTime += 60000; // Add a minute
_timers.playerTimeNextMinute = currentPlayTime + bootSummary->playerTimeMinuteLength;
}
handleInput();
if (g_nancy->getState() == NancyState::kMainMenu) {
// Player pressed esc, do not process further
return;
}
_actionManager.processActionRecords();
if (_lightning) {
_lightning->run();
}
// Do this after the first records are processed to fix the text in nancy3 intro
if (_specialEffects.size()) {
if (_specialEffects.front().isInitialized()) {
if (_specialEffects.front().isDone()) {
_specialEffects.pop();
g_nancy->_graphics->redrawAll();
}
} else {
_specialEffects.front().afterSceneChange();
}
}
g_nancy->_sound->soundEffectMaintenance();
if (_state == kLoad) {
g_nancy->_graphics->suppressNextDraw();
}
}
void Scene::handleInput() {
NancyInput input = g_nancy->_input->getInput();
// Warp the mouse below the inactive zone during dialogue scenes
if (_activeConversation != nullptr) {
const Common::Rect &inactiveZone = g_nancy->_cursor->getPrimaryVideoInactiveZone();
if (g_nancy->getGameType() == kGameTypeVampire) {
const Common::Point cursorHotspot = g_nancy->_cursor->getCurrentCursorHotspot();
Common::Point adjustedMousePos = input.mousePos;
adjustedMousePos.y -= cursorHotspot.y;
if (inactiveZone.bottom > adjustedMousePos.y) {
input.mousePos.y = inactiveZone.bottom + cursorHotspot.y;
g_nancy->_cursor->warpCursor(input.mousePos);
}
} else {
if (inactiveZone.bottom > input.mousePos.y) {
input.mousePos.y = inactiveZone.bottom;
g_nancy->_cursor->warpCursor(input.mousePos);
}
}
} else if (!_activeMovie) {
// Check if player has pressed esc
if (input.input & NancyInput::kOpenMainMenu) {
g_nancy->setState(NancyState::kMainMenu);
return;
}
}
// We handle the textbox and inventory box first because of their scrollbars, which
// need to take highest priority
_textbox.handleInput(input);
_inventoryBox.handleInput(input);
// Handle invisible map button
// We do this before the viewport since TVD's map button overlaps the viewport's right hotspot
for (uint16 id : g_nancy->getStaticData().mapAccessSceneIDs) {
if ((int)_sceneState.currentScene.sceneID == id) {
if (_mapHotspot.contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(g_nancy->getGameType() == kGameTypeVampire ? CursorManager::kHotspot : CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
requestStateChange(NancyState::kMap);
if (g_nancy->getGameType() == kGameTypeVampire) {
g_nancy->setMouseEnabled(false);
}
}
input.eatMouseInput();
}
break;
}
}
// Handle clock before viewport since it overlaps the left hotspot in TVD
if (getClock()) {
getClock()->handleInput(input);
}
_viewport.handleInput(input);
_sceneState.currentScene.verticalOffset = _viewport.getCurVerticalScroll();
if (_sceneState.currentScene.frameID != _viewport.getCurFrame()) {
_sceneState.currentScene.frameID = _viewport.getCurFrame();
g_nancy->_sound->recalculateSoundEffects();
}
_actionManager.handleInput(input);
// Menu/help are disabled when a movie is active
if (!_activeMovie) {
if (_menuButton) {
_menuButton->handleInput(input);
if (_menuButton->_isClicked) {
if (_buttonPressActivationTime == 0) {
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
g_nancy->_sound->playSound("BUOK");
_buttonPressActivationTime = g_system->getMillis() + bootSummary->buttonPressTimeDelay;
} else if (g_system->getMillis() > _buttonPressActivationTime) {
_menuButton->_isClicked = false;
requestStateChange(NancyState::kMainMenu);
_buttonPressActivationTime = 0;
}
}
}
if (_helpButton) {
_helpButton->handleInput(input);
if (_helpButton->_isClicked) {
if (_buttonPressActivationTime == 0) {
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
g_nancy->_sound->playSound("BUOK");
_buttonPressActivationTime = g_system->getMillis() + bootSummary->buttonPressTimeDelay;
} else if (g_system->getMillis() > _buttonPressActivationTime) {
_helpButton->_isClicked = false;
requestStateChange(NancyState::kHelp);
_buttonPressActivationTime = 0;
}
}
}
}
}
void Scene::initStaticData() {
auto *bootSummary = GetEngineData(BSUM);
assert(bootSummary);
const ImageChunk *fr0 = (const ImageChunk *)g_nancy->getEngineData("FR0");
assert(fr0);
auto *mapData = GetEngineData(MAP);
_frame.init(fr0->imageName);
_viewport.init();
_textbox.init();
_inventoryBox.init();
// Init buttons
if (g_nancy->getGameType() == kGameTypeVampire) {
_mapHotspot = bootSummary->extraButtonHotspot;
} else if (mapData) {
_mapHotspot = mapData->buttonDest;
}
_menuButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->menuButtonSrc, bootSummary->menuButtonDest, bootSummary->menuButtonHighlightSrc);
_helpButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->helpButtonSrc, bootSummary->helpButtonDest, bootSummary->helpButtonHighlightSrc);
g_nancy->setMouseEnabled(true);
// Init ornaments and clock (TVD only)
if (g_nancy->getGameType() == kGameTypeVampire) {
_viewportOrnaments = new UI::ViewportOrnaments(9);
_viewportOrnaments->init();
_textboxOrnaments = new UI::TextboxOrnaments(9);
_textboxOrnaments->init();
_inventoryBoxOrnaments = new UI::InventoryBoxOrnaments(9);
_inventoryBoxOrnaments->init();
_clock = new UI::Clock();
_clock->init();
}
// Init just the clock (nancy2 and up; nancy1 has no clock, only a map button)
if (g_nancy->getGameType() >= kGameTypeNancy2) {
auto *clok = GetEngineData(CLOK);
if (clok->clockIsDay) {
// nancy5 uses a different "clock" that mostly just indicates the in-game day
_clock = new UI::Nancy5Clock();
_clock->init();
} else if (!clok->clockIsDisabled) {
_clock = new UI::Clock();
_clock->init();
} else {
// In nancy7 the clock is entirely disabled
_clock = nullptr;
}
}
_state = kLoad;
}
void Scene::clearSceneData() {
// Clear generic flags only
for (uint16 id : g_nancy->getStaticData().genericEventFlags) {
_flags.eventFlags[id] = g_nancy->_false;
}
clearLogicConditions();
_actionManager.clearActionRecords();
if (_lightning) {
_lightning->endLightning();
}
if (_textbox.hasBeenDrawn()) {
// Improvement: the dog portrait scenes in nancy7 queue a piece of text,
// then immediately change the scene. This makes the text disappear instantly;
// instead, we check if the textbox has been drawn, and don't clear it if it hasn't.
// Hopefully this doesn't cause issues with earlier games.
_textbox.clear();
}
_activeConversation = nullptr;
_activeMovie = nullptr;
}
void Scene::clearPuzzleData() {
for (auto &pd : _puzzleData) {
delete pd._value;
}
_puzzleData.clear();
}
Scene::PlayFlags::LogicCondition::LogicCondition() : flag(g_nancy->_false) {}
} // End of namespace State
} // End of namespace Nancy