From 3b459f6eb3409a2b4bcaddecd3a12a01a9299287 Mon Sep 17 00:00:00 2001
From: zhupengfei <zhupengfei321@sina.cn>
Date: Wed, 8 Aug 2018 23:42:23 +0800
Subject: [PATCH] citra_qt, movie: allow recording/playback before emulation
 starts

---
 src/citra_qt/game_list.cpp | 18 ++++++++
 src/citra_qt/game_list.h   |  4 ++
 src/citra_qt/main.cpp      | 92 +++++++++++++++++++++++++++++---------
 src/citra_qt/main.h        |  5 +++
 src/citra_qt/main.ui       |  4 +-
 src/core/movie.cpp         | 28 +++++++++---
 src/core/movie.h           |  5 ++-
 7 files changed, 126 insertions(+), 30 deletions(-)

diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index 544299bdc..32da663f4 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -628,6 +628,24 @@ void GameList::RefreshGameDirectory() {
     }
 }
 
+QString GameList::FindGameByProgramID(u64 program_id) {
+    return FindGameByProgramID(item_model->invisibleRootItem(), program_id);
+}
+
+QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_id) {
+    if (current_item->type() == static_cast<int>(GameListItemType::Game) &&
+        current_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
+        return current_item->data(GameListItemPath::FullPathRole).toString();
+    } else if (current_item->hasChildren()) {
+        for (int child_id = 0; child_id < current_item->rowCount(); child_id++) {
+            QString path = FindGameByProgramID(current_item->child(child_id, 0), program_id);
+            if (!path.isEmpty())
+                return path;
+        }
+    }
+    return "";
+}
+
 void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
                                              GameListDir* parent_dir) {
     const auto callback = [this, recursion, parent_dir](u64* num_entries_out,
diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h
index a0d383c3e..f8102e0b9 100644
--- a/src/citra_qt/game_list.h
+++ b/src/citra_qt/game_list.h
@@ -59,6 +59,8 @@ public:
 
     QStandardItemModel* GetModel() const;
 
+    QString FindGameByProgramID(u64 program_id);
+
     static const QStringList supported_file_extensions;
 
 signals:
@@ -91,6 +93,8 @@ private:
     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
 
+    QString FindGameByProgramID(QStandardItem* current_item, u64 program_id);
+
     GameListSearchField* search_field;
     GMainWindow* main_window = nullptr;
     QVBoxLayout* layout = nullptr;
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 7c5ab9422..0c5fe68c3 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -769,6 +769,9 @@ void GMainWindow::ShutdownGame() {
     Core::Movie::GetInstance().Shutdown();
     if (was_recording) {
         QMessageBox::information(this, "Movie Saved", "The movie is successfully saved.");
+        ui.action_Record_Movie->setEnabled(true);
+        ui.action_Play_Movie->setEnabled(true);
+        ui.action_Stop_Recording_Playback->setEnabled(false);
     }
     emu_thread->RequestStop();
 
@@ -798,9 +801,6 @@ 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())
@@ -1064,6 +1064,13 @@ void GMainWindow::OnMenuRecentFile() {
 
 void GMainWindow::OnStartGame() {
     Camera::QtMultimediaCameraHandler::ResumeCameras();
+
+    if (movie_record_on_start) {
+        Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString());
+        movie_record_on_start = false;
+        movie_record_path.clear();
+    }
+
     emu_thread->SetRunning(true);
     qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus");
     qRegisterMetaType<std::string>("std::string");
@@ -1075,9 +1082,6 @@ 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();
@@ -1251,19 +1255,23 @@ void GMainWindow::OnRecordMovie() {
         QFileDialog::getSaveFileName(this, tr("Record Movie"), "", tr("Citra TAS Movie (*.ctm)"));
     if (path.isEmpty())
         return;
-    Core::Movie::GetInstance().StartRecording(path.toStdString());
+    if (emulation_running) {
+        Core::Movie::GetInstance().StartRecording(path.toStdString());
+    } else {
+        movie_record_on_start = true;
+        movie_record_path = path;
+        QMessageBox::information(this, tr("Record Movie"),
+                                 tr("Recording will start once you boot a game."));
+    }
     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;
+bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) {
     using namespace Core;
-    Movie::ValidationResult result = Core::Movie::GetInstance().ValidateMovie(path.toStdString());
+    Movie::ValidationResult result =
+        Core::Movie::GetInstance().ValidateMovie(path.toStdString(), program_id);
     const QString revision_dismatch_text =
         tr("The movie file you are trying to load was created on a different revision of Citra."
            "<br/>Citra has had some changes during the time, and the playback may desync or not "
@@ -1284,21 +1292,56 @@ void GMainWindow::OnPlayMovie() {
         answer = QMessageBox::question(this, tr("Revision Dismatch"), revision_dismatch_text,
                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
         if (answer != QMessageBox::Yes)
-            return;
+            return false;
         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;
+            return false;
         break;
     case Movie::ValidationResult::Invalid:
         QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text);
-        return;
+        return false;
     default:
         break;
     }
-    Movie::GetInstance().StartPlayback(path.toStdString(), [this] {
+    return true;
+}
+
+void GMainWindow::OnPlayMovie() {
+    const QString path =
+        QFileDialog::getOpenFileName(this, tr("Play Movie"), "", tr("Citra TAS Movie (*.ctm)"));
+    if (path.isEmpty())
+        return;
+
+    if (emulation_running) {
+        if (!ValidateMovie(path))
+            return;
+    } else {
+        const QString invalid_movie_text =
+            tr("The movie file you are trying to load is invalid."
+               "<br/>Either the file is corrupted, or Citra has had made some major changes to the "
+               "Movie module."
+               "<br/>Please choose a different movie file and try again.");
+        u64 program_id = Core::Movie::GetInstance().GetMovieProgramID(path.toStdString());
+        if (!program_id) {
+            QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text);
+            return;
+        }
+        QString game_path = game_list->FindGameByProgramID(program_id);
+        if (game_path.isEmpty()) {
+            QMessageBox::warning(this, tr("Game Not Found"),
+                                 tr("The movie you are trying to play is from a game that is not "
+                                    "in the game list. If you own the game, please add the game "
+                                    "folder to the game list and try to play the movie again."));
+            return;
+        }
+        if (!ValidateMovie(path, program_id))
+            return;
+        BootGame(game_path);
+    }
+    Core::Movie::GetInstance().StartPlayback(path.toStdString(), [this] {
         QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted");
     });
     ui.action_Record_Movie->setEnabled(false);
@@ -1307,10 +1350,17 @@ void GMainWindow::OnPlayMovie() {
 }
 
 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."));
+    if (movie_record_on_start) {
+        QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled."));
+        movie_record_on_start = false;
+        movie_record_path.clear();
+    } else {
+        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);
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 7fb26a43f..3bdbf9aaa 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -187,6 +187,7 @@ private slots:
     void OnLanguageChanged(const QString& locale);
 
 private:
+    bool ValidateMovie(const QString& path, u64 program_id = 0);
     Q_INVOKABLE void OnMoviePlaybackCompleted();
     void UpdateStatusBar();
     void LoadTranslation();
@@ -218,6 +219,10 @@ private:
     // The path to the game currently running
     QString game_path;
 
+    // Movie
+    bool movie_record_on_start = false;
+    QString movie_record_path;
+
     // Debugger panes
     ProfilerWidget* profilerWidget;
     MicroProfileDialog* microProfileDialog;
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index 01590f882..ef0f1aae2 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -254,7 +254,7 @@
   </action>
   <action name="action_Record_Movie">
    <property name="enabled">
-    <bool>false</bool>
+    <bool>true</bool>
    </property>
    <property name="text">
     <string>Record Movie</string>
@@ -262,7 +262,7 @@
   </action>
   <action name="action_Play_Movie">
    <property name="enabled">
-    <bool>false</bool>
+    <bool>true</bool>
    </property>
    <property name="text">
     <string>Play Movie</string>
diff --git a/src/core/movie.cpp b/src/core/movie.cpp
index 0372d94da..04b470782 100644
--- a/src/core/movie.cpp
+++ b/src/core/movie.cpp
@@ -344,7 +344,7 @@ void Movie::Record(const Service::IR::ExtraHIDResponse& extra_hid_response) {
     Record(s);
 }
 
-Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const {
+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");
         return ValidationResult::Invalid;
@@ -354,8 +354,8 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const {
         Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false);
     revision = Common::ToLower(revision);
 
-    u64 program_id;
-    Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
+    if (!program_id)
+        Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
     if (program_id != header.program_id) {
         LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id");
         return ValidationResult::GameDismatch;
@@ -424,7 +424,7 @@ void Movie::StartRecording(const std::string& movie_file) {
     record_movie_file = movie_file;
 }
 
-Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) const {
+Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const {
     LOG_INFO(Movie, "Validating Movie file '{}'", movie_file);
     FileUtil::IOFile save_record(movie_file, "rb");
     const u64 size = save_record.GetSize();
@@ -435,7 +435,25 @@ Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) cons
 
     CTMHeader header;
     save_record.ReadArray(&header, 1);
-    return ValidateHeader(header);
+    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;
+    }
+
+    CTMHeader header;
+    save_record.ReadArray(&header, 1);
+
+    if (header_magic_bytes != header.filetype) {
+        return 0;
+    }
+
+    return static_cast<u64>(header.program_id);
 }
 
 void Movie::Shutdown() {
diff --git a/src/core/movie.h b/src/core/movie.h
index 4a0d96b81..6923db3d5 100644
--- a/src/core/movie.h
+++ b/src/core/movie.h
@@ -44,7 +44,8 @@ public:
     void StartPlayback(const std::string& movie_file,
                        std::function<void()> completion_callback = {});
     void StartRecording(const std::string& movie_file);
-    ValidationResult ValidateMovie(const std::string& movie_file) const;
+    ValidationResult ValidateMovie(const std::string& movie_file, u64 program_id = 0) const;
+    u64 GetMovieProgramID(const std::string& movie_file) const;
 
     void Shutdown();
 
@@ -111,7 +112,7 @@ private:
     void Record(const Service::IR::PadState& pad_state, const s16& c_stick_x, const s16& c_stick_y);
     void Record(const Service::IR::ExtraHIDResponse& extra_hid_response);
 
-    ValidationResult ValidateHeader(const CTMHeader& header) const;
+    ValidationResult ValidateHeader(const CTMHeader& header, u64 program_id = 0) const;
 
     void SaveMovie();