diff --git a/Core/Replay.cpp b/Core/Replay.cpp index a155b3792d..e3402fb3d5 100644 --- a/Core/Replay.cpp +++ b/Core/Replay.cpp @@ -19,6 +19,7 @@ #include #include #include "Common/Common.h" +#include "Common/FileUtil.h" #include "Common/StringUtils.h" #include "Core/Replay.h" #include "Core/FileSystems/FileSystem.h" @@ -88,15 +89,173 @@ struct ReplayItem { }; static std::vector replayItems; +// One more than the last executed item. +static size_t replayExecPos = 0; +static bool replaySaveWroteHeader = false; static ReplayState replayState = ReplayState::IDLE; static size_t replayCtrlPos = 0; -static uint32_t lastButtons; -static uint8_t lastAnalog[2][2]; +static uint32_t lastButtons = 0; +static uint8_t lastAnalog[2][2]{}; static size_t replayDiskPos = 0; static bool diskFailed = false; +// TODO: File format either needs rtc and rand seed, or must be paired with a save state. + +void ReplayExecuteBlob(const std::vector &data) { + ReplayAbort(); + + // Rough estimate. + replayItems.reserve(data.size() / sizeof(ReplayItemHeader)); + for (size_t i = 0, sz = data.size(); i < sz; ) { + if (i + sizeof(ReplayItemHeader) > sz) { + ERROR_LOG(SYSTEM, "Truncated replay data at %lld during item header", (long long)i); + break; + } + ReplayItemHeader *info = (ReplayItemHeader *)&data[i]; + ReplayItem item(*info); + i += sizeof(ReplayItemHeader); + + if ((int)item.info.action & (int)ReplayAction::MASK_SIDEDATA) { + if (i + item.info.size > sz) { + ERROR_LOG(SYSTEM, "Truncated replay data at %lld during side data", (long long)i); + break; + } + if (item.info.size != 0) { + item.data.resize(item.info.size); + memcpy(&item.data[0], &data[i], item.info.size); + i += item.info.size; + } + } + + replayItems.push_back(item); + } + + replayState = ReplayState::EXECUTE; + INFO_LOG(SYSTEM, "Executing replay with %lld items", (long long)replayItems.size()); +} + +bool ReplayExecuteFile(const std::string &filename) { + ReplayAbort(); + + FILE *fp = File::OpenCFile(filename, "rb"); + if (!fp) { + DEBUG_LOG(SYSTEM, "Failed to open replay file: %s", filename.c_str()); + return false; + } + + // TODO: Header handling. + // Include initial rand state or timestamp? + + // TODO: Maybe stream instead. + size_t sz = File::GetFileSize(fp); + if (sz == 0) { + ERROR_LOG(SYSTEM, "Empty replay data"); + fclose(fp); + return false; + } + + std::vector data; + data.resize(sz); + + if (fread(&data[0], sz, 1, fp) != 1) { + ERROR_LOG(SYSTEM, "Could not read replay data"); + fclose(fp); + return false; + } + + ReplayExecuteBlob(data); + return true; +} + +bool ReplayHasMoreEvents() { + return replayExecPos < replayItems.size(); +} + +void ReplayBeginSave() { + if (replayState != ReplayState::EXECUTE) { + // Restart any save operation. + ReplayAbort(); + } else { + // Discard any unexecuted items, but resume from there. + // The parameter isn't used here, since we'll always be resizing down. + replayItems.resize(replayExecPos, ReplayItem(ReplayItemHeader(ReplayAction::BUTTONS, 0))); + } + + replayState = ReplayState::SAVE; +} + +void ReplayFlushBlob(std::vector *data) { + size_t sz = replayItems.size() * sizeof(ReplayItemHeader); + // Add in any side data. + for (const auto &item : replayItems) { + if ((int)item.info.action & (int)ReplayAction::MASK_SIDEDATA) { + sz += item.info.size; + } + } + + data->resize(sz); + + size_t pos = 0; + for (const auto &item : replayItems) { + memcpy(&(*data)[pos], &item.info, sizeof(item.info)); + pos += sizeof(item.info); + + if ((int)item.info.action & (int)ReplayAction::MASK_SIDEDATA) { + memcpy(&(*data)[pos], &item.data[0], item.data.size()); + pos += item.data.size(); + } + } + + // Keep recording, but throw away our buffered items. + replayItems.clear(); +} + +bool ReplayFlushFile(const std::string &filename) { + FILE *fp = File::OpenCFile(filename, replaySaveWroteHeader ? "ab" : "wb"); + if (!fp) { + ERROR_LOG(SYSTEM, "Failed to open replay file: %s", filename.c_str()); + return false; + } + + // TODO: Header handling. + // Include initial rand state or timestamp? + replaySaveWroteHeader = true; + + size_t c = replayItems.size(); + bool success = true; + if (c != 0) { + // TODO: Maybe stream instead. + std::vector data; + ReplayFlushBlob(&data); + + success = fwrite(&data[0], data.size(), 1, fp) == 1; + } + fclose(fp); + + if (success) { + DEBUG_LOG(SYSTEM, "Flushed %lld replay items", (long long)c); + } else { + ERROR_LOG(SYSTEM, "Could not write %lld replay items (disk full?)", (long long)c); + } + return success; +} + +void ReplayAbort() { + replayItems.clear(); + replayExecPos = 0; + replaySaveWroteHeader = false; + replayState = ReplayState::IDLE; + + replayCtrlPos = 0; + lastButtons = 0; + memset(lastAnalog, 0, sizeof(lastAnalog)); + + replayDiskPos = 0; + diskFailed = false; +} + static void ReplaySaveCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t t) { if (lastButtons != buttons) { replayItems.push_back(ReplayItemHeader(ReplayAction::BUTTONS, t, buttons)); @@ -114,10 +273,12 @@ static void ReplayExecuteCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t switch (item.info.action) { case ReplayAction::BUTTONS: buttons = item.info.buttons; + lastButtons = item.info.buttons; break; case ReplayAction::ANALOG: memcpy(analog, item.info.analog, sizeof(analog)); + memcpy(lastAnalog, item.info.analog, sizeof(analog)); break; default: @@ -125,6 +286,10 @@ static void ReplayExecuteCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t break; } } + + if (replayExecPos < replayCtrlPos) { + replayExecPos = replayCtrlPos; + } } void ReplayApplyCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t t) { @@ -168,6 +333,10 @@ static const ReplayItem *ReplayNextDisk(ReplayAction action, uint64_t t) { return nullptr; } + if (replayExecPos < replayDiskPos) { + replayExecPos = replayDiskPos; + } + return item; } diff --git a/Core/Replay.h b/Core/Replay.h index 905947500c..3c91ce6524 100644 --- a/Core/Replay.h +++ b/Core/Replay.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include // Be careful about changing these values (used in file data.) @@ -45,6 +46,25 @@ enum class ReplayAction : uint8_t { struct PSPFileInfo; +// Replay from data in memory. +void ReplayExecuteBlob(const std::vector &data); +// Replay from data in a file. Returns false if invalid. +bool ReplayExecuteFile(const std::string &filename); +// Returns whether there are unexected events to replay. +bool ReplayHasMoreEvents(); + +// Begin recording. If currently executing, discards unexecuted events. +void ReplayBeginSave(); +// Flush buffered events to memory. Continues recording (next call will receive new events only.) +// No header is flushed with this operation - don't mix with ReplayFlushFile(). +void ReplayFlushBlob(std::vector *data); +// Flush buffered events to file. Continues recording (next call will receive new events only.) +// Do not call with a different filename before ReplayAbort(). +bool ReplayFlushFile(const std::string &filename); + +// Abort any execute or record operation in progress. +void ReplayAbort(); + void ReplayApplyCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t t); uint32_t ReplayApplyDisk(ReplayAction action, uint32_t result, uint64_t t); uint64_t ReplayApplyDisk64(ReplayAction action, uint64_t result, uint64_t t);