mirror of https://github.com/OpenRCT2/OpenRCT2.git
Merge pull request #8374 from ZehMatt/replay-feature
Add support to record and replay game commands/actions.
This commit is contained in:
commit
a065806b20
|
@ -22,6 +22,7 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
4C29DEB3218C6AE500E8707F /* RCT12.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C29DEB2218C6AE500E8707F /* RCT12.cpp */; };
|
||||
4C358E5221C445F700ADE6BC /* ReplayManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C358E5021C445F700ADE6BC /* ReplayManager.cpp */; };
|
||||
4C3B4236205914F7000C5BB7 /* InGameConsole.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C3B4234205914F7000C5BB7 /* InGameConsole.cpp */; };
|
||||
4C93F1AD1F8CD9F000A9330D /* Input.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C93F1AC1F8CD9F000A9330D /* Input.cpp */; };
|
||||
4C93F1AF1F8CD9F600A9330D /* KeyboardShortcut.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4C93F1AE1F8CD9F600A9330D /* KeyboardShortcut.cpp */; };
|
||||
|
@ -613,6 +614,8 @@
|
|||
4C04D69F2056AA9600F82EBA /* linenoise.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = linenoise.hpp; sourceTree = "<group>"; };
|
||||
4C1A53EC205FD19F000F8EF5 /* SceneryObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SceneryObject.cpp; sourceTree = "<group>"; };
|
||||
4C29DEB2218C6AE500E8707F /* RCT12.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RCT12.cpp; sourceTree = "<group>"; };
|
||||
4C358E5021C445F700ADE6BC /* ReplayManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ReplayManager.cpp; sourceTree = "<group>"; };
|
||||
4C358E5121C445F700ADE6BC /* ReplayManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReplayManager.h; sourceTree = "<group>"; };
|
||||
4C3B4234205914F7000C5BB7 /* InGameConsole.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InGameConsole.cpp; sourceTree = "<group>"; };
|
||||
4C3B4235205914F7000C5BB7 /* InGameConsole.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InGameConsole.h; sourceTree = "<group>"; };
|
||||
4C3B423720591513000C5BB7 /* StdInOutConsole.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StdInOutConsole.cpp; sourceTree = "<group>"; };
|
||||
|
@ -2321,6 +2324,8 @@
|
|||
F76C83551EC4E7CC00FA49E2 /* libopenrct2 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C358E5021C445F700ADE6BC /* ReplayManager.cpp */,
|
||||
4C358E5121C445F700ADE6BC /* ReplayManager.h */,
|
||||
C6352B871F477032006CCEE3 /* actions */,
|
||||
F76C83561EC4E7CC00FA49E2 /* audio */,
|
||||
F76C83621EC4E7CC00FA49E2 /* cmdline */,
|
||||
|
@ -3709,6 +3714,7 @@
|
|||
9308D9FE209908090079EE96 /* TileElement.cpp in Sources */,
|
||||
F76C888D1EC5324E00FA49E2 /* UiContext.Linux.cpp in Sources */,
|
||||
9346F9D8208A191900C77D91 /* Guest.cpp in Sources */,
|
||||
4C358E5221C445F700ADE6BC /* ReplayManager.cpp in Sources */,
|
||||
F76C888E1EC5324E00FA49E2 /* UiContext.Win32.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
- Feature: [#8190] Allow building footpaths on 'corner down' terrain.
|
||||
- Feature: [#8191] Allow building on-ride photos and water S-bends on the Water Coaster.
|
||||
- Feature: [#8259] Add say command to in-game console.
|
||||
- Feature: [#8374] Add replay system.
|
||||
- Change: [#7961] Add new object types: station, terrain surface, and terrain edge.
|
||||
- Change: [#8222] The climate setting has been moved from objective options to scenario options.
|
||||
- Fix: [#3832] Changing the colour scheme of track pieces does not work in multiplayer.
|
||||
|
|
|
@ -2478,7 +2478,7 @@ static void window_ride_main_textinput(rct_window* w, rct_widgetindex widgetInde
|
|||
if (widgetIndex != WIDX_RENAME || text == nullptr)
|
||||
return;
|
||||
|
||||
ride_set_name(w->number, text);
|
||||
ride_set_name(w->number, text, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -283,7 +283,8 @@ static void window_track_place_toolupdate(rct_window* w, rct_widgetindex widgetI
|
|||
for (int32_t i = 0; i < 7; i++)
|
||||
{
|
||||
uint8_t rideIndex;
|
||||
window_track_place_attempt_placement(_trackDesign, mapX, mapY, mapZ, 105, &cost, &rideIndex);
|
||||
uint16_t flags = GAME_COMMAND_FLAG_APPLY | GAME_COMMAND_FLAG_5 | GAME_COMMAND_FLAG_GHOST;
|
||||
window_track_place_attempt_placement(_trackDesign, mapX, mapY, mapZ, flags, &cost, &rideIndex);
|
||||
if (cost != MONEY32_UNDEFINED)
|
||||
{
|
||||
_window_track_place_ride_index = rideIndex;
|
||||
|
|
|
@ -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<IObjectManager> _objectManager;
|
||||
std::unique_ptr<ITrackDesignRepository> _trackDesignRepository;
|
||||
std::unique_ptr<IScenarioRepository> _scenarioRepository;
|
||||
std::unique_ptr<IReplayManager> _replayManager;
|
||||
#ifdef __ENABLE_DISCORD__
|
||||
std::unique_ptr<DiscordService> _discordService;
|
||||
#endif
|
||||
|
@ -203,6 +205,11 @@ namespace OpenRCT2
|
|||
return _scenarioRepository.get();
|
||||
}
|
||||
|
||||
IReplayManager* GetReplayManager() override
|
||||
{
|
||||
return _replayManager.get();
|
||||
}
|
||||
|
||||
int32_t GetDrawingEngineType() override
|
||||
{
|
||||
return _drawingEngineType;
|
||||
|
@ -326,6 +333,7 @@ namespace OpenRCT2
|
|||
_objectManager = CreateObjectManager(*_objectRepository);
|
||||
_trackDesignRepository = CreateTrackDesignRepository(_env);
|
||||
_scenarioRepository = CreateScenarioRepository(_env);
|
||||
_replayManager = CreateReplayManager();
|
||||
#ifdef __ENABLE_DISCORD__
|
||||
_discordService = std::make_unique<DiscordService>();
|
||||
#endif
|
||||
|
@ -980,6 +988,7 @@ namespace OpenRCT2
|
|||
DIRID::HEIGHTMAP,
|
||||
DIRID::THEME,
|
||||
DIRID::SEQUENCE,
|
||||
DIRID::REPLAY,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -387,6 +388,18 @@ int32_t game_do_command_p(
|
|||
|
||||
flags = *ebx;
|
||||
|
||||
auto* replayManager = GetContext()->GetReplayManager();
|
||||
if (replayManager->IsReplaying())
|
||||
{
|
||||
// We only accept replay commands as long the replay is active.
|
||||
if ((flags & GAME_COMMAND_FLAG_REPLAY) == 0)
|
||||
{
|
||||
// TODO: Introduce proper error.
|
||||
gGameCommandErrorText = STR_CHEAT_BUILD_IN_PAUSE_MODE;
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
if (gGameCommandNestLevel == 0)
|
||||
{
|
||||
gGameCommandErrorText = STR_NONE;
|
||||
|
@ -422,6 +435,11 @@ int32_t game_do_command_p(
|
|||
|
||||
*ebx &= ~GAME_COMMAND_FLAG_APPLY;
|
||||
|
||||
// Make sure the camera position won't change if the command skips setting them.
|
||||
gCommandPosition.x = LOCATION_NULL;
|
||||
gCommandPosition.y = LOCATION_NULL;
|
||||
gCommandPosition.z = LOCATION_NULL;
|
||||
|
||||
// First call for validity and price check
|
||||
new_game_command_table[command](eax, ebx, ecx, edx, esi, edi, ebp);
|
||||
cost = *ebx;
|
||||
|
@ -472,6 +490,27 @@ 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);
|
||||
|
||||
if (replayManager != nullptr)
|
||||
{
|
||||
bool recordCommand = false;
|
||||
bool commandExecutes = (flags & GAME_COMMAND_FLAG_APPLY) && (flags & GAME_COMMAND_FLAG_GHOST) == 0
|
||||
&& (flags & GAME_COMMAND_FLAG_5) == 0;
|
||||
|
||||
if (replayManager->IsRecording() && commandExecutes)
|
||||
recordCommand = true;
|
||||
else if (replayManager->IsNormalising() && commandExecutes && (flags & GAME_COMMAND_FLAG_REPLAY) != 0)
|
||||
recordCommand = true;
|
||||
|
||||
if (recordCommand && gGameCommandNestLevel == 1)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -532,8 +571,11 @@ int32_t game_do_command_p(
|
|||
|
||||
// Show error window
|
||||
if (gGameCommandNestLevel == 0 && (flags & GAME_COMMAND_FLAG_APPLY) && gUnk141F568 == gUnk13CA740
|
||||
&& !(flags & GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED) && !(flags & GAME_COMMAND_FLAG_NETWORKED))
|
||||
&& !(flags & GAME_COMMAND_FLAG_ALLOW_DURING_PAUSED) && !(flags & GAME_COMMAND_FLAG_NETWORKED)
|
||||
&& !(flags & GAME_COMMAND_FLAG_GHOST))
|
||||
{
|
||||
context_show_error(gGameCommandErrorTitle, gGameCommandErrorText);
|
||||
}
|
||||
|
||||
return MONEY32_UNDEFINED;
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@ enum : uint32_t
|
|||
GAME_COMMAND_FLAG_5 = (1 << 5),
|
||||
GAME_COMMAND_FLAG_GHOST = (1 << 6),
|
||||
GAME_COMMAND_FLAG_PATH_SCENERY = (1 << 7),
|
||||
GAME_COMMAND_FLAG_REPLAY = (1u << 30), // Command was issued from replay manager.
|
||||
GAME_COMMAND_FLAG_NETWORKED = (1u << 31) // Game command is coming from network
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -220,6 +220,7 @@ const char * PlatformEnvironment::DirectoryNamesOpenRCT2[] =
|
|||
"themes", // THEME
|
||||
"track", // TRACK
|
||||
"heightmap", // HEIGHTMAP
|
||||
"replay", // REPLAY
|
||||
};
|
||||
|
||||
const char * PlatformEnvironment::FileNames[] =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,853 @@
|
|||
/*****************************************************************************
|
||||
* 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 "config/Config.h"
|
||||
#include "core/DataSerialiser.h"
|
||||
#include "core/Path.hpp"
|
||||
#include "management/NewsItem.h"
|
||||
#include "object/ObjectManager.h"
|
||||
#include "object/ObjectRepository.h"
|
||||
#include "rct2/S6Exporter.h"
|
||||
#include "world/Park.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
namespace OpenRCT2
|
||||
{
|
||||
// NOTE: This is currently very close to what the network version uses.
|
||||
// Should be refactored once the old game commands are gone.
|
||||
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<GameAction>&& 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
|
||||
{
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
std::string networkId;
|
||||
MemoryStream parkData;
|
||||
MemoryStream spriteSpatialData;
|
||||
MemoryStream parkParams;
|
||||
std::string name; // Name of play
|
||||
std::string filePath; // File path of replay.
|
||||
uint64_t timeRecorded; // Posix Time.
|
||||
uint32_t tickStart; // First tick of replay.
|
||||
uint32_t tickEnd; // Last tick of replay.
|
||||
std::multiset<ReplayCommand> commands;
|
||||
std::vector<std::pair<uint32_t, rct_sprite_checksum>> checksums;
|
||||
uint32_t checksumIndex;
|
||||
};
|
||||
|
||||
class ReplayManager final : public IReplayManager
|
||||
{
|
||||
static constexpr uint16_t ReplayVersion = 1;
|
||||
static constexpr uint32_t ReplayMagic = 0x5243524F; // ORCR.
|
||||
|
||||
enum class ReplayMode
|
||||
{
|
||||
NONE = 0,
|
||||
RECORDING,
|
||||
PLAYING,
|
||||
NORMALISATION,
|
||||
};
|
||||
|
||||
public:
|
||||
virtual ~ReplayManager()
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool IsReplaying() const override
|
||||
{
|
||||
return _mode == ReplayMode::PLAYING;
|
||||
}
|
||||
|
||||
virtual bool IsRecording() const override
|
||||
{
|
||||
return _mode == ReplayMode::RECORDING;
|
||||
}
|
||||
|
||||
virtual bool IsNormalising() const override
|
||||
{
|
||||
return _mode == ReplayMode::NORMALISATION;
|
||||
}
|
||||
|
||||
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 (_currentRecording == 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;
|
||||
|
||||
_currentRecording->commands.emplace(gCurrentTicks, args, callback, _commandId++);
|
||||
|
||||
// Force a checksum record the next tick.
|
||||
_nextChecksumTick = tick + 1;
|
||||
}
|
||||
|
||||
virtual void AddGameAction(uint32_t tick, const GameAction* action) override
|
||||
{
|
||||
if (_currentRecording == nullptr)
|
||||
return;
|
||||
|
||||
auto ga = GameActions::Clone(action);
|
||||
|
||||
_currentRecording->commands.emplace(gCurrentTicks, std::move(ga), _commandId++);
|
||||
|
||||
// Force a checksum record the next tick.
|
||||
_nextChecksumTick = tick + 1;
|
||||
}
|
||||
|
||||
void AddChecksum(uint32_t tick, rct_sprite_checksum&& checksum)
|
||||
{
|
||||
_currentRecording->checksums.emplace_back(std::make_pair(tick, checksum));
|
||||
}
|
||||
|
||||
// Function runs each Tick.
|
||||
virtual void Update() override
|
||||
{
|
||||
if (_mode == ReplayMode::NONE)
|
||||
return;
|
||||
|
||||
if ((_mode == ReplayMode::RECORDING || _mode == ReplayMode::NORMALISATION) && gCurrentTicks == _nextChecksumTick)
|
||||
{
|
||||
rct_sprite_checksum checksum = sprite_checksum();
|
||||
AddChecksum(gCurrentTicks, std::move(checksum));
|
||||
|
||||
if (_mode == ReplayMode::RECORDING)
|
||||
{
|
||||
// Record checksums every ~200ms.
|
||||
_nextChecksumTick = gCurrentTicks + 5;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wait for next command.
|
||||
_nextChecksumTick = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_mode == ReplayMode::RECORDING)
|
||||
{
|
||||
if (gCurrentTicks >= _currentRecording->tickEnd)
|
||||
{
|
||||
StopRecording();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (_mode == ReplayMode::PLAYING)
|
||||
{
|
||||
#ifndef DISABLE_NETWORK
|
||||
// If the network is disabled we will only get a dummy hash which will cause
|
||||
// false positives during replay.
|
||||
CheckState();
|
||||
#endif
|
||||
ReplayCommands();
|
||||
|
||||
// Normal playback will always end at the specific tick.
|
||||
if (gCurrentTicks >= _currentReplay->tickEnd)
|
||||
{
|
||||
StopPlayback();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (_mode == ReplayMode::NORMALISATION)
|
||||
{
|
||||
ReplayCommands();
|
||||
|
||||
// If we run out of commands we can just stop
|
||||
if (_currentReplay->commands.empty() && _nextChecksumTick == 0)
|
||||
{
|
||||
StopPlayback();
|
||||
StopRecording();
|
||||
|
||||
// Reset mode, in normalisation nothing will set it.
|
||||
_mode = ReplayMode::NONE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool StartRecording(const std::string& name, uint32_t maxTicks /*= k_MaxReplayTicks*/) override
|
||||
{
|
||||
if (_mode != ReplayMode::NONE && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
auto replayData = std::make_unique<ReplayRecordData>();
|
||||
replayData->magic = ReplayMagic;
|
||||
replayData->version = ReplayVersion;
|
||||
replayData->networkId = network_get_version();
|
||||
replayData->name = name;
|
||||
replayData->tickStart = gCurrentTicks;
|
||||
if (maxTicks != k_MaxReplayTicks)
|
||||
replayData->tickEnd = gCurrentTicks + maxTicks;
|
||||
else
|
||||
replayData->tickEnd = k_MaxReplayTicks;
|
||||
|
||||
std::string replayName = String::StdFormat("%s.sv6r", name.c_str());
|
||||
std::string outPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(DIRBASE::USER, DIRID::REPLAY);
|
||||
replayData->filePath = Path::Combine(outPath, replayName);
|
||||
|
||||
auto context = GetContext();
|
||||
auto& objManager = context->GetObjectManager();
|
||||
auto objects = objManager.GetPackableObjects();
|
||||
|
||||
auto s6exporter = std::make_unique<S6Exporter>();
|
||||
s6exporter->ExportObjectsList = objects;
|
||||
s6exporter->Export();
|
||||
s6exporter->SaveGame(&replayData->parkData);
|
||||
|
||||
replayData->spriteSpatialData.Write(gSpriteSpatialIndex, sizeof(gSpriteSpatialIndex));
|
||||
replayData->timeRecorded = std::chrono::seconds(std::time(nullptr)).count();
|
||||
|
||||
DataSerialiser parkParams(true, replayData->parkParams);
|
||||
SerialiseParkParameters(parkParams);
|
||||
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::RECORDING;
|
||||
|
||||
_currentRecording = std::move(replayData);
|
||||
_nextChecksumTick = gCurrentTicks + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool StopRecording() override
|
||||
{
|
||||
if (_mode != ReplayMode::RECORDING && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
_currentRecording->tickEnd = gCurrentTicks;
|
||||
|
||||
// Serialise Body.
|
||||
DataSerialiser serialiser(true);
|
||||
Serialise(serialiser, *_currentRecording);
|
||||
|
||||
bool result = true;
|
||||
|
||||
const std::string& outFile = _currentRecording->filePath;
|
||||
|
||||
FILE* fp = fopen(outFile.c_str(), "wb");
|
||||
if (fp)
|
||||
{
|
||||
const auto& stream = serialiser.GetStream();
|
||||
fwrite(stream.GetData(), 1, stream.GetLength(), fp);
|
||||
|
||||
fclose(fp);
|
||||
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_error("Unable to write to file '%s'", outFile.c_str());
|
||||
result = false;
|
||||
}
|
||||
|
||||
// When normalizing the output we don't touch the mode.
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::NONE;
|
||||
|
||||
_currentRecording.reset();
|
||||
|
||||
NewsItem* news = news_item_add_to_queue_raw(NEWS_ITEM_BLANK, "Replay recording stopped", 0);
|
||||
news->Flags |= NEWS_FLAG_HAS_BUTTON; // Has no subject.
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual bool GetCurrentReplayInfo(ReplayRecordInfo& info) const override
|
||||
{
|
||||
ReplayRecordData* data = nullptr;
|
||||
|
||||
if (_mode == ReplayMode::PLAYING)
|
||||
data = _currentReplay.get();
|
||||
else if (_mode == ReplayMode::RECORDING)
|
||||
data = _currentRecording.get();
|
||||
else if (_mode == ReplayMode::NORMALISATION)
|
||||
data = _currentRecording.get();
|
||||
|
||||
if (data == nullptr)
|
||||
return false;
|
||||
|
||||
info.FilePath = data->filePath;
|
||||
info.Name = data->name;
|
||||
info.Version = data->version;
|
||||
info.TimeRecorded = data->timeRecorded;
|
||||
if (_mode == ReplayMode::RECORDING)
|
||||
info.Ticks = gCurrentTicks - data->tickStart;
|
||||
else if (_mode == ReplayMode::PLAYING)
|
||||
info.Ticks = data->tickEnd - data->tickStart;
|
||||
info.NumCommands = (uint32_t)data->commands.size();
|
||||
info.NumChecksums = (uint32_t)data->checksums.size();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool StartPlayback(const std::string& file) override
|
||||
{
|
||||
if (_mode != ReplayMode::NONE && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
auto replayData = std::make_unique<ReplayRecordData>();
|
||||
|
||||
if (!ReadReplayData(file, *replayData))
|
||||
{
|
||||
log_error("Unable to read replay data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TranslateDeprecatedGameCommands(*replayData))
|
||||
{
|
||||
log_error("Unable to translate deprecated game commands.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadReplayDataMap(*replayData))
|
||||
{
|
||||
log_error("Unable to load map.");
|
||||
return false;
|
||||
}
|
||||
|
||||
gCurrentTicks = replayData->tickStart;
|
||||
|
||||
_currentReplay = std::move(replayData);
|
||||
_currentReplay->checksumIndex = 0;
|
||||
_faultyChecksumIndex = -1;
|
||||
|
||||
// Make sure game is not paused.
|
||||
gGamePaused = 0;
|
||||
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::PLAYING;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool IsPlaybackStateMismatching() const override
|
||||
{
|
||||
if (_mode != ReplayMode::PLAYING)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _faultyChecksumIndex != -1;
|
||||
}
|
||||
|
||||
virtual bool StopPlayback() override
|
||||
{
|
||||
if (_mode != ReplayMode::PLAYING && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
// During normal playback we pause the game if stopped.
|
||||
if (_mode == ReplayMode::PLAYING)
|
||||
{
|
||||
NewsItem* news = news_item_add_to_queue_raw(NEWS_ITEM_BLANK, "Replay playback complete", 0);
|
||||
news->Flags |= NEWS_FLAG_HAS_BUTTON; // Has no subject.
|
||||
}
|
||||
|
||||
// When normalizing the output we don't touch the mode.
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
{
|
||||
_mode = ReplayMode::NONE;
|
||||
}
|
||||
|
||||
_currentReplay.reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool NormaliseReplay(const std::string& file, const std::string& outFile) override
|
||||
{
|
||||
_mode = ReplayMode::NORMALISATION;
|
||||
|
||||
if (StartPlayback(file) == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StartRecording(outFile, k_MaxReplayTicks) == false)
|
||||
{
|
||||
StopPlayback();
|
||||
return false;
|
||||
}
|
||||
|
||||
_nextReplayTick = gCurrentTicks + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ConvertDeprecatedGameCommand(const ReplayCommand& command, ReplayCommand& result)
|
||||
{
|
||||
// NOTE: If game actions are being ported it is required to implement temporarily
|
||||
// a mapping from game command to game action. This will allow the normalisation
|
||||
// stage to save a new replay file with the game action being used instead of the
|
||||
// old game command. Once normalised the code will be no longer required.
|
||||
|
||||
/* Example case
|
||||
case GAME_COMMAND_RAISE_WATER:
|
||||
{
|
||||
uint32_t param1 = command.ebp;
|
||||
uint32_t param2 = command.edi;
|
||||
result.action = std::make_unique<LandRaiseWaterAction>(param1, param2, ...);
|
||||
}
|
||||
*/
|
||||
|
||||
switch (command.esi)
|
||||
{
|
||||
case GAME_COMMAND_COUNT: // prevent default without case warning.
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("Deprecated game command requires replay translation.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TranslateDeprecatedGameCommands(ReplayRecordData& data)
|
||||
{
|
||||
for (auto it = data.commands.begin(); it != data.commands.end(); it++)
|
||||
{
|
||||
const ReplayCommand& replayCommand = *it;
|
||||
|
||||
if (replayCommand.action == nullptr)
|
||||
{
|
||||
// Check if we can create a game action with the command id.
|
||||
uint32_t commandId = replayCommand.esi;
|
||||
if (GameActions::IsValidId(commandId))
|
||||
{
|
||||
// Convert
|
||||
ReplayCommand converted;
|
||||
converted.commandIndex = replayCommand.commandIndex;
|
||||
|
||||
if (!ConvertDeprecatedGameCommand(replayCommand, converted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove deprecated command.
|
||||
data.commands.erase(it);
|
||||
|
||||
// Insert new game action, iterator points to the replaced element.
|
||||
it = data.commands.emplace(std::move(converted));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Guard::Assert(sizeof(gSpriteSpatialIndex) >= data.spriteSpatialData.GetLength());
|
||||
|
||||
// In case the sprite limit will be increased we keep the unused fields cleared.
|
||||
std::fill_n(gSpriteSpatialIndex, std::size(gSpriteSpatialIndex), SPRITE_INDEX_NULL);
|
||||
std::memcpy(gSpriteSpatialIndex, data.spriteSpatialData.GetData(), data.spriteSpatialData.GetLength());
|
||||
|
||||
// Load all map global variables.
|
||||
DataSerialiser parkParams(false, data.parkParams);
|
||||
SerialiseParkParameters(parkParams);
|
||||
|
||||
game_load_init();
|
||||
fix_invalid_vehicle_sprite_sizes();
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
log_error("Exception: %s", ex.what());
|
||||
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 numBytesRead = fread(buffer, 1, 128, fp);
|
||||
if (numBytesRead == 0)
|
||||
break;
|
||||
stream.Write(buffer, numBytesRead);
|
||||
}
|
||||
|
||||
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, fileName);
|
||||
|
||||
bool loaded = false;
|
||||
if (ReadReplayFromFile(outFile, stream))
|
||||
{
|
||||
data.filePath = outFile;
|
||||
loaded = true;
|
||||
}
|
||||
else if (ReadReplayFromFile(file, stream))
|
||||
{
|
||||
data.filePath = file;
|
||||
loaded = true;
|
||||
}
|
||||
if (!loaded)
|
||||
return false;
|
||||
|
||||
stream.SetPosition(0);
|
||||
|
||||
if (!Serialise(serialiser, data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset position of all streams.
|
||||
data.parkData.SetPosition(0);
|
||||
data.parkParams.SetPosition(0);
|
||||
data.spriteSpatialData.SetPosition(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SerialiseParkParameters(DataSerialiser& serialiser)
|
||||
{
|
||||
serialiser << _guestGenerationProbability;
|
||||
serialiser << _suggestedGuestMaximum;
|
||||
serialiser << gCheatsSandboxMode;
|
||||
serialiser << gCheatsDisableClearanceChecks;
|
||||
serialiser << gCheatsDisableSupportLimits;
|
||||
serialiser << gCheatsDisableTrainLengthLimit;
|
||||
serialiser << gCheatsEnableChainLiftOnAllTrack;
|
||||
serialiser << gCheatsShowAllOperatingModes;
|
||||
serialiser << gCheatsShowVehiclesFromOtherTrackTypes;
|
||||
serialiser << gCheatsFastLiftHill;
|
||||
serialiser << gCheatsDisableBrakesFailure;
|
||||
serialiser << gCheatsDisableAllBreakdowns;
|
||||
serialiser << gCheatsBuildInPauseMode;
|
||||
serialiser << gCheatsIgnoreRideIntensity;
|
||||
serialiser << gCheatsDisableVandalism;
|
||||
serialiser << gCheatsDisableLittering;
|
||||
serialiser << gCheatsNeverendingMarketing;
|
||||
serialiser << gCheatsFreezeWeather;
|
||||
serialiser << gCheatsDisablePlantAging;
|
||||
serialiser << gCheatsAllowArbitraryRideTypeChanges;
|
||||
serialiser << gCheatsDisableRideValueAging;
|
||||
serialiser << gConfigGeneral.show_real_names_of_guests;
|
||||
serialiser << gCheatsIgnoreResearchStatus;
|
||||
|
||||
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.magic;
|
||||
if (data.magic != ReplayMagic)
|
||||
{
|
||||
log_error("Magic does not match %08X, expected: %08X", data.magic, ReplayMagic);
|
||||
return false;
|
||||
}
|
||||
serialiser << data.version;
|
||||
if (data.version != ReplayVersion)
|
||||
{
|
||||
log_error("Invalid version detected %04X, expected: %04X", data.version, ReplayVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
serialiser << data.networkId;
|
||||
#ifndef DISABLE_NETWORK
|
||||
// NOTE: This does not mean the replay will not function, only a warning.
|
||||
if (data.networkId != network_get_version())
|
||||
{
|
||||
log_warning(
|
||||
"Replay network version mismatch: '%s', expected: '%s'", data.networkId.c_str(),
|
||||
network_get_version().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
serialiser << data.name;
|
||||
serialiser << data.timeRecorded;
|
||||
serialiser << data.parkData;
|
||||
serialiser << data.parkParams;
|
||||
serialiser << data.spriteSpatialData;
|
||||
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<ReplayCommand&>(command));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint32_t i = 0; i < countCommands; i++)
|
||||
{
|
||||
ReplayCommand command = {};
|
||||
SerialiseCommand(serialiser, command);
|
||||
|
||||
data.commands.emplace(std::move(command));
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t countChecksums = (uint32_t)data.checksums.size();
|
||||
serialiser << countChecksums;
|
||||
|
||||
if (serialiser.IsLoading())
|
||||
{
|
||||
data.checksums.resize(countChecksums);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < countChecksums; i++)
|
||||
{
|
||||
serialiser << data.checksums[i].first;
|
||||
serialiser << data.checksums[i].second.raw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifndef DISABLE_NETWORK
|
||||
void CheckState()
|
||||
{
|
||||
uint32_t checksumIndex = _currentReplay->checksumIndex;
|
||||
|
||||
if (checksumIndex >= _currentReplay->checksums.size())
|
||||
return;
|
||||
|
||||
const auto& savedChecksum = _currentReplay->checksums[checksumIndex];
|
||||
if (_currentReplay->checksums[checksumIndex].first == gCurrentTicks)
|
||||
{
|
||||
rct_sprite_checksum checksum = sprite_checksum();
|
||||
if (savedChecksum.second.raw != checksum.raw)
|
||||
{
|
||||
// Detected different game state.
|
||||
log_verbose(
|
||||
"Different sprite checksum at tick %u ; Saved: %s, Current: %s", gCurrentTicks,
|
||||
savedChecksum.second.ToString().c_str(), checksum.ToString().c_str());
|
||||
|
||||
_faultyChecksumIndex = checksumIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Good state.
|
||||
log_verbose(
|
||||
"Good state at tick %u ; Saved: %s, Current: %s", gCurrentTicks,
|
||||
savedChecksum.second.ToString().c_str(), checksum.ToString().c_str());
|
||||
}
|
||||
_currentReplay->checksumIndex++;
|
||||
}
|
||||
}
|
||||
#endif // DISABLE_NETWORK
|
||||
|
||||
void ReplayCommands()
|
||||
{
|
||||
auto& replayQueue = _currentReplay->commands;
|
||||
|
||||
while (replayQueue.begin() != replayQueue.end())
|
||||
{
|
||||
const ReplayCommand& command = (*replayQueue.begin());
|
||||
|
||||
if (_mode == ReplayMode::PLAYING)
|
||||
{
|
||||
// If this is a normal playback wait for the correct tick.
|
||||
if (command.tick != gCurrentTicks)
|
||||
break;
|
||||
}
|
||||
else if (_mode == ReplayMode::NORMALISATION)
|
||||
{
|
||||
// Allow one entry per tick.
|
||||
if (gCurrentTicks != _nextReplayTick)
|
||||
break;
|
||||
|
||||
_nextReplayTick = gCurrentTicks + 1;
|
||||
}
|
||||
|
||||
bool isPositionValid = false;
|
||||
|
||||
if (command.action != nullptr)
|
||||
{
|
||||
GameAction* action = command.action.get();
|
||||
action->SetFlags(action->GetFlags() | GAME_COMMAND_FLAG_REPLAY);
|
||||
|
||||
GameActionResult::Ptr result = GameActions::Execute(action);
|
||||
if (result->Error == GA_ERROR::OK)
|
||||
{
|
||||
isPositionValid = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t flags = command.ebx | GAME_COMMAND_FLAG_REPLAY;
|
||||
int32_t res = game_do_command(
|
||||
command.eax, flags, command.ecx, command.edx, command.esi, command.edi, command.ebp);
|
||||
if (res != MONEY32_UNDEFINED)
|
||||
{
|
||||
isPositionValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus camera on event.
|
||||
if (isPositionValid && gCommandPosition.x != LOCATION_NULL)
|
||||
{
|
||||
auto* mainWindow = window_get_main();
|
||||
if (mainWindow != nullptr)
|
||||
window_scroll_to_location(mainWindow, gCommandPosition.x, gCommandPosition.y, gCommandPosition.z);
|
||||
}
|
||||
|
||||
replayQueue.erase(replayQueue.begin());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
ReplayMode _mode = ReplayMode::NONE;
|
||||
std::unique_ptr<ReplayRecordData> _currentRecording;
|
||||
std::unique_ptr<ReplayRecordData> _currentReplay;
|
||||
int32_t _faultyChecksumIndex = -1;
|
||||
uint32_t _commandId = 0;
|
||||
uint32_t _nextChecksumTick = 0;
|
||||
uint32_t _nextReplayTick = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IReplayManager> CreateReplayManager()
|
||||
{
|
||||
return std::make_unique<ReplayManager>();
|
||||
}
|
||||
|
||||
} // namespace OpenRCT2
|
|
@ -0,0 +1,66 @@
|
|||
/*****************************************************************************
|
||||
* 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 <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
struct GameAction;
|
||||
|
||||
namespace OpenRCT2
|
||||
{
|
||||
static constexpr uint32_t k_MaxReplayTicks = 0xFFFFFFFF;
|
||||
|
||||
struct ReplayRecordInfo
|
||||
{
|
||||
uint16_t Version;
|
||||
uint32_t Ticks;
|
||||
uint64_t TimeRecorded;
|
||||
uint32_t NumCommands;
|
||||
uint32_t NumChecksums;
|
||||
std::string Name;
|
||||
std::string FilePath;
|
||||
};
|
||||
|
||||
interface IReplayManager
|
||||
{
|
||||
public:
|
||||
virtual ~IReplayManager() = default;
|
||||
|
||||
virtual void Update() = 0;
|
||||
|
||||
virtual bool IsReplaying() const = 0;
|
||||
virtual bool IsRecording() const = 0;
|
||||
virtual bool IsNormalising() 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 = k_MaxReplayTicks) = 0;
|
||||
virtual bool StopRecording() = 0;
|
||||
virtual bool GetCurrentReplayInfo(ReplayRecordInfo & info) const = 0;
|
||||
|
||||
virtual bool StartPlayback(const std::string& file) = 0;
|
||||
virtual bool IsPlaybackStateMismatching() const = 0;
|
||||
virtual bool StopPlayback() = 0;
|
||||
|
||||
virtual bool NormaliseReplay(const std::string& inputFile, const std::string& outputFile) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IReplayManager> CreateReplayManager();
|
||||
|
||||
} // namespace OpenRCT2
|
|
@ -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"
|
||||
|
@ -56,6 +57,15 @@ namespace GameActions
|
|||
return factory;
|
||||
}
|
||||
|
||||
bool IsValidId(uint32_t id)
|
||||
{
|
||||
if (id < std::size(_actions))
|
||||
{
|
||||
return _actions[id] != nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
static bool initialized = false;
|
||||
|
@ -84,6 +94,25 @@ namespace GameActions
|
|||
return std::unique_ptr<GameAction>(result);
|
||||
}
|
||||
|
||||
GameAction::Ptr Clone(const GameAction* action)
|
||||
{
|
||||
std::unique_ptr<GameAction> ga = GameActions::Create(action->GetType());
|
||||
ga->SetCallback(action->GetCallback());
|
||||
|
||||
// Serialise action data into stream.
|
||||
DataSerialiser dsOut(true);
|
||||
action->Serialise(dsOut);
|
||||
|
||||
// Serialise into new action.
|
||||
MemoryStream& stream = dsOut.GetStream();
|
||||
stream.SetPosition(0);
|
||||
|
||||
DataSerialiser dsIn(false, stream);
|
||||
ga->Serialise(dsIn);
|
||||
|
||||
return ga;
|
||||
}
|
||||
|
||||
static bool CheckActionInPausedMode(uint32_t actionFlags)
|
||||
{
|
||||
if (gGamePaused == 0)
|
||||
|
@ -199,6 +228,23 @@ namespace GameActions
|
|||
uint16_t actionFlags = action->GetActionFlags();
|
||||
uint32_t flags = action->GetFlags();
|
||||
|
||||
auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
|
||||
if (replayManager != nullptr && (replayManager->IsReplaying() || replayManager->IsNormalising()))
|
||||
{
|
||||
// We only accept replay commands as long the replay is active.
|
||||
if ((flags & GAME_COMMAND_FLAG_REPLAY) == 0)
|
||||
{
|
||||
// TODO: Introduce proper error.
|
||||
GameActionResult::Ptr result = std::make_unique<GameActionResult>();
|
||||
|
||||
result->Error = GA_ERROR::GAME_PAUSED;
|
||||
result->ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
|
||||
result->ErrorMessage = STR_CONSTRUCTION_NOT_POSSIBLE_WHILE_GAME_IS_PAUSED;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
GameActionResult::Ptr result = Query(action);
|
||||
if (result->Error == GA_ERROR::OK)
|
||||
{
|
||||
|
@ -247,9 +293,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 +308,23 @@ namespace GameActions
|
|||
network_add_player_money_spent(playerIndex, result->Cost);
|
||||
}
|
||||
}
|
||||
else if (network_get_mode() == NETWORK_MODE_NONE)
|
||||
{
|
||||
bool commandExecutes = (flags & GAME_COMMAND_FLAG_GHOST) == 0 && (flags & GAME_COMMAND_FLAG_5) == 0;
|
||||
|
||||
bool recordAction = false;
|
||||
if (replayManager)
|
||||
{
|
||||
if (replayManager->IsRecording() && commandExecutes)
|
||||
recordAction = true;
|
||||
else if (replayManager->IsNormalising() && (flags & GAME_COMMAND_FLAG_REPLAY) != 0)
|
||||
recordAction = true; // In normalisation we only feed back actions issued by the replay manager.
|
||||
}
|
||||
if (recordAction)
|
||||
{
|
||||
replayManager->AddGameAction(gCurrentTicks, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow autosave to commence
|
||||
|
|
|
@ -69,7 +69,7 @@ public:
|
|||
rct_string_id ErrorTitle = STR_NONE;
|
||||
rct_string_id ErrorMessage = STR_NONE;
|
||||
std::array<uint8_t, 12> ErrorMessageArgs;
|
||||
CoordsXYZ Position = {};
|
||||
CoordsXYZ Position = { LOCATION_NULL, LOCATION_NULL, LOCATION_NULL };
|
||||
money32 Cost = 0;
|
||||
uint16_t ExpenditureType = 0;
|
||||
|
||||
|
@ -241,7 +241,9 @@ namespace GameActions
|
|||
{
|
||||
void Initialize();
|
||||
void Register();
|
||||
bool IsValidId(uint32_t id);
|
||||
GameAction::Ptr Create(uint32_t id);
|
||||
GameAction::Ptr Clone(const GameAction* action);
|
||||
GameActionResult::Ptr Query(const GameAction* action);
|
||||
GameActionResult::Ptr Execute(const GameAction* action);
|
||||
GameActionFactory Register(uint32_t id, GameActionFactory action);
|
||||
|
|
|
@ -169,9 +169,10 @@ void game_command_set_ride_status(
|
|||
#pragma endregion
|
||||
|
||||
#pragma region RideSetNameAction
|
||||
void ride_set_name(int32_t rideIndex, const char* name)
|
||||
void ride_set_name(int32_t rideIndex, const char* name, uint32_t flags)
|
||||
{
|
||||
auto gameAction = RideSetNameAction(rideIndex, name);
|
||||
gameAction.SetFlags(flags);
|
||||
GameActions::Execute(&gameAction);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,14 +51,14 @@ public:
|
|||
return _stream;
|
||||
}
|
||||
|
||||
template<typename T> DataSerialiser& operator<<(T& data)
|
||||
template<typename T> DataSerialiser& operator<<(const T& data)
|
||||
{
|
||||
if (!_isLogging)
|
||||
{
|
||||
if (_isSaving)
|
||||
DataSerializerTraits<T>::encode(_activeStream, data);
|
||||
else
|
||||
DataSerializerTraits<T>::decode(_activeStream, data);
|
||||
DataSerializerTraits<T>::decode(_activeStream, const_cast<T&>(data));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "../core/MemoryStream.h"
|
||||
#include "../localisation/Localisation.h"
|
||||
#include "../network/NetworkTypes.h"
|
||||
#include "../network/network.h"
|
||||
|
@ -18,12 +19,16 @@
|
|||
#include "MemoryStream.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
template<typename T> struct DataSerializerTraits
|
||||
{
|
||||
static void encode(IStream* stream, const T& v) = delete;
|
||||
static void decode(IStream* stream, T& val) = delete;
|
||||
static void log(IStream* stream, T& val) = delete;
|
||||
static void log(IStream* stream, const T& val) = delete;
|
||||
};
|
||||
|
||||
template<typename T> struct DataSerializerTraitsIntegral
|
||||
|
@ -39,21 +44,13 @@ template<typename T> struct DataSerializerTraitsIntegral
|
|||
stream->Read(&temp);
|
||||
val = ByteSwapBE(temp);
|
||||
}
|
||||
static void log(IStream* stream, T& val)
|
||||
static void log(IStream* stream, const T& val)
|
||||
{
|
||||
char temp[32] = {};
|
||||
if constexpr (sizeof(T) == 1)
|
||||
snprintf(temp, sizeof(temp), "%02X", val);
|
||||
else if constexpr (sizeof(T) == 2)
|
||||
snprintf(temp, sizeof(temp), "%04X", val);
|
||||
else if constexpr (sizeof(T) == 4)
|
||||
snprintf(temp, sizeof(temp), "%08X", val);
|
||||
else if constexpr (sizeof(T) == 8)
|
||||
snprintf(temp, sizeof(temp), "%16X", val);
|
||||
else
|
||||
static_assert("Invalid size");
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setw(sizeof(T) * 2) << std::setfill('0') << +val;
|
||||
|
||||
stream->Write(temp, strlen(temp));
|
||||
std::string str = ss.str();
|
||||
stream->Write(str.c_str(), str.size());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -67,7 +64,7 @@ template<> struct DataSerializerTraits<bool>
|
|||
{
|
||||
stream->Read(&val);
|
||||
}
|
||||
static void log(IStream* stream, bool& val)
|
||||
static void log(IStream* stream, const bool& val)
|
||||
{
|
||||
if (val)
|
||||
stream->Write("true", 4);
|
||||
|
@ -100,6 +97,14 @@ template<> struct DataSerializerTraits<int32_t> : public DataSerializerTraitsInt
|
|||
{
|
||||
};
|
||||
|
||||
template<> struct DataSerializerTraits<uint64_t> : public DataSerializerTraitsIntegral<uint64_t>
|
||||
{
|
||||
};
|
||||
|
||||
template<> struct DataSerializerTraits<int64_t> : public DataSerializerTraitsIntegral<int64_t>
|
||||
{
|
||||
};
|
||||
|
||||
template<> struct DataSerializerTraits<std::string>
|
||||
{
|
||||
static void encode(IStream* stream, const std::string& str)
|
||||
|
@ -122,9 +127,9 @@ template<> struct DataSerializerTraits<std::string>
|
|||
}
|
||||
static void log(IStream* stream, const std::string& str)
|
||||
{
|
||||
stream->Write("\"");
|
||||
stream->Write("\"", 1);
|
||||
stream->Write(str.data(), str.size());
|
||||
stream->Write("\"");
|
||||
stream->Write("\"", 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -141,7 +146,7 @@ template<> struct DataSerializerTraits<NetworkPlayerId_t>
|
|||
stream->Read(&temp);
|
||||
val.id = ByteSwapBE(temp);
|
||||
}
|
||||
static void log(IStream* stream, NetworkPlayerId_t& val)
|
||||
static void log(IStream* stream, const NetworkPlayerId_t& val)
|
||||
{
|
||||
char playerId[28] = {};
|
||||
snprintf(playerId, sizeof(playerId), "%u", val.id);
|
||||
|
@ -175,7 +180,7 @@ template<> struct DataSerializerTraits<NetworkRideId_t>
|
|||
stream->Read(&temp);
|
||||
val.id = ByteSwapBE(temp);
|
||||
}
|
||||
static void log(IStream* stream, NetworkRideId_t& val)
|
||||
static void log(IStream* stream, const NetworkRideId_t& val)
|
||||
{
|
||||
char rideId[28] = {};
|
||||
snprintf(rideId, sizeof(rideId), "%u", val.id);
|
||||
|
@ -207,7 +212,7 @@ template<typename T> struct DataSerializerTraits<DataSerialiserTag<T>>
|
|||
DataSerializerTraits<T> s;
|
||||
s.decode(stream, tag.Data());
|
||||
}
|
||||
static void log(IStream* stream, DataSerialiserTag<T>& tag)
|
||||
static void log(IStream* stream, const DataSerialiserTag<T>& tag)
|
||||
{
|
||||
const char* name = tag.Name();
|
||||
stream->Write(name, strlen(name));
|
||||
|
@ -219,3 +224,71 @@ template<typename T> struct DataSerializerTraits<DataSerialiserTag<T>>
|
|||
stream->Write("; ", 2);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct DataSerializerTraits<MemoryStream>
|
||||
{
|
||||
static void encode(IStream* stream, const MemoryStream& val)
|
||||
{
|
||||
DataSerializerTraits<uint32_t> s;
|
||||
s.encode(stream, val.GetLength());
|
||||
|
||||
stream->Write(val.GetData(), val.GetLength());
|
||||
}
|
||||
static void decode(IStream* stream, MemoryStream& val)
|
||||
{
|
||||
DataSerializerTraits<uint32_t> s;
|
||||
|
||||
uint32_t length = 0;
|
||||
s.decode(stream, length);
|
||||
|
||||
std::unique_ptr<uint8_t[]> buf(new uint8_t[length]);
|
||||
stream->Read(buf.get(), length);
|
||||
|
||||
val.Write(buf.get(), length);
|
||||
}
|
||||
static void log(IStream* stream, const MemoryStream& tag)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template<typename _Ty, size_t _Size> struct DataSerializerTraits<std::array<_Ty, _Size>>
|
||||
{
|
||||
static void encode(IStream* stream, const std::array<_Ty, _Size>& val)
|
||||
{
|
||||
uint16_t len = (uint16_t)_Size;
|
||||
uint16_t swapped = ByteSwapBE(len);
|
||||
stream->Write(&swapped);
|
||||
|
||||
DataSerializerTraits<_Ty> s;
|
||||
for (auto&& sub : val)
|
||||
{
|
||||
s.encode(stream, sub);
|
||||
}
|
||||
}
|
||||
static void decode(IStream* stream, std::array<_Ty, _Size>& val)
|
||||
{
|
||||
uint16_t len;
|
||||
stream->Read(&len);
|
||||
len = ByteSwapBE(len);
|
||||
|
||||
if (len != _Size)
|
||||
throw std::runtime_error("Invalid size, can't decode");
|
||||
|
||||
DataSerializerTraits<_Ty> s;
|
||||
for (auto&& sub : val)
|
||||
{
|
||||
s.decode(stream, sub);
|
||||
}
|
||||
}
|
||||
static void log(IStream* stream, const std::array<_Ty, _Size>& val)
|
||||
{
|
||||
stream->Write("{", 1);
|
||||
DataSerializerTraits<_Ty> s;
|
||||
for (auto&& sub : val)
|
||||
{
|
||||
s.log(stream, sub);
|
||||
stream->Write("; ", 2);
|
||||
}
|
||||
stream->Write("}", 1);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -39,6 +39,17 @@ template<> struct ByteSwapT<4>
|
|||
}
|
||||
};
|
||||
|
||||
template<> struct ByteSwapT<8>
|
||||
{
|
||||
static uint64_t SwapBE(uint64_t value)
|
||||
{
|
||||
value = (value & 0x00000000FFFFFFFF) << 32 | (value & 0xFFFFFFFF00000000) >> 32;
|
||||
value = (value & 0x0000FFFF0000FFFF) << 16 | (value & 0xFFFF0000FFFF0000) >> 16;
|
||||
value = (value & 0x00FF00FF00FF00FF) << 8 | (value & 0xFF00FF00FF00FF00) >> 8;
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T> static T ByteSwapBE(const T& value)
|
||||
{
|
||||
return ByteSwapT<sizeof(T)>::SwapBE(value);
|
||||
|
|
|
@ -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,167 @@ 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 <replay_name> [<max_ticks = 0xFFFFFFFF>]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string name = argv[0];
|
||||
|
||||
// If ticks are specified by user use that otherwise maximum ticks specified by const.
|
||||
uint32_t maxTicks = OpenRCT2::k_MaxReplayTicks;
|
||||
if (argc >= 2)
|
||||
{
|
||||
maxTicks = atol(argv[1]);
|
||||
}
|
||||
|
||||
auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
|
||||
if (replayManager->StartRecording(name, maxTicks))
|
||||
{
|
||||
OpenRCT2::ReplayRecordInfo info;
|
||||
replayManager->GetCurrentReplayInfo(info);
|
||||
|
||||
const char* logFmt = "Replay recording started: (%s) %s";
|
||||
console.WriteFormatLine(logFmt, info.Name.c_str(), info.FilePath.c_str());
|
||||
log_info(logFmt, info.Name.c_str(), info.FilePath.c_str());
|
||||
|
||||
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->IsRecording() == false && replayManager->IsNormalising() == false)
|
||||
{
|
||||
console.WriteFormatLine("Replay currently not recording");
|
||||
return 0;
|
||||
}
|
||||
|
||||
OpenRCT2::ReplayRecordInfo info;
|
||||
replayManager->GetCurrentReplayInfo(info);
|
||||
|
||||
if (replayManager->StopRecording())
|
||||
{
|
||||
const char* logFmt = "Replay recording stopped: (%s) %s\n"
|
||||
" Ticks: %u\n"
|
||||
" Commands: %u\n"
|
||||
" Checksums: %u";
|
||||
|
||||
console.WriteFormatLine(
|
||||
logFmt, info.Name.c_str(), info.FilePath.c_str(), info.Ticks, info.NumCommands, info.NumChecksums);
|
||||
log_info(logFmt, info.Name.c_str(), info.FilePath.c_str(), info.Ticks, info.NumCommands, info.NumChecksums);
|
||||
|
||||
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 <replay_name>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string name = argv[0];
|
||||
|
||||
auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
|
||||
if (replayManager->StartPlayback(name))
|
||||
{
|
||||
OpenRCT2::ReplayRecordInfo info;
|
||||
replayManager->GetCurrentReplayInfo(info);
|
||||
|
||||
std::time_t ts = info.TimeRecorded;
|
||||
|
||||
char recordingDate[128] = {};
|
||||
std::strftime(recordingDate, sizeof(recordingDate), "%c", std::localtime(&ts));
|
||||
|
||||
const char* logFmt = "Replay playback started: %s\n"
|
||||
" Date Recorded: %s\n"
|
||||
" Ticks: %u\n"
|
||||
" Commands: %u\n"
|
||||
" Checksums: %u";
|
||||
|
||||
console.WriteFormatLine(logFmt, info.FilePath.c_str(), recordingDate, info.Ticks, info.NumCommands, info.NumChecksums);
|
||||
log_info(logFmt, info.FilePath.c_str(), recordingDate, info.Ticks, info.NumCommands, info.NumChecksums);
|
||||
|
||||
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->StopPlayback())
|
||||
{
|
||||
console.WriteFormatLine("Stopped replay");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t cc_replay_normalise(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 < 2)
|
||||
{
|
||||
console.WriteFormatLine("Parameters required <replay_input> <replay_output>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string inputFile = argv[0];
|
||||
std::string outputFile = argv[1];
|
||||
|
||||
auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
|
||||
if (replayManager->NormaliseReplay(inputFile, outputFile))
|
||||
{
|
||||
console.WriteFormatLine("Stopped replay");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4702) // unreachable code
|
||||
static int32_t cc_abort(
|
||||
|
@ -1451,6 +1613,12 @@ 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 <name> [max_ticks]"},
|
||||
{ "replay_stoprecord", cc_replay_stoprecord, "Stops recording a new replay.", "replay_stoprecord"},
|
||||
{ "replay_start", cc_replay_start, "Starts a replay", "replay_start <name>"},
|
||||
{ "replay_stop", cc_replay_stop, "Stops the replay", "replay_stop"},
|
||||
{ "replay_normalise", cc_replay_normalise, "Normalises the replay to remove all gaps", "replay_normalise <input file> <output file>"},
|
||||
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
|
|
@ -279,17 +279,17 @@ void news_item_get_subject_location(int32_t type, int32_t subject, int32_t* x, i
|
|||
*
|
||||
* rct2: 0x0066DF55
|
||||
*/
|
||||
void news_item_add_to_queue(uint8_t type, rct_string_id string_id, uint32_t assoc)
|
||||
NewsItem* news_item_add_to_queue(uint8_t type, rct_string_id string_id, uint32_t assoc)
|
||||
{
|
||||
utf8 buffer[256];
|
||||
void* args = gCommonFormatArgs;
|
||||
|
||||
// overflows possible?
|
||||
format_string(buffer, 256, string_id, args);
|
||||
news_item_add_to_queue_raw(type, buffer, assoc);
|
||||
return news_item_add_to_queue_raw(type, buffer, assoc);
|
||||
}
|
||||
|
||||
void news_item_add_to_queue_raw(uint8_t type, const utf8* text, uint32_t assoc)
|
||||
NewsItem* news_item_add_to_queue_raw(uint8_t type, const utf8* text, uint32_t assoc)
|
||||
{
|
||||
NewsItem* newsItem = gNewsItems;
|
||||
|
||||
|
@ -311,10 +311,14 @@ void news_item_add_to_queue_raw(uint8_t type, const utf8* text, uint32_t assoc)
|
|||
newsItem->Day = ((days_in_month[date_get_month(newsItem->MonthYear)] * gDateMonthTicks) >> 16) + 1;
|
||||
safe_strcpy(newsItem->Text, text, sizeof(newsItem->Text));
|
||||
|
||||
NewsItem* res = newsItem;
|
||||
|
||||
// Blatant disregard for what happens on the last element.
|
||||
// TODO: Change this when we implement the queue ourselves.
|
||||
newsItem++;
|
||||
newsItem->Type = NEWS_ITEM_NULL;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -63,8 +63,8 @@ void news_item_close_current();
|
|||
|
||||
void news_item_get_subject_location(int32_t type, int32_t subject, int32_t* x, int32_t* y, int32_t* z);
|
||||
|
||||
void news_item_add_to_queue(uint8_t type, rct_string_id string_id, uint32_t assoc);
|
||||
void news_item_add_to_queue_raw(uint8_t type, const utf8* text, uint32_t assoc);
|
||||
NewsItem* news_item_add_to_queue(uint8_t type, rct_string_id string_id, uint32_t assoc);
|
||||
NewsItem* news_item_add_to_queue_raw(uint8_t type, const utf8* text, uint32_t assoc);
|
||||
|
||||
void news_item_open_subject(int32_t type, int32_t subject);
|
||||
|
||||
|
|
|
@ -941,14 +941,14 @@ bool Network::CheckSRAND(uint32_t tick, uint32_t srand0)
|
|||
{
|
||||
server_srand0_tick = 0;
|
||||
// Check that the server and client sprite hashes match
|
||||
const char* client_sprite_hash = sprite_checksum();
|
||||
const bool sprites_mismatch = server_sprite_hash[0] != '\0'
|
||||
&& strcmp(client_sprite_hash, server_sprite_hash.c_str()) != 0;
|
||||
rct_sprite_checksum checksum = sprite_checksum();
|
||||
std::string client_sprite_hash = checksum.ToString();
|
||||
const bool sprites_mismatch = server_sprite_hash[0] != '\0' && client_sprite_hash != server_sprite_hash;
|
||||
// Check PRNG values and sprite hashes, if exist
|
||||
if ((srand0 != server_srand0) || sprites_mismatch)
|
||||
{
|
||||
# ifdef DEBUG_DESYNC
|
||||
dbg_report_desync(tick, srand0, server_srand0, client_sprite_hash, server_sprite_hash.c_str());
|
||||
dbg_report_desync(tick, srand0, server_srand0, client_sprite_hash.c_str(), server_sprite_hash.c_str());
|
||||
# endif
|
||||
return false;
|
||||
}
|
||||
|
@ -1613,7 +1613,8 @@ void Network::Server_Send_TICK()
|
|||
*packet << flags;
|
||||
if (flags & NETWORK_TICK_FLAG_CHECKSUMS)
|
||||
{
|
||||
packet->WriteString(sprite_checksum());
|
||||
rct_sprite_checksum checksum = sprite_checksum();
|
||||
packet->WriteString(checksum.ToString().c_str());
|
||||
}
|
||||
SendPacketToClients(*packet);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "../Game.h"
|
||||
#include "../Intro.h"
|
||||
#include "../OpenRCT2.h"
|
||||
#include "../ReplayManager.h"
|
||||
#include "../config/Config.h"
|
||||
#include "../drawing/Drawing.h"
|
||||
#include "../drawing/IDrawingEngine.h"
|
||||
|
@ -57,6 +58,19 @@ void Painter::Paint(IDrawingEngine& de)
|
|||
de.PaintRain();
|
||||
}
|
||||
|
||||
auto* replayManager = GetContext()->GetReplayManager();
|
||||
const char* text = nullptr;
|
||||
|
||||
if (replayManager->IsReplaying())
|
||||
text = "Replaying...";
|
||||
else if (replayManager->IsRecording())
|
||||
text = "Recording...";
|
||||
else if (replayManager->IsNormalising())
|
||||
text = "Normalising...";
|
||||
|
||||
if (text != nullptr)
|
||||
PaintReplayNotice(dpi, text);
|
||||
|
||||
if (gConfigGeneral.show_fps)
|
||||
{
|
||||
PaintFPS(dpi);
|
||||
|
@ -64,6 +78,30 @@ void Painter::Paint(IDrawingEngine& de)
|
|||
gCurrentDrawCount++;
|
||||
}
|
||||
|
||||
void Painter::PaintReplayNotice(rct_drawpixelinfo* dpi, const char* text)
|
||||
{
|
||||
int32_t x = _uiContext->GetWidth() / 2;
|
||||
int32_t y = _uiContext->GetHeight() - 44;
|
||||
|
||||
// Format string
|
||||
utf8 buffer[64] = { 0 };
|
||||
utf8* ch = buffer;
|
||||
ch = utf8_write_codepoint(ch, FORMAT_MEDIUMFONT);
|
||||
ch = utf8_write_codepoint(ch, FORMAT_OUTLINE);
|
||||
ch = utf8_write_codepoint(ch, FORMAT_RED);
|
||||
|
||||
snprintf(ch, 64 - (ch - buffer), "%s", text);
|
||||
|
||||
int32_t stringWidth = gfx_get_string_width(buffer);
|
||||
x = x - stringWidth;
|
||||
|
||||
if (((gCurrentTicks >> 1) & 0xF) > 4)
|
||||
gfx_draw_string(dpi, buffer, COLOUR_SATURATED_RED, x, y);
|
||||
|
||||
// Make area dirty so the text doesn't get drawn over the last
|
||||
gfx_set_dirty_blocks(x, y, x + stringWidth, y + 16);
|
||||
}
|
||||
|
||||
void Painter::PaintFPS(rct_drawpixelinfo* dpi)
|
||||
{
|
||||
int32_t x = _uiContext->GetWidth() / 2;
|
||||
|
|
|
@ -44,6 +44,7 @@ namespace OpenRCT2
|
|||
void Paint(Drawing::IDrawingEngine& de);
|
||||
|
||||
private:
|
||||
void PaintReplayNotice(rct_drawpixelinfo* dpi, const char* text);
|
||||
void PaintFPS(rct_drawpixelinfo* dpi);
|
||||
void MeasureFPS();
|
||||
};
|
||||
|
|
|
@ -1051,7 +1051,7 @@ TileElement* ride_get_station_exit_element(int32_t x, int32_t y, int32_t z);
|
|||
void ride_set_status(int32_t rideIndex, int32_t status);
|
||||
void game_command_set_ride_status(
|
||||
int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, int32_t* esi, int32_t* edi, int32_t* ebp);
|
||||
void ride_set_name(int32_t rideIndex, const char* name);
|
||||
void ride_set_name(int32_t rideIndex, const char* name, uint32_t flags);
|
||||
void game_command_set_ride_name(
|
||||
int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, int32_t* esi, int32_t* edi, int32_t* ebp);
|
||||
void game_command_set_ride_setting(
|
||||
|
|
|
@ -1881,7 +1881,7 @@ static money32 place_track_design(int16_t x, int16_t y, int16_t z, uint8_t flags
|
|||
|
||||
uint8_t rideIndex;
|
||||
uint8_t rideColour;
|
||||
money32 createRideResult = ride_create_command(td6->type, entryIndex, GAME_COMMAND_FLAG_APPLY, &rideIndex, &rideColour);
|
||||
money32 createRideResult = ride_create_command(td6->type, entryIndex, flags, &rideIndex, &rideColour);
|
||||
if (createRideResult == MONEY32_UNDEFINED)
|
||||
{
|
||||
gGameCommandErrorTitle = STR_CANT_CREATE_NEW_RIDE_ATTRACTION;
|
||||
|
@ -1925,7 +1925,7 @@ static money32 place_track_design(int16_t x, int16_t y, int16_t z, uint8_t flags
|
|||
if (cost == MONEY32_UNDEFINED || !(flags & GAME_COMMAND_FLAG_APPLY))
|
||||
{
|
||||
rct_string_id error_reason = gGameCommandErrorText;
|
||||
ride_action_modify(rideIndex, RIDE_MODIFY_DEMOLISH, GAME_COMMAND_FLAG_APPLY);
|
||||
ride_action_modify(rideIndex, RIDE_MODIFY_DEMOLISH, flags);
|
||||
gGameCommandErrorText = error_reason;
|
||||
gCommandExpenditureType = RCT_EXPENDITURE_TYPE_RIDE_CONSTRUCTION;
|
||||
*outRideIndex = rideIndex;
|
||||
|
@ -1934,40 +1934,27 @@ static money32 place_track_design(int16_t x, int16_t y, int16_t z, uint8_t flags
|
|||
|
||||
if (entryIndex != 0xFF)
|
||||
{
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (2 << 8), 0, rideIndex | (entryIndex << 8), GAME_COMMAND_SET_RIDE_VEHICLES, 0, 0);
|
||||
game_do_command(0, flags | (2 << 8), 0, rideIndex | (entryIndex << 8), GAME_COMMAND_SET_RIDE_VEHICLES, 0, 0);
|
||||
}
|
||||
|
||||
game_do_command(0, flags | (td6->ride_mode << 8), 0, rideIndex | (0 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(0, flags | (0 << 8), 0, rideIndex | (td6->number_of_trains << 8), GAME_COMMAND_SET_RIDE_VEHICLES, 0, 0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (td6->ride_mode << 8), 0, rideIndex | (0 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
0, flags | (1 << 8), 0, rideIndex | (td6->number_of_cars_per_train << 8), GAME_COMMAND_SET_RIDE_VEHICLES, 0, 0);
|
||||
game_do_command(0, flags | (td6->depart_flags << 8), 0, rideIndex | (1 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(0, flags | (td6->min_waiting_time << 8), 0, rideIndex | (2 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(0, flags | (td6->max_waiting_time << 8), 0, rideIndex | (3 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(0, flags | (td6->operation_setting << 8), 0, rideIndex | (4 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (0 << 8), 0, rideIndex | (td6->number_of_trains << 8), GAME_COMMAND_SET_RIDE_VEHICLES, 0,
|
||||
0, flags | ((td6->lift_hill_speed_num_circuits & 0x1F) << 8), 0, rideIndex | (8 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0,
|
||||
0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (1 << 8), 0, rideIndex | (td6->number_of_cars_per_train << 8),
|
||||
GAME_COMMAND_SET_RIDE_VEHICLES, 0, 0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (td6->depart_flags << 8), 0, rideIndex | (1 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (td6->min_waiting_time << 8), 0, rideIndex | (2 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0,
|
||||
0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (td6->max_waiting_time << 8), 0, rideIndex | (3 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0,
|
||||
0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (td6->operation_setting << 8), 0, rideIndex | (4 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0,
|
||||
0);
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | ((td6->lift_hill_speed_num_circuits & 0x1F) << 8), 0, rideIndex | (8 << 8),
|
||||
GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
|
||||
uint8_t num_circuits = td6->lift_hill_speed_num_circuits >> 5;
|
||||
if (num_circuits == 0)
|
||||
{
|
||||
num_circuits = 1;
|
||||
}
|
||||
game_do_command(
|
||||
0, GAME_COMMAND_FLAG_APPLY | (num_circuits << 8), 0, rideIndex | (9 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
game_do_command(0, flags | (num_circuits << 8), 0, rideIndex | (9 << 8), GAME_COMMAND_SET_RIDE_SETTING, 0, 0);
|
||||
|
||||
ride_set_to_default_inspection_interval(rideIndex);
|
||||
ride->lifecycle_flags |= RIDE_LIFECYCLE_NOT_CUSTOM_DESIGN;
|
||||
|
@ -1989,7 +1976,7 @@ static money32 place_track_design(int16_t x, int16_t y, int16_t z, uint8_t flags
|
|||
ride->vehicle_colours_extended[i] = td6->vehicle_additional_colour[i];
|
||||
}
|
||||
|
||||
ride_set_name(rideIndex, td6->name);
|
||||
ride_set_name(rideIndex, td6->name, flags);
|
||||
|
||||
gCommandExpenditureType = RCT_EXPENDITURE_TYPE_RIDE_CONSTRUCTION;
|
||||
*outRideIndex = rideIndex;
|
||||
|
|
|
@ -53,6 +53,21 @@ static LocationXYZ16 _spritelocations2[MAX_SPRITES];
|
|||
|
||||
static size_t GetSpatialIndexOffset(int32_t x, int32_t y);
|
||||
|
||||
std::string rct_sprite_checksum::ToString() const
|
||||
{
|
||||
std::string result;
|
||||
|
||||
result.reserve(raw.size() * 2);
|
||||
for (auto b : raw)
|
||||
{
|
||||
char buf[3];
|
||||
snprintf(buf, 3, "%02x", b);
|
||||
result.append(buf);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
rct_sprite* try_get_sprite(size_t spriteIndex)
|
||||
{
|
||||
rct_sprite* sprite = nullptr;
|
||||
|
@ -207,14 +222,15 @@ static size_t GetSpatialIndexOffset(int32_t x, int32_t y)
|
|||
|
||||
#ifndef DISABLE_NETWORK
|
||||
|
||||
const char* sprite_checksum()
|
||||
rct_sprite_checksum sprite_checksum()
|
||||
{
|
||||
using namespace Crypt;
|
||||
|
||||
// TODO Remove statics, should be one of these per sprite manager / OpenRCT2 context.
|
||||
// Alternatively, make a new class for this functionality.
|
||||
static std::unique_ptr<HashAlgorithm<20>> _spriteHashAlg;
|
||||
static std::string result;
|
||||
|
||||
rct_sprite_checksum checksum;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -245,29 +261,21 @@ const char* sprite_checksum()
|
|||
}
|
||||
}
|
||||
|
||||
auto hash = _spriteHashAlg->Finish();
|
||||
|
||||
result.clear();
|
||||
result.reserve(hash.size() * 2);
|
||||
for (auto b : hash)
|
||||
{
|
||||
char buf[3];
|
||||
snprintf(buf, 3, "%02x", b);
|
||||
result.append(buf);
|
||||
}
|
||||
return result.c_str();
|
||||
checksum.raw = _spriteHashAlg->Finish();
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
log_error("sprite_checksum failed: %s", e.what());
|
||||
throw;
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
#else
|
||||
|
||||
const char* sprite_checksum()
|
||||
rct_sprite_checksum sprite_checksum()
|
||||
{
|
||||
return nullptr;
|
||||
return rct_sprite_checksum{};
|
||||
}
|
||||
|
||||
#endif // DISABLE_NETWORK
|
||||
|
|
|
@ -201,6 +201,13 @@ union rct_sprite
|
|||
};
|
||||
assert_struct_size(rct_sprite, 0x100);
|
||||
|
||||
struct rct_sprite_checksum
|
||||
{
|
||||
std::array<uint8_t, 20> raw;
|
||||
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
enum
|
||||
|
@ -305,7 +312,7 @@ void crashed_vehicle_particle_update(rct_crashed_vehicle_particle* particle);
|
|||
void crash_splash_create(int32_t x, int32_t y, int32_t z);
|
||||
void crash_splash_update(rct_crash_splash* splash);
|
||||
|
||||
const char* sprite_checksum();
|
||||
rct_sprite_checksum sprite_checksum();
|
||||
|
||||
void sprite_set_flashing(rct_sprite* sprite, bool flashing);
|
||||
bool sprite_get_flashing(rct_sprite* sprite);
|
||||
|
|
|
@ -188,3 +188,12 @@ set(TILE_ELEMENT_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/TileElements.cpp"
|
|||
add_executable(test_tile_elements ${TILE_ELEMENT_TEST_SOURCES})
|
||||
target_link_libraries(test_tile_elements ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
|
||||
add_test(NAME tile_elements COMMAND test_tile_elements)
|
||||
|
||||
if (NOT DISABLE_NETWORK)
|
||||
# Replay tests
|
||||
set(REPLAY_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/ReplayTests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/TestData.cpp")
|
||||
add_executable(test_replays ${REPLAY_TEST_SOURCES})
|
||||
target_link_libraries(test_replays ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
|
||||
add_test(NAME replay_tests COMMAND test_replays)
|
||||
endif ()
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*****************************************************************************
|
||||
* 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 "TestData.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <openrct2/Context.h>
|
||||
#include <openrct2/Game.h>
|
||||
#include <openrct2/GameState.h>
|
||||
#include <openrct2/OpenRCT2.h>
|
||||
#include <openrct2/ReplayManager.h>
|
||||
#include <openrct2/audio/AudioContext.h>
|
||||
#include <openrct2/core/File.h>
|
||||
#include <openrct2/core/FileScanner.h>
|
||||
#include <openrct2/core/Path.hpp>
|
||||
#include <openrct2/core/String.hpp>
|
||||
#include <openrct2/platform/platform.h>
|
||||
#include <openrct2/ride/Ride.h>
|
||||
#include <string>
|
||||
|
||||
using namespace OpenRCT2;
|
||||
|
||||
struct ReplayTestData
|
||||
{
|
||||
std::string name;
|
||||
std::string filePath;
|
||||
};
|
||||
|
||||
// NOTE: gtests expects the name to have no special characters.
|
||||
static std::string sanitizeTestName(const std::string& name)
|
||||
{
|
||||
std::string nameOnly = Path::GetFileNameWithoutExtension(name);
|
||||
std::string res;
|
||||
for (char c : nameOnly)
|
||||
{
|
||||
if (isalnum(c))
|
||||
res += c;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static std::vector<ReplayTestData> GetReplayFiles()
|
||||
{
|
||||
std::vector<ReplayTestData> res;
|
||||
std::string basePath = TestData::GetBasePath();
|
||||
std::string replayPath = Path::Combine(basePath, "replays");
|
||||
std::string replayPathPattern = Path::Combine(replayPath, "*.sv6r");
|
||||
std::vector<std::string> files;
|
||||
|
||||
std::unique_ptr<IFileScanner> scanner = std::unique_ptr<IFileScanner>(Path::ScanDirectory(replayPathPattern, true));
|
||||
while (scanner->Next())
|
||||
{
|
||||
ReplayTestData test;
|
||||
test.name = sanitizeTestName(scanner->GetFileInfo()->Name);
|
||||
test.filePath = scanner->GetPath();
|
||||
res.push_back(test);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
class ReplayTests : public testing::TestWithParam<ReplayTestData>
|
||||
{
|
||||
protected:
|
||||
};
|
||||
|
||||
TEST_P(ReplayTests, RunReplay)
|
||||
{
|
||||
gOpenRCT2Headless = true;
|
||||
gOpenRCT2NoGraphics = true;
|
||||
core_init();
|
||||
|
||||
auto testData = GetParam();
|
||||
auto replayFile = testData.filePath;
|
||||
|
||||
auto context = CreateContext();
|
||||
bool initialised = context->Initialise();
|
||||
ASSERT_TRUE(initialised);
|
||||
|
||||
auto gs = context->GetGameState();
|
||||
ASSERT_NE(gs, nullptr);
|
||||
|
||||
IReplayManager* replayManager = context->GetReplayManager();
|
||||
ASSERT_NE(replayManager, nullptr);
|
||||
|
||||
bool startedReplay = replayManager->StartPlayback(replayFile);
|
||||
ASSERT_TRUE(startedReplay);
|
||||
|
||||
while (replayManager->IsReplaying())
|
||||
{
|
||||
gs->UpdateLogic();
|
||||
ASSERT_TRUE(replayManager->IsPlaybackStateMismatching() == false);
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintTo(const ReplayTestData& testData, std::ostream* os)
|
||||
{
|
||||
*os << testData.filePath;
|
||||
}
|
||||
|
||||
struct PrintReplayParameter
|
||||
{
|
||||
template<class ParamType> std::string operator()(const testing::TestParamInfo<ParamType>& info) const
|
||||
{
|
||||
auto data = static_cast<ReplayTestData>(info.param);
|
||||
return data.name;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(Replay, ReplayTests, testing::ValuesIn(GetReplayFiles()), PrintReplayParameter());
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -63,6 +63,7 @@
|
|||
<ClCompile Include="IniWriterTest.cpp" />
|
||||
<ClCompile Include="Localisation.cpp" />
|
||||
<ClCompile Include="MultiLaunch.cpp" />
|
||||
<ClCompile Include="ReplayTests.cpp" />
|
||||
<ClCompile Include="RideRatings.cpp" />
|
||||
<ClCompile Include="sawyercoding_test.cpp" />
|
||||
<ClCompile Include="$(GtestDir)\src\gtest-all.cc" />
|
||||
|
|
Loading…
Reference in New Issue