diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index 12a9c55d6..ec0e09316 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -271,6 +271,13 @@ int main(int argc, char** argv) {
return -1;
}
+ if (!movie_record.empty()) {
+ Core::Movie::GetInstance().PrepareForRecording();
+ }
+ if (!movie_play.empty()) {
+ Core::Movie::GetInstance().PrepareForPlayback(movie_play);
+ }
+
// Apply the command line arguments
Settings::values.gdbstub_port = gdb_port;
Settings::values.use_gdbstub = use_gdbstub;
@@ -332,7 +339,7 @@ int main(int argc, char** argv) {
}
if (!movie_play.empty()) {
- Core::Movie::GetInstance().StartPlayback(movie_play);
+ Core::Movie::GetInstance().StartPlayback(movie_play, [] {});
}
if (!movie_record.empty()) {
Core::Movie::GetInstance().StartRecording(movie_record);
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index a13378eb7..850934b57 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -746,6 +746,10 @@ void GMainWindow::BootGame(const QString& filename) {
LOG_INFO(Frontend, "Citra starting...");
StoreRecentFile(filename); // Put the filename on top of the list
+ if (movie_record_on_start) {
+ Core::Movie::GetInstance().PrepareForRecording();
+ }
+
if (!LoadROM(filename))
return;
@@ -1261,6 +1265,15 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
}
void GMainWindow::OnRecordMovie() {
+ if (emulation_running) {
+ QMessageBox::StandardButton answer = QMessageBox::warning(
+ this, tr("Record Movie"),
+ tr("To keep consistency with the RNG, it is recommended to record the movie from game "
+ "start.
Are you sure you still want to record movies now?"),
+ QMessageBox::Yes | QMessageBox::No);
+ if (answer == QMessageBox::No)
+ return;
+ }
const QString path =
QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path,
tr("Citra TAS Movie (*.ctm)"));
@@ -1322,6 +1335,16 @@ bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) {
}
void GMainWindow::OnPlayMovie() {
+ if (emulation_running) {
+ QMessageBox::StandardButton answer = QMessageBox::warning(
+ this, tr("Play Movie"),
+ tr("To keep consistency with the RNG, it is recommended to play the movie from game "
+ "start.
Are you sure you still want to play movies now?"),
+ QMessageBox::Yes | QMessageBox::No);
+ if (answer == QMessageBox::No)
+ return;
+ }
+
const QString path =
QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path,
tr("Citra TAS Movie (*.ctm)"));
@@ -1353,6 +1376,7 @@ void GMainWindow::OnPlayMovie() {
}
if (!ValidateMovie(path, program_id))
return;
+ Core::Movie::GetInstance().PrepareForPlayback(path.toStdString());
BootGame(game_path);
}
Core::Movie::GetInstance().StartPlayback(path.toStdString(), [this] {
diff --git a/src/core/hle/shared_page.cpp b/src/core/hle/shared_page.cpp
index ec813546e..962293609 100644
--- a/src/core/hle/shared_page.cpp
+++ b/src/core/hle/shared_page.cpp
@@ -7,6 +7,7 @@
#include "core/core_timing.h"
#include "core/hle/service/ptm/ptm.h"
#include "core/hle/shared_page.h"
+#include "core/movie.h"
#include "core/settings.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -14,6 +15,12 @@
namespace SharedPage {
static std::chrono::seconds GetInitTime() {
+ u64 override_init_time = Core::Movie::GetInstance().GetOverrideInitTime();
+ if (override_init_time) {
+ // Override the clock init time with the one in the movie
+ return std::chrono::seconds(override_init_time);
+ }
+
switch (Settings::values.init_clock) {
case Settings::InitClock::SystemTime: {
auto now = std::chrono::system_clock::now();
diff --git a/src/core/movie.cpp b/src/core/movie.cpp
index 04b470782..3b05f77d0 100644
--- a/src/core/movie.cpp
+++ b/src/core/movie.cpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include "common/bit_field.h"
#include "common/common_types.h"
@@ -13,6 +14,7 @@
#include "common/scm_rev.h"
#include "common/string_util.h"
#include "common/swap.h"
+#include "common/timer.h"
#include "core/core.h"
#include "core/hle/service/hid/hid.h"
#include "core/hle/service/ir/extra_hid.h"
@@ -112,8 +114,9 @@ struct CTMHeader {
std::array filetype; /// Unique Identifier to check the file type (always "CTM"0x1B)
u64_le program_id; /// ID of the ROM being executed. Also called title_id
std::array revision; /// Git hash of the revision this movie was created with
+ u64_le clock_init_time; /// The init time of the system clock
- std::array reserved; /// Make heading 256 bytes so it has consistent size
+ std::array reserved; /// Make heading 256 bytes so it has consistent size
};
static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes");
#pragma pack(pop)
@@ -129,6 +132,7 @@ void Movie::CheckInputEnd() {
if (current_byte + sizeof(ControllerState) > recorded_input.size()) {
LOG_INFO(Movie, "Playback finished");
play_mode = PlayMode::None;
+ init_time = 0;
playback_completion_callback();
}
}
@@ -344,6 +348,10 @@ void Movie::Record(const Service::IR::ExtraHIDResponse& extra_hid_response) {
Record(s);
}
+u64 Movie::GetOverrideInitTime() const {
+ return init_time;
+}
+
Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 program_id) const {
if (header_magic_bytes != header.filetype) {
LOG_ERROR(Movie, "Playback file does not have valid header");
@@ -381,6 +389,7 @@ void Movie::SaveMovie() {
CTMHeader header = {};
header.filetype = header_magic_bytes;
+ header.clock_init_time = init_time;
Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id);
@@ -424,36 +433,53 @@ void Movie::StartRecording(const std::string& movie_file) {
record_movie_file = movie_file;
}
-Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const {
- LOG_INFO(Movie, "Validating Movie file '{}'", movie_file);
+static boost::optional ReadHeader(const std::string& movie_file) {
FileUtil::IOFile save_record(movie_file, "rb");
const u64 size = save_record.GetSize();
if (!save_record || size <= sizeof(CTMHeader)) {
- return ValidationResult::Invalid;
- }
-
- CTMHeader header;
- save_record.ReadArray(&header, 1);
- return ValidateHeader(header, program_id);
-}
-
-u64 Movie::GetMovieProgramID(const std::string& movie_file) const {
- FileUtil::IOFile save_record(movie_file, "rb");
- const u64 size = save_record.GetSize();
-
- if (!save_record || size <= sizeof(CTMHeader)) {
- return 0;
+ return boost::none;
}
CTMHeader header;
save_record.ReadArray(&header, 1);
if (header_magic_bytes != header.filetype) {
- return 0;
+ return boost::none;
}
- return static_cast(header.program_id);
+ return header;
+}
+
+void Movie::PrepareForPlayback(const std::string& movie_file) {
+ auto header = ReadHeader(movie_file);
+ if (header != boost::none)
+ return;
+
+ init_time = header.value().clock_init_time;
+}
+
+void Movie::PrepareForRecording() {
+ init_time = (Settings::values.init_clock == Settings::InitClock::SystemTime
+ ? Common::Timer::GetTimeSinceJan1970().count()
+ : Settings::values.init_time);
+}
+
+Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const {
+ LOG_INFO(Movie, "Validating Movie file '{}'", movie_file);
+ auto header = ReadHeader(movie_file);
+ if (header != boost::none)
+ return ValidationResult::Invalid;
+
+ return ValidateHeader(header.value(), program_id);
+}
+
+u64 Movie::GetMovieProgramID(const std::string& movie_file) const {
+ auto header = ReadHeader(movie_file);
+ if (header != boost::none)
+ return 0;
+
+ return static_cast(header.value().program_id);
}
void Movie::Shutdown() {
@@ -465,6 +491,7 @@ void Movie::Shutdown() {
recorded_input.resize(0);
record_movie_file.clear();
current_byte = 0;
+ init_time = 0;
}
template
diff --git a/src/core/movie.h b/src/core/movie.h
index 20ab5a06e..4996976a7 100644
--- a/src/core/movie.h
+++ b/src/core/movie.h
@@ -44,7 +44,17 @@ public:
void StartPlayback(const std::string& movie_file,
std::function completion_callback = {});
void StartRecording(const std::string& movie_file);
+
+ /// Prepare to override the clock before playing back movies
+ void PrepareForPlayback(const std::string& movie_file);
+
+ /// Prepare to override the clock before recording movies
+ void PrepareForRecording();
+
ValidationResult ValidateMovie(const std::string& movie_file, u64 program_id = 0) const;
+
+ /// Get the init time that would override the one in the settings
+ u64 GetOverrideInitTime() const;
u64 GetMovieProgramID(const std::string& movie_file) const;
void Shutdown();
@@ -119,6 +129,7 @@ private:
PlayMode play_mode;
std::string record_movie_file;
std::vector recorded_input;
+ u64 init_time;
std::function playback_completion_callback;
std::size_t current_byte = 0;
};