2013-05-16 21:18:09 +00:00
|
|
|
/* 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.
|
2014-02-18 01:34:18 +00:00
|
|
|
*
|
2013-05-16 21:18:09 +00:00
|
|
|
* 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.
|
2014-02-18 01:34:18 +00:00
|
|
|
*
|
2013-05-16 21:18:09 +00:00
|
|
|
* 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;
|
2013-07-14 15:55:38 +00:00
|
|
|
|
|
|
|
_recordFile = 0;
|
|
|
|
_headerDumped = false;
|
|
|
|
_recordCount = 0;
|
|
|
|
_eventsSize = 0;
|
2013-07-15 11:31:34 +00:00
|
|
|
memset(_tmpBuffer, 1, kRecordBuffSize);
|
2013-11-02 22:50:36 +00:00
|
|
|
|
|
|
|
_playbackParseState = kFileStateCheckFormat;
|
2013-05-16 21:18:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2018-04-15 12:00:56 +00:00
|
|
|
_readStream->skip(header.len - 8);
|
2013-05-16 21:18:09 +00:00
|
|
|
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;
|
2019-10-04 01:30:19 +00:00
|
|
|
default:
|
|
|
|
// fallthrough intended
|
2013-05-16 21:18:09 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-09-09 14:43:55 +00:00
|
|
|
event.kbdRepeat = true;
|
2013-05-16 21:18:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2019-10-04 01:30:19 +00:00
|
|
|
default:
|
|
|
|
// fallthrough intended
|
2013-05-16 21:18:09 +00:00
|
|
|
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();
|
2018-04-15 12:00:56 +00:00
|
|
|
_readStream->skip(size - 8);
|
2013-05-16 21:18:09 +00:00
|
|
|
++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);
|
ALL: Load savegame thumbnail only when necessary
This commit introduces the following changes:
1. Graphics::loadThumbnail()
Now returns a boolean and takes a new argument skipThumbnail which
defaults to false. In case of true, loadThumbnail() reads past the
thumbnail data in the input stream instead of actually loading the
thumbnail. This simplifies savegame handling where, up until now,
many engines always read the whole savegame metadata (including
the thumbnail) and then threw away the thumbnail when not needed
(which is in almost all cases, the most common exception being
MetaEngine::querySaveMetaInfos() which is responsible for loading
savegame metadata for displaying it in the GUI launcher.
2. readSavegameHeader()
Engines which already implement such a method (name varies) now take
a new argument skipThumbnail (default: true) which is passed
through to loadThumbnail(). This means that the default case for
readSavegameHeader() is now _not_ loading the thumbnail from a
savegame and just reading past it. In those cases, e.g.
querySaveMetaInfos(), where we actually are interested in loading
the thumbnail readSavegameHeader() needs to explicitely be called
with skipThumbnail == false.
Engines whose readSavegameHeader() (name varies) already takes an
argument loadThumbnail have been adapted to have a similar
prototype and semantics.
I.e. readSaveHeader(in, loadThumbnail, header) now is
readSaveHeader(in, header, skipThumbnail).
3. Error handling
Engines which previously did not check the return value of
readSavegameHeader() (name varies) now do so ensuring that possibly
broken savegames (be it a broken thumbnail or something else) don't
make it into the GUI launcher list in the first place.
2018-04-05 22:06:38 +00:00
|
|
|
Graphics::Surface *thumbnail;
|
|
|
|
return Graphics::loadThumbnail(*_readStream, thumbnail) ? thumbnail : NULL;
|
2013-05-16 21:18:09 +00:00
|
|
|
} else {
|
|
|
|
uint32 size = _readStream->readUint32BE();
|
2018-04-15 12:00:56 +00:00
|
|
|
_readStream->skip(size - 8);
|
2013-05-16 21:18:09 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|