mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	citra_qt, movie: allow recording/playback before emulation starts
This commit is contained in:
		
							parent
							
								
									a9ad8daf47
								
							
						
					
					
						commit
						3b459f6eb3
					
				
					 7 changed files with 126 additions and 30 deletions
				
			
		|  | @ -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, | void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, | ||||||
|                                              GameListDir* parent_dir) { |                                              GameListDir* parent_dir) { | ||||||
|     const auto callback = [this, recursion, parent_dir](u64* num_entries_out, |     const auto callback = [this, recursion, parent_dir](u64* num_entries_out, | ||||||
|  |  | ||||||
|  | @ -59,6 +59,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     QStandardItemModel* GetModel() const; |     QStandardItemModel* GetModel() const; | ||||||
| 
 | 
 | ||||||
|  |     QString FindGameByProgramID(u64 program_id); | ||||||
|  | 
 | ||||||
|     static const QStringList supported_file_extensions; |     static const QStringList supported_file_extensions; | ||||||
| 
 | 
 | ||||||
| signals: | signals: | ||||||
|  | @ -91,6 +93,8 @@ private: | ||||||
|     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); |     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); | ||||||
|     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); |     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); | ||||||
| 
 | 
 | ||||||
|  |     QString FindGameByProgramID(QStandardItem* current_item, u64 program_id); | ||||||
|  | 
 | ||||||
