/* 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; _recordFile = 0; _headerDumped = false; _recordCount = 0; _eventsSize = 0; memset(_tmpBuffer, 1, kRecordBuffSize); _playbackParseState = kFileStateCheckFormat; } 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::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; default: // fallthrough intended 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.kbdRepeat = 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; default: // fallthrough intended 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); Graphics::Surface *thumbnail; return Graphics::loadThumbnail(*_readStream, thumbnail) ? thumbnail : NULL; } 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::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::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(); } }