Merge pull request #8374 from ZehMatt/replay-feature

Add support to record and replay game commands/actions.
This commit is contained in:
ζeh Matt 2019-01-02 09:15:10 +01:00 committed by GitHub
commit a065806b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1558 additions and 82 deletions

View File

@ -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;

View File

@ -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.

View File

@ -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);
}
/**

View File

@ -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;

View File

@ -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,
});
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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
};

View File

@ -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)
{

View File

@ -220,6 +220,7 @@ const char * PlatformEnvironment::DirectoryNamesOpenRCT2[] =
"themes", // THEME
"track", // TRACK
"heightmap", // HEIGHTMAP
"replay", // REPLAY
};
const char * PlatformEnvironment::FileNames[] =

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);
}

View File

@ -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
{

View File

@ -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);
}
};

View File

@ -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);

View File

@ -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

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
};

View File

@ -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(

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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 ()

115
test/tests/ReplayTests.cpp Normal file
View File

@ -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.

View File

@ -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" />