|     GameListSearchField* search_field; |     GameListSearchField* search_field; | ||||||
|     GMainWindow* main_window = nullptr; |     GMainWindow* main_window = nullptr; | ||||||
|     QVBoxLayout* layout = nullptr; |     QVBoxLayout* layout = nullptr; | ||||||
|  |  | ||||||
|  | @ -769,6 +769,9 @@ void GMainWindow::ShutdownGame() { | ||||||
|     Core::Movie::GetInstance().Shutdown(); |     Core::Movie::GetInstance().Shutdown(); | ||||||
|     if (was_recording) { |     if (was_recording) { | ||||||
|         QMessageBox::information(this, "Movie Saved", "The movie is successfully saved."); |         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(); |     emu_thread->RequestStop(); | ||||||
| 
 | 
 | ||||||
|  | @ -798,9 +801,6 @@ void GMainWindow::ShutdownGame() { | ||||||
|     ui.action_Pause->setEnabled(false); |     ui.action_Pause->setEnabled(false); | ||||||
|     ui.action_Stop->setEnabled(false); |     ui.action_Stop->setEnabled(false); | ||||||
|     ui.action_Restart->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); |     ui.action_Report_Compatibility->setEnabled(false); | ||||||
|     render_window->hide(); |     render_window->hide(); | ||||||
|     if (game_list->isEmpty()) |     if (game_list->isEmpty()) | ||||||
|  | @ -1064,6 +1064,13 @@ void GMainWindow::OnMenuRecentFile() { | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnStartGame() { | void GMainWindow::OnStartGame() { | ||||||
|     Camera::QtMultimediaCameraHandler::ResumeCameras(); |     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); |     emu_thread->SetRunning(true); | ||||||
|     qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus"); |     qRegisterMetaType<Core::System::ResultStatus>("Core::System::ResultStatus"); | ||||||
|     qRegisterMetaType<std::string>("std::string"); |     qRegisterMetaType<std::string>("std::string"); | ||||||
|  | @ -1075,9 +1082,6 @@ void GMainWindow::OnStartGame() { | ||||||
|     ui.action_Pause->setEnabled(true); |     ui.action_Pause->setEnabled(true); | ||||||
|     ui.action_Stop->setEnabled(true); |     ui.action_Stop->setEnabled(true); | ||||||
|     ui.action_Restart->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); |     ui.action_Report_Compatibility->setEnabled(true); | ||||||
| 
 | 
 | ||||||
|     discord_rpc->Update(); |     discord_rpc->Update(); | ||||||
|  | @ -1251,19 +1255,23 @@ void GMainWindow::OnRecordMovie() { | ||||||
|         QFileDialog::getSaveFileName(this, tr("Record Movie"), "", tr("Citra TAS Movie (*.ctm)")); |         QFileDialog::getSaveFileName(this, tr("Record Movie"), "", tr("Citra TAS Movie (*.ctm)")); | ||||||
|     if (path.isEmpty()) |     if (path.isEmpty()) | ||||||
|         return; |         return; | ||||||
|  |     if (emulation_running) { | ||||||
|         Core::Movie::GetInstance().StartRecording(path.toStdString()); |         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_Record_Movie->setEnabled(false); | ||||||
|     ui.action_Play_Movie->setEnabled(false); |     ui.action_Play_Movie->setEnabled(false); | ||||||
|     ui.action_Stop_Recording_Playback->setEnabled(true); |     ui.action_Stop_Recording_Playback->setEnabled(true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnPlayMovie() { | bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) { | ||||||
|     const QString path = |  | ||||||
|         QFileDialog::getOpenFileName(this, tr("Play Movie"), "", tr("Citra TAS Movie (*.ctm)")); |  | ||||||
|     if (path.isEmpty()) |  | ||||||
|         return; |  | ||||||
|     using namespace Core; |     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 = |     const QString revision_dismatch_text = | ||||||
|         tr("The movie file you are trying to load was created on a different revision of Citra." |         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 " |            "<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, |         answer = QMessageBox::question(this, tr("Revision Dismatch"), revision_dismatch_text, | ||||||
|                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No); |                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||||
|         if (answer != QMessageBox::Yes) |         if (answer != QMessageBox::Yes) | ||||||
|             return; |             return false; | ||||||
|         break; |         break; | ||||||
|     case Movie::ValidationResult::GameDismatch: |     case Movie::ValidationResult::GameDismatch: | ||||||
|         answer = QMessageBox::question(this, tr("Game Dismatch"), game_dismatch_text, |         answer = QMessageBox::question(this, tr("Game Dismatch"), game_dismatch_text, | ||||||
|                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No); |                                        QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||||
|         if (answer != QMessageBox::Yes) |         if (answer != QMessageBox::Yes) | ||||||
|             return; |             return false; | ||||||
|         break; |         break; | ||||||
|     case Movie::ValidationResult::Invalid: |     case Movie::ValidationResult::Invalid: | ||||||
|         QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text); |         QMessageBox::critical(this, tr("Invalid Movie File"), invalid_movie_text); | ||||||
|         return; |         return false; | ||||||
|     default: |     default: | ||||||
|         break; |         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"); |         QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted"); | ||||||
|     }); |     }); | ||||||
|     ui.action_Record_Movie->setEnabled(false); |     ui.action_Record_Movie->setEnabled(false); | ||||||
|  | @ -1307,10 +1350,17 @@ void GMainWindow::OnPlayMovie() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnStopRecordingPlayback() { | 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(); |         const bool was_recording = Core::Movie::GetInstance().IsRecordingInput(); | ||||||
|         Core::Movie::GetInstance().Shutdown(); |         Core::Movie::GetInstance().Shutdown(); | ||||||
|         if (was_recording) { |         if (was_recording) { | ||||||
|         QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); |             QMessageBox::information(this, tr("Movie Saved"), | ||||||
|  |                                      tr("The movie is successfully saved.")); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     ui.action_Record_Movie->setEnabled(true); |     ui.action_Record_Movie->setEnabled(true); | ||||||
|     ui.action_Play_Movie->setEnabled(true); |     ui.action_Play_Movie->setEnabled(true); | ||||||
|  |  | ||||||
|  | @ -187,6 +187,7 @@ private slots: | ||||||
|     void OnLanguageChanged(const QString& locale); |     void OnLanguageChanged(const QString& locale); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  |     bool ValidateMovie(const QString& path, u64 program_id = 0); | ||||||
|     Q_INVOKABLE void OnMoviePlaybackCompleted(); |     Q_INVOKABLE void OnMoviePlaybackCompleted(); | ||||||
|     void UpdateStatusBar(); |     void UpdateStatusBar(); | ||||||
|     void LoadTranslation(); |     void LoadTranslation(); | ||||||
|  | @ -218,6 +219,10 @@ private: | ||||||
|     // The path to the game currently running
 |     // The path to the game currently running
 | ||||||
|     QString game_path; |     QString game_path; | ||||||
| 
 | 
 | ||||||
|  |     // Movie
 | ||||||
|  |     bool movie_record_on_start = false; | ||||||
|  |     QString movie_record_path; | ||||||
|  | 
 | ||||||
|     // Debugger panes
 |     // Debugger panes
 | ||||||
|     ProfilerWidget* profilerWidget; |     ProfilerWidget* profilerWidget; | ||||||
|     MicroProfileDialog* microProfileDialog; |     MicroProfileDialog* microProfileDialog; | ||||||
|  |  | ||||||
|  | @ -254,7 +254,7 @@ | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Record_Movie"> |   <action name="action_Record_Movie"> | ||||||
|    <property name="enabled"> |    <property name="enabled"> | ||||||
|     <bool>false</bool> |     <bool>true</bool> | ||||||
|    </property> |    </property> | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Record Movie</string> |     <string>Record Movie</string> | ||||||
|  | @ -262,7 +262,7 @@ | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Play_Movie"> |   <action name="action_Play_Movie"> | ||||||
|    <property name="enabled"> |    <property name="enabled"> | ||||||
|     <bool>false</bool> |     <bool>true</bool> | ||||||
|    </property> |    </property> | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Play Movie</string> |     <string>Play Movie</string> | ||||||
|  |  | ||||||
|  | @ -344,7 +344,7 @@ void Movie::Record(const Service::IR::ExtraHIDResponse& extra_hid_response) { | ||||||
|     Record(s); |     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) { |     if (header_magic_bytes != header.filetype) { | ||||||
|         LOG_ERROR(Movie, "Playback file does not have valid header"); |         LOG_ERROR(Movie, "Playback file does not have valid header"); | ||||||
|         return ValidationResult::Invalid; |         return ValidationResult::Invalid; | ||||||
|  | @ -354,7 +354,7 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const { | ||||||
|         Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false); |         Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false); | ||||||
|     revision = Common::ToLower(revision); |     revision = Common::ToLower(revision); | ||||||
| 
 | 
 | ||||||
|     u64 program_id; |     if (!program_id) | ||||||
|         Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); |         Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); | ||||||
|     if (program_id != header.program_id) { |     if (program_id != header.program_id) { | ||||||
|         LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id"); |         LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id"); | ||||||
|  | @ -424,7 +424,7 @@ void Movie::StartRecording(const std::string& movie_file) { | ||||||
|     record_movie_file = 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); |     LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); | ||||||
|     FileUtil::IOFile save_record(movie_file, "rb"); |     FileUtil::IOFile save_record(movie_file, "rb"); | ||||||
|     const u64 size = save_record.GetSize(); |     const u64 size = save_record.GetSize(); | ||||||
|  | @ -435,7 +435,25 @@ Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) cons | ||||||
| 
 | 
 | ||||||
|     CTMHeader header; |     CTMHeader header; | ||||||
|     save_record.ReadArray(&header, 1); |     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() { | void Movie::Shutdown() { | ||||||
|  |  | ||||||
|  | @ -44,7 +44,8 @@ public: | ||||||
|     void StartPlayback(const std::string& movie_file, |     void StartPlayback(const std::string& movie_file, | ||||||
|                        std::function<void()> completion_callback = {}); |                        std::function<void()> completion_callback = {}); | ||||||
|     void StartRecording(const std::string& movie_file); |     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(); |     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::PadState& pad_state, const s16& c_stick_x, const s16& c_stick_y); | ||||||
|     void Record(const Service::IR::ExtraHIDResponse& extra_hid_response); |     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(); |     void SaveMovie(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue