From 29d6758c0d829291e9ce96b8742058ae42d29e48 Mon Sep 17 00:00:00 2001 From: Scott Percival Date: Mon, 2 Aug 2021 20:37:51 +0800 Subject: [PATCH] EVENTRECORDER: Add update mode This new mode is functionally identical to playback mode, however a new recording file is used to track the actual output of ScummVM. This feature can be used to update a suite of existing recordings after a renderer or a timing change. --- base/commandLine.cpp | 2 +- base/main.cpp | 2 + gui/EventRecorder.cpp | 145 ++++++++++++++++++++++++++++++------------ gui/EventRecorder.h | 4 +- 4 files changed, 111 insertions(+), 42 deletions(-) diff --git a/base/commandLine.cpp b/base/commandLine.cpp index e7f653bcbe6..95db610a222 100644 --- a/base/commandLine.cpp +++ b/base/commandLine.cpp @@ -171,7 +171,7 @@ static const char HELP_STRING[] = " atari, macintosh, macintoshbw)\n" #ifdef ENABLE_EVENTRECORDER " --record-mode=MODE Specify record mode for event recorder (record, playback,\n" - " passthrough [default])\n" + " info, update, passthrough [default])\n" " --record-file-name=FILE Specify record file name\n" " --disable-display Disable any gfx output. Used for headless events\n" " playback by Event Recorder\n" diff --git a/base/main.cpp b/base/main.cpp index 8d57c8e1f15..0a96ca0bd26 100644 --- a/base/main.cpp +++ b/base/main.cpp @@ -602,6 +602,8 @@ extern "C" int scummvm_main(int argc, const char * const argv[]) { if (recordMode == "record") { Common::String targetFileName = ConfMan.hasKey("record_file_name") ? recordFileName : g_eventRec.generateRecordFileName(ConfMan.getActiveDomainName()); g_eventRec.init(targetFileName, GUI::EventRecorder::kRecorderRecord); + } else if (recordMode == "update") { + g_eventRec.init(recordFileName, GUI::EventRecorder::kRecorderUpdate); } else if (recordMode == "playback") { g_eventRec.init(recordFileName, GUI::EventRecorder::kRecorderPlayback); } else if ((recordMode == "info") && (!recordFileName.empty())) { diff --git a/gui/EventRecorder.cpp b/gui/EventRecorder.cpp index 7d77ba15518..c2f8e7bb0a3 100644 --- a/gui/EventRecorder.cpp +++ b/gui/EventRecorder.cpp @@ -97,6 +97,7 @@ EventRecorder::EventRecorder() { _lastScreenshotTime = 0; _screenshotPeriod = 0; _playbackFile = nullptr; + _recordFile = nullptr; } EventRecorder::~EventRecorder() { @@ -116,12 +117,18 @@ void EventRecorder::deinit() { _controlPanel->close(); delete _controlPanel; debugC(1, kDebugLevelEventRec, "playback:action=stopplayback"); - Common::EventDispatcher *eventDispater = g_system->getEventManager()->getEventDispatcher(); - eventDispater->unregisterSource(this); - eventDispater->ignoreSources(false); + Common::EventDispatcher *eventDispatcher = g_system->getEventManager()->getEventDispatcher(); + eventDispatcher->unregisterSource(this); + eventDispatcher->ignoreSources(false); _recordMode = kPassthrough; - _playbackFile->close(); - delete _playbackFile; + if (_playbackFile) { + _playbackFile->close(); + delete _playbackFile; + } + if (_recordFile) { + _recordFile->close(); + delete _recordFile; + } switchMixer(); switchTimerManagers(); DebugMan.disableDebugChannel("EventRec"); @@ -143,31 +150,41 @@ void EventRecorder::processTimeAndDate(TimeDate &td, bool skipRecord) { return; } Common::RecorderEvent timeDateEvent; - switch (_recordMode) { - case kRecorderRecord: + if (_recordMode == kRecorderRecord) { timeDateEvent.recordedtype = Common::kRecorderEventTypeTimeDate; timeDateEvent.timeDate = td; _lastTimeDate = td; - _playbackFile->writeEvent(timeDateEvent); - break; - case kRecorderPlayback: + _recordFile->writeEvent(timeDateEvent); + } + if ((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) { if (_nextEvent.recordedtype != Common::kRecorderEventTypeTimeDate) { // just re-use any previous date time value td = _lastTimeDate; + if (_recordMode == kRecorderUpdate) { + // if we make it to here, we're expecting there to be an event in the file + // so add one to the updated recording + debug(3, "inserting additional timedate event"); + timeDateEvent.recordedtype = Common::kRecorderEventTypeTimeDate; + timeDateEvent.timeDate = td; + _recordFile->writeEvent(timeDateEvent); + } return; } _lastTimeDate = _nextEvent.timeDate; td = _lastTimeDate; debug(3, "timedate event"); + if (_recordMode == kRecorderUpdate) { + // copy the event to the updated recording + timeDateEvent.recordedtype = Common::kRecorderEventTypeTimeDate; + timeDateEvent.timeDate = td; + _recordFile->writeEvent(timeDateEvent); + } + _nextEvent = _playbackFile->getNextEvent(); - break; - case kRecorderPlaybackPause: - td = _lastTimeDate; - break; - default: - break; } + if (_recordMode == kRecorderPlaybackPause) + td = _lastTimeDate; } void EventRecorder::processMillis(uint32 &millis, bool skipRecord) { @@ -192,6 +209,7 @@ void EventRecorder::processMillis(uint32 &millis, bool skipRecord) { _recordFile->writeEvent(timerEvent); _timerManager->handler(); break; + case kRecorderUpdate: // fallthrough case kRecorderPlayback: if (_nextEvent.recordedtype != Common::kRecorderEventTypeTimer) { // just re-use any previous millis value @@ -200,11 +218,25 @@ void EventRecorder::processMillis(uint32 &millis, bool skipRecord) { // is registered and thus dispatched after those EventSource instances, it // might look like it ran out-of-sync. millis = _fakeTimer; + if (_recordMode == kRecorderUpdate) { + // if we make it to here, we're expecting there to be an event in the file + // so add one to the updated recording + debug(3, "inserting additional timer event"); + timerEvent.recordedtype = Common::kRecorderEventTypeTimer; + timerEvent.time = _fakeTimer; + _recordFile->writeEvent(timerEvent); + } return; } _processingMillis = true; _fakeTimer = _nextEvent.time; millis = _fakeTimer; + if (_recordMode == kRecorderUpdate) { + // copy the event to the updated recording + timerEvent.recordedtype = Common::kRecorderEventTypeTimer; + timerEvent.time = _fakeTimer; + _recordFile->writeEvent(timerEvent); + } updateSubsystems(); _nextEvent = _playbackFile->getNextEvent(); _timerManager->handler(); @@ -242,6 +274,7 @@ void EventRecorder::processScreenUpdate() { takeScreenshot(); _timerManager->handler(); break; + case kRecorderUpdate: // fallthrough case kRecorderPlayback: if (_playbackFile->hasTrackScreenUpdate()) { // if the file has screen update support, but the next event @@ -261,6 +294,13 @@ void EventRecorder::processScreenUpdate() { _fakeTimer = _nextEvent.time; updateSubsystems(); _nextEvent = _playbackFile->getNextEvent(); + if (_recordMode == kRecorderUpdate) { + // write event to the updated file and update screenshot if necessary + screenUpdateEvent.recordedtype = Common::kRecorderEventTypeScreenUpdate; + screenUpdateEvent.time = _fakeTimer; + _recordFile->writeEvent(screenUpdateEvent); + takeScreenshot(); + } _timerManager->handler(); _controlPanel->setReplayedTime(_fakeTimer); _processingMillis = false; @@ -278,7 +318,9 @@ void EventRecorder::checkForKeyCode(const Common::Event &event) { } bool EventRecorder::pollEvent(Common::Event &ev) { - if ((_recordMode != kRecorderPlayback) || !_initialized) + if (((_recordMode != kRecorderPlayback) && + (_recordMode != kRecorderUpdate)) || + !_initialized) return false; if (_nextEvent.recordedtype == Common::kRecorderEventTypeTimer @@ -317,6 +359,7 @@ void EventRecorder::togglePause() { switch (_recordMode) { case kRecorderPlayback: case kRecorderRecord: + case kRecorderUpdate: oldState = _recordMode; _recordMode = kRecorderPlaybackPause; _controlPanel->runModal(); @@ -341,7 +384,7 @@ uint32 EventRecorder::getRandomSeed(const Common::String &name) { } uint32 result = g_system->getMillis(); if (_recordMode == kRecorderRecord) { - _playbackFile->getHeader().randomSourceRecords[name] = result; + _recordFile->getHeader().randomSourceRecords[name] = result; } return result; } @@ -366,7 +409,6 @@ void EventRecorder::init(const Common::String &recordFileName, RecordMode mode) _fakeMixerManager->suspendAudio(); _fakeTimer = 0; _lastMillis = g_system->getMillis(); - _playbackFile = new Common::PlaybackFile(); _lastScreenshotTime = 0; _recordMode = mode; _needcontinueGame = false; @@ -374,12 +416,12 @@ void EventRecorder::init(const Common::String &recordFileName, RecordMode mode) DebugMan.enableDebugChannel("EventRec"); gDebugLevel = 1; } - if (_recordMode == kRecorderPlayback) { + if ((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) { 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); + Common::EventDispatcher *eventDispatcher = g_system->getEventManager()->getEventDispatcher(); + eventDispatcher->clearEvents(); + eventDispatcher->ignoreSources(true); + eventDispatcher->registerSource(this, false); } _screenshotPeriod = ConfMan.getInt("screenshot_period"); if (_screenshotPeriod == 0) { @@ -394,11 +436,11 @@ void EventRecorder::init(const Common::String &recordFileName, RecordMode mode) _controlPanel = new GUI::OnScreenDialog(_recordMode == kRecorderRecord); _controlPanel->reflowLayout(); } - if (_recordMode == kRecorderPlayback) { + if ((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) { applyPlaybackSettings(); _nextEvent = _playbackFile->getNextEvent(); } - if (_recordMode == kRecorderRecord) { + if ((_recordMode == kRecorderRecord) || (_recordMode == kRecorderUpdate)) { getConfig(); } @@ -419,12 +461,22 @@ bool EventRecorder::openRecordFile(const Common::String &fileName) { bool result; switch (_recordMode) { case kRecorderRecord: - return _playbackFile->openWrite(fileName); + _recordFile = new Common::PlaybackFile(); + return _recordFile->openWrite(fileName); case kRecorderPlayback: _recordMode = kPassthrough; + _playbackFile = new Common::PlaybackFile(); result = _playbackFile->openRead(fileName); _recordMode = kRecorderPlayback; return result; + case kRecorderUpdate: + _recordMode = kPassthrough; + _playbackFile = new Common::PlaybackFile(); + result = _playbackFile->openRead(fileName); + _recordMode = kRecorderUpdate; + _recordFile = new Common::PlaybackFile(); + result &= _recordFile->openWrite(fileName + ".new"); + return result; default: return false; } @@ -478,19 +530,24 @@ MixerManager *EventRecorder::getMixerManager() { 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; + _recordFile->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"); + _recordFile->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) { + if (_recordMode == kRecorderUpdate) { + // copy the header values to the new recording + _recordFile->getHeader().settingsRecords[i->_key] = i->_value; + } + Common::String currentValue = ConfMan.get(i->_key); if (currentValue != i->_value) { ConfMan.set(i->_key, i->_value, ConfMan.kTransientDomain); @@ -549,10 +606,18 @@ bool EventRecorder::notifyEvent(const Common::Event &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 kRecorderUpdate: // passthrough case kRecorderPlayback: // pass through screen updates to avoid loss of sync! if (evt.type == Common::EVENT_SCREEN_CHANGED) g_gui.processEvent(evt, _controlPanel); + if (_recordMode == kRecorderUpdate) { + // write a copy of the event to the output buffer + Common::RecorderEvent e(ev); + e.recordedtype = Common::kRecorderEventTypeNormal; + e.time = _fakeTimer; + _recordFile->writeEvent(e); + } return false; case kRecorderRecord: g_gui.processEvent(evt, _controlPanel); @@ -562,7 +627,7 @@ bool EventRecorder::notifyEvent(const Common::Event &ev) { Common::RecorderEvent e(ev); e.recordedtype = Common::kRecorderEventTypeNormal; e.time = _fakeTimer; - _playbackFile->writeEvent(e); + _recordFile->writeEvent(e); return false; } case kRecorderPlaybackPause: { @@ -586,16 +651,16 @@ bool EventRecorder::notifyEvent(const Common::Event &ev) { 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; + _recordFile->getHeader().hashRecords[fileDesc->fileName] = fileDesc->md5; } } } void EventRecorder::processGameDescription(const ADGameDescription *desc) { - if (_recordMode == kRecorderRecord) { + if ((_recordMode == kRecorderRecord) || (_recordMode == kRecorderUpdate)) { setGameMd5(desc); } - if ((_recordMode == kRecorderPlayback) && !checkGameHash(desc)) { + if (((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) && !checkGameHash(desc)) { deinit(); error("playback:action=error reason=\"\""); } @@ -611,7 +676,7 @@ void EventRecorder::takeScreenshot() { uint8 md5[16]; if (grabScreenAndComputeMD5(screen, md5)) { _lastScreenshotTime = _fakeTimer; - _playbackFile->saveScreenShot(screen, md5); + _recordFile->saveScreenShot(screen, md5); screen.free(); } } @@ -636,7 +701,7 @@ Common::SeekableReadStream *EventRecorder::processSaveStream(const Common::Strin case kRecorderRecord: saveFile = _realSaveManager->openForLoading(fileName); if (saveFile != nullptr) { - _playbackFile->addSaveFile(fileName, saveFile); + _recordFile->addSaveFile(fileName, saveFile); saveFile->seek(0); } return saveFile; @@ -681,7 +746,7 @@ void EventRecorder::postDrawOverlayGui() { } Common::StringArray EventRecorder::listSaveFiles(const Common::String &pattern) { - if (_recordMode == kRecorderPlayback) { + if ((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) { Common::StringArray result; for (Common::HashMap::iterator i = _playbackFile->getHeader().saveFiles.begin(); i != _playbackFile->getHeader().saveFiles.end(); ++i) { if (i->_key.matchString(pattern, false, "/")) { @@ -695,7 +760,7 @@ Common::StringArray EventRecorder::listSaveFiles(const Common::String &pattern) } void EventRecorder::setFileHeader() { - if (_recordMode != kRecorderRecord) { + if ((_recordMode != kRecorderRecord) && (_recordMode != kRecorderUpdate)) { return; } TimeDate t; @@ -707,9 +772,9 @@ void EventRecorder::setFileHeader() { 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; + _recordFile->getHeader().author = _author; + _recordFile->getHeader().notes = _desc; + _recordFile->getHeader().name = _name; } SDL_Surface *EventRecorder::getSurface(int width, int height) { diff --git a/gui/EventRecorder.h b/gui/EventRecorder.h index 10eb0b3145a..fa7a78fae29 100644 --- a/gui/EventRecorder.h +++ b/gui/EventRecorder.h @@ -74,7 +74,8 @@ public: kPassthrough = 0, /**< kPassthrough, do nothing */ kRecorderRecord = 1, /**< kRecorderRecord, do the recording */ kRecorderPlayback = 2, /**< kRecorderPlayback, playback existing recording */ - kRecorderPlaybackPause = 3 /**< kRecordetPlaybackPause, internal state when user pauses the playback */ + kRecorderPlaybackPause = 3, /**< kRecorderPlaybackPause, internal state when user pauses the playback */ + kRecorderUpdate = 4 /**< kRecorderUpdate, playback existing recording and update all hashes */ }; void init(const Common::String &recordFileName, RecordMode mode); @@ -237,6 +238,7 @@ private: uint32 _lastScreenshotTime; uint32 _screenshotPeriod; Common::PlaybackFile *_playbackFile; + Common::PlaybackFile *_recordFile; void saveScreenShot(); void checkRecordedMD5();