mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2025-12-12 18:42:36 +01:00
Add normalisation support to remove gaps.
This commit is contained in:
@@ -485,13 +485,25 @@ 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 && replayManager->IsRecording() && (flags & GAME_COMMAND_FLAG_APPLY)
|
||||
&& (flags & GAME_COMMAND_FLAG_GHOST) == 0 && (flags & GAME_COMMAND_FLAG_5) == 0)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
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;
|
||||
@@ -75,10 +77,12 @@ namespace OpenRCT2
|
||||
{
|
||||
MemoryStream parkData;
|
||||
std::string name; // Name of play
|
||||
uint32_t timeRecorded; // Posix Time.
|
||||
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;
|
||||
int32_t checksumIndex;
|
||||
};
|
||||
|
||||
class ReplayManager final : public IReplayManager
|
||||
@@ -88,6 +92,7 @@ namespace OpenRCT2
|
||||
NONE = 0,
|
||||
RECORDING,
|
||||
PLAYING,
|
||||
NORMALISATION,
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -105,11 +110,16 @@ namespace OpenRCT2
|
||||
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 (_current == nullptr)
|
||||
if (_currentRecording == nullptr)
|
||||
return;
|
||||
|
||||
uint32_t args[7];
|
||||
@@ -121,12 +131,15 @@ namespace OpenRCT2
|
||||
args[5] = edi;
|
||||
args[6] = ebp;
|
||||
|
||||
_current->commands.emplace(gCurrentTicks, args, callback, _commandId++);
|
||||
_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 (_current == nullptr)
|
||||
if (_currentRecording == nullptr)
|
||||
return;
|
||||
|
||||
MemoryStream stream;
|
||||
@@ -140,17 +153,35 @@ namespace OpenRCT2
|
||||
DataSerialiser dsIn(false, stream);
|
||||
ga->Serialise(dsIn);
|
||||
|
||||
_current->commands.emplace(gCurrentTicks, std::move(ga), _commandId++);
|
||||
_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));
|
||||
|
||||
// Reset.
|
||||
_nextChecksumTick = 0;
|
||||
}
|
||||
|
||||
if (_mode == ReplayMode::RECORDING)
|
||||
{
|
||||
if (gCurrentTicks >= _current->tickEnd)
|
||||
if (gCurrentTicks >= _currentRecording->tickEnd)
|
||||
{
|
||||
StopRecording();
|
||||
return;
|
||||
@@ -158,13 +189,36 @@ namespace OpenRCT2
|
||||
}
|
||||
else if (_mode == ReplayMode::PLAYING)
|
||||
{
|
||||
CheckState();
|
||||
ReplayCommands();
|
||||
|
||||
// If we run out of commands we can stop the replay and checked all checksums we can stop.
|
||||
if (_currentReplay->commands.empty() && _currentReplay->checksumIndex >= _currentReplay->checksums.size())
|
||||
{
|
||||
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 /*= 0xFFFFFFFF*/) override
|
||||
{
|
||||
if (_mode != ReplayMode::NONE)
|
||||
if (_mode != ReplayMode::NONE && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
auto replayData = std::make_unique<ReplayRecordData>();
|
||||
@@ -184,22 +238,26 @@ namespace OpenRCT2
|
||||
s6exporter->Export();
|
||||
s6exporter->SaveGame(&replayData->parkData);
|
||||
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::RECORDING;
|
||||
_current = std::move(replayData);
|
||||
|
||||
_currentRecording = std::move(replayData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool StopRecording() override
|
||||
{
|
||||
if (_mode != ReplayMode::RECORDING)
|
||||
if (_mode != ReplayMode::RECORDING && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
_currentRecording->tickEnd = gCurrentTicks;
|
||||
|
||||
DataSerialiser serialiser(true);
|
||||
Serialise(serialiser, *_current);
|
||||
Serialise(serialiser, *_currentRecording);
|
||||
|
||||
char replayName[512] = {};
|
||||
snprintf(replayName, sizeof(replayName), "replay_%s_%d.sv6r", _current->name.c_str(), 0);
|
||||
snprintf(replayName, sizeof(replayName), "%s.sv6r", _currentRecording->name.c_str());
|
||||
|
||||
std::string outPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(DIRBASE::USER, DIRID::REPLAY);
|
||||
std::string outFile = Path::Combine(outPath, replayName);
|
||||
@@ -213,14 +271,20 @@ namespace OpenRCT2
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
//_current.reset();
|
||||
// When normalizing the output we don't touch the mode.
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::NONE;
|
||||
|
||||
_currentRecording.reset();
|
||||
|
||||
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))
|
||||
@@ -235,7 +299,10 @@ namespace OpenRCT2
|
||||
|
||||
gCurrentTicks = replayData->tickStart;
|
||||
|
||||
_current = std::move(replayData);
|
||||
_currentReplay = std::move(replayData);
|
||||
_currentReplay->checksumIndex = 0;
|
||||
|
||||
if (_mode != ReplayMode::NORMALISATION)
|
||||
_mode = ReplayMode::PLAYING;
|
||||
|
||||
return true;
|
||||
@@ -243,11 +310,36 @@ namespace OpenRCT2
|
||||
|
||||
virtual bool StopPlayback() override
|
||||
{
|
||||
if (_mode != ReplayMode::PLAYING)
|
||||
if (_mode != ReplayMode::PLAYING && _mode != ReplayMode::NORMALISATION)
|
||||
return false;
|
||||
|
||||
_current.reset();
|
||||
// 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, 0xFFFFFFFF) == false)
|
||||
{
|
||||
StopPlayback();
|
||||
return false;
|
||||
}
|
||||
|
||||
_nextReplayTick = gCurrentTicks + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -310,7 +402,7 @@ namespace OpenRCT2
|
||||
}
|
||||
|
||||
std::string outPath = GetContext()->GetPlatformEnvironment()->GetDirectoryPath(DIRBASE::USER, DIRID::REPLAY);
|
||||
std::string outFile = Path::Combine(outPath, file);
|
||||
std::string outFile = Path::Combine(outPath, fileName);
|
||||
|
||||
bool loaded = false;
|
||||
if (ReadReplayFromFile(outFile, stream))
|
||||
@@ -405,25 +497,72 @@ namespace OpenRCT2
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void CheckState()
|
||||
{
|
||||
int32_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.ToString() != checksum.ToString())
|
||||
{
|
||||
// Detected different game state.
|
||||
log_info(
|
||||
"Different sprite checksum at tick %u ; Saved: %s, Current: %s", gCurrentTicks,
|
||||
savedChecksum.second.ToString().c_str(), checksum.ToString().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Good state.
|
||||
log_info(
|
||||
"Good state at tick %u ; Saved: %s, Current: %s", gCurrentTicks,
|
||||
savedChecksum.second.ToString().c_str(), checksum.ToString().c_str());
|
||||
}
|
||||
_currentReplay->checksumIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayCommands()
|
||||
{
|
||||
auto& replayQueue = _current->commands;
|
||||
|
||||
// If we run out of commands we can stop the replay.
|
||||
if (replayQueue.empty())
|
||||
{
|
||||
StopPlayback();
|
||||
return;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (gCurrentTicks != _nextReplayTick)
|
||||
break;
|
||||
_nextReplayTick = gCurrentTicks + 1;
|
||||
}
|
||||
|
||||
if (command.action != nullptr)
|
||||
{
|
||||
@@ -446,8 +585,11 @@ namespace OpenRCT2
|
||||
|
||||
private:
|
||||
ReplayMode _mode = ReplayMode::NONE;
|
||||
std::unique_ptr<ReplayRecordData> _current;
|
||||
std::unique_ptr<ReplayRecordData> _currentRecording;
|
||||
std::unique_ptr<ReplayRecordData> _currentReplay;
|
||||
uint32_t _commandId = 0;
|
||||
uint32_t _nextChecksumTick = 0;
|
||||
uint32_t _nextReplayTick = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IReplayManager> CreateReplayManager()
|
||||
|
||||
@@ -25,9 +25,10 @@ namespace OpenRCT2
|
||||
virtual ~IReplayManager() = default;
|
||||
|
||||
virtual void Update() = 0;
|
||||
virtual bool IsReplaying() const = 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(
|
||||
@@ -41,6 +42,8 @@ namespace OpenRCT2
|
||||
|
||||
virtual bool StartPlayback(const std::string& file) = 0;
|
||||
virtual bool StopPlayback() = 0;
|
||||
|
||||
virtual bool NormaliseReplay(const std::string& inputFile, const std::string& outputFile) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IReplayManager> CreateReplayManager();
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace GameActions
|
||||
uint32_t flags = action->GetFlags();
|
||||
|
||||
auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
|
||||
if (replayManager != nullptr && replayManager->IsReplaying())
|
||||
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)
|
||||
@@ -282,7 +282,15 @@ namespace GameActions
|
||||
}
|
||||
else if (network_get_mode() == NETWORK_MODE_NONE)
|
||||
{
|
||||
if (replayManager != nullptr && replayManager->IsRecording())
|
||||
bool recordAction = false;
|
||||
if (replayManager)
|
||||
{
|
||||
if (replayManager->IsRecording())
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1438,6 +1438,36 @@ static int32_t cc_replay_stop(InteractiveConsole& console, const utf8** argv, in
|
||||
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 != nullptr)
|
||||
{
|
||||
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(
|
||||
@@ -1561,6 +1591,8 @@ static constexpr const console_command console_command_table[] = {
|
||||
{ "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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user