diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 544299bdc6..32da663f45 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(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 a0d383c3eb..f8102e0b9d 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 7c5ab9422f..0c5fe68c31 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"); qRegisterMetaType("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." "
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." + "
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."); + 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 7fb26a43f8..3bdbf9aaaf 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 01590f8829..ef0f1aae2b 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -254,7 +254,7 @@ - false + true Record Movie @@ -262,7 +262,7 @@ - false + true Play Movie diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 0372d94da8..04b4707823 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(header.program_id); } void Movie::Shutdown() { diff --git a/src/core/movie.h b/src/core/movie.h index 4a0d96b81f..6923db3d5a 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 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();