From a6efef1e8136b9c42aea563055b5157166ce2430 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 6 Dec 2018 06:34:27 +0100 Subject: [PATCH] Add support to record and replay game commands/actions. --- src/openrct2/Context.cpp | 9 + src/openrct2/Context.h | 3 + src/openrct2/Game.cpp | 11 + src/openrct2/GameState.cpp | 3 + src/openrct2/PlatformEnvironment.cpp | 1 + src/openrct2/PlatformEnvironment.h | 1 + src/openrct2/ReplayManager.cpp | 457 ++++++++++++++++++ src/openrct2/ReplayManager.h | 46 ++ src/openrct2/actions/GameAction.cpp | 13 +- src/openrct2/core/DataSerialiser.h | 8 +- src/openrct2/core/DataSerialiserTraits.h | 27 ++ src/openrct2/interface/InteractiveConsole.cpp | 110 +++++ 12 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/openrct2/ReplayManager.cpp create mode 100644 src/openrct2/ReplayManager.h diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 5908c3b2e7..6e7a92fd44 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -21,6 +21,7 @@ #include "OpenRCT2.h" #include "ParkImporter.h" #include "PlatformEnvironment.h" +#include "ReplayManager.h" #include "Version.h" #include "audio/AudioContext.h" #include "audio/audio.h" @@ -90,6 +91,7 @@ namespace OpenRCT2 std::unique_ptr _objectManager; std::unique_ptr _trackDesignRepository; std::unique_ptr _scenarioRepository; + std::unique_ptr _replayManager; #ifdef __ENABLE_DISCORD__ std::unique_ptr _discordService; #endif @@ -200,6 +202,11 @@ namespace OpenRCT2 return _scenarioRepository.get(); } + IReplayManager* GetReplayManager() override + { + return _replayManager.get(); + } + int32_t GetDrawingEngineType() override { return _drawingEngineType; @@ -323,6 +330,7 @@ namespace OpenRCT2 _objectManager = CreateObjectManager(*_objectRepository); _trackDesignRepository = CreateTrackDesignRepository(_env); _scenarioRepository = CreateScenarioRepository(_env); + _replayManager = CreateReplayManager(); #ifdef __ENABLE_DISCORD__ _discordService = std::make_unique(); #endif @@ -977,6 +985,7 @@ namespace OpenRCT2 DIRID::HEIGHTMAP, DIRID::THEME, DIRID::SEQUENCE, + DIRID::REPLAY, }); } diff --git a/src/openrct2/Context.h b/src/openrct2/Context.h index ea76cbed4d..ba940adc04 100644 --- a/src/openrct2/Context.h +++ b/src/openrct2/Context.h @@ -64,7 +64,9 @@ enum namespace OpenRCT2 { class GameState; + interface IPlatformEnvironment; + interface IReplayManager; namespace Audio { @@ -102,6 +104,7 @@ namespace OpenRCT2 virtual IObjectRepository& GetObjectRepository() abstract; virtual ITrackDesignRepository* GetTrackDesignRepository() abstract; virtual IScenarioRepository* GetScenarioRepository() abstract; + virtual IReplayManager* GetReplayManager() abstract; virtual int32_t GetDrawingEngineType() abstract; virtual Drawing::IDrawingEngine* GetDrawingEngine() abstract; diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp index 0f30d5419f..4c9b94614e 100644 --- a/src/openrct2/Game.cpp +++ b/src/openrct2/Game.cpp @@ -16,6 +16,7 @@ #include "Input.h" #include "OpenRCT2.h" #include "ParkImporter.h" +#include "ReplayManager.h" #include "audio/audio.h" #include "config/Config.h" #include "core/FileScanner.h" @@ -472,6 +473,16 @@ int32_t game_do_command_p( // Second call to actually perform the operation new_game_command_table[command](eax, ebx, ecx, edx, esi, edi, ebp); + auto* replayManager = GetContext()->GetReplayManager(); + if (replayManager != nullptr && replayManager->IsRecording() && (flags & GAME_COMMAND_FLAG_APPLY) + && (flags & GAME_COMMAND_FLAG_GHOST) == 0 && (flags & GAME_COMMAND_FLAG_5) == 0) + { + int32_t callback = game_command_callback_get_index(game_command_callback); + + replayManager->AddGameCommand( + gCurrentTicks, *eax, original_ebx, *ecx, original_edx, original_esi, original_edi, original_ebp, callback); + } + // Do the callback (required for multiplayer to work correctly), but only for top level commands if (gGameCommandNestLevel == 1) { diff --git a/src/openrct2/GameState.cpp b/src/openrct2/GameState.cpp index ba717c346e..b9be0b3eec 100644 --- a/src/openrct2/GameState.cpp +++ b/src/openrct2/GameState.cpp @@ -14,6 +14,7 @@ #include "Game.h" #include "Input.h" #include "OpenRCT2.h" +#include "ReplayManager.h" #include "interface/Screenshot.h" #include "localisation/Date.h" #include "localisation/Localisation.h" @@ -218,6 +219,8 @@ void GameState::UpdateLogic() network_update(); + GetContext()->GetReplayManager()->Update(); + if (network_get_mode() == NETWORK_MODE_CLIENT && network_get_status() == NETWORK_STATUS_CONNECTED && network_get_authstatus() == NETWORK_AUTH_OK) { diff --git a/src/openrct2/PlatformEnvironment.cpp b/src/openrct2/PlatformEnvironment.cpp index 4d042e428e..f595fd4a18 100644 --- a/src/openrct2/PlatformEnvironment.cpp +++ b/src/openrct2/PlatformEnvironment.cpp @@ -220,6 +220,7 @@ const char * PlatformEnvironment::DirectoryNamesOpenRCT2[] = "themes", // THEME "track", // TRACK "heightmap", // HEIGHTMAP + "replay", // REPLAY }; const char * PlatformEnvironment::FileNames[] = diff --git a/src/openrct2/PlatformEnvironment.h b/src/openrct2/PlatformEnvironment.h index 3dd3c8775c..30747c9e32 100644 --- a/src/openrct2/PlatformEnvironment.h +++ b/src/openrct2/PlatformEnvironment.h @@ -46,6 +46,7 @@ namespace OpenRCT2 THEME, // Contains interface themes. TRACK, // Contains track designs. HEIGHTMAP, // Contains heightmap data. + REPLAY, // Contains recorded replays. }; enum class PATHID diff --git a/src/openrct2/ReplayManager.cpp b/src/openrct2/ReplayManager.cpp new file mode 100644 index 0000000000..884b044917 --- /dev/null +++ b/src/openrct2/ReplayManager.cpp @@ -0,0 +1,457 @@ +/***************************************************************************** + * 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 "PlatformEnvironment.h" +#include "actions/GameAction.h" +#include "core/DataSerialiser.h" +#include "core/Path.hpp" +#include "object/ObjectManager.h" +#include "object/ObjectRepository.h" +#include "openrct2/ParkImporter.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 : 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 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) + { + serialiser << command.tick; + serialiser << command.commandIndex; + if (command.action != nullptr) + { + bool isGameAction = true; + serialiser << isGameAction; + + uint32_t actionType = command.action->GetType(); + serialiser << actionType; + command.action->Serialise(serialiser); + } + else + { + bool isGameAction = false; + serialiser << isGameAction; + + serialiser << command.eax; + serialiser << command.ebx; + serialiser << command.ecx; + serialiser << command.edx; + serialiser << command.esi; + serialiser << command.edi; + serialiser << command.ebp; + serialiser << command.callback; + } + } + } + else + { + for (uint32_t i = 0; i < countCommands; i++) + { + ReplayCommand command = {}; + serialiser << command.tick; + serialiser << command.commandIndex; + + bool isGameAction = false; + serialiser << isGameAction; + + if (isGameAction) + { + uint32_t actionType = 0; + serialiser << actionType; + + 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; + } + + data.commands.emplace(std::move(command)); + } + } + + return true; + } + + void ReplayCommands() + { + auto& replayQueue = _current->commands; + + 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()); + + Guard::Assert(action != nullptr); + + GameActionResult::Ptr result = GameActions::Execute(action); + } + else + { + game_do_command(command.eax, command.ebx, 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 diff --git a/src/openrct2/ReplayManager.h b/src/openrct2/ReplayManager.h new file mode 100644 index 0000000000..8035a6a3ad --- /dev/null +++ b/src/openrct2/ReplayManager.h @@ -0,0 +1,46 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +#pragma once + +#include "common.h" + +#include +#include +#include + +struct GameAction; + +namespace OpenRCT2 +{ + interface IReplayManager + { + public: + virtual void Update() = 0; + virtual bool IsReplaying() const = 0; + + virtual bool IsRecording() const = 0; + + // NOTE: Will become obsolete eventually once all game actions are done. + 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) + = 0; + virtual void AddGameAction(uint32_t tick, const GameAction* action) = 0; + + virtual bool StartRecording(const std::string& name, uint32_t maxTicks = 0xFFFFFFFF) = 0; + virtual bool StopRecording() = 0; + + virtual bool StartPlayback(const std::string& file) = 0; + virtual bool StopPlayback() = 0; + }; + + std::unique_ptr CreateReplayManager(); + +} // namespace OpenRCT2 diff --git a/src/openrct2/actions/GameAction.cpp b/src/openrct2/actions/GameAction.cpp index 628d4d5524..b6ed69acc1 100644 --- a/src/openrct2/actions/GameAction.cpp +++ b/src/openrct2/actions/GameAction.cpp @@ -10,6 +10,7 @@ #include "GameAction.h" #include "../Context.h" +#include "../ReplayManager.h" #include "../core/Guard.hpp" #include "../core/Memory.hpp" #include "../core/MemoryStream.h" @@ -247,9 +248,9 @@ namespace GameActions money_effect_create(result->Cost); } - if (!(actionFlags & GA_FLAGS::CLIENT_ONLY)) + if (!(actionFlags & GA_FLAGS::CLIENT_ONLY) && result->Error == GA_ERROR::OK) { - if (network_get_mode() == NETWORK_MODE_SERVER && result->Error == GA_ERROR::OK) + if (network_get_mode() == NETWORK_MODE_SERVER) { NetworkPlayerId_t playerId = action->GetPlayer(); @@ -262,6 +263,14 @@ namespace GameActions network_add_player_money_spent(playerIndex, result->Cost); } } + else if (network_get_mode() == NETWORK_MODE_NONE) + { + auto* replayManager = OpenRCT2::GetContext()->GetReplayManager(); + if (replayManager != nullptr && replayManager->IsRecording()) + { + replayManager->AddGameAction(gCurrentTicks, action); + } + } } // Allow autosave to commence diff --git a/src/openrct2/core/DataSerialiser.h b/src/openrct2/core/DataSerialiser.h index 569a88ff6c..dcae7c2331 100644 --- a/src/openrct2/core/DataSerialiser.h +++ b/src/openrct2/core/DataSerialiser.h @@ -51,18 +51,18 @@ public: return _stream; } - template DataSerialiser& operator<<(T& data) + template DataSerialiser& operator<<(const T& data) { if (!_isLogging) { if (_isSaving) - DataSerializerTraits::encode(_activeStream, data); + DataSerializerTraits::encode(_activeStream, const_cast(data)); else - DataSerializerTraits::decode(_activeStream, data); + DataSerializerTraits::decode(_activeStream, const_cast(data)); } else { - DataSerializerTraits::log(_activeStream, data); + DataSerializerTraits::log(_activeStream, const_cast(data)); } return *this; diff --git a/src/openrct2/core/DataSerialiserTraits.h b/src/openrct2/core/DataSerialiserTraits.h index be82afd5d9..019c43bc63 100644 --- a/src/openrct2/core/DataSerialiserTraits.h +++ b/src/openrct2/core/DataSerialiserTraits.h @@ -9,6 +9,7 @@ #pragma once +#include "../core/MemoryStream.h" #include "../localisation/Localisation.h" #include "../network/NetworkTypes.h" #include "../network/network.h" @@ -219,3 +220,29 @@ template struct DataSerializerTraits> stream->Write("; ", 2); } }; + +template<> struct DataSerializerTraits +{ + static void encode(IStream* stream, const MemoryStream& val) + { + DataSerializerTraits s; + s.encode(stream, val.GetLength()); + + stream->Write(val.GetData(), val.GetLength()); + } + static void decode(IStream* stream, MemoryStream& val) + { + DataSerializerTraits s; + + uint32_t length = 0; + s.decode(stream, length); + + std::unique_ptr buf(new uint8_t[length]); + stream->Read(buf.get(), length); + + val.Write(buf.get(), length); + } + static void log(IStream* stream, MemoryStream& tag) + { + } +}; diff --git a/src/openrct2/interface/InteractiveConsole.cpp b/src/openrct2/interface/InteractiveConsole.cpp index c42d18b37b..9b142a76c1 100644 --- a/src/openrct2/interface/InteractiveConsole.cpp +++ b/src/openrct2/interface/InteractiveConsole.cpp @@ -13,6 +13,7 @@ #include "../EditorObjectSelectionSession.h" #include "../Game.h" #include "../OpenRCT2.h" +#include "../ReplayManager.h" #include "../Version.h" #include "../actions/ClimateSetAction.hpp" #include "../config/Config.h" @@ -1332,6 +1333,111 @@ static int32_t cc_say(InteractiveConsole& console, const utf8** argv, int32_t ar } } +static int32_t cc_replay_startrecord(InteractiveConsole& console, const utf8** argv, int32_t argc) +{ + if (network_get_mode() != NETWORK_MODE_NONE) + { + console.WriteFormatLine("This command is currently not supported in multiplayer mode."); + return 0; + } + + if (argc < 1) + { + console.WriteFormatLine("Parameters required []"); + return 0; + } + + std::string name = argv[0]; + uint32_t maxTicks = 0xFFFFFFFF; + if (argc >= 2) + { + maxTicks = atol(argv[1]); + } + + auto* replayManager = OpenRCT2::GetContext()->GetReplayManager(); + if (replayManager != nullptr) + { + if (replayManager->StartRecording(name, maxTicks)) + { + console.WriteFormatLine("Replay recording start"); + return 1; + } + } + + return 0; +} + +static int32_t cc_replay_stoprecord(InteractiveConsole& console, const utf8** argv, int32_t argc) +{ + if (network_get_mode() != NETWORK_MODE_NONE) + { + console.WriteFormatLine("This command is currently not supported in multiplayer mode."); + return 0; + } + + auto* replayManager = OpenRCT2::GetContext()->GetReplayManager(); + if (replayManager != nullptr) + { + if (replayManager->StopRecording()) + { + console.WriteFormatLine("Replay recording stopped"); + return 1; + } + } + + return 0; +} + +static int32_t cc_replay_start(InteractiveConsole& console, const utf8** argv, int32_t argc) +{ + if (network_get_mode() != NETWORK_MODE_NONE) + { + console.WriteFormatLine("This command is currently not supported in multiplayer mode."); + return 0; + } + + if (argc < 1) + { + console.WriteFormatLine("Parameters required "); + return 0; + } + + std::string name = argv[0]; + + auto* replayManager = OpenRCT2::GetContext()->GetReplayManager(); + if (replayManager != nullptr) + { + if (replayManager->StartPlayback(name)) + { + console.WriteFormatLine("Started replay"); + return 1; + } + } + + return 0; +} + +static int32_t cc_replay_stop(InteractiveConsole& console, const utf8** argv, int32_t argc) +{ + if (network_get_mode() != NETWORK_MODE_NONE) + { + console.WriteFormatLine("This command is currently not supported in multiplayer mode."); + return 0; + } + + auto* replayManager = OpenRCT2::GetContext()->GetReplayManager(); + if (replayManager != nullptr) + { + if (replayManager->StopPlayback()) + { + console.WriteFormatLine("Stopped replay"); + return 1; + } + } + + return 0; +} + #pragma warning(push) #pragma warning(disable : 4702) // unreachable code static int32_t cc_abort( @@ -1451,6 +1557,10 @@ static constexpr const console_command console_command_table[] = { { "twitch", cc_twitch, "Twitch API", "twitch" }, { "variables", cc_variables, "Lists all the variables that can be used with get and sometimes set.", "variables" }, { "windows", cc_windows, "Lists all the windows that can be opened.", "windows" }, + { "replay_startrecord", cc_replay_startrecord, "Starts recording a new replay.", "replay_startrecord [max_ticks]"}, + { "replay_stoprecord", cc_replay_stoprecord, "Stops recording a new replay.", "replay_stoprecord"}, + { "replay_start", cc_replay_start, "Starts a replay", "replay_start "}, + { "replay_stop", cc_replay_stop, "Stops the replay", "replay_stop"}, }; // clang-format on