/* 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 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "gui/EventRecorder.h" #ifdef ENABLE_EVENTRECORDER namespace Common { DECLARE_SINGLETON(GUI::EventRecorder); } #include "common/debug-channels.h" #include "backends/timer/sdl/sdl-timer.h" #include "backends/mixer/mixer.h" #include "common/config-manager.h" #include "common/md5.h" #include "gui/gui-manager.h" #include "gui/widget.h" #include "gui/onscreendialog.h" #include "common/random.h" #include "common/savefile.h" #include "common/textconsole.h" #include "graphics/thumbnail.h" #include "graphics/surface.h" #include "graphics/scaler.h" namespace GUI { const int kMaxRecordsNames = 0x64; const int kDefaultScreenshotPeriod = 60000; uint32 readTime(Common::ReadStream *inFile) { uint32 d = inFile->readByte(); if (d == 0xff) { d = inFile->readUint32LE(); } return d; } void writeTime(Common::WriteStream *outFile, uint32 d) { //Simple RLE compression if (d >= 0xff) { outFile->writeByte(0xff); outFile->writeUint32LE(d); } else { outFile->writeByte(d); } } EventRecorder::EventRecorder() { _timerManager = nullptr; _recordMode = kPassthrough; _fakeMixerManager = nullptr; _initialized = false; _needRedraw = false; _processingMillis = false; _fastPlayback = false; _lastTimeDate.tm_sec = 0; _lastTimeDate.tm_min = 0; _lastTimeDate.tm_hour = 0; _lastTimeDate.tm_mday = 0; _lastTimeDate.tm_mon = 0; _lastTimeDate.tm_year = 0; _lastTimeDate.tm_wday = 0; _fakeTimer = 0; _savedState = false; _needcontinueGame = false; _temporarySlot = 0; _realSaveManager = nullptr; _realMixerManager = nullptr; _controlPanel = nullptr; _lastMillis = 0; _lastScreenshotTime = 0; _screenshotPeriod = 0; _playbackFile = nullptr; } EventRecorder::~EventRecorder() { delete _timerManager; } void EventRecorder::deinit() { if (!_initialized) { return; } setFileHeader(); _needRedraw = false; _initialized = false; _recordMode = kPassthrough; delete _fakeMixerManager; _fakeMixerManager = nullptr; _controlPanel->close(); delete _controlPanel; debugC(1, kDebugLevelEventRec, "playback:action=stopplayback"); Common::EventDispatcher *eventDispater = g_system->getEventManager()->getEventDispatcher(); eventDispater->unregisterSource(this); eventDispater->ignoreSources(false); _recordMode = kPassthrough; _playbackFile->close(); delete _playbackFile; switchMixer(); switchTimerManagers(); DebugMan.disableDebugChannel("EventRec"); } void EventRecorder::processTimeAndDate(TimeDate &td, bool skipRecord) { if (!_initialized) { return; } if (skipRecord) { td = _lastTimeDate; return; } Common::RecorderEvent timeDateEvent; switch (_recordMode) { case kRecorderRecord: timeDateEvent.recordedtype = Common::kRecorderEventTypeTimeDate; timeDateEvent.timeDate = td; _lastTimeDate = td; _playbackFile->writeEvent(timeDateEvent); break; case kRecorderPlayback: if (_nextEvent.recordedtype != Common::kRecorderEventTypeTimeDate) { // just re-use any previous date time value td = _lastTimeDate; return; } _lastTimeDate = _nextEvent.timeDate; td = _lastTimeDate; debug(3, "timedate event"); _nextEvent = _playbackFile->getNextEvent(); break; case kRecorderPlaybackPause: td = _lastTimeDate; break; default: break; } } void EventRecorder::processMillis(uint32 &millis, bool skipRecord) { if (!_initialized) { return; } if (skipRecord || _processingMillis) { millis = _fakeTimer; return; } // to prevent calling this recursively if (_recordMode == kRecorderPlaybackPause) { millis = _fakeTimer; } uint32 millisDelay; Common::RecorderEvent timerEvent; switch (_recordMode) { case kRecorderRecord: updateSubsystems(); millisDelay = millis - _lastMillis; _lastMillis = millis; _fakeTimer += millisDelay; _controlPanel->setReplayedTime(_fakeTimer); timerEvent.recordedtype = Common::kRecorderEventTypeTimer; timerEvent.time = _fakeTimer; _playbackFile->writeEvent(timerEvent); takeScreenshot(); _timerManager->handler(); break; case kRecorderPlayback: if (_nextEvent.recordedtype != Common::kRecorderEventTypeTimer) { // just re-use any previous millis value // this might happen if you have EventSource instances registered, that // are querying the millis by themselves, too. If the EventRecorder::poll // is registered and thus dispatched after those EventSource instances, it // might look like it ran out-of-sync. millis = _fakeTimer; return; } _processingMillis = true; _fakeTimer = _nextEvent.time; millis = _fakeTimer; debug(3, "millis event: %u", millis); updateSubsystems(); _nextEvent = _playbackFile->getNextEvent(); _timerManager->handler(); _controlPanel->setReplayedTime(_fakeTimer); _processingMillis = false; break; case kRecorderPlaybackPause: millis = _fakeTimer; break; default: break; } } bool EventRecorder::processDelayMillis() { return _fastPlayback; } void EventRecorder::checkForKeyCode(const Common::Event &event) { if ((event.type == Common::EVENT_KEYDOWN) && (event.kbd.flags & Common::KBD_CTRL) && (event.kbd.keycode == Common::KEYCODE_p) && (!event.kbdRepeat)) { togglePause(); } } bool EventRecorder::pollEvent(Common::Event &ev) { if ((_recordMode != kRecorderPlayback) || !_initialized) return false; if (_nextEvent.recordedtype == Common::kRecorderEventTypeTimer || _nextEvent.recordedtype == Common::kRecorderEventTypeTimeDate || _nextEvent.type == Common::EVENT_INVALID) { return false; } ev = _nextEvent; _nextEvent = _playbackFile->getNextEvent(); switch (ev.type) { case Common::EVENT_MOUSEMOVE: case Common::EVENT_LBUTTONDOWN: case Common::EVENT_LBUTTONUP: case Common::EVENT_RBUTTONDOWN: case Common::EVENT_RBUTTONUP: case Common::EVENT_WHEELUP: case Common::EVENT_WHEELDOWN: g_system->warpMouse(ev.mouse.x, ev.mouse.y); break; default: break; } return true; } void EventRecorder::switchFastMode() { if (_recordMode == kRecorderPlaybackPause) { _fastPlayback = !_fastPlayback; } } void EventRecorder::togglePause() { RecordMode oldState; switch (_recordMode) { case kRecorderPlayback: case kRecorderRecord: oldState = _recordMode; _recordMode = kRecorderPlaybackPause; _controlPanel->runModal(); _recordMode = oldState; _initialized = true; break; case kRecorderPlaybackPause: _controlPanel->close(); break; default: break; } } void EventRecorder::RegisterEventSource() { g_system->getEventManager()->getEventDispatcher()->registerObserver(this, Common::EventManager::kEventRecorderPriority, false); } uint32 EventRecorder::getRandomSeed(const Common::String &name) { if (_recordMode == kRecorderPlayback) { return _playbackFile->getHeader().randomSourceRecords[name]; } uint32 result = g_system->getMillis(); if (_recordMode == kRecorderRecord) { _playbackFile->getHeader().randomSourceRecords[name] = result; } return result; } Common::String EventRecorder::generateRecordFileName(const Common::String &target) { Common::String pattern(target + ".r??"); Common::StringArray files = g_system->getSavefileManager()->listSavefiles(pattern); for (int i = 0; i < kMaxRecordsNames; ++i) { Common::String recordName = Common::String::format("%s.r%02d", target.c_str(), i); if (find(files.begin(), files.end(), recordName) != files.end()) { continue; } return recordName; } return ""; } void EventRecorder::init(const Common::String &recordFileName, RecordMode mode) { _fakeMixerManager = new NullMixerManager(); _fakeMixerManager->init(); _fakeMixerManager->suspendAudio(); _fakeTimer = 0; _lastMillis = g_system->getMillis(); _playbackFile = new Common::PlaybackFile(); _lastScreenshotTime = 0; _recordMode = mode; _needcontinueGame = false; if (ConfMan.hasKey("disable_display")) { DebugMan.enableDebugChannel("EventRec"); gDebugLevel = 1; } if (_recordMode == kRecorderPlayback) { debugC(1, kDebugLevelEventRec, "playback:action=\"Load file\" filename=%s", recordFileName.c_str()); Common::EventDispatcher *eventDispater = g_system->getEventManager()->getEventDispatcher(); eventDispater->clearEvents(); eventDispater->ignoreSources(true); eventDispater->registerSource(this, false); } _screenshotPeriod = ConfMan.getInt("screenshot_period"); if (_screenshotPeriod == 0) { _screenshotPeriod = kDefaultScreenshotPeriod; } if (!openRecordFile(recordFileName)) { deinit(); error("playback:action=error reason=\"Record file loading error\""); return; } if (_recordMode != kPassthrough) { _controlPanel = new GUI::OnScreenDialog(_recordMode == kRecorderRecord); _controlPanel->reflowLayout(); } if (_recordMode == kRecorderPlayback) { applyPlaybackSettings(); _nextEvent = _playbackFile->getNextEvent(); } if (_recordMode == kRecorderRecord) { getConfig(); } switchMixer(); switchTimerManagers(); _needRedraw = true; _initialized = true; } /** * Opens or creates file depend of recording mode. * * @param id of recording or playing back game * @return true in case of success, false in case of error */ bool EventRecorder::openRecordFile(const Common::String &fileName) { bool result; switch (_recordMode) { case kRecorderRecord: return _playbackFile->openWrite(fileName); case kRecorderPlayback: _recordMode = kPassthrough; result = _playbackFile->openRead(fileName); _recordMode = kRecorderPlayback; return result; default: return false; } return true; } bool EventRecorder::checkGameHash(const ADGameDescription *gameDesc) { if (_playbackFile->getHeader().hashRecords.size() == 0) { warning("Engine doesn't contain description table"); return false; } for (const ADGameFileDescription *fileDesc = gameDesc->filesDescriptions; fileDesc->fileName; fileDesc++) { if (fileDesc->md5 == nullptr) continue; if (_playbackFile->getHeader().hashRecords.find(fileDesc->fileName) == _playbackFile->getHeader().hashRecords.end()) { warning("MD5 hash for file %s not found in record file", fileDesc->fileName); debugC(1, kDebugLevelEventRec, "playback:action=\"Check game hash\" filename=%s filehash=%s storedhash=\"\" result=different", fileDesc->fileName, fileDesc->md5); return false; } if (_playbackFile->getHeader().hashRecords[fileDesc->fileName] != fileDesc->md5) { warning("Incorrect version of game file %s. Stored MD5 is %s. MD5 of loaded game is %s", fileDesc->fileName, _playbackFile->getHeader().hashRecords[fileDesc->fileName].c_str(), fileDesc->md5); debugC(1, kDebugLevelEventRec, "playback:action=\"Check game hash\" filename=%s filehash=%s storedhash=%s result=different", fileDesc->fileName, fileDesc->md5, _playbackFile->getHeader().hashRecords[fileDesc->fileName].c_str()); return false; } debugC(1, kDebugLevelEventRec, "playback:action=\"Check game hash\" filename=%s filehash=%s storedhash=%s result=equal", fileDesc->fileName, fileDesc->md5, _playbackFile->getHeader().hashRecords[fileDesc->fileName].c_str()); } return true; } void EventRecorder::registerMixerManager(MixerManager *mixerManager) { _realMixerManager = mixerManager; } void EventRecorder::switchMixer() { if (_recordMode == kPassthrough) { _realMixerManager->resumeAudio(); } else { _realMixerManager->suspendAudio(); _fakeMixerManager->resumeAudio(); } } MixerManager *EventRecorder::getMixerManager() { if (_recordMode == kPassthrough) { return _realMixerManager; } else { return _fakeMixerManager; } } void EventRecorder::getConfigFromDomain(const Common::ConfigManager::Domain *domain) { for (Common::ConfigManager::Domain::const_iterator entry = domain->begin(); entry!= domain->end(); ++entry) { _playbackFile->getHeader().settingsRecords[entry->_key] = entry->_value; } } void EventRecorder::getConfig() { getConfigFromDomain(ConfMan.getDomain(ConfMan.kApplicationDomain)); getConfigFromDomain(ConfMan.getActiveDomain()); _playbackFile->getHeader().settingsRecords["save_slot"] = ConfMan.get("save_slot"); } void EventRecorder::applyPlaybackSettings() { for (Common::StringMap::const_iterator i = _playbackFile->getHeader().settingsRecords.begin(); i != _playbackFile->getHeader().settingsRecords.end(); ++i) { Common::String currentValue = ConfMan.get(i->_key); if (currentValue != i->_value) { ConfMan.set(i->_key, i->_value, ConfMan.kTransientDomain); debugC(1, kDebugLevelEventRec, "playback:action=\"Apply settings\" key=%s storedvalue=%s currentvalue=%s result=different", i->_key.c_str(), i->_value.c_str(), currentValue.c_str()); } else { debugC(1, kDebugLevelEventRec, "playback:action=\"Apply settings\" key=%s storedvalue=%s currentvalue=%s result=equal", i->_key.c_str(), i->_value.c_str(), currentValue.c_str()); } } removeDifferentEntriesInDomain(ConfMan.getDomain(ConfMan.kApplicationDomain)); removeDifferentEntriesInDomain(ConfMan.getActiveDomain()); } void EventRecorder::removeDifferentEntriesInDomain(Common::ConfigManager::Domain *domain) { for (Common::ConfigManager::Domain::const_iterator entry = domain->begin(); entry!= domain->end(); ++entry) { if (_playbackFile->getHeader().settingsRecords.find(entry->_key) == _playbackFile->getHeader().settingsRecords.end()) { debugC(1, kDebugLevelEventRec, "playback:action=\"Apply settings\" checksettings:key=%s storedvalue=%s currentvalue="" result=different", entry->_key.c_str(), entry->_value.c_str()); domain->erase(entry->_key); } } } DefaultTimerManager *EventRecorder::getTimerManager() { return _timerManager; } void EventRecorder::registerTimerManager(DefaultTimerManager *timerManager) { _timerManager = timerManager; } void EventRecorder::switchTimerManagers() { delete _timerManager; if (_recordMode == kPassthrough) { _timerManager = new SdlTimerManager(); } else { _timerManager = new DefaultTimerManager(); } } void EventRecorder::updateSubsystems() { if (_recordMode == kPassthrough) { return; } RecordMode oldRecordMode = _recordMode; _recordMode = kPassthrough; _fakeMixerManager->update(); _recordMode = oldRecordMode; } bool EventRecorder::notifyEvent(const Common::Event &ev) { if ((!_initialized) && (_recordMode != kRecorderPlaybackPause)) { return false; } checkForKeyCode(ev); Common::Event evt = ev; evt.mouse.x = evt.mouse.x * (g_system->getOverlayWidth() / g_system->getWidth()); evt.mouse.y = evt.mouse.y * (g_system->getOverlayHeight() / g_system->getHeight()); switch (_recordMode) { case kRecorderPlayback: return false; case kRecorderRecord: g_gui.processEvent(evt, _controlPanel); if (((evt.type == Common::EVENT_LBUTTONDOWN) || (evt.type == Common::EVENT_LBUTTONUP) || (evt.type == Common::EVENT_MOUSEMOVE)) && _controlPanel->isMouseOver()) { return true; } else { Common::RecorderEvent e(ev); e.recordedtype = Common::kRecorderEventTypeNormal; e.time = _fakeTimer; _playbackFile->writeEvent(e); return false; } case kRecorderPlaybackPause: { Common::Event dialogEvent; if (_controlPanel->isEditDlgVisible()) { dialogEvent = ev; } else { dialogEvent = evt; } g_gui.processEvent(dialogEvent, _controlPanel->getActiveDlg()); if (((dialogEvent.type == Common::EVENT_LBUTTONDOWN) || (dialogEvent.type == Common::EVENT_LBUTTONUP) || (dialogEvent.type == Common::EVENT_MOUSEMOVE)) && _controlPanel->isMouseOver()) { return true; } return false; } default: return false; } } void EventRecorder::setGameMd5(const ADGameDescription *gameDesc) { for (const ADGameFileDescription *fileDesc = gameDesc->filesDescriptions; fileDesc->fileName; fileDesc++) { if (fileDesc->md5 != nullptr) { _playbackFile->getHeader().hashRecords[fileDesc->fileName] = fileDesc->md5; } } } void EventRecorder::processGameDescription(const ADGameDescription *desc) { if (_recordMode == kRecorderRecord) { setGameMd5(desc); } if ((_recordMode == kRecorderPlayback) && !checkGameHash(desc)) { deinit(); error("playback:action=error reason=\"\""); } } void EventRecorder::deleteRecord(const Common::String& fileName) { g_system->getSavefileManager()->removeSavefile(fileName); } void EventRecorder::takeScreenshot() { if ((_fakeTimer - _lastScreenshotTime) > _screenshotPeriod) { Graphics::Surface screen; uint8 md5[16]; if (grabScreenAndComputeMD5(screen, md5)) { _lastScreenshotTime = _fakeTimer; _playbackFile->saveScreenShot(screen, md5); screen.free(); } } } bool EventRecorder::grabScreenAndComputeMD5(Graphics::Surface &screen, uint8 md5[16]) { if (!createScreenShot(screen)) { warning("Can't save screenshot"); return false; } Common::MemoryReadStream bitmapStream((const byte*)screen.getPixels(), screen.w * screen.h * screen.format.bytesPerPixel); computeStreamMD5(bitmapStream, md5); return true; } Common::SeekableReadStream *EventRecorder::processSaveStream(const Common::String &fileName) { Common::InSaveFile *saveFile; switch (_recordMode) { case kRecorderPlayback: debugC(1, kDebugLevelEventRec, "playback:action=\"Process save file\" filename=%s len=%d", fileName.c_str(), _playbackFile->getHeader().saveFiles[fileName].size); return new Common::MemoryReadStream(_playbackFile->getHeader().saveFiles[fileName].buffer, _playbackFile->getHeader().saveFiles[fileName].size); case kRecorderRecord: saveFile = _realSaveManager->openForLoading(fileName); if (saveFile != nullptr) { _playbackFile->addSaveFile(fileName, saveFile); saveFile->seek(0); } return saveFile; default: return nullptr; } } Common::SaveFileManager *EventRecorder::getSaveManager(Common::SaveFileManager *realSaveManager) { _realSaveManager = realSaveManager; if (_recordMode != kPassthrough) { return &_fakeSaveManager; } else { return realSaveManager; } } void EventRecorder::preDrawOverlayGui() { if ((_initialized) || (_needRedraw)) { RecordMode oldMode = _recordMode; _recordMode = kPassthrough; g_system->showOverlay(); g_gui.checkScreenChange(); g_gui.theme()->clearAll(); g_gui.theme()->drawToBackbuffer(); _controlPanel->drawDialog(kDrawLayerBackground); g_gui.theme()->drawToScreen(); g_gui.theme()->copyBackBufferToScreen(); _controlPanel->drawDialog(kDrawLayerForeground); g_gui.theme()->updateScreen(); _recordMode = oldMode; } } void EventRecorder::postDrawOverlayGui() { if ((_initialized) || (_needRedraw)) { RecordMode oldMode = _recordMode; _recordMode = kPassthrough; g_system->hideOverlay(); _recordMode = oldMode; } } Common::StringArray EventRecorder::listSaveFiles(const Common::String &pattern) { if (_recordMode == kRecorderPlayback) { Common::StringArray result; for (Common::HashMap::iterator i = _playbackFile->getHeader().saveFiles.begin(); i != _playbackFile->getHeader().saveFiles.end(); ++i) { if (i->_key.matchString(pattern, false, "/")) { result.push_back(i->_key); } } return result; } else { return _realSaveManager->listSavefiles(pattern); } } void EventRecorder::setFileHeader() { if (_recordMode != kRecorderRecord) { return; } TimeDate t; QualifiedGameDescriptor desc = EngineMan.findTarget(ConfMan.getActiveDomainName()); g_system->getTimeAndDate(t); if (_author.empty()) { setAuthor("Unknown Author"); } if (_name.empty()) { g_eventRec.setName(Common::String::format("%.2d.%.2d.%.4d ", t.tm_mday, t.tm_mon + 1, 1900 + t.tm_year) + desc.description); } _playbackFile->getHeader().author = _author; _playbackFile->getHeader().notes = _desc; _playbackFile->getHeader().name = _name; } SDL_Surface *EventRecorder::getSurface(int width, int height) { // Create a RGB565 surface of the requested dimensions. return SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 16, 0xF800, 0x07E0, 0x001F, 0x0000); } bool EventRecorder::switchMode() { const Plugin *plugin = EngineMan.findPlugin(ConfMan.get("engineid")); bool metaInfoSupport = plugin->get().hasFeature(MetaEngine::kSavesSupportMetaInfo); bool featuresSupport = metaInfoSupport && g_engine->canSaveGameStateCurrently() && plugin->get().hasFeature(MetaEngine::kSupportsListSaves) && plugin->get().hasFeature(MetaEngine::kSupportsDeleteSave); if (!featuresSupport) { return false; } const Common::String target = ConfMan.getActiveDomainName(); SaveStateList saveList = plugin->get().listSaves(target.c_str()); int emptySlot = 1; for (SaveStateList::const_iterator x = saveList.begin(); x != saveList.end(); ++x) { int saveSlot = x->getSaveSlot(); if (saveSlot == 0) { continue; } if (emptySlot != saveSlot) { break; } emptySlot++; } Common::String saveName; if (emptySlot >= 0) { saveName = Common::String::format("Save %d", emptySlot + 1); Common::Error status = g_engine->saveGameState(emptySlot, saveName); if (status.getCode() == Common::kNoError) { Common::Event eventReturnToLauncher; eventReturnToLauncher.type = Common::EVENT_RETURN_TO_LAUNCHER; g_system->getEventManager()->pushEvent(eventReturnToLauncher); } } ConfMan.set("record_mode", "", Common::ConfigManager::kTransientDomain); ConfMan.setInt("save_slot", emptySlot, Common::ConfigManager::kTransientDomain); _needcontinueGame = true; return true; } bool EventRecorder::checkForContinueGame() { bool result = _needcontinueGame; _needcontinueGame = false; return result; } void EventRecorder::deleteTemporarySave() { if (_temporarySlot == -1) return; const Plugin *plugin = EngineMan.findPlugin(ConfMan.get("engineid")); const Common::String target = ConfMan.getActiveDomainName(); plugin->get().removeSaveState(target.c_str(), _temporarySlot); _temporarySlot = -1; } } // End of namespace GUI #endif // ENABLE_EVENTRECORDER