mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Merge pull request #3922 from zhaowenlan1779/qt-movie
movie: Add Qt Movie feature
This commit is contained in:
		
						commit
						13262c187c
					
				
					 12 changed files with 309 additions and 47 deletions
				
			
		|  | @ -230,6 +230,8 @@ void Config::ReadValues() { | |||
|     qt_config->beginGroup("Paths"); | ||||
|     UISettings::values.roms_path = ReadSetting("romsPath").toString(); | ||||
|     UISettings::values.symbols_path = ReadSetting("symbolsPath").toString(); | ||||
|     UISettings::values.movie_record_path = ReadSetting("movieRecordPath").toString(); | ||||
|     UISettings::values.movie_playback_path = ReadSetting("moviePlaybackPath").toString(); | ||||
|     UISettings::values.game_dir_deprecated = ReadSetting("gameListRootDir", ".").toString(); | ||||
|     UISettings::values.game_dir_deprecated_deepscan = | ||||
|         ReadSetting("gameListDeepScan", false).toBool(); | ||||
|  | @ -461,6 +463,8 @@ void Config::SaveValues() { | |||
|     qt_config->beginGroup("Paths"); | ||||
|     WriteSetting("romsPath", UISettings::values.roms_path); | ||||
|     WriteSetting("symbolsPath", UISettings::values.symbols_path); | ||||
|     WriteSetting("movieRecordPath", UISettings::values.movie_record_path); | ||||
|     WriteSetting("moviePlaybackPath", UISettings::values.movie_playback_path); | ||||
|     qt_config->beginWriteArray("gamedirs"); | ||||
|     for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { | ||||
|         qt_config->setArrayIndex(i); | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -527,6 +528,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/")); }); | ||||
|  | @ -775,6 +782,7 @@ void GMainWindow::BootGame(const QString& filename) { | |||
| 
 | ||||
| void GMainWindow::ShutdownGame() { | ||||
|     discord_rpc->Pause(); | ||||
|     OnStopRecordingPlayback(); | ||||
|     emu_thread->RequestStop(); | ||||
| 
 | ||||
|     // Release emu threads from any breakpoints
 | ||||
|  | @ -1066,6 +1074,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"); | ||||
|  | @ -1245,6 +1260,127 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { | |||
|     graphicsSurfaceViewerWidget->show(); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnRecordMovie() { | ||||
|     const QString path = | ||||
|         QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path, | ||||
|                                      tr("Citra TAS Movie (*.ctm)")); | ||||
|     if (path.isEmpty()) | ||||
|         return; | ||||
|     UISettings::values.movie_record_path = QFileInfo(path).path(); | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) { | ||||
|     using namespace Core; | ||||
|     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 " | ||||
|            "work as expected." | ||||
|            "<br/><br/>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." | ||||
|            "<br/>The playback may not work as expected, and it may cause unexpected results." | ||||
|            "<br/><br/>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." | ||||
|            "<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."); | ||||
|     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 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 false; | ||||
|         break; | ||||
|     case Movie::ValidationResult::Invalid: | ||||
|         QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text); | ||||
|         return false; | ||||
|     default: | ||||
|         break; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnPlayMovie() { | ||||
|     const QString path = | ||||
|         QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path, | ||||
|                                      tr("Citra TAS Movie (*.ctm)")); | ||||
|     if (path.isEmpty()) | ||||
|         return; | ||||
|     UISettings::values.movie_playback_path = QFileInfo(path).path(); | ||||
| 
 | ||||
|     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); | ||||
|     ui.action_Play_Movie->setEnabled(false); | ||||
|     ui.action_Stop_Recording_Playback->setEnabled(true); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnStopRecordingPlayback() { | ||||
|     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); | ||||
|     ui.action_Stop_Recording_Playback->setEnabled(false); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::UpdateStatusBar() { | ||||
|     if (emu_thread == nullptr) { | ||||
|         status_bar_update_timer.stop(); | ||||
|  | @ -1480,6 +1616,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)); | ||||
|  |  | |||
|  | @ -176,6 +176,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(); | ||||
|  | @ -185,6 +188,8 @@ 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(); | ||||
|     void SetupUIStrings(); | ||||
|  | @ -215,6 +220,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; | ||||
|  |  | |||
|  | @ -107,6 +107,14 @@ | |||
|     <addaction name="separator"/> | ||||
|     <addaction name="menu_View_Debugging"/> | ||||
|    </widget> | ||||
|    <widget class="QMenu" name="menu_Movie"> | ||||
|     <property name="title"> | ||||
|      <string>Movie</string> | ||||
|     </property> | ||||
|     <addaction name="action_Record_Movie"/> | ||||
|     <addaction name="action_Play_Movie"/> | ||||
|     <addaction name="action_Stop_Recording_Playback"/> | ||||
|    </widget> | ||||
|    <widget class="QMenu" name="menu_Multiplayer"> | ||||
|     <property name="enabled"> | ||||
|      <bool>true</bool> | ||||
|  | @ -136,6 +144,7 @@ | |||
|    <addaction name="menu_File"/> | ||||
|    <addaction name="menu_Emulation"/> | ||||
|    <addaction name="menu_View"/> | ||||
|    <addaction name="menu_Movie"/> | ||||
|    <addaction name="menu_Multiplayer"/> | ||||
|    <addaction name="menu_Help"/> | ||||
|   </widget> | ||||
|  | @ -243,6 +252,30 @@ | |||
|     <string>Create Pica Surface Viewer</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="action_Record_Movie"> | ||||
|    <property name="enabled"> | ||||
|     <bool>true</bool> | ||||
|    </property> | ||||
|    <property name="text"> | ||||
|     <string>Record Movie</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="action_Play_Movie"> | ||||
|    <property name="enabled"> | ||||
|     <bool>true</bool> | ||||
|    </property> | ||||
|    <property name="text"> | ||||
|     <string>Play Movie</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="action_Stop_Recording_Playback"> | ||||
|    <property name="enabled"> | ||||
|     <bool>false</bool> | ||||
|    </property> | ||||
|    <property name="text"> | ||||
|     <string>Stop Recording / Playback</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="action_View_Lobby"> | ||||
|    <property name="enabled"> | ||||
|     <bool>true</bool> | ||||
|  |  | |||
|  | @ -63,6 +63,8 @@ struct Values { | |||
| 
 | ||||
|     QString roms_path; | ||||
|     QString symbols_path; | ||||
|     QString movie_record_path; | ||||
|     QString movie_playback_path; | ||||
|     QString game_dir_deprecated; | ||||
|     bool game_dir_deprecated_deepscan; | ||||
|     QList<UISettings::GameDir> game_dirs; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue