/***************************************************************************** * Copyright (c) 2014-2018 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "ReplayManager.h" #include "Context.h" #include "Game.h" #include "OpenRCT2.h" #include "ParkImporter.h" #include "PlatformEnvironment.h" #include "actions/GameAction.h" #include "core/DataSerialiser.h" #include "core/Path.hpp" #include "object/ObjectManager.h" #include "object/ObjectRepository.h" #include "rct2/S6Exporter.h" #include namespace OpenRCT2 { struct ReplayCommand { ReplayCommand() = default; ReplayCommand(uint32_t t, uint32_t* args, uint8_t cb, uint32_t id) { tick = t; eax = args[0]; ebx = args[1]; ecx = args[2]; edx = args[3]; esi = args[4]; edi = args[5]; ebp = args[6]; callback = cb; action = nullptr; commandIndex = id; } ReplayCommand(uint32_t t, std::unique_ptr&& ga, uint32_t id) { tick = t; action = std::move(ga); commandIndex = id; } uint32_t tick = 0; uint32_t eax = 0, ebx = 0, ecx = 0, edx = 0, esi = 0, edi = 0, ebp = 0; GameAction::Ptr action; uint8_t playerid = 0; uint8_t callback = 0; uint32_t commandIndex = 0; bool operator<(const ReplayCommand& comp) const { // First sort by tick if (tick < comp.tick) return true; if (tick > comp.tick) return false; // If the ticks are equal sort by commandIndex return commandIndex < comp.commandIndex; } }; struct ReplayRecordData { MemoryStream parkData; std::string name; // Name of play uint32_t timeRecorded; // Posix Time. uint32_t tickStart; // First tick of replay. uint32_t tickEnd; // Last tick of replay. std::multiset commands; }; class ReplayManager final : public IReplayManager { enum class ReplayMode { NONE = 0, RECORDING, PLAYING, }; public: virtual ~ReplayManager() { } virtual bool IsReplaying() const override { return _mode == ReplayMode::PLAYING; } virtual bool IsRecording() const override { return _mode == ReplayMode::RECORDING; } virtual void AddGameCommand( uint32_t tick, uint32_t eax, uint32_t ebx, uint32_t ecx, uint32_t edx, uint32_t esi, uint32_t edi, uint32_t ebp, uint8_t callback) override { if (_current == nullptr) return; uint32_t args[7]; args[0] = eax; args[1] = ebx; args[2] = ecx; args[3] = edx; args[4] = esi; args[5] = edi; args[6] = ebp; _current->commands.emplace(gCurrentTicks, args, callback, _commandId++); } virtual void AddGameAction(uint32_t tick, const GameAction* action) override { if (_current == nullptr) return; MemoryStream stream; DataSerialiser dsOut(true, stream); action->Serialise(dsOut); std::unique_ptr ga = GameActions::Create(action->GetType()); ga->SetCallback(action->GetCallback()); stream.SetPosition(0); DataSerialiser dsIn(false, stream); ga->Serialise(dsIn); _current->commands.emplace(gCurrentTicks, std::move(ga), _commandId++); } virtual void Update() override { if (_mode == ReplayMode::NONE) return; if (_mode == ReplayMode::RECORDING) { if (gCurrentTicks >= _current->tickEnd) { StopRecording(); return; } } else if (_mode == ReplayMode::PLAYING) { ReplayCommands(); } } virtual bool StartRecording(const std::string& name, uint32_t maxTicks /*= 0xFFFFFFFF*/) override { if (_mode != ReplayMode::NONE) return false; auto replayData = std::make_unique(); replayData->name = name; replayData->tickStart = gCurrentTicks; if (maxTicks != 0xFFFFFFFF) replayData->tickEnd = gCurrentTicks + maxTicks; else replayData->tickEnd = 0xFFFFFFFF; auto context = GetContext(); auto& objManager = context->GetObjectManager(); auto objects = objManager.GetPackableObjects(); auto s6exporter = std::make_unique(); s6exporter->ExportObjectsList = objects; s6exporter->Export(); s6exporter->SaveGame(&replayData->parkData); _mode = ReplayMode::RECORDING; _current = std::move(replayData); return true; } virtual bool StopRecording() override { if (_mode != ReplayMode::RECORDING) return false; DataSerialiser serialiser(true); Serialise(serialiser, *_current); char replayName[512] = {}; snprintf(replayName, sizeof(replayName), "replay_%s_%d.sv6r", _current->name.c_str(), 0); std::string outPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(DIRBASE::USER, DIRID::REPLAY); std::string outFile = Path::Combine(outPath, replayName); FILE* fp = fopen(outFile.c_str(), "wb"); if (fp) { auto& stream = serialiser.GetStream(); fwrite(stream.GetData(), 1, stream.GetLength(), fp); fclose(fp); } //_current.reset(); _mode = ReplayMode::NONE; return true; } virtual bool StartPlayback(const std::string& file) override { auto replayData = std::make_unique(); if (!ReadReplayData(file, *replayData)) { return false; } if (!LoadReplayDataMap(*replayData)) { return false; } gCurrentTicks = replayData->tickStart; _current = std::move(replayData); _mode = ReplayMode::PLAYING; return true; } virtual bool StopPlayback() override { if (_mode != ReplayMode::PLAYING) return false; _current.reset(); _mode = ReplayMode::NONE; return true; } private: bool LoadReplayDataMap(ReplayRecordData& data) { try { data.parkData.SetPosition(0); auto context = GetContext(); auto& objManager = context->GetObjectManager(); auto importer = ParkImporter::CreateS6(context->GetObjectRepository()); auto loadResult = importer->LoadFromStream(&data.parkData, false); objManager.LoadObjects(loadResult.RequiredObjects.data(), loadResult.RequiredObjects.size()); importer->Import(); sprite_position_tween_reset(); game_load_init(); fix_invalid_vehicle_sprite_sizes(); } catch (const std::exception&) { return false; } return true; } bool ReadReplayFromFile(const std::string& file, MemoryStream& stream) { FILE* fp = fopen(file.c_str(), "rb"); if (!fp) return false; char buffer[128]; while (feof(fp) == false) { size_t read = fread(buffer, 1, 128, fp); if (read == 0) break; stream.Write(buffer, read); } fclose(fp); return true; } bool ReadReplayData(const std::string& file, ReplayRecordData& data) { MemoryStream stream; DataSerialiser serialiser(false, stream); std::string fileName = file; if (fileName.size() < 5 || fileName.substr(fileName.size() - 5) != ".sv6r") { fileName += ".sv6r"; } std::string outPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(DIRBASE::USER, DIRID::REPLAY); std::string outFile = Path::Combine(outPath, file); bool loaded = false; if (ReadReplayFromFile(outFile, stream)) { loaded = true; } else if (ReadReplayFromFile(file, stream)) { loaded = true; } if (!loaded) return false; stream.SetPosition(0); if (!Serialise(serialiser, data)) { return false; } return true; } bool SerialiseCommand(DataSerialiser& serialiser, ReplayCommand& command) { serialiser << command.tick; serialiser << command.commandIndex; bool isGameAction = false; if (serialiser.IsSaving()) { isGameAction = command.action != nullptr; } serialiser << isGameAction; if (isGameAction) { uint32_t actionType = 0; if (serialiser.IsSaving()) { actionType = command.action->GetType(); } serialiser << actionType; if (serialiser.IsLoading()) { command.action = GameActions::Create(actionType); Guard::Assert(command.action != nullptr); } command.action->Serialise(serialiser); } else { serialiser << command.eax; serialiser << command.ebx; serialiser << command.ecx; serialiser << command.edx; serialiser << command.esi; serialiser << command.edi; serialiser << command.ebp; serialiser << command.callback; } return true; } bool Serialise(DataSerialiser& serialiser, ReplayRecordData& data) { serialiser << data.name; serialiser << data.parkData; serialiser << data.tickStart; serialiser << data.tickEnd; uint32_t countCommands = (uint32_t)data.commands.size(); serialiser << countCommands; if (serialiser.IsSaving()) { for (auto& command : data.commands) { SerialiseCommand(serialiser, const_cast(command)); } } else { for (uint32_t i = 0; i < countCommands; i++) { ReplayCommand command = {}; SerialiseCommand(serialiser, command); data.commands.emplace(std::move(command)); } } return true; } void ReplayCommands() { auto& replayQueue = _current->commands; // If we run out of commands we can stop the replay. if (replayQueue.empty()) { StopPlayback(); return; } while (replayQueue.begin() != replayQueue.end()) { const ReplayCommand& command = (*replayQueue.begin()); if (command.tick != gCurrentTicks) break; if (command.action != nullptr) { GameAction* action = command.action.get(); action->SetFlags(action->GetFlags() | GAME_COMMAND_FLAG_REPLAY); Guard::Assert(action != nullptr); GameActionResult::Ptr result = GameActions::Execute(action); } else { uint32_t flags = command.ebx | GAME_COMMAND_FLAG_REPLAY; game_do_command(command.eax, flags, command.ecx, command.edx, command.esi, command.edi, command.ebp); } replayQueue.erase(replayQueue.begin()); } } private: ReplayMode _mode = ReplayMode::NONE; std::unique_ptr _current; uint32_t _commandId = 0; }; std::unique_ptr CreateReplayManager() { return std::make_unique(); } } // namespace OpenRCT2