diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 8495a83d5..7c5ab9422 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -57,6 +57,7 @@
#include "core/gdbstub/gdbstub.h"
#include "core/hle/service/fs/archive.h"
#include "core/loader/loader.h"
+#include "core/movie.h"
#include "core/settings.h"
#ifdef USE_DISCORD_PRESENCE
@@ -522,6 +523,12 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Screen_Layout_Swap_Screens, &QAction::triggered, this,
&GMainWindow::OnSwapScreens);
+ // Movie
+ connect(ui.action_Record_Movie, &QAction::triggered, this, &GMainWindow::OnRecordMovie);
+ connect(ui.action_Play_Movie, &QAction::triggered, this, &GMainWindow::OnPlayMovie);
+ connect(ui.action_Stop_Recording_Playback, &QAction::triggered, this,
+ &GMainWindow::OnStopRecordingPlayback);
+
// Help
connect(ui.action_FAQ, &QAction::triggered,
[]() { QDesktopServices::openUrl(QUrl("https://citra-emu.org/wiki/faq/")); });
@@ -757,6 +764,12 @@ void GMainWindow::BootGame(const QString& filename) {
void GMainWindow::ShutdownGame() {
discord_rpc->Pause();
+
+ const bool was_recording = Core::Movie::GetInstance().IsRecordingInput();
+ Core::Movie::GetInstance().Shutdown();
+ if (was_recording) {
+ QMessageBox::information(this, "Movie Saved", "The movie is successfully saved.");
+ }
emu_thread->RequestStop();
// Release emu threads from any breakpoints
@@ -785,6 +798,9 @@ void GMainWindow::ShutdownGame() {
ui.action_Pause->setEnabled(false);
ui.action_Stop->setEnabled(false);
ui.action_Restart->setEnabled(false);
+ ui.action_Record_Movie->setEnabled(false);
+ ui.action_Play_Movie->setEnabled(false);
+ ui.action_Stop_Recording_Playback->setEnabled(false);
ui.action_Report_Compatibility->setEnabled(false);
render_window->hide();
if (game_list->isEmpty())
@@ -1059,6 +1075,9 @@ void GMainWindow::OnStartGame() {
ui.action_Pause->setEnabled(true);
ui.action_Stop->setEnabled(true);
ui.action_Restart->setEnabled(true);
+ ui.action_Record_Movie->setEnabled(true);
+ ui.action_Play_Movie->setEnabled(true);
+ ui.action_Stop_Recording_Playback->setEnabled(false);
ui.action_Report_Compatibility->setEnabled(true);
discord_rpc->Update();
@@ -1227,6 +1246,77 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
graphicsSurfaceViewerWidget->show();
}
+void GMainWindow::OnRecordMovie() {
+ const QString path =
+ QFileDialog::getSaveFileName(this, tr("Record Movie"), "", tr("Citra TAS Movie (*.ctm)"));
+ if (path.isEmpty())
+ return;
+ Core::Movie::GetInstance().StartRecording(path.toStdString());
+ ui.action_Record_Movie->setEnabled(false);
+ ui.action_Play_Movie->setEnabled(false);
+ ui.action_Stop_Recording_Playback->setEnabled(true);
+}
+
+void GMainWindow::OnPlayMovie() {
+ const QString path =
+ QFileDialog::getOpenFileName(this, tr("Play Movie"), "", tr("Citra TAS Movie (*.ctm)"));
+ if (path.isEmpty())
+ return;
+ using namespace Core;
+ Movie::ValidationResult result = Core::Movie::GetInstance().ValidateMovie(path.toStdString());
+ const QString revision_dismatch_text =
+ tr("The movie file you are trying to load was created on a different revision of Citra."
+ "
Citra has had some changes during the time, and the playback may desync or not "
+ "work as expected."
+ "
Are you sure you still want to load the movie file?");
+ const QString game_dismatch_text =
+ tr("The movie file you are trying to load was recorded with a different game."
+ "
The playback may not work as expected, and it may cause unexpected results."
+ "
Are you sure you still want to load the movie file?");
+ const QString invalid_movie_text =
+ tr("The movie file you are trying to load is invalid."
+ "
Either the file is corrupted, or Citra has had made some major changes to the "
+ "Movie module."
+ "
Please choose a different movie file and try again.");
+ int answer;
+ switch (result) {
+ case Movie::ValidationResult::RevisionDismatch:
+ answer = QMessageBox::question(this, tr("Revision Dismatch"), revision_dismatch_text,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if (answer != QMessageBox::Yes)
+ return;
+ break;
+ case Movie::ValidationResult::GameDismatch:
+ answer = QMessageBox::question(this, tr("Game Dismatch"), game_dismatch_text,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if (answer != QMessageBox::Yes)
+ return;
+ break;
+ case Movie::ValidationResult::Invalid:
+ QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text);
+ return;
+ default:
+ break;
+ }
+ Movie::GetInstance().StartPlayback(path.toStdString(), [this] {
+ QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted");
+ });
+ ui.action_Record_Movie->setEnabled(false);
+ ui.action_Play_Movie->setEnabled(false);
+ ui.action_Stop_Recording_Playback->setEnabled(true);
+}
+
+void GMainWindow::OnStopRecordingPlayback() {
+ const bool was_recording = Core::Movie::GetInstance().IsRecordingInput();
+ Core::Movie::GetInstance().Shutdown();
+ if (was_recording) {
+ QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved."));
+ }
+ ui.action_Record_Movie->setEnabled(true);
+ ui.action_Play_Movie->setEnabled(true);
+ ui.action_Stop_Recording_Playback->setEnabled(false);
+}
+
void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) {
status_bar_update_timer.stop();
@@ -1462,6 +1552,13 @@ void GMainWindow::OnLanguageChanged(const QString& locale) {
ui.action_Start->setText(tr("Continue"));
}
+void GMainWindow::OnMoviePlaybackCompleted() {
+ QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed."));
+ ui.action_Record_Movie->setEnabled(true);
+ ui.action_Play_Movie->setEnabled(true);
+ ui.action_Stop_Recording_Playback->setEnabled(false);
+}
+
void GMainWindow::SetupUIStrings() {
if (game_title.isEmpty()) {
setWindowTitle(tr("Citra %1").arg(Common::g_build_fullname));
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 4d35d202f..7fb26a43f 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -175,6 +175,9 @@ private slots:
void HideFullscreen();
void ToggleWindowMode();
void OnCreateGraphicsSurfaceViewer();
+ void OnRecordMovie();
+ void OnPlayMovie();
+ void OnStopRecordingPlayback();
void OnCoreError(Core::System::ResultStatus, std::string);
/// Called whenever a user selects Help->About Citra
void OnMenuAboutCitra();
@@ -184,6 +187,7 @@ private slots:
void OnLanguageChanged(const QString& locale);
private:
+ Q_INVOKABLE void OnMoviePlaybackCompleted();
void UpdateStatusBar();
void LoadTranslation();
void SetupUIStrings();
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index bee688600..01590f882 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -107,6 +107,14 @@
+
@@ -243,6 +252,30 @@
Create Pica Surface Viewer
+
+
+ false
+
+
+ Record Movie
+
+
+
+
+ false
+
+
+ Play Movie
+
+
+
+
+ false
+
+
+ Stop Recording / Playback
+
+
true