mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-15 06:08:35 +00:00
820 lines
26 KiB
C++
820 lines
26 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
|
|
#include "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;
|
|
|
|
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;
|
|
_acquireCount = 0;
|
|
_needcontinueGame = false;
|
|
_temporarySlot = 0;
|
|
_realSaveManager = nullptr;
|
|
_realMixerManager = nullptr;
|
|
_controlPanel = nullptr;
|
|
_lastMillis = 0;
|
|
_lastScreenshotTime = 0;
|
|
_screenshotPeriod = 0;
|
|
_playbackFile = nullptr;
|
|
_recordFile = 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 *eventDispatcher = g_system->getEventManager()->getEventDispatcher();
|
|
eventDispatcher->unregisterSource(this);
|
|
eventDispatcher->ignoreSources(false);
|
|
_recordMode = kPassthrough;
|
|
if (_playbackFile) {
|
|
_playbackFile->close();
|
|
delete _playbackFile;
|
|
}
|
|
if (_recordFile) {
|
|
_recordFile->close();
|
|
delete _recordFile;
|
|
}
|
|
switchMixer();
|
|
switchTimerManagers();
|
|
DebugMan.disableDebugChannel("EventRec");
|
|
}
|
|
|
|
void EventRecorder::updateFakeTimer(uint32 millis) {
|
|
uint32 millisDelay = millis - _lastMillis;
|
|
_lastMillis = millis;
|
|
_fakeTimer += millisDelay;
|
|
_controlPanel->setReplayedTime(_fakeTimer);
|
|
}
|
|
|
|
void EventRecorder::processTimeAndDate(TimeDate &td, bool skipRecord) {
|
|
if (!_initialized) {
|
|
return;
|
|
}
|
|
if (skipRecord) {
|
|
td = _lastTimeDate;
|
|
return;
|
|
}
|
|
Common::RecorderEvent timeDateEvent;
|
|
if (_recordMode == kRecorderRecord) {
|
|
timeDateEvent.recordedtype = Common::kRecorderEventTypeTimeDate;
|
|
timeDateEvent.timeDate = td;
|
|
_lastTimeDate = td;
|
|
_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();
|
|
}
|
|
if (_recordMode == kRecorderPlaybackPause)
|
|
td = _lastTimeDate;
|
|
}
|
|
|
|
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;
|
|
}
|
|
Common::RecorderEvent timerEvent;
|
|
switch (_recordMode) {
|
|
case kRecorderRecord:
|
|
updateSubsystems();
|
|
updateFakeTimer(millis);
|
|
timerEvent.recordedtype = Common::kRecorderEventTypeTimer;
|
|
timerEvent.time = _fakeTimer;
|
|
_recordFile->writeEvent(timerEvent);
|
|
_timerManager->handler();
|
|
break;
|
|
case kRecorderUpdate: // fallthrough
|
|
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;
|
|
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();
|
|
_controlPanel->setReplayedTime(_fakeTimer);
|
|
_processingMillis = false;
|
|
break;
|
|
case kRecorderPlaybackPause:
|
|
millis = _fakeTimer;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool EventRecorder::processDelayMillis() {
|
|
return _fastPlayback;
|
|
}
|
|
|
|
bool EventRecorder::processAutosave() {
|
|
return _recordMode == kPassthrough;
|
|
}
|
|
|
|
void EventRecorder::processScreenUpdate() {
|
|
if (!_initialized) {
|
|
return;
|
|
}
|
|
|
|
Common::RecorderEvent screenUpdateEvent;
|
|
switch (_recordMode) {
|
|
case kRecorderRecord:
|
|
updateSubsystems();
|
|
screenUpdateEvent.recordedtype = Common::kRecorderEventTypeScreenUpdate;
|
|
screenUpdateEvent.time = _fakeTimer;
|
|
_recordFile->writeEvent(screenUpdateEvent);
|
|
takeScreenshot();
|
|
_timerManager->handler();
|
|
break;
|
|
case kRecorderUpdate: // fallthrough
|
|
case kRecorderPlayback:
|
|
// if the next event isn't a screen update, fast forward until we find one.
|
|
if (_nextEvent.recordedtype != Common::kRecorderEventTypeScreenUpdate) {
|
|
int numSkipped = 0;
|
|
while (true) {
|
|
_nextEvent = _playbackFile->getNextEvent();
|
|
numSkipped += 1;
|
|
if (_nextEvent.recordedtype == Common::kRecorderEventTypeScreenUpdate) {
|
|
warning("Skipped %d events to get to the next screen update at %d", numSkipped, _nextEvent.time);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_processingMillis = true;
|
|
_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;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
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) &&
|
|
(_recordMode != kRecorderUpdate)) ||
|
|
!_initialized)
|
|
return false;
|
|
|
|
if (_nextEvent.recordedtype == Common::kRecorderEventTypeTimer
|
|
|| _nextEvent.recordedtype == Common::kRecorderEventTypeTimeDate
|
|
|| _nextEvent.recordedtype == Common::kRecorderEventTypeScreenUpdate
|
|
|| _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:
|
|
case kRecorderUpdate:
|
|
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) {
|
|
_recordFile->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();
|
|
_lastScreenshotTime = 0;
|
|
_recordMode = mode;
|
|
_needcontinueGame = false;
|
|
if (ConfMan.hasKey("disable_display")) {
|
|
DebugMan.enableDebugChannel("EventRec");
|
|
gDebugLevel = 1;
|
|
}
|
|
if ((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) {
|
|
debugC(1, kDebugLevelEventRec, "playback:action=\"Load file\" filename=%s", recordFileName.c_str());
|
|
Common::EventDispatcher *eventDispatcher = g_system->getEventManager()->getEventDispatcher();
|
|
eventDispatcher->clearEvents();
|
|
eventDispatcher->ignoreSources(true);
|
|
eventDispatcher->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) || (_recordMode == kRecorderUpdate)) {
|
|
applyPlaybackSettings();
|
|
_nextEvent = _playbackFile->getNextEvent();
|
|
}
|
|
if ((_recordMode == kRecorderRecord) || (_recordMode == kRecorderUpdate)) {
|
|
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:
|
|
_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;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EventRecorder::checkGameHash(const ADGameDescription *gameDesc) {
|
|
if (_playbackFile->getHeader().hashRecords.size() == 0) {
|
|
warning("Engine doesn't contain description table, skipping hash check");
|
|
return true;
|
|
}
|
|
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) {
|
|
_recordFile->getHeader().settingsRecords[entry->_key] = entry->_value;
|
|
}
|
|
}
|
|
|
|
void EventRecorder::getConfig() {
|
|
getConfigFromDomain(ConfMan.getDomain(ConfMan.kApplicationDomain));
|
|
getConfigFromDomain(ConfMan.getActiveDomain());
|
|
_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);
|
|
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 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);
|
|
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;
|
|
_recordFile->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) {
|
|
_recordFile->getHeader().hashRecords[fileDesc->fileName] = fileDesc->md5;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EventRecorder::processGameDescription(const ADGameDescription *desc) {
|
|
if ((_recordMode == kRecorderRecord) || (_recordMode == kRecorderUpdate)) {
|
|
setGameMd5(desc);
|
|
}
|
|
if (((_recordMode == kRecorderPlayback) || (_recordMode == kRecorderUpdate)) && !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;
|
|
_recordFile->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) {
|
|
_recordFile->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) || (_recordMode == kRecorderUpdate)) {
|
|
Common::StringArray result;
|
|
for (Common::HashMap<Common::String, Common::PlaybackFile::SaveFileBuffer>::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) && (_recordMode != kRecorderUpdate)) {
|
|
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);
|
|
}
|
|
_recordFile->getHeader().author = _author;
|
|
_recordFile->getHeader().notes = _desc;
|
|
_recordFile->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<MetaEngine>().hasFeature(MetaEngine::kSavesSupportMetaInfo);
|
|
bool featuresSupport = metaInfoSupport &&
|
|
g_engine->canSaveGameStateCurrently() &&
|
|
plugin->get<MetaEngine>().hasFeature(MetaEngine::kSupportsListSaves) &&
|
|
plugin->get<MetaEngine>().hasFeature(MetaEngine::kSupportsDeleteSave);
|
|
if (!featuresSupport) {
|
|
return false;
|
|
}
|
|
|
|
const Common::String target = ConfMan.getActiveDomainName();
|
|
SaveStateList saveList = plugin->get<MetaEngine>().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<MetaEngine>().removeSaveState(target.c_str(), _temporarySlot);
|
|
_temporarySlot = -1;
|
|
}
|
|
|
|
} // End of namespace GUI
|
|
|
|
#endif // ENABLE_EVENTRECORDER
|