/* 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 "audio/audiostream.h" #include "audio/decoders/wave.h" #include "common/archive.h" #include "common/compression/installshieldv3_archive.h" #include "common/config-manager.h" #include "common/debug-channels.h" #include "common/debug.h" #include "common/error.h" #include "common/events.h" #include "common/file.h" #include "common/savefile.h" #include "common/str.h" #include "common/system.h" #include "common/timer.h" #include "common/macresman.h" #include "common/language.h" #include "common/compression/stuffit.h" #include "graphics/paletteman.h" #include "engines/util.h" #include "image/bmp.h" #include "private/decompiler.h" #include "private/grammar.h" #include "private/private.h" #include "private/savegame.h" #include "private/tokens.h" namespace Private { PrivateEngine *g_private = nullptr; extern int parse(const char *); PrivateEngine::PrivateEngine(OSystem *syst, const ADGameDescription *gd) : Engine(syst), _gameDescription(gd), _image(nullptr), _videoDecoder(nullptr), _compositeSurface(nullptr), _transparentColor(0), _frameImage(nullptr), _framePalette(nullptr), _videoSubtitles(nullptr), _sfxSubtitles(false), _useSubtitles(false), _defaultCursor(nullptr), _screenW(640), _screenH(480) { _highlightMasks = false; _rnd = new Common::RandomSource("private"); // Global object for external reference g_private = this; // Setting execution _nextSetting = ""; _currentSetting = ""; _pausedSetting = ""; _pausedMovieName = ""; _modified = false; _mode = -1; _toTake = false; _haveTakenItem = false; // Movies _nextMovie = ""; _currentMovie = ""; _nextVS = ""; _repeatedMovieExit = ""; // Save and load _saveGameMask.clear(); _loadGameMask.clear(); // Interface _framePath = "inface/general/inface2.bmp"; // Police resetPoliceBust(); _sirenSound = "po/audio/posfx002.wav"; // General sounds _globalAudioPath = "global/audio/"; _noStopSounds = false; // Radios and phone _policeRadioArea.clear(); _AMRadioArea.clear(); _phoneArea.clear(); _AMRadio.path = "inface/radio/comm_/"; _AMRadio.sound = &_AMRadioSound; _policeRadio.path = "inface/radio/police/"; _policeRadio.sound = &_policeRadioSound; _phonePrefix = "inface/telephon/"; // Dossiers _dossierPage = 0; _dossierSuspect = 0; _dossierPageMask.clear(); _dossierNextSuspectMask.clear(); _dossierPrevSuspectMask.clear(); _dossierNextSheetMask.clear(); _dossierPrevSheetMask.clear(); // Diary _diaryLocPrefix = "inface/diary/loclist/"; _currentDiaryPage = -1; // Safe _safeNumberPath = "sg/search_s/sgsaf%d.bmp"; for (uint d = 0 ; d < 3; d++) { _safeDigitArea[d].clear(); _safeDigitRect[d] = Common::Rect(0, 0); } // Timer clearTimer(); } PrivateEngine::~PrivateEngine() { destroyVideo(); destroySubtitles(); delete _compositeSurface; if (_frameImage != nullptr) { _frameImage->free(); delete _frameImage; } if (_mframeImage != nullptr) { _mframeImage->free(); delete _mframeImage; } free(_framePalette); delete _rnd; delete _image; delete Gen::g_vm; delete Settings::g_setts; delete _defaultCursor; for (uint i = 0; i < _cursors.size(); i++) { if (_cursors[i].winCursorGroup == nullptr) { delete _cursors[i].cursor; } delete _cursors[i].winCursorGroup; } for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) { const MaskInfo &m = *it; if (m.surf != nullptr) { m.surf->free(); delete m.surf; } } if (_phoneArea.surf != nullptr) { _phoneArea.surf->free(); delete _phoneArea.surf; } for (uint i = 0; i < ARRAYSIZE(_safeDigitArea); i++) { if (_safeDigitArea[i].surf != nullptr) { _safeDigitArea[i].surf->free(); delete _safeDigitArea[i].surf; } } for (RectList::iterator it = _rects.begin(); it != _rects.end(); ++it) { Common::Rect *r = (*it); delete r; } } void PrivateEngine::initializePath(const Common::FSNode &gamePath) { SearchMan.addDirectory(gamePath, 0, 10); } Common::SeekableReadStream *PrivateEngine::loadAssets() { Common::File *test = new Common::File(); if (isDemo() && test->open("SUPPORT/ASSETS/DEMOGAME.WIN")) return test; if (isDemo() && test->open("SUPPORT/DEMOGAME.MAC")) return test; if (test->open("SUPPORT/ASSETS/GAME.WIN")) return test; if (test->open("SUPPORT/GAME.MAC")) return test; delete test; if (_platform == Common::kPlatformMacintosh) { Common::ScopedPtr macInstaller(loadMacInstaller()); if (macInstaller) { const char *macFileName = isDemo() ? "demogame.mac" : "game.mac"; Common::SeekableReadStream *file = macInstaller->createReadStreamForMember(macFileName); if (file != nullptr) { return file; } } } Common::InstallShieldV3 installerArchive; if (!installerArchive.open("SUPPORT/ASSETS.Z")) error("Failed to open SUPPORT/ASSETS.Z"); // if the full game is used if (!isDemo()) { if (installerArchive.hasFile("GAME.DAT")) return installerArchive.createReadStreamForMember("GAME.DAT"); if (installerArchive.hasFile("GAME.WIN")) return installerArchive.createReadStreamForMember("GAME.WIN"); error("Unknown version"); return nullptr; } // if the demo from archive.org is used if (installerArchive.hasFile("GAME.TXT")) return installerArchive.createReadStreamForMember("GAME.TXT"); // if the demo from the full retail CDROM is used if (installerArchive.hasFile("DEMOGAME.DAT")) return installerArchive.createReadStreamForMember("DEMOGAME.DAT"); if (installerArchive.hasFile("DEMOGAME.WIN")) return installerArchive.createReadStreamForMember("DEMOGAME.WIN"); error("Unknown version"); return nullptr; } Common::Archive *PrivateEngine::loadMacInstaller() { const char *fileName; if (_language == Common::JA_JPN) { fileName = "xn--16jc8na7ay6a0eyg9e5nud0e4525d"; } else if (isDemo()) { fileName = "Private Eye Demo Installer"; } else { fileName = "Private Eye Installer"; } Common::SeekableReadStream *archiveFile = Common::MacResManager::openFileOrDataFork(fileName); if (archiveFile == nullptr) { return nullptr; } // createStuffItArchive() takes ownership of incoming stream, even on failure return createStuffItArchive(archiveFile, true); } Common::Error PrivateEngine::run() { // Only enable if subtitles are available if (!Common::parseBool(ConfMan.get("subtitles"), _useSubtitles)) warning("Failed to parse bool from subtitles options"); if (!Common::parseBool(ConfMan.get("sfxSubtitles"), _sfxSubtitles)) warning("Failed to parse bool from sfxSubtitles options"); if (!Common::parseBool(ConfMan.get("highlightMasks"), _shouldHighlightMasks)) warning("Failed to parse bool from highlightMasks options"); if (!_useSubtitles && _sfxSubtitles) { warning("SFX subtitles are enabled, but no subtitles will be shown"); } _language = Common::parseLanguage(ConfMan.get("language")); _platform = Common::parsePlatform(ConfMan.get("platform")); Common::SeekableReadStream *file = loadAssets(); // Read assets file const uint32 fileSize = file->size(); char *buf = (char *)malloc(fileSize + 1); file->read(buf, fileSize); buf[fileSize] = '\0'; Decompiler decomp(buf, fileSize, _platform == Common::kPlatformMacintosh); free(buf); Common::String scripts = decomp.getResult(); debugC(1, kPrivateDebugCode, "code:\n%s", scripts.c_str()); // Initialize stuff Gen::g_vm = new Gen::VM(); Settings::g_setts = new Settings::SettingMaps(); initFuncs(); parse(scripts.c_str()); delete file; if (maps.constants.size() == 0) error("Failed to parse game script"); initializeWallSafeValue(); // Initialize graphics _pixelFormat = Graphics::PixelFormat::createFormatCLUT8(); initGraphics(_screenW, _screenH, &_pixelFormat); _transparentColor = 250; _screenRect = Common::Rect(0, 0, _screenW, _screenH); loadCursors(); changeCursor("default"); _origin = Common::Point(0, 0); _image = new Image::BitmapDecoder(); _compositeSurface = new Graphics::ManagedSurface(); _compositeSurface->create(_screenW, _screenH, _pixelFormat); _compositeSurface->setTransparentColor(_transparentColor); _currentDiaryPage = -1; // Load the game frame once byte *palette; bool isNewPalette; _frameImage = decodeImage(_framePath, &palette, &isNewPalette); if (isNewPalette) { free(palette); palette = nullptr; } _mframeImage = decodeImage(_framePath, &palette, &isNewPalette); _framePalette = (byte *) malloc(3*256); memcpy(_framePalette, palette, 3*256); if (isNewPalette) { free(palette); palette = nullptr; } byte *initialPalette; bool isNewInitialPalette; Graphics::Surface *surf = decodeImage("inface/general/inface1.bmp", &initialPalette, &isNewInitialPalette); _compositeSurface->setPalette(initialPalette, 0, 256); surf->free(); delete surf; _image->destroy(); if (isNewInitialPalette) { free(initialPalette); initialPalette = nullptr; } // Main event loop Common::Event event; Common::Point mousePos; _videoDecoder = nullptr; _pausedVideo = nullptr; int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0) { // load the savegame loadGameState(saveSlot); } else { _nextSetting = getGoIntroSetting(); } _needToDrawScreenFrame = false; while (!shouldQuit()) { bool mouseMoved = false; checkTimer(); checkPhoneCall(); while (_system->getEventManager()->pollEvent(event)) { mousePos = _system->getEventManager()->getMousePos(); // Events switch (event.type) { case Common::EVENT_CUSTOM_ENGINE_ACTION_START: if (event.customType == kActionSkip) { if (!_timerSkipSetting.empty()) { skipTimer(); } else { skipVideo(); } } break; case Common::EVENT_SCREEN_CHANGED: adjustSubtitleSize(); break; case Common::EVENT_QUIT: case Common::EVENT_RETURN_TO_LAUNCHER: break; case Common::EVENT_LBUTTONDOWN: if (selectDossierNextSuspect(mousePos)) break; if (selectDossierPrevSuspect(mousePos)) break; if (selectDossierNextSheet(mousePos)) break; if (selectDossierPrevSheet(mousePos)) break; if (selectDossierPage(mousePos)) break; if (selectSafeDigit(mousePos)) break; if (selectDiaryNextPage(mousePos)) break; if (selectDiaryPrevPage(mousePos)) break; if (selectLocation(mousePos)) break; if (selectMemory(mousePos)) { _needToDrawScreenFrame = true; break; } if (selectSkipMemoryVideo(mousePos)) break; if (selectPhoneArea(mousePos)) break; if (selectPoliceRadioArea(mousePos)) break; if (selectAMRadioArea(mousePos)) break; if (selectLoadGame(mousePos)) break; if (selectSaveGame(mousePos)) break; if (_nextSetting.empty()) if (selectMask(mousePos)) break; if (_nextSetting.empty()) if (selectExit(mousePos)) break; selectPauseGame(mousePos); break; case Common::EVENT_MOUSEMOVE: mouseMoved = true; updateCursor(mousePos); break; default: break; } } checkPoliceBust(); // Movies if (!_nextMovie.empty()) { clearTimer(); _videoDecoder = new Video::SmackerDecoder(); playVideo(_nextMovie); _currentMovie = _nextMovie; _nextMovie = ""; updateCursor(mousePos); } if (_videoDecoder && !_videoDecoder->isPaused()) { if (_videoDecoder->getCurFrame() == 0) { stopSounds(); } if (_videoDecoder->endOfVideo()) { delete _videoDecoder; _videoDecoder = nullptr; destroySubtitles(); _currentMovie = ""; } else if (!_videoDecoder->needsUpdate() && mouseMoved) { _system->updateScreen(); } else if (_videoDecoder->needsUpdate()) { drawScreen(); } _system->delayMillis(5); // Yield to the system continue; } if (!_nextSetting.empty()) { clearTimer(); debugC(1, kPrivateDebugFunction, "Executing %s", _nextSetting.c_str()); clearAreas(); _currentSetting = _nextSetting; Settings::g_setts->load(_nextSetting); _nextSetting = ""; _currentVS = ""; Gen::g_vm->run(); // Draw the screen once the VM has processed the last setting. // This prevents the screen from flickering images as VM settings // are executed. Fixes the previous screen from being displayed // when a video finishes playing. if (_nextSetting.empty()) { if (!_nextVS.empty() && _currentVS.empty() && _currentSetting == getMainDesktopSetting()) { loadImage(_nextVS, 160, 120); _currentVS = _nextVS; } updateCursor(mousePos); drawScreen(); } } _system->updateScreen(); _system->delayMillis(10); updateSubtitles(); } return Common::kNoError; } void PrivateEngine::initFuncs() { for (const Private::FuncTable *fnc = funcTable; fnc->name; fnc++) { Common::String name(fnc->name); _functions.setVal(name, (void *)fnc->func); } } void PrivateEngine::clearAreas() { for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) { const MaskInfo &m = *it; if (m.surf != nullptr) { m.surf->free(); delete m.surf; } } _exits.clear(); _masks.clear(); _highlightMasks = false; _locationMasks.clear(); _memoryMasks.clear(); _loadGameMask.clear(); _saveGameMask.clear(); _policeRadioArea.clear(); _AMRadioArea.clear(); if (_phoneArea.surf != nullptr) { _phoneArea.surf->free(); delete _phoneArea.surf; } _phoneArea.clear(); _dossierPageMask.clear(); _dossierNextSuspectMask.clear(); _dossierPrevSuspectMask.clear(); _dossierNextSheetMask.clear(); _dossierPrevSheetMask.clear(); _diaryNextPageExit.clear(); _diaryPrevPageExit.clear(); for (uint d = 0 ; d < 3; d++) { if (_safeDigitArea[d].surf) { _safeDigitArea[d].surf->free(); delete _safeDigitArea[d].surf; } _safeDigitArea[d].clear(); _safeDigitRect[d] = Common::Rect(0, 0); } } void PrivateEngine::resetPoliceBust() { _policeBustEnabled = false; _policeSirenPlayed = false; _numberOfClicks = 0; _numberClicksAfterSiren = 0; _policeBustMovieIndex = 0; _policeBustMovie = ""; _policeBustPreviousSetting = ""; } void PrivateEngine::startPoliceBust() { _policeBustEnabled = true; _policeSirenPlayed = false; // Calculate two click counts: // 1. the number of clicks until the siren warning // 2. the number of clicks after the siren warning until the bust // This logic was extracted from the executable. int policeIndex = maps.variables.getVal(getPoliceIndexVariable())->u.val; if (policeIndex > 20) { policeIndex = 21; } int r = _rnd->getRandomNumber(11); int numberOfClicks = r + ((policeIndex * 14) / -21) + 16; _numberClicksAfterSiren = _rnd->getRandomNumber(6) + 3; if ((numberOfClicks - _numberClicksAfterSiren) <= 2) { _numberOfClicks = 2; } else { _numberOfClicks = numberOfClicks - _numberClicksAfterSiren; } } void PrivateEngine::stopPoliceBust() { _policeBustEnabled = false; } void PrivateEngine::wallSafeAlarm() { // This logic was extracted from the executable. // It skips the siren and randomly alters the number of clicks // until the police arrive. This may increase or decrease the // number of clicks, but there will always be at least 1 left. _policeSirenPlayed = true; int r1 = _rnd->getRandomNumber(3); int r2 = _rnd->getRandomNumber(3); if (r1 + r2 + 1 <= _numberOfClicks) { r1 = _rnd->getRandomNumber(3); r2 = _rnd->getRandomNumber(3); _numberOfClicks = r1 + r2 + 1; } } void PrivateEngine::completePoliceBust() { if (!_policeBustPreviousSetting.empty()) { _nextSetting = _policeBustPreviousSetting; } int policeIndex = maps.variables.getVal(getPoliceIndexVariable())->u.val; if (policeIndex > 13) { return; } // Set kPoliceArrived. This flag is cleared by the wall safe alarm. Symbol *policeArrived = maps.variables.getVal(getPoliceArrivedVariable()); setSymbol(policeArrived, 1); // Select the movie for BustMovie() to play _policeBustMovie = Common::String::format("po/animatio/spoc%02dxs.smk", kPoliceBustVideos[_policeBustMovieIndex]); // Play audio on the second bust movie if (kPoliceBustVideos[_policeBustMovieIndex] == 2) { Common::String s("global/transiti/audio/spoc02VO.wav"); stopSounds(); playForegroundSound(s); changeCursor("default"); waitForSoundsToStop(); } // Cycle to the next movie and wrap around _policeBustMovieIndex = (_policeBustMovieIndex + 1) % ARRAYSIZE(kPoliceBustVideos); _nextSetting = getPOGoBustMovieSetting(); } void PrivateEngine::checkPoliceBust() { if (_mode != 1) { return; } if (!_policeBustEnabled) { return; } if (_numberOfClicks >= 0) { return; } if (!_policeSirenPlayed) { // Play siren playForegroundSound(_sirenSound); _policeSirenPlayed = true; _numberOfClicks = _numberClicksAfterSiren; } else { // Bust Marlowe. // The original seems to record _currentSetting instead of // _nextSetting, but that causes a click to do nothing if it // triggers a police bust that doesn't do anything except for // restoring the current scene. if (!_nextSetting.empty()) { _policeBustPreviousSetting = _nextSetting; } else { _policeBustPreviousSetting = _currentSetting; } // The next setting is indeed kPoliceBustFromMO, even though it // occurs from all locations and is unrelated to Marlowe's office. // According to comments in the game script, Marlowe's office // originally required a special mode but it was later removed. // Apparently the developers didn't rename the setting. _nextSetting = getPoliceBustFromMOSetting(); _policeBustEnabled = false; } } void PrivateEngine::updateCursor(Common::Point mousePos) { // If a function returns true then it changed the cursor. if (cursorPhoneArea(mousePos)) { return; } if (cursorSafeDigit(mousePos)) { return; } if (cursorMask(mousePos)) { return; } if (cursorExit(mousePos)) { return; } if (cursorPauseMovie(mousePos)) { return; } changeCursor("default"); } bool PrivateEngine::cursorExit(Common::Point mousePos) { mousePos = mousePos - _origin; int rs = 100000000; int cs = 0; Common::String cursor; for (ExitList::const_iterator it = _exits.begin(); it != _exits.end(); ++it) { const ExitInfo &e = *it; cs = e.rect.width() * e.rect.height(); if (e.rect.contains(mousePos)) { if (cs < rs && !e.cursor.empty()) { rs = cs; cursor = e.cursor; } } } if (!cursor.empty()) { changeCursor(cursor); return true; } return false; } bool PrivateEngine::cursorSafeDigit(Common::Point mousePos) { if (_safeDigitArea[0].surf == nullptr) { return false; } mousePos = mousePos - _origin; if (mousePos.x < 0 || mousePos.y < 0) { return false; } for (uint i = 0; i < 3; i++) { MaskInfo &m = _safeDigitArea[i]; if (m.surf != nullptr) { if (_safeDigitRect[i].contains(mousePos) && !m.cursor.empty()) { changeCursor(m.cursor); return true; } } } return false; } bool PrivateEngine::inMask(Graphics::Surface *surf, Common::Point mousePos) { if (surf == nullptr) return false; mousePos = mousePos - _origin; if (mousePos.x < 0 || mousePos.y < 0) return false; if (mousePos.x > surf->w || mousePos.y > surf->h) return false; return (surf->getPixel(mousePos.x, mousePos.y) != _transparentColor); } bool PrivateEngine::cursorMask(Common::Point mousePos) { bool inside = false; for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) { const MaskInfo &m = *it; bool inArea = m.useBoxCollision ? m.box.contains(mousePos) : inMask(m.surf, mousePos); if (inArea) { if (!m.cursor.empty()) { // TODO: check this inside = true; changeCursor(m.cursor); break; } } } return inside; } bool PrivateEngine::cursorPauseMovie(Common::Point mousePos) { if (_mode == 1) { uint32 tol = 15; Common::Rect window(_origin.x - tol, _origin.y - tol, _screenW - _origin.x + tol, _screenH - _origin.y + tol); if (!window.contains(mousePos)) { changeCursor("default"); return true; } } return false; } Common::String PrivateEngine::getPauseMovieSetting() { return getSymbolName("kPauseMovie", "k3"); } Common::String PrivateEngine::getGoIntroSetting() { return getSymbolName("kGoIntro", "k1"); } Common::String PrivateEngine::getAlternateGameVariable() { return getSymbolName("kAlternateGame", "k2"); } Common::String PrivateEngine::getMainDesktopSetting() { return getSymbolName("kMainDesktop", "k183", "k45"); } Common::String PrivateEngine::getDiaryTOCSetting() { return getSymbolName("kDiaryTOC", "k185"); } Common::String PrivateEngine::getDiaryMiddleSetting() { return getSymbolName("kDiaryMiddle", "k186"); } Common::String PrivateEngine::getDiaryLastPageSetting() { return getSymbolName("kDiaryLastPage", "k187"); } Common::String PrivateEngine::getPoliceIndexVariable() { return getSymbolName("kPoliceIndex", "k0"); } Common::String PrivateEngine::getPOGoBustMovieSetting() { return getSymbolName("kPOGoBustMovie", "k7"); } Common::String PrivateEngine::getPoliceBustFromMOSetting() { return getSymbolName("kPoliceBustFromMO", "k6"); } Common::String PrivateEngine::getListenToPhoneSetting() { return getSymbolName("kListenToPhone", "k9"); } Common::String PrivateEngine::getWallSafeValueVariable() { return getSymbolName("kWallSafeValue", "k3"); } Common::String PrivateEngine::getPoliceArrivedVariable() { return getSymbolName("kPoliceArrived", "k7"); } Common::String PrivateEngine::getBeenDowntownVariable() { return getSymbolName("kBeenDowntown", "k8"); } Common::String PrivateEngine::getPoliceStationLocation() { return getSymbolName("kLocationPO", "k12"); } Common::String PrivateEngine::getExitCursor() { return getSymbolName("kExit", "k5"); } Common::String PrivateEngine::getInventoryCursor() { return getSymbolName("kInventory", "k7"); } const char *PrivateEngine::getSymbolName(const char *name, const char *strippedName, const char *demoName) { if (_platform == Common::kPlatformWindows) { if (_language == Common::EN_USA || _language == Common::JA_JPN || _language == Common::KO_KOR || _language == Common::RU_RUS) { return name; } } if (demoName != nullptr && isDemo()) { return demoName; } return strippedName; } bool PrivateEngine::isSlotActive(const SubtitleSlot &slot) { return slot.subs != nullptr && _mixer->isSoundHandleActive(slot.handle); } bool PrivateEngine::isSfxSubtitle(const Video::Subtitles *subs) { if (!subs) return false; return subs->isSfx(); } void PrivateEngine::selectPauseGame(Common::Point mousePos) { if (_mode == 1) { uint32 tol = 15; Common::Rect window(_origin.x - tol, _origin.y - tol, _screenW - _origin.x + tol, _screenH - _origin.y + tol); if (!window.contains(mousePos)) { // Pause game and return to desktop if (_pausedSetting.empty()) { if (!_nextSetting.empty()) _pausedSetting = _nextSetting; else _pausedSetting = _currentSetting; _nextSetting = getPauseMovieSetting(); if (_videoDecoder) { _videoDecoder->pauseVideo(true); _pausedVideo = _videoDecoder; _pausedMovieName = _currentMovie; } if (_videoSubtitles || _voiceSlot.subs || _sfxSlot.subs) { _system->hideOverlay(); } _pausedBackgroundSoundName = _bgSound.name; _compositeSurface->fillRect(_screenRect, 0); _compositeSurface->setPalette(_framePalette, 0, 256); _origin = Common::Point(kOriginZero[0], kOriginZero[1]); drawMask(_frameImage); _origin = Common::Point(kOriginOne[0], kOriginOne[1]); } } } } void PrivateEngine::resumeGame() { _nextSetting = _pausedSetting; _pausedSetting = ""; _mode = 1; _origin = Common::Point(kOriginOne[0], kOriginOne[1]); if (_pausedVideo != nullptr) { _videoDecoder = _pausedVideo; _pausedVideo = nullptr; // restore the name we saved in selectPauseGame if (!_pausedMovieName.empty()) { _currentMovie = _pausedMovieName; _pausedMovieName.clear(); } } // always reload subtitles if a movie is active // we do this unconditionally because the casebook might have loaded // different subtitles while we were paused if (!_currentMovie.empty()) { loadSubtitles(convertPath(_currentMovie), kSubtitleVideo); } if (_videoDecoder) { _videoDecoder->pauseVideo(false); _needToDrawScreenFrame = true; } if (!_pausedBackgroundSoundName.empty()) { playBackgroundSound(_pausedBackgroundSoundName); _pausedBackgroundSoundName.clear(); } // force draw the subtitle once // the screen was likely wiped by the pause menu // to account for the subtitle which was already rendered and we wiped the screen before it finished we must // force the subtitle system to ignore its cache and redraw the text. // calling adjustSubtitleSize() makes the next drawSubtitle call perform a full redraw // automatically, so we don't need to pass 'true' adjustSubtitleSize(); if (_videoSubtitles || _voiceSlot.subs || _sfxSlot.subs) { _system->showOverlay(false); _system->clearOverlay(); // redraw video subtitles if (_videoDecoder && _videoSubtitles) _videoSubtitles->drawSubtitle(_videoDecoder->getTime(), false, _sfxSubtitles); // draw all remaining active subtitles if (isSlotActive(_voiceSlot)) { uint32 time = _mixer->getElapsedTime(_voiceSlot.handle).msecs(); _voiceSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } else if (isSlotActive(_sfxSlot)) { uint32 time = _mixer->getElapsedTime(_sfxSlot.handle).msecs(); _sfxSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } } } bool PrivateEngine::selectExit(Common::Point mousePos) { mousePos = mousePos - _origin; Common::String ns = ""; int rs = 100000000; for (ExitList::const_iterator it = _exits.begin(); it != _exits.end(); ++it) { const ExitInfo &e = *it; int cs = e.rect.width() * e.rect.height(); //debug("Testing exit %s %d", e.nextSetting->c_str(), cs); if (e.rect.contains(mousePos)) { //debug("Inside! %d %d", cs, rs); if (cs < rs && !e.nextSetting.empty()) { // TODO: check this // an item was not taken if (_toTake) { playForegroundSound(_takeLeaveSound, getLeaveSound()); _toTake = false; } //debug("Found Exit %s %d", e.nextSetting->c_str(), cs); rs = cs; ns = e.nextSetting; } } } if (!ns.empty()) { if (_mode == 1) { _numberOfClicks--; // count click only if it hits a hotspot } _nextSetting = ns; _highlightMasks = false; return true; } return false; } bool PrivateEngine::selectMask(Common::Point mousePos) { Common::String ns; for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) { const MaskInfo &m = *it; //debug("Testing mask %s", m.nextSetting->c_str()); if (inMask(m.surf, mousePos)) { //debug("Inside!"); if (!m.nextSetting.empty()) { // TODO: check this //debug("Found Mask %s", m.nextSetting->c_str()); ns = m.nextSetting; } if (m.flag1 != nullptr) { // TODO: check this // an item was taken if (_toTake) { addInventory(m.inventoryItem, *(m.flag1->name)); playForegroundSound(_takeLeaveSound, getTakeSound()); _toTake = false; _haveTakenItem = true; } } if (m.flag2 != nullptr) { setSymbol(m.flag2, 1); } break; } } if (!ns.empty()) { if (_mode == 1) { _numberOfClicks--; // count click only if it hits a hotspot } _nextSetting = ns; _highlightMasks = false; return true; } return false; } bool PrivateEngine::selectLocation(const Common::Point &mousePos) { if (_locationMasks.size() == 0) { return false; } uint i = 0; int totalLocations = 0; for (auto &it : maps.locationList) { const Private::Symbol *sym = maps.locations.getVal(it); if (sym->u.val) { if (_locationMasks[i].box.contains(mousePos)) { bool diaryPageSet = false; for (uint j = 0; j < _diaryPages.size(); j++) { if (_diaryPages[j].locationID == totalLocations + 1) { _currentDiaryPage = j; diaryPageSet = true; break; } } // Prevent crash if there are no memories for this location if (!diaryPageSet) { return true; } _nextSetting = _locationMasks[i].nextSetting; return true; } i++; } totalLocations++; } return false; } bool PrivateEngine::selectDiaryNextPage(Common::Point mousePos) { mousePos = mousePos - _origin; if (mousePos.x < 0 || mousePos.y < 0) return false; if (_diaryNextPageExit.rect.contains(mousePos)) { _currentDiaryPage++; _nextSetting = _diaryNextPageExit.nextSetting; playForegroundSound(getPaperShuffleSound()); return true; } return false; } bool PrivateEngine::selectDiaryPrevPage(Common::Point mousePos) { mousePos = mousePos - _origin; if (mousePos.x < 0 || mousePos.y < 0) return false; if (_diaryPrevPageExit.rect.contains(mousePos)) { _currentDiaryPage--; _nextSetting = _diaryPrevPageExit.nextSetting; playForegroundSound(getPaperShuffleSound()); return true; } return false; } bool PrivateEngine::selectMemory(const Common::Point &mousePos) { for (uint i = 0; i < _memoryMasks.size(); i++) { if (inMask(_memoryMasks[i].surf, mousePos)) { clearAreas(); _nextMovie = _diaryPages[_currentDiaryPage].memories[i].movie; _nextSetting = getDiaryMiddleSetting(); return true; } } return false; } void PrivateEngine::addMemory(const Common::String &path) { size_t index = path.findLastOf('\\'); Common::String location = path.substr(index + 2, 2); Common::String imagePath; // Paths to the global folder have a different pattern from other paths if (path.contains("global")) { if (path.contains("spoc00xs")) { imagePath = "inface/diary/ss_icons/global/transiti/ipoc00.bmp"; } else { imagePath = "inface/diary/ss_icons/global/transiti/animatio/mo/imo" + path.substr(index + 4, 3) + ".bmp"; } } else { // First letter after the last \ is an s, which isn't needed; next 2 are location; and the next 3 are what image to use imagePath = "inface/diary/ss_icons/" + location + "/i" + location + path.substr(index + 4, 3) + ".bmp"; } if (!Common::File::exists(convertPath(imagePath))) { return; } MemoryInfo memory; memory.movie = path; memory.image = imagePath; for (int i = _diaryPages.size() - 1; i >= 0; i--) { if (_diaryPages[i].locationName == location) { if (_diaryPages[i].memories.size() == 6) { DiaryPage diaryPage; diaryPage.locationName = location; diaryPage.locationID = _diaryPages[i].locationID; diaryPage.memories.push_back(memory); _diaryPages.insert_at(i + 1, diaryPage); return; } _diaryPages[i].memories.push_back(memory); return; } } DiaryPage diaryPage; diaryPage.locationName = location; diaryPage.locationID = -1; uint locationIndex = 0; for (auto &it : maps.locationList) { Private::Symbol *sym = maps.locations.getVal(it); locationIndex++; Common::String currentLocation = it.substr(9); if (it.size() <= 3) { if (it == "k0") { currentLocation = "mo"; } else if (it == "k1") { currentLocation = "is"; } else if (it == "k2") { currentLocation = "mw"; } else if (it == "k3") { currentLocation = "cs"; } else if (it == "k4") { currentLocation = "cw"; } else if (it == "k5") { currentLocation = "ts"; } else if (it == "k6") { currentLocation = "bo"; } else if (it == "k7") { currentLocation = "gz"; } else if (it == "k8") { currentLocation = "sg"; } else if (it == "k9") { currentLocation = "da"; } else if (it == "k10") { currentLocation = "dl"; } else if (it == "k11") { currentLocation = "vn"; } else if (it == "k12") { currentLocation = "po"; } else if (it == "k13") { currentLocation = "dc"; } else error("Unknown location symbol %s", it.c_str()); } currentLocation.toLowercase(); if (currentLocation == location) { // Ensure that the location is marked as visited. // Police station video spoc00xs can be played before the // police station has been visited if the player has not // been busted by the police yet. setLocationAsVisited(sym); diaryPage.locationID = locationIndex; break; } } assert(diaryPage.locationID != -1); diaryPage.memories.push_back(memory); for (int i = _diaryPages.size() - 1; i >= 0; i--) { if (_diaryPages[i].locationID < diaryPage.locationID) { _diaryPages.insert_at(i + 1, diaryPage); return; } } _diaryPages.insert_at(0, diaryPage); } bool PrivateEngine::inInventory(const Common::String &bmp) const { for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) { if (it->diaryImage == bmp) return true; } return false; } void PrivateEngine::addInventory(const Common::String &bmp, Common::String &flag) { // set game flag if (!flag.empty()) { Symbol *sym = maps.lookupVariable(&flag); setSymbol(sym, 1); } // add to casebook if (!inInventory(bmp)) { InventoryItem i; i.diaryImage = bmp; i.flag = flag; inventory.push_back(i); } } void PrivateEngine::removeInventory(const Common::String &bmp) { for (InvList::iterator it = inventory.begin(); it != inventory.end(); ++it) { if (it->diaryImage == bmp) { // clear game flag if (!it->flag.empty()) { Symbol *sym = maps.lookupVariable(&(it->flag)); setSymbol(sym, 0); } // remove from casebook inventory.erase(it); break; } } } void PrivateEngine::removeRandomInventory() { // This logic was extracted from the executable. // Examples: // 0-3 items: 0 items removed // 4-6 items: 1 item removed // 7-10 items: 2 items removed uint numberOfItemsToRemove = (inventory.size() * 30) / 100; for (uint i = 0; i < numberOfItemsToRemove; i++) { uint indexToRemove = _rnd->getRandomNumber(inventory.size() - 1); uint index = 0; for (InvList::iterator it = inventory.begin(); it != inventory.end(); ++it) { if (index == indexToRemove) { removeInventory(it->diaryImage); break; } index++; } } } bool PrivateEngine::selectAMRadioArea(Common::Point mousePos) { if (_AMRadioArea.surf == nullptr) return false; if (inMask(_AMRadioArea.surf, mousePos)) { playRadio(_AMRadio, false); return true; } return false; } bool PrivateEngine::selectPoliceRadioArea(Common::Point mousePos) { if (_policeRadioArea.surf == nullptr) return false; if (inMask(_policeRadioArea.surf, mousePos)) { playRadio(_policeRadio, true); return true; } return false; } void PrivateEngine::addDossier(Common::String &page1, Common::String &page2) { // Each dossier page can only be added once. // Do this even when loading games to fix saves with duplicates. for (uint i = 0; i < _dossiers.size(); i++) { if (_dossiers[i].page1 == page1) { return; } } DossierInfo d; d.page1 = page1; d.page2 = page2; _dossiers.push_back(d); } void PrivateEngine::loadDossier() { int x = 40; int y = 30; MaskInfo m; DossierInfo d = _dossiers[_dossierSuspect]; if (_dossierPage == 0) { m.surf = loadMask(d.page1, x, y, true); } else if (_dossierPage == 1) { m.surf = loadMask(d.page2, x, y, true); } else { error("Invalid page"); } m.cursor = "default"; _dossierPageMask = m; _masks.push_back(m); // not push_front, as this occurs after DossierChgSheet } bool PrivateEngine::selectDossierPage(Common::Point mousePos) { if (_dossierPageMask.surf == nullptr) { return false; } if (inMask(_dossierPageMask.surf, mousePos)) { return true; } return false; } bool PrivateEngine::selectDossierNextSuspect(Common::Point mousePos) { if (_dossierNextSuspectMask.surf == nullptr) return false; if (inMask(_dossierNextSuspectMask.surf, mousePos)) { if ((_dossierSuspect + 1) < _dossiers.size()) { playForegroundSound(getPaperShuffleSound()); _dossierSuspect++; _dossierPage = 0; // reload kDossierOpen _nextSetting = _currentSetting; } return true; } return false; } bool PrivateEngine::selectDossierPrevSheet(Common::Point mousePos) { if (_dossierNextSheetMask.surf == nullptr) return false; if (inMask(_dossierPrevSheetMask.surf, mousePos)) { if (_dossierPage == 1) { playForegroundSound(getPaperShuffleSound()); _dossierPage = 0; // reload kDossierOpen _nextSetting = _currentSetting; } return true; } return false; } bool PrivateEngine::selectDossierNextSheet(Common::Point mousePos) { if (_dossierNextSheetMask.surf == nullptr) return false; if (inMask(_dossierNextSheetMask.surf, mousePos)) { DossierInfo m = _dossiers[_dossierSuspect]; if (_dossierPage == 0 && !m.page2.empty()) { playForegroundSound(getPaperShuffleSound()); _dossierPage = 1; // reload kDossierOpen _nextSetting = _currentSetting; } return true; } return false; } bool PrivateEngine::selectDossierPrevSuspect(Common::Point mousePos) { if (_dossierPrevSuspectMask.surf == nullptr) return false; if (inMask(_dossierPrevSuspectMask.surf, mousePos)) { if (_dossierSuspect > 0) { playForegroundSound(getPaperShuffleSound()); _dossierSuspect--; _dossierPage = 0; // reload kDossierOpen _nextSetting = _currentSetting; } return true; } return false; } void PrivateEngine::addRadioClip( Radio &radio, const Common::String &name, int priority, int disabledPriority1, bool exactPriorityMatch1, int disabledPriority2, bool exactPriorityMatch2, const Common::String &flagName, int flagValue) { // lookup radio clip by name RadioClip *clip = nullptr; for (uint i = 0; i < radio.clips.size(); i++) { if (radio.clips[i].name == name) { clip = &radio.clips[i]; break; } } // add clip if new if (clip == nullptr) { RadioClip newClip; newClip.name = name; newClip.played = false; newClip.priority = priority; newClip.disabledPriority1 = disabledPriority1; newClip.exactPriorityMatch1 = exactPriorityMatch1; newClip.disabledPriority2 = disabledPriority2; newClip.exactPriorityMatch2 = exactPriorityMatch2; newClip.flagName = flagName; newClip.flagValue = flagValue; radio.clips.push_back(newClip); clip = &radio.clips[radio.clips.size() - 1]; } // disable other clips based on the clip's priority disableRadioClips(radio, clip->priority); } void PrivateEngine::initializeAMRadioChannels(uint clipCount) { Radio &radio = _AMRadio; assert(clipCount < radio.clips.size()); // clear all channels for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) { radio.channels[i] = -1; } // build array of playable clip indexes (up to clipCount) Common::Array playableClips; for (uint i = 0; i < clipCount; i++) { if (!radio.clips[i].played) { playableClips.push_back(i); } } // place the highest priority clips in the channels (up to two) uint channelCount; switch (playableClips.size()) { case 0: channelCount = 0; break; case 1: channelCount = 1; break; case 2: channelCount = 1; break; case 3: channelCount = 1; break; default: channelCount = 2; break; } uint channel = 0; uint end = 0; while (channel < channelCount) { channel++; if (channel < playableClips.size()) { uint start = channel; uint remainingClips = playableClips.size() - start; while (remainingClips--) { RadioClip &clip1 = radio.clips[playableClips[start]]; RadioClip &clip2 = radio.clips[playableClips[end]]; if (clip1.priority < clip2.priority) { SWAP(playableClips[start], playableClips[end]); } start++; } } radio.channels[channel - 1] = playableClips[end]; end++; } // build another array of playable clip indexes, starting at clipCount Common::Array morePlayableClips; for (uint i = clipCount; i < radio.clips.size(); i++) { if (!radio.clips[i].played) { morePlayableClips.push_back(i); } } // shuffle second array if (!morePlayableClips.empty()) { for (uint i = morePlayableClips.size() - 1; i > 0; i--) { uint n = _rnd->getRandomNumber(i); SWAP(morePlayableClips[i], morePlayableClips[n]); } } // install some of the clips from the second array into channels, starting // at the end of the channel array to keep the highest priority clips. uint copyCount = morePlayableClips.size(); if (playableClips.size() <= 3) { // not morePlayableClips copyCount = MIN(copyCount, 2); } else { copyCount = MIN(copyCount, 1); } for (uint i = 0; i < copyCount; i++) { radio.channels[2 - i] = morePlayableClips[i]; } // shuffle channels for (uint i = ARRAYSIZE(radio.channels) - 1; i > 0; i--) { uint n = _rnd->getRandomNumber(i); SWAP(radio.channels[i], radio.channels[n]); } } void PrivateEngine::initializePoliceRadioChannels() { Radio &radio = _policeRadio; // clear all channels for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) { radio.channels[i] = -1; } // build array of playable clip indexes Common::Array playableClips; for (uint i = 0; i < radio.clips.size(); i++) { if (!radio.clips[i].played) { playableClips.push_back(i); } } // place the highest priority clips in the channels (up to three) uint channelCount = MIN(playableClips.size(), ARRAYSIZE(radio.channels)); uint channel = 0; uint end = 0; while (channel < channelCount) { channel++; if (channel < playableClips.size()) { uint start = channel; uint remainingClips = playableClips.size() - start; while (remainingClips--) { RadioClip &clip1 = radio.clips[playableClips[start]]; RadioClip &clip2 = radio.clips[playableClips[end]]; if (clip1.priority < clip2.priority) { SWAP(playableClips[start], playableClips[end]); } start++; } } radio.channels[channel - 1] = playableClips[end]; end++; } } void PrivateEngine::disableRadioClips(Radio &radio, int priority) { for (uint i = 0; i < radio.clips.size(); i++) { RadioClip &clip = radio.clips[i]; if (clip.played) { continue; } if (clip.disabledPriority1) { if ((clip.exactPriorityMatch1 && priority == clip.disabledPriority1) || (!clip.exactPriorityMatch1 && priority <= clip.disabledPriority1)) { clip.played = true; } } if (clip.disabledPriority2) { if ((clip.exactPriorityMatch2 && priority == clip.disabledPriority2) || (!clip.exactPriorityMatch2 && priority <= clip.disabledPriority2)) { clip.played = true; } } } } void PrivateEngine::playRadio(Radio &radio, bool randomlyDisableClips) { // if radio is already playing then turn it off if (isSoundPlaying(*(radio.sound))) { stopForegroundSounds(); return; } // search channels for first available clip for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) { // skip empty channels if (radio.channels[i] == -1) { continue; } // verify that clip hasn't been already been played RadioClip &clip = radio.clips[radio.channels[i]]; radio.channels[i] = -1; if (clip.played) { continue; } // the police radio randomly disables clips (!) if (randomlyDisableClips) { uint r = _rnd->getRandomNumber(9); if (r < 3) { clip.played = true; break; // play radio.wav } } // play the clip Common::String sound = radio.path + clip.name + ".wav"; stopForegroundSounds(); playForegroundSound(*(radio.sound), sound); clip.played = true; if (!clip.flagName.empty()) { Symbol *flag = maps.lookupVariable(&(clip.flagName)); setSymbol(flag, clip.flagValue); } return; } // play default radio sound stopForegroundSounds(); playForegroundSound(*(radio.sound), "inface/radio/radio.wav"); } void PrivateEngine::addPhone(const Common::String &name, bool once, int startIndex, int endIndex, const Common::String &flagName, int flagValue) { // lookup phone clip by name and index range PhoneInfo *phone = nullptr; for (PhoneList::iterator it = _phones.begin(); it != _phones.end(); ++it) { if (it->name == name && it->startIndex == startIndex && it->endIndex == endIndex) { phone = &(*it); break; } } // add or update phone clip if (phone == nullptr) { PhoneInfo newPhone; newPhone.name = name; newPhone.once = once; newPhone.startIndex = startIndex; newPhone.endIndex = endIndex; newPhone.flagName = flagName; newPhone.flagValue = flagValue; newPhone.status = kPhoneStatusWaiting; newPhone.callCount = 0; newPhone.soundIndex = 0; // add single clip or a range of clips that occur in a random order if (startIndex == endIndex) { Common::String sound = name + ".wav"; newPhone.sounds.push_back(sound); } else { for (int i = startIndex; i <= endIndex; i++) { Common::String sound = Common::String::format("%s%02d.wav", name.c_str(), i); newPhone.sounds.push_back(sound); } // shuffle for (uint i = newPhone.sounds.size() - 1; i > 0; i--) { uint n = _rnd->getRandomNumber(i); SWAP(newPhone.sounds[i], newPhone.sounds[n]); } } _phones.push_back(newPhone); } else { // update an available phone clip's state if its sounds haven't been played yet if (phone->soundIndex < phone->sounds.size()) { // reset the call count phone->callCount = 0; // the first PhoneClip() call does not cause the phone clip to ring, // but the second call does. if a phone clip has multiple sounds and // one has been answered then its status changes to waiting so that // the next PhoneClip() call will make the next sound available. if (phone->status == kPhoneStatusWaiting) { phone->status = kPhoneStatusAvailable; } else if (phone->status == kPhoneStatusAnswered) { phone->status = kPhoneStatusWaiting; } } } } void PrivateEngine::initializePhoneOnDesktop() { // any phone clips that were missed, or left ringing, are available // unless they are phone clips that only occur once. for (PhoneList::iterator it = _phones.begin(); it != _phones.end(); ++it) { if (!it->once && (it->status == kPhoneStatusCalling || it->status == kPhoneStatusMissed)) { it->status = kPhoneStatusAvailable; } } } void PrivateEngine::checkPhoneCall() { if (_phoneArea.surf == nullptr) { return; } if (isSoundPlaying()) { return; } // any phone clips that were calling have been missed for (PhoneList::iterator it = _phones.begin(); it != _phones.end(); ++it) { if (it->status == kPhoneStatusCalling) { it->status = kPhoneStatusMissed; } } // get the next available phone clip PhoneInfo *phone = nullptr; for (PhoneList::iterator it = _phones.begin(); it != _phones.end(); ++it) { if (it->status == kPhoneStatusAvailable && it->soundIndex < it->sounds.size() && it->callCount < (it->once ? 1 : 3)) { phone = &(*it); break; } } if (phone == nullptr) { return; } phone->status = kPhoneStatusCalling; phone->callCount++; playForegroundSound(_phoneCallSound, _phonePrefix + "phone.wav"); } bool PrivateEngine::cursorPhoneArea(Common::Point mousePos) { if (_phoneArea.surf == nullptr) { return false; } if (!isSoundPlaying(_phoneCallSound)) { return false; } if (inMask(_phoneArea.surf, mousePos)) { changeCursor(_phoneArea.cursor); return true; } return false; } bool PrivateEngine::selectPhoneArea(Common::Point mousePos) { if (_phoneArea.surf == nullptr) { return false; } if (!isSoundPlaying(_phoneCallSound)) { return false; } if (inMask(_phoneArea.surf, mousePos)) { // get phone clip to answer PhoneInfo *phone = nullptr; for (PhoneList::iterator it = _phones.begin(); it != _phones.end(); ++it) { if (it->status == kPhoneStatusCalling) { phone = &(*it); break; } } if (phone == nullptr) { return true; } // phone clip has been answered, select sound phone->status = kPhoneStatusAnswered; Common::String sound = _phonePrefix + phone->sounds[phone->soundIndex]; phone->soundIndex++; // -100 indicates that the variable should be decremented Symbol *flag = maps.lookupVariable(&(phone->flagName)); if (phone->flagValue == -100) { setSymbol(flag, flag->u.val - 1); } else { setSymbol(flag, phone->flagValue); } stopForegroundSounds(); // stop phone ringing playForegroundSound(sound); _nextSetting = getListenToPhoneSetting(); changeCursor("default"); return true; } return false; } void PrivateEngine::initializeWallSafeValue() { if (isDemo()) { return; } // initialize to a random value that is not the combination Private::Symbol *sym = maps.variables.getVal(getWallSafeValueVariable()); int value; do { value = _rnd->getRandomNumber(999); } while (value == 426); sym->u.val = value; } bool PrivateEngine::selectSafeDigit(Common::Point mousePos) { if (_safeDigitArea[0].surf == nullptr) return false; mousePos = mousePos - _origin; if (mousePos.x < 0 || mousePos.y < 0) return false; for (uint d = 0 ; d < 3; d ++) if (_safeDigitRect[d].contains(mousePos)) { incrementSafeDigit(d); _nextSetting = _safeDigitArea[d].nextSetting; return true; } return false; } void PrivateEngine::addSafeDigit(uint32 d, Common::Rect *rect) { MaskInfo m; _safeDigitRect[d] = *rect; int digitValue = getSafeDigit(d); m.surf = loadMask(Common::String::format(_safeNumberPath.c_str(), digitValue), _safeDigitRect[d].left, _safeDigitRect[d].top, true); m.cursor = getExitCursor(); m.nextSetting = _currentSetting; m.flag1 = nullptr; m.flag2 = nullptr; _safeDigitArea[d] = m; } int PrivateEngine::getSafeDigit(uint32 d) { assert(d < 3); Private::Symbol *sym = maps.variables.getVal(getWallSafeValueVariable()); int value = sym->u.val; byte digits[3]; digits[0] = value / 100; digits[1] = (value / 10) % 10; digits[2] = value % 10; return digits[d]; } void PrivateEngine::incrementSafeDigit(uint32 d) { assert(d < 3); Private::Symbol *sym = maps.variables.getVal(getWallSafeValueVariable()); int value = sym->u.val; byte digits[3]; digits[0] = value / 100; digits[1] = (value / 10) % 10; digits[2] = value % 10; digits[d] = (digits[d] + 1) % 10; sym->u.val = (100 * digits[0]) + (10 * digits[1]) + digits[2]; } bool PrivateEngine::selectLoadGame(Common::Point mousePos) { if (_loadGameMask.surf == nullptr) return false; if (inMask(_loadGameMask.surf, mousePos)) { loadGameDialog(); return true; } return false; } bool PrivateEngine::selectSaveGame(Common::Point mousePos) { if (_saveGameMask.surf == nullptr) return false; if (inMask(_saveGameMask.surf, mousePos)) { saveGameDialog(); return true; } return false; } bool PrivateEngine::hasFeature(EngineFeature f) const { return (f == kSupportsReturnToLauncher); } void PrivateEngine::restartGame() { debugC(1, kPrivateDebugFunction, "restartGame"); Common::String alternateGameVariableName = getAlternateGameVariable(); for (NameList::iterator it = maps.variableList.begin(); it != maps.variableList.end(); ++it) { Private::Symbol *sym = maps.variables.getVal(*it); if (*(sym->name) != alternateGameVariableName) sym->u.val = 0; } // Police Bust resetPoliceBust(); // Diary for (NameList::iterator it = maps.locationList.begin(); it != maps.locationList.end(); ++it) { Private::Symbol *sym = maps.locations.getVal(*it); sym->u.val = 0; } inventory.clear(); _toTake = false; _haveTakenItem = false; _dossiers.clear(); _diaryPages.clear(); // Sounds _AMRadio.clear(); _policeRadio.clear(); _phones.clear(); _pausedBackgroundSoundName.clear(); // Movies _repeatedMovieExit = ""; _playedMovies.clear(); destroyVideo(); // Pause _pausedSetting = ""; _pausedMovieName.clear(); // VSPicture _nextVS = ""; // Wall Safe initializeWallSafeValue(); // Timer clearTimer(); } Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream) { // We don't want to continue with any sound or videos from a previous game stopSounds(); destroyVideo(); _pausedMovieName.clear(); debugC(1, kPrivateDebugFunction, "loadGameStream"); // Read and validate metadata header SavegameMetadata meta; if (!readSavegameMetadata(stream, meta)) { return Common::kReadingFailed; } // Log unexpected language or platform if (meta.language != _language) { warning("Save language %d different than game %d", meta.language, _language); } if (meta.platform != _platform) { warning("Save platform %d different than game %d", meta.platform, _platform); } Common::Serializer s(stream, nullptr); int val; for (NameList::iterator it = maps.variableList.begin(); it != maps.variableList.end(); ++it) { s.syncAsUint32LE(val); Private::Symbol *sym = maps.variables.getVal(*it); sym->u.val = val; } // Diary for (NameList::iterator it = maps.locationList.begin(); it != maps.locationList.end(); ++it) { s.syncAsUint32LE(val); Private::Symbol *sym = maps.locations.getVal(*it); sym->u.val = val; } // Inventory inventory.clear(); uint32 size = stream->readUint32LE(); for (uint32 i = 0; i < size; ++i) { InventoryItem inv; inv.diaryImage = stream->readString(); inv.flag = stream->readString(); inventory.push_back(inv); } _toTake = (stream->readByte() == 1); _haveTakenItem = (stream->readByte() == 1); // Diary pages _diaryPages.clear(); uint32 diaryPagesSize = stream->readUint32LE(); for (uint32 i = 0; i < diaryPagesSize; i++) { DiaryPage diaryPage; diaryPage.locationName = stream->readString(); diaryPage.locationID = stream->readUint32LE(); uint32 memoriesSize = stream->readUint32LE(); for (uint32 j = 0; j < memoriesSize; j++) { MemoryInfo memory; memory.image = stream->readString(); memory.movie = stream->readString(); diaryPage.memories.push_back(memory); } _diaryPages.push_back(diaryPage); } // Dossiers _dossiers.clear(); size = stream->readUint32LE(); for (uint32 i = 0; i < size; ++i) { Common::String page1 = stream->readString(); Common::String page2 = stream->readString(); addDossier(page1, page2); } // Police Bust _policeBustEnabled = (stream->readByte() == 1); _policeSirenPlayed = (stream->readByte() == 1); _numberOfClicks = stream->readSint32LE(); _numberClicksAfterSiren = stream->readSint32LE(); _policeBustMovieIndex = stream->readSint32LE(); _policeBustMovie = stream->readString(); _policeBustPreviousSetting = stream->readString(); // Radios Radio *radios[] = { &_AMRadio, &_policeRadio }; for (uint r = 0; r < ARRAYSIZE(radios); r++) { Radio *radio = radios[r]; radio->clear(); size = stream->readUint32LE(); for (uint32 i = 0; i < size; ++i) { RadioClip clip; clip.name = stream->readString(); clip.played = (stream->readByte() == 1); clip.priority = stream->readSint32LE(); clip.disabledPriority1 = stream->readSint32LE(); clip.exactPriorityMatch1 = (stream->readByte() == 1); clip.disabledPriority2 = stream->readSint32LE(); clip.exactPriorityMatch2 = (stream->readByte() == 1); clip.flagName = stream->readString(); clip.flagValue = stream->readSint32LE(); radio->clips.push_back(clip); } for (uint i = 0; i < ARRAYSIZE(radio->channels); i++) { radio->channels[i] = stream->readSint32LE(); } } size = stream->readUint32LE(); _phones.clear(); for (uint32 j = 0; j < size; ++j) { PhoneInfo p; p.name = stream->readString(); p.once = (stream->readByte() == 1); p.startIndex = stream->readSint32LE(); p.endIndex = stream->readSint32LE(); p.flagName = stream->readString(); p.flagValue = stream->readSint32LE(); p.status = (PhoneStatus)stream->readByte(); p.callCount = stream->readSint32LE(); p.soundIndex = stream->readUint32LE(); uint32 phoneSoundsSize = stream->readUint32LE(); for (uint32 i = 0; i < phoneSoundsSize; i++) { p.sounds.push_back(stream->readString()); } _phones.push_back(p); } // Played media _repeatedMovieExit = stream->readString(); _playedMovies.clear(); size = stream->readUint32LE(); for (uint32 i = 0; i < size; ++i) { _playedMovies.setVal(stream->readString(), true); } // VSPicture _nextVS = stream->readString(); // Paused setting _pausedSetting = stream->readString(); // Restore a movie that was playing _currentMovie = stream->readString(); /* int currentTime = */ stream->readUint32LE(); if (!_currentMovie.empty()) { _videoDecoder = new Video::SmackerDecoder(); playVideo(_currentMovie); _videoDecoder->pauseVideo(true); // TODO: implement seek } if (_pausedSetting.empty()) _nextSetting = getMainDesktopSetting(); else _nextSetting = getPauseMovieSetting(); // Sounds if (meta.version >= 4) { _pausedBackgroundSoundName = stream->readString(); } else { _pausedBackgroundSoundName.clear(); } return Common::kNoError; } Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) { debugC(1, kPrivateDebugFunction, "saveGameStream(%d)", isAutosave); if (isAutosave) return Common::kNoError; // Metadata SavegameMetadata meta; meta.version = kCurrentSavegameVersion; meta.language = _language; meta.platform = _platform; writeSavegameMetadata(stream, meta); // Variables for (NameList::const_iterator it = maps.variableList.begin(); it != maps.variableList.end(); ++it) { const Private::Symbol *sym = maps.variables.getVal(*it); stream->writeUint32LE(sym->u.val); } // Diary for (NameList::const_iterator it = maps.locationList.begin(); it != maps.locationList.end(); ++it) { const Private::Symbol *sym = maps.locations.getVal(*it); stream->writeUint32LE(sym->u.val); } stream->writeUint32LE(inventory.size()); for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) { stream->writeString(it->diaryImage); stream->writeByte(0); stream->writeString(it->flag); stream->writeByte(0); } stream->writeByte(_toTake ? 1 : 0); stream->writeByte(_haveTakenItem ? 1 : 0); stream->writeUint32LE(_diaryPages.size()); for (uint i = 0; i < _diaryPages.size(); i++) { stream->writeString(_diaryPages[i].locationName); stream->writeByte(0); stream->writeUint32LE(_diaryPages[i].locationID); stream->writeUint32LE(_diaryPages[i].memories.size()); for (uint j = 0; j < _diaryPages[i].memories.size(); j++) { stream->writeString(_diaryPages[i].memories[j].image); stream->writeByte(0); stream->writeString(_diaryPages[i].memories[j].movie); stream->writeByte(0); } } // Dossiers stream->writeUint32LE(_dossiers.size()); for (DossierArray::const_iterator it = _dossiers.begin(); it != _dossiers.end(); ++it) { stream->writeString(it->page1.c_str()); stream->writeByte(0); if (!it->page2.empty()) stream->writeString(it->page2.c_str()); stream->writeByte(0); } // Police Bust stream->writeByte(_policeBustEnabled ? 1 : 0); stream->writeByte(_policeSirenPlayed ? 1 : 0); stream->writeSint32LE(_numberOfClicks); stream->writeSint32LE(_numberClicksAfterSiren); stream->writeSint32LE(_policeBustMovieIndex); stream->writeString(_policeBustMovie); stream->writeByte(0); stream->writeString(_policeBustPreviousSetting); stream->writeByte(0); // Radios Radio *radios[] = { &_AMRadio, &_policeRadio }; for (uint r = 0; r < ARRAYSIZE(radios); r++) { Radio *radio = radios[r]; stream->writeUint32LE(radio->clips.size()); for (uint i = 0; i < radio->clips.size(); i++) { RadioClip &clip = radio->clips[i]; stream->writeString(clip.name); stream->writeByte(0); stream->writeByte(clip.played ? 1 : 0); stream->writeSint32LE(clip.priority); stream->writeSint32LE(clip.disabledPriority1); stream->writeByte(clip.exactPriorityMatch1 ? 1 : 0); stream->writeSint32LE(clip.disabledPriority2); stream->writeByte(clip.exactPriorityMatch2 ? 1 : 0); stream->writeString(clip.flagName); stream->writeByte(0); stream->writeSint32LE(clip.flagValue); } for (uint i = 0; i < ARRAYSIZE(radio->channels); i++) { stream->writeSint32LE(radio->channels[i]); } } // Phone stream->writeUint32LE(_phones.size()); for (PhoneList::const_iterator it = _phones.begin(); it != _phones.end(); ++it) { stream->writeString(it->name); stream->writeByte(0); stream->writeByte(it->once ? 1 : 0); stream->writeSint32LE(it->startIndex); stream->writeSint32LE(it->endIndex); stream->writeString(it->flagName); stream->writeByte(0); stream->writeSint32LE(it->flagValue); stream->writeByte(it->status); stream->writeSint32LE(it->callCount); stream->writeUint32LE(it->soundIndex); stream->writeUint32LE(it->sounds.size()); for (uint i = 0; i < it->sounds.size(); i++) { stream->writeString(it->sounds[i]); stream->writeByte(0); } } // Played media stream->writeString(_repeatedMovieExit); stream->writeByte(0); stream->writeUint32LE(_playedMovies.size()); for (PlayedMediaTable::const_iterator it = _playedMovies.begin(); it != _playedMovies.end(); ++it) { stream->writeString(it->_key); stream->writeByte(0); } // VSPicture stream->writeString(_nextVS); stream->writeByte(0); // In case the game was saved during a pause stream->writeString(_pausedSetting); stream->writeByte(0); // If we were playing a movie stream->writeString(_currentMovie); stream->writeByte(0); if (_videoDecoder) stream->writeUint32LE(_videoDecoder->getCurFrame()); else stream->writeUint32LE(0); // Sounds stream->writeString(_pausedBackgroundSoundName); stream->writeByte(0); return Common::kNoError; } Common::Path PrivateEngine::convertPath(const Common::String &name) { Common::String path(name); Common::String s1("\\"); Common::String s2("/"); while (path.contains(s1)) Common::replace(path, s1, s2); s1 = Common::String("\""); s2 = Common::String(""); Common::replace(path, s1, s2); Common::replace(path, s1, s2); path.toLowercase(); return Common::Path(path); } void PrivateEngine::playBackgroundSound(const Common::String &name) { playSound(_bgSound, name, true); } void PrivateEngine::playForegroundSound(const Common::String &name) { // stop sound if already playing. for example, the wall safe alarm. for (uint i = 0; i < ARRAYSIZE(_fgSounds); i++) { if (_fgSounds[i].name == name) { if (isSoundPlaying(_fgSounds[i])) { stopSound(_fgSounds[i]); break; } } } // play using the first available sound for (uint i = 0; i < ARRAYSIZE(_fgSounds); i++) { if (!isSoundPlaying(_fgSounds[i])) { playSound(_fgSounds[i], name, false); break; } } } void PrivateEngine::playForegroundSound(Sound &sound, const Common::String &name) { playSound(sound, name, false); } void PrivateEngine::playSound(Sound &sound, const Common::String &name, bool loop) { sound.name = name; Common::Path path = convertPath(name); Common::SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(path); if (file == nullptr) { error("unable to find sound file %s", path.toString().c_str()); } Audio::LoopingAudioStream *stream = new Audio::LoopingAudioStream(Audio::makeWAVStream(file, DisposeAfterUse::YES), loop ? 0 : 1); _mixer->stopHandle(sound.handle); _mixer->playStream(Audio::Mixer::kSFXSoundType, &sound.handle, stream, -1, Audio::Mixer::kMaxChannelVolume); loadSubtitles(path, kSubtitleAudio, &sound); } void PrivateEngine::stopForegroundSounds() { for (uint i = 0; i < ARRAYSIZE(_fgSounds); i++) { stopSound(_fgSounds[i]); } stopSound(_phoneCallSound); stopSound(_AMRadioSound); stopSound(_policeRadioSound); stopSound(_takeLeaveSound); } void PrivateEngine::stopSounds() { stopSound(_bgSound); stopForegroundSounds(); } void PrivateEngine::stopSound(Sound &sound) { _mixer->stopHandle(sound.handle); if (_voiceSlot.handle == sound.handle && _voiceSlot.subs) { delete _voiceSlot.subs; _voiceSlot.subs = nullptr; } if (_sfxSlot.handle == sound.handle && _sfxSlot.subs) { delete _sfxSlot.subs; _sfxSlot.subs = nullptr; } sound.name.clear(); } bool PrivateEngine::isSoundPlaying() { return _mixer->isSoundIDActive(-1); } bool PrivateEngine::isSoundPlaying(Sound &sound) { return _mixer->isSoundHandleActive(sound.handle); } void PrivateEngine::waitForSoundsToStop() { while (isSoundPlaying()) { // since this is a blocking wait loop, the main engine loop in run() is not called until this loop finishes // we must manually update and draw subtitles here otherwise sounds // played via fSyncSound will play audio but show no subtitles. updateSubtitles(); if (consumeEvents()) { stopSounds(); return; } } uint32 i = 100; while (i--) { // one second extra if (consumeEvents()) { stopSounds(); return; } } } // returns true if interrupted by user or engine quitting bool PrivateEngine::consumeEvents() { if (shouldQuit()) { return true; } Common::Event event; while (_system->getEventManager()->pollEvent(event)) { switch (event.type) { case Common::EVENT_RETURN_TO_LAUNCHER: case Common::EVENT_QUIT: return true; case Common::EVENT_CUSTOM_ENGINE_ACTION_START: if (event.customType == kActionSkip) { return true; } break; default: break;; } } _system->updateScreen(); _system->delayMillis(10); return false; } void PrivateEngine::adjustSubtitleSize() { debugC(1, kPrivateDebugFunction, "%s()", __FUNCTION__); if (!_videoSubtitles && !_voiceSlot.subs && !_sfxSlot.subs) return; // calculate layout first then apply to both active sounds and video subtitled sound // Subtitle positioning constants (as percentages of screen height) const int HORIZONTAL_MARGIN = 20; const float BOTTOM_MARGIN_PERCENT = 0.009f; // ~20px at 2160p const float MAIN_MENU_HEIGHT_PERCENT = 0.093f; // ~200px at 2160p const float ALTERNATE_MODE_HEIGHT_PERCENT = 0.102f; // ~220px at 2160p const float DEFAULT_HEIGHT_PERCENT = 0.074f; // ~160px at 2160p // Font sizing constants (as percentage of screen height) const int MIN_FONT_SIZE = 8; const float BASE_FONT_SIZE_PERCENT = 0.023f; // ~50px at 2160p int16 h = _system->getOverlayHeight(); int16 w = _system->getOverlayWidth(); int bottomMargin = int(h * BOTTOM_MARGIN_PERCENT); int topOffset = 0; // If we are in the main menu, we need to adjust the position of the subtitles if (_mode == 0) { topOffset = int(h * MAIN_MENU_HEIGHT_PERCENT); } else if (_mode == -1) { topOffset = int(h * ALTERNATE_MODE_HEIGHT_PERCENT); } else { topOffset = int(h * DEFAULT_HEIGHT_PERCENT); } Common::Rect rect(HORIZONTAL_MARGIN, h - topOffset, w - HORIZONTAL_MARGIN, h - bottomMargin); int fontSize = MAX(MIN_FONT_SIZE, int(h * BASE_FONT_SIZE_PERCENT)); // apply to video subtitles if (_videoSubtitles) { _videoSubtitles->setBBox(rect); _videoSubtitles->setColor(0xff, 0xff, 0x80); _videoSubtitles->setFont("LiberationSans-Regular.ttf", fontSize, Video::Subtitles::kFontStyleRegular); _videoSubtitles->setFont("LiberationSans-Italic.ttf", fontSize, Video::Subtitles::kFontStyleItalic); } // apply to all active audio subtitles if (_voiceSlot.subs) { _voiceSlot.subs->setBBox(rect); _voiceSlot.subs->setColor(0xff, 0xff, 0x80); _voiceSlot.subs->setFont("LiberationSans-Regular.ttf", fontSize, Video::Subtitles::kFontStyleRegular); _voiceSlot.subs->setFont("LiberationSans-Italic.ttf", fontSize, Video::Subtitles::kFontStyleItalic); } if (_sfxSlot.subs) { _sfxSlot.subs->setBBox(rect); _sfxSlot.subs->setColor(0xff, 0xff, 0x80); _sfxSlot.subs->setFont("LiberationSans-Regular.ttf", fontSize, Video::Subtitles::kFontStyleRegular); _sfxSlot.subs->setFont("LiberationSans-Italic.ttf", fontSize, Video::Subtitles::kFontStyleItalic); } } Common::Path PrivateEngine::getSubtitlePath(const Common::String &soundName) { // call convertPath to fix slashes, make lowercase etc. Common::Path path = convertPath(soundName); // add extension and replace '/' with '_' (audio/file -> audio_file) Common::String subPathStr = path.toString() + ".srt"; subPathStr.replace('/', '_'); // get language code Common::String language(Common::getLanguageCode(_language)); if (language == "us") language = "en"; // construct full path: subtitles/language/subPathStr Common::Path subPath = "subtitles"; subPath = subPath.appendComponent(language); subPath = subPath.appendComponent(subPathStr); return subPath; } void PrivateEngine::loadSubtitles(const Common::Path &path, SubtitleType type, Sound *sound) { debugC(1, kPrivateDebugFunction, "%s(%s)", __FUNCTION__, path.toString().c_str()); if (!_useSubtitles) return; Common::Path subPath = getSubtitlePath(path.toString()); debugC(1, kPrivateDebugFunction, "Loading subtitles from %s", subPath.toString().c_str()); // instantiate and load on heap once Video::Subtitles *newSub = new Video::Subtitles(); newSub->loadSRTFile(subPath); // if the subtitle failed loading we should return if (!newSub->isLoaded()) { delete newSub; return; } if (type == kSubtitleVideo) { if (_videoSubtitles) delete _videoSubtitles; _videoSubtitles = newSub; } else if (type == kSubtitleAudio) { if (!sound) { warning("PrivateEngine::loadSubtitles: Audio type requested but no Sound provided"); delete newSub; return; } bool isSfx = isSfxSubtitle(newSub); if (isSfx) { // if voice is currently playing, ignore incoming sfx if (isSlotActive(_voiceSlot)) { delete newSub; return; } // load sfx (overwrites any previous sfx) if (_sfxSlot.subs) delete _sfxSlot.subs; _sfxSlot.handle = sound->handle; _sfxSlot.subs = newSub; } else { // voice always loads and takes priority if (_voiceSlot.subs) delete _voiceSlot.subs; _voiceSlot.handle = sound->handle; _voiceSlot.subs = newSub; } } // we skip clearing the overlay because updateSubtitle() handles it in the main loop // if we clear here as well then minor flickering occurs adjustSubtitleSize(); } void PrivateEngine::updateSubtitles() { if (!_useSubtitles) return; // remove subtitles for sounds that finished playing if (_voiceSlot.subs && !_mixer->isSoundHandleActive(_voiceSlot.handle)) { delete _voiceSlot.subs; _voiceSlot.subs = nullptr; } if (_sfxSlot.subs && !_mixer->isSoundHandleActive(_sfxSlot.handle)) { delete _sfxSlot.subs; _sfxSlot.subs = nullptr; } if (_voiceSlot.subs) { // if voice is active draw voice only uint32 time = _mixer->getElapsedTime(_voiceSlot.handle).msecs(); _voiceSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } else if (_sfxSlot.subs) { // if voice is empty draw sfx uint32 time = _mixer->getElapsedTime(_sfxSlot.handle).msecs(); _sfxSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } } void PrivateEngine::destroySubtitles() { if (_voiceSlot.subs) { delete _voiceSlot.subs; _voiceSlot.subs = nullptr; } if (_sfxSlot.subs) { delete _sfxSlot.subs; _sfxSlot.subs = nullptr; } if (_videoSubtitles) { delete _videoSubtitles; _videoSubtitles = nullptr; } _system->hideOverlay(); } void PrivateEngine::playVideo(const Common::String &name) { debugC(1, kPrivateDebugFunction, "%s(%s)", __FUNCTION__, name.c_str()); Common::Path path = convertPath(name); Common::SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(path); if (!file) error("unable to find video file %s", path.toString().c_str()); if (!_videoDecoder->loadStream(file)) error("unable to load video %s", path.toString().c_str()); loadSubtitles(path, kSubtitleVideo); _videoDecoder->start(); // set the view screen based on the video, unless playing from diary if (_currentSetting != getDiaryMiddleSetting()) { Common::String videoViewScreen = getVideoViewScreen(name); if (!videoViewScreen.empty()) { _nextVS = videoViewScreen; } } } Common::String PrivateEngine::getVideoViewScreen(Common::String video) { video = convertPath(video).toString(); // find the separator const char *separators[] = { "/animatio/", "/" }; size_t separatorPos = Common::String::npos; size_t separatorLength = Common::String::npos; for (uint i = 0; i < ARRAYSIZE(separators); i++) { separatorPos = video.find(separators[i]); if (separatorPos != Common::String::npos) { separatorLength = strlen(separators[i]); break; } } if (separatorPos == Common::String::npos) { return ""; } // find the video suffix. these suffixes are from the executable. size_t suffixPos = Common::String::npos; const char *suffixes[] = { "ys.smk", "xs.smk", "a.smk", "s.smk", ".smk" }; for (uint i = 0; i < ARRAYSIZE(suffixes); i++) { if (video.hasSuffix(suffixes[i])) { suffixPos = video.size() - strlen(suffixes[i]); break; } } if (suffixPos == Common::String::npos) { return ""; } // build the view screen picture name Common::String picture = Common::String::format( "\"inface/views/%s/%s.bmp\"", video.substr(0, separatorPos).c_str(), video.substr(separatorPos + separatorLength, suffixPos - (separatorPos + separatorLength)).c_str()); // not every video has a picture if (!Common::File::exists(convertPath(picture))) { return ""; } return picture; } void PrivateEngine::skipVideo() { if (_videoDecoder == nullptr || _videoDecoder->isPaused()) { return; } delete _videoDecoder; _videoDecoder = nullptr; destroySubtitles(); _currentMovie = ""; } void PrivateEngine::destroyVideo() { if (_videoDecoder != _pausedVideo) { delete _pausedVideo; } delete _videoDecoder; _videoDecoder = nullptr; _pausedVideo = nullptr; destroySubtitles(); } Graphics::Surface *PrivateEngine::decodeImage(const Common::String &name, byte **palette, bool *isNewPalette) { debugC(1, kPrivateDebugFunction, "%s(%s)", __FUNCTION__, name.c_str()); Common::Path path = convertPath(name); Common::ScopedPtr file(Common::MacResManager::openFileOrDataFork(path)); if (!file) error("unable to load image %s", name.c_str()); _image->loadStream(*file); const Graphics::Surface *oldImage = _image->getSurface(); Graphics::Surface *newImage; const byte *oldPalette = _image->getPalette().data(); byte *currentPalette; uint16 ncolors = _image->getPalette().size(); if (ncolors < 256 || path.toString('/').hasPrefix("intro")) { // For some reason, requires color remapping currentPalette = (byte *) malloc(3*256); drawScreen(); _system->getPaletteManager()->grabPalette(currentPalette, 0, 256); newImage = oldImage->convertTo(_pixelFormat, currentPalette); remapImage(ncolors, oldImage, oldPalette, newImage, currentPalette); *palette = currentPalette; *isNewPalette = true; } else { currentPalette = const_cast(oldPalette); newImage = oldImage->convertTo(_pixelFormat, currentPalette); *palette = currentPalette; *isNewPalette = false; } // Most images store the transparent color (green) in color 250, except for // those in Mavis' apartment. Our engine assumes that all images share the // the same transparent color number (250), so if this image stores it in // a different palette entry then swap it with 250. uint32 maskTransparentColor = findMaskTransparentColor(currentPalette, _transparentColor); if (maskTransparentColor != _transparentColor) { swapImageColors(newImage, currentPalette, maskTransparentColor, _transparentColor); } return newImage; } void PrivateEngine::remapImage(uint16 ncolors, const Graphics::Surface *oldImage, const byte *oldPalette, Graphics::Surface *newImage, const byte *currentPalette) { debugC(1, kPrivateDebugFunction, "%s(..)", __FUNCTION__); byte paletteMap[256]; // Run through every color in old palette for (int i = 0; i != ncolors; ++i) { byte r0 = oldPalette[3 * i + 0]; byte g0 = oldPalette[3 * i + 1]; byte b0 = oldPalette[3 * i + 2]; // Find the closest color in current palette int closest_distance = 10000; int closest_j = 0; for (int j = 0; j != 256; ++j) { byte r1 = currentPalette[3 * j + 0]; byte g1 = currentPalette[3 * j + 1]; byte b1 = currentPalette[3 * j + 2]; int distance = (MAX(r0, r1) - MIN(r0, r1)) + (MAX(g0, g1) - MIN(g0, g1)) + (MAX(b0, b1) - MIN(b0, b1)); if (distance < closest_distance) { closest_distance = distance; closest_j = j; } } paletteMap[i] = closest_j; } const byte *src = (const byte*) oldImage->getPixels(); byte *dst = (byte *) newImage->getPixels(); int pitch = oldImage->pitch; for (int y = 0; y != oldImage->h; ++y) { for (int x = 0; x != oldImage->w; ++x) { dst[y * pitch + x] = paletteMap[src[y * pitch + x]]; } } } uint32 PrivateEngine::findMaskTransparentColor(const byte *palette, uint32 defaultColor) { // Green is used for the transparent color in masks. It is not always // the same shade of green, and it is not always the same palette // index in the bitmap image. It appears that the original searched // each bitmap's palette for the nearest match to RGB 00 FF 00. // Some masks use 00 FC 00. Green is usually color 250 in masks, // but it is color 2 in the masks in Mavis' apartment. uint32 transparentColor = defaultColor; for (uint32 c = 0; c < 256; c++) { byte r = palette[3 * c + 0]; byte g = palette[3 * c + 1]; byte b = palette[3 * c + 2]; if (r == 0 && b == 0) { if (g == 0xff) { // exact match, stop scanning transparentColor = c; break; } if (g == 0xfc) { // almost green, keep scanning transparentColor = c; } } } return transparentColor; } // swaps two colors in an image void PrivateEngine::swapImageColors(Graphics::Surface *image, byte *palette, uint32 a, uint32 b) { SWAP(palette[3 * a + 0], palette[3 * b + 0]); SWAP(palette[3 * a + 1], palette[3 * b + 1]); SWAP(palette[3 * a + 2], palette[3 * b + 2]); for (int y = 0; y < image->h; y++) { for (int x = 0; x < image->w; x++) { uint32 pixel = image->getPixel(x, y); if (pixel == a) { image->setPixel(x, y, b); } else if (pixel == b) { image->setPixel(x, y, a); } } } } void PrivateEngine::loadImage(const Common::String &name, int x, int y) { debugC(1, kPrivateDebugFunction, "%s(%s,%d,%d)", __FUNCTION__, name.c_str(), x, y); byte *palette; bool isNewPalette; Graphics::Surface *surf = decodeImage(name, &palette, &isNewPalette); _compositeSurface->setPalette(palette, 0, 256); _compositeSurface->setTransparentColor(_transparentColor); _compositeSurface->transBlitFrom(*surf, _origin + Common::Point(x, y), _transparentColor); surf->free(); delete surf; _image->destroy(); if (isNewPalette) { free(palette); } } void PrivateEngine::fillRect(uint32 color, Common::Rect rect) { debugC(1, kPrivateDebugFunction, "%s(%d,..)", __FUNCTION__, color); rect.translate(_origin.x, _origin.y); _compositeSurface->fillRect(rect, color); } void PrivateEngine::drawScreenFrame(const byte *newPalette) { debugC(1, kPrivateDebugFunction, "%s(..)", __FUNCTION__); remapImage(256, _frameImage, _framePalette, _mframeImage, newPalette); _system->copyRectToScreen(_mframeImage->getPixels(), _mframeImage->pitch, 0, 0, _screenW, _screenH); } void PrivateEngine::loadMaskAndInfo(MaskInfo *m, const Common::String &name, int x, int y, bool drawn) { m->surf = new Graphics::Surface(); m->surf->create(_screenW, _screenH, _pixelFormat); m->surf->fillRect(_screenRect, _transparentColor); byte *palette; bool isNewPalette; Graphics::Surface *csurf = decodeImage(name, &palette, &isNewPalette); uint32 hdiff = 0; uint32 wdiff = 0; if (x + csurf->h > _screenH) hdiff = x + csurf->h - _screenH; if (y + csurf->w > _screenW) wdiff = y + csurf->w - _screenW; Common::Rect crect(csurf->w - wdiff, csurf->h - hdiff); m->surf->copyRectToSurface(*csurf, x, y, crect); m->box = Common::Rect(x, y, x + csurf->w, y + csurf->h); if (drawn) { _compositeSurface->setPalette(palette, 0, 256); _compositeSurface->setTransparentColor(_transparentColor); drawMask(m->surf); } csurf->free(); delete csurf; _image->destroy(); if (isNewPalette) { free(palette); } } Graphics::Surface *PrivateEngine::loadMask(const Common::String &name, int x, int y, bool drawn) { debugC(1, kPrivateDebugFunction, "%s(%s,%d,%d,%d)", __FUNCTION__, name.c_str(), x, y, drawn); if (_shouldHighlightMasks && name.contains("\\decision\\")) _highlightMasks = true; MaskInfo m; loadMaskAndInfo(&m, name, x, y, drawn); return m.surf; } void PrivateEngine::drawMask(Graphics::Surface *surf) { _compositeSurface->transBlitFrom(*surf, _origin, _transparentColor); } void drawCircle(Graphics::ManagedSurface *surface, int x, int y, int radius, int color) { int cx = 0; int cy = radius; int df = 1 - radius; int d_e = 3; int d_se = -2 * radius + 5; do { surface->setPixel(x + cx, y + cy, color); surface->setPixel(x - cx, y + cy, color); surface->setPixel(x + cx, y - cy, color); surface->setPixel(x - cx, y - cy, color); surface->setPixel(x + cy, y + cx, color); surface->setPixel(x - cy, y + cx, color); surface->setPixel(x + cy, y - cx, color); surface->setPixel(x - cy, y - cx, color); if (df < 0) { df += d_e; d_e += 2; d_se += 2; } else { df += d_se; d_e += 2; d_se += 4; cy--; } cx++; } while (cx <= cy); for (int i = -radius; i <= radius; i++) { surface->setPixel(x + i, y, color); surface->setPixel(x, y + i, color); } } void PrivateEngine::drawScreen() { if (_videoDecoder && !_videoDecoder->isPaused()) { const Graphics::Surface *frame = _videoDecoder->decodeNextFrame(); Common::Point center((_screenW - _videoDecoder->getWidth()) / 2, (_screenH - _videoDecoder->getHeight()) / 2); if (_needToDrawScreenFrame && _videoDecoder->getCurFrame() >= 0) { const byte *videoPalette = _videoDecoder->getPalette(); _system->getPaletteManager()->setPalette(videoPalette, 0, 256); drawScreenFrame(videoPalette); _needToDrawScreenFrame = false; } else if (_videoDecoder->hasDirtyPalette()) { const byte *videoPalette = _videoDecoder->getPalette(); _system->getPaletteManager()->setPalette(videoPalette, 0, 256); if (_mode == 1) { drawScreenFrame(videoPalette); } } // No use of _compositeSurface, we write the frame directly to the screen in the expected position _system->copyRectToScreen(frame->getPixels(), frame->pitch, center.x, center.y, frame->w, frame->h); } else { byte newPalette[256 * 3]; _compositeSurface->grabPalette(newPalette, 0, 256); _system->getPaletteManager()->setPalette(newPalette, 0, 256); if (_mode == 1) { // We can reuse newPalette _system->getPaletteManager()->grabPalette((byte *) &newPalette, 0, 256); drawScreenFrame((byte *) &newPalette); } if (_highlightMasks) { byte redIndex = 0; int min_dist = 1000 * 1000; for (int i = 0; i < 256; ++i) { int r = newPalette[i * 3 + 0]; int g = newPalette[i * 3 + 1]; int b = newPalette[i * 3 + 2]; int dist = (255 - r) * (255 - r) + g * g + b * b; if (dist < min_dist) { min_dist = dist; redIndex = i; } } for (MaskList::const_iterator it = _masks.begin(); it != _masks.end(); ++it) { const MaskInfo &m = *it; if (m.surf == nullptr) continue; long sumX = 0; long sumY = 0; int count = 0; for (int sx = 0; sx < m.surf->w; ++sx) { for (int sy = 0; sy < m.surf->h; ++sy) { if (m.surf->getPixel(sx, sy) != _transparentColor) { sumX += sx; sumY += sy; count++; } } } if (count > 0) { int centerX = sumX / count; int centerY = sumY / count; drawCircle(_compositeSurface, centerX + _origin.x, centerY + _origin.y, 7, redIndex); } } } Common::Rect w(_origin.x, _origin.y, _screenW - _origin.x, _screenH - _origin.y); Graphics::Surface sa = _compositeSurface->getSubArea(w); _system->copyRectToScreen(sa.getPixels(), sa.pitch, _origin.x, _origin.y, sa.w, sa.h); } // audio subtitles are handled in updateSubtitles() in the main loop so only draw video subtitles here if (_videoSubtitles && _videoDecoder && !_videoDecoder->isPaused()) _videoSubtitles->drawSubtitle(_videoDecoder->getTime(), false, _sfxSubtitles); _system->updateScreen(); } void PrivateEngine::pauseEngineIntern(bool pause) { Engine::pauseEngineIntern(pause); // If we are unpausing (returning from quit dialog, etc.) if (!pause) { // reset the overlay _system->showOverlay(false); _system->clearOverlay(); // force draw the subtitle once // the screen was likely wiped by the dialog/menu // to account for the subtitle which was already rendered and we wiped the screen before it finished we must // force the subtitle system to ignore its cache and redraw the text. // calling adjustSubtitleSize() makes the next drawSubtitle call perform a full redraw // automatically, so we don't need to pass 'true'. adjustSubtitleSize(); if (_videoDecoder && _videoSubtitles) _videoSubtitles->drawSubtitle(_videoDecoder->getTime(), false, _sfxSubtitles); // draw all remaining active subtitles if (isSlotActive(_voiceSlot)) { uint32 time = _mixer->getElapsedTime(_voiceSlot.handle).msecs(); _voiceSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } else if (isSlotActive(_sfxSlot)) { uint32 time = _mixer->getElapsedTime(_sfxSlot.handle).msecs(); _sfxSlot.subs->drawSubtitle(time, false, _sfxSubtitles); } } } bool PrivateEngine::getRandomBool(uint p) { uint r = _rnd->getRandomNumber(100); return (r <= p); } Common::String PrivateEngine::getPaperShuffleSound() { uint r = _rnd->getRandomNumber(6); return Common::String::format("%sglsfx0%d.wav", _globalAudioPath.c_str(), kPaperShuffleSound[r]); } Common::String PrivateEngine::getTakeSound() { if (isDemo()) return (_globalAudioPath + "mvo007.wav"); // Only the first four sounds are available when taking the first item. const char *sounds[7] = { "mvo007.wav", "mvo003.wav", "took1.wav", "took2.wav", "took3.wav", "took4.wav", "took5.wav" }; uint r = _rnd->getRandomNumber(_haveTakenItem ? 6 : 3); return _globalAudioPath + sounds[r]; } Common::String PrivateEngine::getTakeLeaveSound() { uint r = _rnd->getRandomNumber(1); if (r == 0) { return (_globalAudioPath + "mvo001.wav"); } else { return (_globalAudioPath + "mvo006.wav"); } } Common::String PrivateEngine::getLeaveSound() { if (isDemo()) return (_globalAudioPath + "mvo008.wav"); // The last sound is only available after going to the police station. const char *sounds[7] = { "mvo008.wav", "mvo004.wav", "left1.wav", "left2.wav", "left3.wav", "left4.wav", "left5.wav" // "I've had enough trouble with the police" }; Private::Symbol *beenDowntown = maps.variables.getVal(getBeenDowntownVariable()); uint r = _rnd->getRandomNumber(beenDowntown->u.val ? 6 : 5); return _globalAudioPath + sounds[r]; } // Timer void PrivateEngine::setTimer(uint32 delay, const Common::String &setting, const Common::String &skipSetting) { _timerSetting = setting; _timerSkipSetting = skipSetting; _timerStartTime = _system->getMillis(); _timerDelay = delay; } void PrivateEngine::clearTimer() { _timerSetting.clear(); _timerSkipSetting.clear(); _timerStartTime = 0; _timerDelay = 0; } void PrivateEngine::skipTimer() { _nextSetting = _timerSkipSetting; clearTimer(); } void PrivateEngine::checkTimer() { if (_timerSetting.empty()) { return; } uint32 now = _system->getMillis(); if (now - _timerStartTime >= _timerDelay) { _nextSetting = _timerSetting; clearTimer(); } } // Diary void PrivateEngine::loadLocations(const Common::Rect &rect) { // Locations are displayed in the order they are visited. // maps.locations and maps.locationList contain all locations. // A non-zero symbol value indicates that a location has been // visited and the order in which it was visited. // Create an array of visited locations, sorted by order visited Common::Array visitedLocations; Common::HashMap locationIDs; int locationID = 1; // one-based for image file names for (NameList::const_iterator it = maps.locationList.begin(); it != maps.locationList.end(); ++it) { const Private::Symbol *sym = maps.locations.getVal(*it); if (sym->u.val != 0) { visitedLocations.push_back(sym); locationIDs[sym] = locationID; } locationID++; } Common::sort(visitedLocations.begin(), visitedLocations.end(), [](const Symbol *a, const Symbol *b) { return a->u.val < b->u.val; }); // Load the sorted visited locations int16 offset = 54; for (uint i = 0; i < visitedLocations.size(); i++) { const Private::Symbol *sym = visitedLocations[i]; Common::String s = Common::String::format("%sdryloc%d.bmp", _diaryLocPrefix.c_str(), locationIDs[sym]); MaskInfo m; loadMaskAndInfo(&m, s, rect.left + 90, rect.top + offset, true); m.cursor = getExitCursor(); m.nextSetting = getDiaryMiddleSetting(); m.flag1 = nullptr; m.flag2 = nullptr; m.useBoxCollision = true; _masks.push_front(m); _locationMasks.push_back(m); offset += 26; } } void PrivateEngine::loadInventory(uint32 x, const Common::Rect &r1, const Common::Rect &r2) { int16 offset = 0; for (InvList::const_iterator it = inventory.begin(); it != inventory.end(); ++it) { Graphics::Surface *surface = loadMask(it->diaryImage, r1.left, r1.top + offset, true); surface->free(); delete surface; offset += 20; } } void PrivateEngine::loadMemories(const Common::Rect &rect, uint rightPageOffset, uint verticalOffset) { if (_currentDiaryPage < 0 ||_currentDiaryPage >= (int)_diaryPages.size()) return; Common::String s = Common::String::format("inface/diary/loctabs/drytab%d.bmp", _diaryPages[_currentDiaryPage].locationID); loadImage(s, 0, 0); uint memoriesLoaded = 0; uint currentVerticalOffset = 0; uint horizontalOffset = 0; for (uint i = 0; i < _diaryPages[_currentDiaryPage].memories.size(); i++) { MaskInfo m; m.surf = loadMask(_diaryPages[_currentDiaryPage].memories[i].image, rect.left + horizontalOffset, rect.top + currentVerticalOffset, true); m.cursor = getExitCursor(); m.nextSetting = getDiaryMiddleSetting(); m.flag1 = nullptr; m.flag2 = nullptr; _masks.push_front(m); _memoryMasks.push_back(m); currentVerticalOffset += verticalOffset; memoriesLoaded++; if (memoriesLoaded == 3) { horizontalOffset = rightPageOffset; currentVerticalOffset = 0; } } } void PrivateEngine::setLocationAsVisited(Symbol *location) { if (location->u.val == 0) { // visited locations have non-zero values. // set to an incrementing value to record the order visited. int maxLocationValue = getMaxLocationValue(); setSymbol(location, maxLocationValue + 1); } } int PrivateEngine::getMaxLocationValue() { int maxValue = 0; for (SymbolMap::iterator it = maps.locations.begin(); it != maps.locations.end(); ++it) { Symbol *s = it->_value; maxValue = MAX(maxValue, s->u.val); } return maxValue; } bool PrivateEngine::selectSkipMemoryVideo(Common::Point mousePos) { // this is mode 2 in the original, but we don't use kGoThumbnailMovie if (_mode == 0 && _videoDecoder != nullptr && _currentSetting == getDiaryMiddleSetting()) { const uint32 tol = 15; const Common::Point origin(kOriginOne[0], kOriginOne[1]); const Common::Rect window(origin.x - tol, origin.y - tol, _screenW - origin.x + tol, _screenH - origin.y + tol); if (!window.contains(mousePos)) { skipVideo(); return true; } } return false; } } // End of namespace Private