diff options
author | Paul Gilbert | 2013-07-04 08:44:49 -0400 |
---|---|---|
committer | Paul Gilbert | 2013-07-04 08:44:49 -0400 |
commit | fa737fd5af3763a152e92c4b74c114876b3a8573 (patch) | |
tree | 3e698f9c095ba692c6e305b35785a8f45f5e061b /common/recorderfile.cpp | |
parent | a49a7d5ad4f4435ed8cee0934c94155586f2dd99 (diff) | |
parent | baafae672f3489b0eaf77c22be0c65ba31e6b73d (diff) | |
download | scummvm-rg350-fa737fd5af3763a152e92c4b74c114876b3a8573.tar.gz scummvm-rg350-fa737fd5af3763a152e92c4b74c114876b3a8573.tar.bz2 scummvm-rg350-fa737fd5af3763a152e92c4b74c114876b3a8573.zip |
Merge branch 'master' into tsage_r2r
Diffstat (limited to 'common/recorderfile.cpp')
-rw-r--r-- | common/recorderfile.cpp | 708 |
1 files changed, 708 insertions, 0 deletions
diff --git a/common/recorderfile.cpp b/common/recorderfile.cpp new file mode 100644 index 0000000000..60c47e11ce --- /dev/null +++ b/common/recorderfile.cpp @@ -0,0 +1,708 @@ +/* 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 "common/system.h" +#include "gui/EventRecorder.h" +#include "common/md5.h" +#include "common/recorderfile.h" +#include "common/savefile.h" +#include "common/bufferedstream.h" +#include "graphics/thumbnail.h" +#include "graphics/surface.h" +#include "graphics/scaler.h" + +#define RECORD_VERSION 1 + +namespace Common { + +PlaybackFile::PlaybackFile() : _tmpRecordFile(_tmpBuffer, kRecordBuffSize), _tmpPlaybackFile(_tmpBuffer, kRecordBuffSize) { + _readStream = NULL; + _writeStream = NULL; + _screenshotsFile = NULL; + _mode = kClosed; +} + +PlaybackFile::~PlaybackFile() { + close(); +} + +bool PlaybackFile::openWrite(const String &fileName) { + close(); + _header.fileName = fileName; + _writeStream = wrapBufferedWriteStream(g_system->getSavefileManager()->openForSaving(fileName), 128 * 1024); + _headerDumped = false; + _recordCount = 0; + if (_writeStream == NULL) { + return false; + } + _mode = kWrite; + return true; +} + +bool PlaybackFile::openRead(const String &fileName) { + close(); + _header.fileName = fileName; + _eventsSize = 0; + _tmpPlaybackFile.seek(0); + _readStream = wrapBufferedSeekableReadStream(g_system->getSavefileManager()->openForLoading(fileName), 128 * 1024, DisposeAfterUse::YES); + if (_readStream == NULL) { + debugC(1, kDebugLevelEventRec, "playback:action=\"Load File\" result=fail reason=\"file %s not found\"", fileName.c_str()); + return false; + } + if (!parseHeader()) { + debugC(1, kDebugLevelEventRec, "playback:action=\"Load File\" result=fail reason=\"header parsing failed\""); + return false; + } + _screenshotsFile = wrapBufferedWriteStream(g_system->getSavefileManager()->openForSaving("screenshots.bin"), 128 * 1024); + debugC(1, kDebugLevelEventRec, "playback:action=\"Load File\" result=success"); + _mode = kRead; + return true; +} + +void PlaybackFile::close() { + delete _readStream; + _readStream = NULL; + if (_writeStream != NULL) { + dumpRecordsToFile(); + _writeStream->finalize(); + delete _writeStream; + _writeStream = NULL; + updateHeader(); + } + if (_screenshotsFile != NULL) { + _screenshotsFile->finalize(); + delete _screenshotsFile; + _screenshotsFile = NULL; + } + for (HashMap<String, SaveFileBuffer>::iterator i = _header.saveFiles.begin(); i != _header.saveFiles.end(); ++i) { + free(i->_value.buffer); + } + _header.saveFiles.clear(); + _mode = kClosed; +} + +bool PlaybackFile::parseHeader() { + PlaybackFileHeader result; + ChunkHeader nextChunk; + _playbackParseState = kFileStateCheckFormat; + if (!readChunkHeader(nextChunk)) { + _playbackParseState = kFileStateError; + return false; + } + while ((_playbackParseState != kFileStateDone) && (_playbackParseState != kFileStateError)) { + if (processChunk(nextChunk)) { + if (!readChunkHeader(nextChunk)) { + warning("Error in header parsing"); + _playbackParseState = kFileStateError; + } + } + } + return _playbackParseState == kFileStateDone; +} + +bool PlaybackFile::checkPlaybackFileVersion() { + uint32 version; + version = _readStream->readUint32LE(); + if (version != RECORD_VERSION) { + warning("Incorrect playback file version. Expected version %d, but got %d.", RECORD_VERSION, version); + return false; + } + return true; +} + + +String PlaybackFile::readString(int len) { + String result; + char buf[50]; + int readSize = 49; + while (len > 0) { + if (len <= 49) { + readSize = len; + } + _readStream->read(buf, readSize); + buf[readSize] = 0; + result += buf; + len -= readSize; + } + return result; +} + +bool PlaybackFile::readChunkHeader(PlaybackFile::ChunkHeader &nextChunk) { + nextChunk.id = (FileTag)_readStream->readUint32LE(); + nextChunk.len = _readStream->readUint32LE(); + return !_readStream->err() && !_readStream->eos(); +} + +bool PlaybackFile::processChunk(ChunkHeader &nextChunk) { + switch (_playbackParseState) { + case kFileStateCheckFormat: + if (nextChunk.id == kFormatIdTag) { + _playbackParseState = kFileStateCheckVersion; + } else { + warning("Unknown playback file signature"); + _playbackParseState = kFileStateError; + } + break; + case kFileStateCheckVersion: + if ((nextChunk.id == kVersionTag) && checkPlaybackFileVersion()) { + _playbackParseState = kFileStateSelectSection; + } else { + _playbackParseState = kFileStateError; + } + break; + case kFileStateSelectSection: + switch (nextChunk.id) { + case kHeaderSectionTag: + _playbackParseState = kFileStateProcessHeader; + break; + case kHashSectionTag: + _playbackParseState = kFileStateProcessHash; + break; + case kRandomSectionTag: + _playbackParseState = kFileStateProcessRandom; + break; + case kEventTag: + case kScreenShotTag: + _readStream->seek(-8, SEEK_CUR); + _playbackParseState = kFileStateDone; + return false; + case kSaveTag: + _playbackParseState = kFileStateProcessSave; + break; + case kSettingsSectionTag: + _playbackParseState = kFileStateProcessSettings; + warning("Loading record header"); + break; + default: + _readStream->skip(nextChunk.len); + break; + } + break; + case kFileStateProcessSave: + if (nextChunk.id == kSaveRecordTag) { + readSaveRecord(); + } else { + _playbackParseState = kFileStateSelectSection; + return false; + } + break; + case kFileStateProcessHeader: + switch (nextChunk.id) { + case kAuthorTag: + _header.author = readString(nextChunk.len); + break; + case kCommentsTag: + _header.notes = readString(nextChunk.len); + break; + case kNameTag: + _header.name = readString(nextChunk.len); + break; + default: + _playbackParseState = kFileStateSelectSection; + return false; + } + break; + case kFileStateProcessHash: + if (nextChunk.id == kHashRecordTag) { + readHashMap(nextChunk); + } else { + _playbackParseState = kFileStateSelectSection; + return false; + } + break; + case kFileStateProcessRandom: + if (nextChunk.id == kRandomRecordTag) { + processRndSeedRecord(nextChunk); + } else { + _playbackParseState = kFileStateSelectSection; + return false; + } + break; + case kFileStateProcessSettings: + if (nextChunk.id == kSettingsRecordTag) { + if (!processSettingsRecord()) { + _playbackParseState = kFileStateError; + return false; + } + } else { + _playbackParseState = kFileStateSelectSection; + return false; + } + break; + default: + return false; + } + return true; +} + +void PlaybackFile::returnToChunkHeader() { + _readStream->seek(-8, SEEK_CUR); +} + +void PlaybackFile::readHashMap(ChunkHeader chunk) { + String hashName = readString(chunk.len - 32); + String hashMd5 = readString(32); + _header.hashRecords[hashName] = hashMd5; +} + +void PlaybackFile::processRndSeedRecord(ChunkHeader chunk) { + String randomSourceName = readString(chunk.len - 4); + uint32 randomSourceSeed = _readStream->readUint32LE(); + _header.randomSourceRecords[randomSourceName] = randomSourceSeed; +} + +bool PlaybackFile::processSettingsRecord() { + ChunkHeader keyChunk; + if (!readChunkHeader(keyChunk) || (keyChunk.id != kSettingsRecordKeyTag)) { + warning("Invalid format of settings section"); + return false; + } + String key = readString(keyChunk.len); + ChunkHeader valueChunk; + if (!readChunkHeader(valueChunk) || (valueChunk.id != kSettingsRecordValueTag)) { + warning("Invalid format of settings section"); + return false; + } + String value = readString(valueChunk.len); + _header.settingsRecords[key] = value; + return true; +} + + +bool PlaybackFile::readSaveRecord() { + ChunkHeader fileNameChunk; + if (!readChunkHeader(fileNameChunk) || (fileNameChunk.id != kSaveRecordNameTag)) { + warning("Invalid format of save section"); + return false; + } + String fileName = readString(fileNameChunk.len); + ChunkHeader saveBufferChunk; + if (!readChunkHeader(saveBufferChunk) || (saveBufferChunk.id != kSaveRecordBufferTag)) { + warning("Invalid format of save section"); + return false; + } + SaveFileBuffer buf; + buf.size = saveBufferChunk.len; + buf.buffer = (byte *)malloc(saveBufferChunk.len); + _readStream->read(buf.buffer, buf.size); + _header.saveFiles[fileName] = buf; + debugC(1, kDebugLevelEventRec, "playback:action=\"Load save file\" filename=%s len=%d", fileName.c_str(), buf.size); + return true; +} + + + +RecorderEvent PlaybackFile::getNextEvent() { + assert(_mode == kRead); + if (isEventsBufferEmpty()) { + PlaybackFile::ChunkHeader header; + header.id = kFormatIdTag; + while (header.id != kEventTag) { + if (!readChunkHeader(header) || _readStream->eos()) { + break; + } + switch (header.id) { + case kEventTag: + readEventsToBuffer(header.len); + break; + case kScreenShotTag: + _readStream->seek(-4, SEEK_CUR); + header.len = _readStream->readUint32BE(); + _readStream->skip(header.len-8); + break; + case kMD5Tag: + checkRecordedMD5(); + break; + default: + _readStream->skip(header.len); + break; + } + } + } + RecorderEvent result; + readEvent(result); + return result; +} + +bool PlaybackFile::isEventsBufferEmpty() { + return (uint32)_tmpPlaybackFile.pos() == _eventsSize; +} + +void PlaybackFile::readEvent(RecorderEvent& event) { + event.recordedtype = (RecorderEventType)_tmpPlaybackFile.readByte(); + switch (event.recordedtype) { + case kRecorderEventTypeTimer: + event.time = _tmpPlaybackFile.readUint32LE(); + break; + case kRecorderEventTypeNormal: + event.type = (EventType)_tmpPlaybackFile.readUint32LE(); + switch (event.type) { + case EVENT_KEYDOWN: + case EVENT_KEYUP: + event.time = _tmpPlaybackFile.readUint32LE(); + event.kbd.keycode = (KeyCode)_tmpPlaybackFile.readSint32LE(); + event.kbd.ascii = _tmpPlaybackFile.readUint16LE(); + event.kbd.flags = _tmpPlaybackFile.readByte(); + break; + case EVENT_MOUSEMOVE: + case EVENT_LBUTTONDOWN: + case EVENT_LBUTTONUP: + case EVENT_RBUTTONDOWN: + case EVENT_RBUTTONUP: + case EVENT_WHEELUP: + case EVENT_WHEELDOWN: + case EVENT_MBUTTONDOWN: + case EVENT_MBUTTONUP: + event.time = _tmpPlaybackFile.readUint32LE(); + event.mouse.x = _tmpPlaybackFile.readSint16LE(); + event.mouse.y = _tmpPlaybackFile.readSint16LE(); + break; + default: + event.time = _tmpPlaybackFile.readUint32LE(); + break; + } + break; + } + event.synthetic = true; +} + +void PlaybackFile::readEventsToBuffer(uint32 size) { + _readStream->read(_tmpBuffer, size); + _tmpPlaybackFile.seek(0); + _eventsSize = size; +} + +void PlaybackFile::saveScreenShot(Graphics::Surface &screen, byte md5[16]) { + dumpRecordsToFile(); + _writeStream->writeUint32LE(kMD5Tag); + _writeStream->writeUint32LE(16); + _writeStream->write(md5, 16); + Graphics::saveThumbnail(*_writeStream, screen); +} + +void PlaybackFile::dumpRecordsToFile() { + if (!_headerDumped) { + dumpHeaderToFile(); + _headerDumped = true; + } + if (_recordCount == 0) { + return; + } + _writeStream->writeUint32LE(kEventTag); + _writeStream->writeUint32LE(_tmpRecordFile.pos()); + _writeStream->write(_tmpBuffer, _tmpRecordFile.pos()); + _tmpRecordFile.seek(0); + _recordCount = 0; +} + +void PlaybackFile::dumpHeaderToFile() { + _writeStream->writeUint32LE(kFormatIdTag); + // Specify size for first tag as NULL since we cannot calculate + // size of the file at time of the header dumping + _writeStream->writeUint32LE(0); + _writeStream->writeUint32LE(kVersionTag); + _writeStream->writeUint32LE(4); + _writeStream->writeUint32LE(RECORD_VERSION); + writeHeaderSection(); + writeGameHash(); + writeRandomRecords(); + writeGameSettings(); + writeSaveFilesSection(); +} + +void PlaybackFile::writeHeaderSection() { + uint32 headerSize = 0; + if (!_header.author.empty()) { + headerSize = _header.author.size() + 8; + } + if (!_header.notes.empty()) { + headerSize += _header.notes.size() + 8; + } + if (!_header.name.empty()) { + headerSize += _header.name.size() + 8; + } + if (headerSize == 0) { + return; + } + _writeStream->writeUint32LE(kHeaderSectionTag); + _writeStream->writeUint32LE(headerSize); + if (!_header.author.empty()) { + _writeStream->writeUint32LE(kAuthorTag); + _writeStream->writeUint32LE(_header.author.size()); + _writeStream->writeString(_header.author); + } + if (!_header.notes.empty()) { + _writeStream->writeUint32LE(kCommentsTag); + _writeStream->writeUint32LE(_header.notes.size()); + _writeStream->writeString(_header.notes); + } + if (!_header.name.empty()) { + _writeStream->writeUint32LE(kNameTag); + _writeStream->writeUint32LE(_header.name.size()); + _writeStream->writeString(_header.name); + } +} + +void PlaybackFile::writeGameHash() { + uint32 hashSectionSize = 0; + for (StringMap::iterator i = _header.hashRecords.begin(); i != _header.hashRecords.end(); ++i) { + hashSectionSize = hashSectionSize + i->_key.size() + i->_value.size() + 8; + } + if (_header.hashRecords.size() == 0) { + return; + } + _writeStream->writeUint32LE(kHashSectionTag); + _writeStream->writeUint32LE(hashSectionSize); + for (StringMap::iterator i = _header.hashRecords.begin(); i != _header.hashRecords.end(); ++i) { + _writeStream->writeUint32LE(kHashRecordTag); + _writeStream->writeUint32LE(i->_key.size() + i->_value.size()); + _writeStream->writeString(i->_key); + _writeStream->writeString(i->_value); + } +} + +void PlaybackFile::writeRandomRecords() { + uint32 randomSectionSize = 0; + for (RandomSeedsDictionary::iterator i = _header.randomSourceRecords.begin(); i != _header.randomSourceRecords.end(); ++i) { + randomSectionSize = randomSectionSize + i->_key.size() + 12; + } + if (_header.randomSourceRecords.size() == 0) { + return; + } + _writeStream->writeUint32LE(kRandomSectionTag); + _writeStream->writeUint32LE(randomSectionSize); + for (RandomSeedsDictionary::iterator i = _header.randomSourceRecords.begin(); i != _header.randomSourceRecords.end(); ++i) { + _writeStream->writeUint32LE(kRandomRecordTag); + _writeStream->writeUint32LE(i->_key.size() + 4); + _writeStream->writeString(i->_key); + _writeStream->writeUint32LE(i->_value); + } +} + +void PlaybackFile::writeEvent(const RecorderEvent &event) { + assert(_mode == kWrite); + _recordCount++; + _tmpRecordFile.writeByte(event.recordedtype); + switch (event.recordedtype) { + case kRecorderEventTypeTimer: + _tmpRecordFile.writeUint32LE(event.time); + break; + case kRecorderEventTypeNormal: + _tmpRecordFile.writeUint32LE((uint32)event.type); + switch(event.type) { + case EVENT_KEYDOWN: + case EVENT_KEYUP: + _tmpRecordFile.writeUint32LE(event.time); + _tmpRecordFile.writeSint32LE(event.kbd.keycode); + _tmpRecordFile.writeUint16LE(event.kbd.ascii); + _tmpRecordFile.writeByte(event.kbd.flags); + break; + case EVENT_MOUSEMOVE: + case EVENT_LBUTTONDOWN: + case EVENT_LBUTTONUP: + case EVENT_RBUTTONDOWN: + case EVENT_RBUTTONUP: + case EVENT_WHEELUP: + case EVENT_WHEELDOWN: + case EVENT_MBUTTONDOWN: + case EVENT_MBUTTONUP: + _tmpRecordFile.writeUint32LE(event.time); + _tmpRecordFile.writeSint16LE(event.mouse.x); + _tmpRecordFile.writeSint16LE(event.mouse.y); + break; + default: + _tmpRecordFile.writeUint32LE(event.time); + break; + } + break; + } + if (_recordCount == kMaxBufferedRecords) { + dumpRecordsToFile(); + } +} + +void PlaybackFile::writeGameSettings() { + _writeStream->writeUint32LE(kSettingsSectionTag); + uint32 settingsSectionSize = 0; + for (StringMap::iterator i = _header.settingsRecords.begin(); i != _header.settingsRecords.end(); ++i) { + settingsSectionSize += i->_key.size() + i->_value.size() + 24; + } + _writeStream->writeUint32LE(settingsSectionSize); + for (StringMap::iterator i = _header.settingsRecords.begin(); i != _header.settingsRecords.end(); ++i) { + _writeStream->writeUint32LE(kSettingsRecordTag); + _writeStream->writeUint32LE(i->_key.size() + i->_value.size() + 16); + _writeStream->writeUint32LE(kSettingsRecordKeyTag); + _writeStream->writeUint32LE(i->_key.size()); + _writeStream->writeString(i->_key); + _writeStream->writeUint32LE(kSettingsRecordValueTag); + _writeStream->writeUint32LE(i->_value.size()); + _writeStream->writeString(i->_value); + } +} + +int PlaybackFile::getScreensCount() { + if (_mode != kRead) { + return 0; + } + _readStream->seek(0); + int result = 0; + while (skipToNextScreenshot()) { + uint32 size = _readStream->readUint32BE(); + _readStream->skip(size-8); + ++result; + } + return result; +} + +bool PlaybackFile::skipToNextScreenshot() { + while (true) { + FileTag id = (FileTag)_readStream->readUint32LE(); + if (_readStream->eos()) { + break; + } + if (id == kScreenShotTag) { + return true; + } + else { + uint32 size = _readStream->readUint32LE(); + _readStream->skip(size); + } + } + return false; +} + +Graphics::Surface *PlaybackFile::getScreenShot(int number) { + if (_mode != kRead) { + return NULL; + } + _readStream->seek(0); + int screenCount = 1; + while (skipToNextScreenshot()) { + if (screenCount == number) { + screenCount++; + _readStream->seek(-4, SEEK_CUR); + return Graphics::loadThumbnail(*_readStream); + } else { + uint32 size = _readStream->readUint32BE(); + _readStream->skip(size-8); + screenCount++; + } + } + return NULL; +} + +void PlaybackFile::updateHeader() { + if (_mode == kWrite) { + _readStream = g_system->getSavefileManager()->openForLoading(_header.fileName); + } + _readStream->seek(0); + skipHeader(); + String tmpFilename = "_" + _header.fileName; + _writeStream = g_system->getSavefileManager()->openForSaving(tmpFilename); + dumpHeaderToFile(); + uint32 readedSize = 0; + do { + readedSize = _readStream->read(_tmpBuffer, kRecordBuffSize); + _writeStream->write(_tmpBuffer, readedSize); + } while (readedSize != 0); + delete _writeStream; + _writeStream = NULL; + delete _readStream; + _readStream = NULL; + g_system->getSavefileManager()->removeSavefile(_header.fileName); + g_system->getSavefileManager()->renameSavefile(tmpFilename, _header.fileName); + if (_mode == kRead) { + openRead(_header.fileName); + } +} + +void PlaybackFile::skipHeader() { + while (true) { + uint32 id = _readStream->readUint32LE(); + if (_readStream->eos()) { + break; + } + if ((id == kScreenShotTag) || (id == kEventTag) || (id == kMD5Tag)) { + _readStream->seek(-4, SEEK_CUR); + return; + } + else { + uint32 size = _readStream->readUint32LE(); + _readStream->skip(size); + } + } +} + +void PlaybackFile::addSaveFile(const String &fileName, InSaveFile *saveStream) { + uint oldPos = saveStream->pos(); + saveStream->seek(0); + _header.saveFiles[fileName].buffer = (byte *)malloc(saveStream->size()); + _header.saveFiles[fileName].size = saveStream->size(); + saveStream->read(_header.saveFiles[fileName].buffer, saveStream->size()); + saveStream->seek(oldPos); +} + +void PlaybackFile::writeSaveFilesSection() { + uint size = 0; + for (HashMap<String, SaveFileBuffer>::iterator i = _header.saveFiles.begin(); i != _header.saveFiles.end(); ++i) { + size += i->_value.size + i->_key.size() + 24; + } + if (size == 0) { + return; + } + _writeStream->writeSint32LE(kSaveTag); + _writeStream->writeSint32LE(size); + for (HashMap<String, SaveFileBuffer>::iterator i = _header.saveFiles.begin(); i != _header.saveFiles.end(); ++i) { + _writeStream->writeSint32LE(kSaveRecordTag); + _writeStream->writeSint32LE(i->_key.size() + i->_value.size + 16); + _writeStream->writeSint32LE(kSaveRecordNameTag); + _writeStream->writeSint32LE(i->_key.size()); + _writeStream->writeString(i->_key); + _writeStream->writeSint32LE(kSaveRecordBufferTag); + _writeStream->writeSint32LE(i->_value.size); + _writeStream->write(i->_value.buffer, i->_value.size); + } +} + + +void PlaybackFile::checkRecordedMD5() { + uint8 currentMD5[16]; + uint8 savedMD5[16]; + Graphics::Surface screen; + _readStream->read(savedMD5, 16); + if (!g_eventRec.grabScreenAndComputeMD5(screen, currentMD5)) { + return; + } + uint32 seconds = g_system->getMillis(true) / 1000; + String screenTime = String::format("%.2d:%.2d:%.2d", seconds / 3600 % 24, seconds / 60 % 60, seconds % 60); + if (memcmp(savedMD5, currentMD5, 16) != 0) { + debugC(1, kDebugLevelEventRec, "playback:action=\"Check screenshot\" time=%s result = fail", screenTime.c_str()); + warning("Recorded and current screenshots are different"); + } else { + debugC(1, kDebugLevelEventRec, "playback:action=\"Check screenshot\" time=%s result = success", screenTime.c_str()); + } + Graphics::saveThumbnail(*_screenshotsFile, screen); + screen.free(); +} + + +} |