From 0f44f7b481fa3b35c7b383e7f7c4f4777d617c86 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sat, 7 Jul 2018 22:30:37 +0800 Subject: [PATCH 1/4] core/movie: Movie refactor, add a completion callback --- src/citra/citra.cpp | 12 +++++-- src/core/core.cpp | 2 -- src/core/movie.cpp | 84 +++++++++++++++++++++++++++------------------ src/core/movie.h | 22 ++++++++---- src/core/settings.h | 4 --- 5 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index 8ecc1b202..30f3a5493 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -39,6 +39,7 @@ #include "core/gdbstub/gdbstub.h" #include "core/hle/service/am/am.h" #include "core/loader/loader.h" +#include "core/movie.h" #include "core/settings.h" #include "network/network.h" @@ -268,8 +269,6 @@ int main(int argc, char** argv) { // Apply the command line arguments Settings::values.gdbstub_port = gdb_port; Settings::values.use_gdbstub = use_gdbstub; - Settings::values.movie_play = std::move(movie_play); - Settings::values.movie_record = std::move(movie_record); Settings::Apply(); // Register frontend applets @@ -327,9 +326,18 @@ int main(int argc, char** argv) { } } + if (!movie_play.empty()) { + Core::Movie::GetInstance().StartPlayback(movie_play); + } + if (!movie_record.empty()) { + Core::Movie::GetInstance().StartRecording(movie_record); + } + while (emu_window->IsOpen()) { system.RunLoop(); } + Core::Movie::GetInstance().Shutdown(); + return 0; } diff --git a/src/core/core.cpp b/src/core/core.cpp index 013ef4c2e..ee4cd2060 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -176,7 +176,6 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) { Kernel::Init(system_mode); Service::Init(service_manager); GDBStub::Init(); - Movie::GetInstance().Init(); if (!VideoCore::Init(emu_window)) { return ResultStatus::ErrorVideoCore; @@ -214,7 +213,6 @@ void System::Shutdown() { perf_results.frametime * 1000.0); // Shutdown emulation session - Movie::GetInstance().Shutdown(); GDBStub::Shutdown(); VideoCore::Shutdown(); Service::Shutdown(); diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 16b3da7ca..0372d94da 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -118,10 +118,10 @@ struct CTMHeader { static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) -bool Movie::IsPlayingInput() { +bool Movie::IsPlayingInput() const { return play_mode == PlayMode::Playing; } -bool Movie::IsRecordingInput() { +bool Movie::IsRecordingInput() const { return play_mode == PlayMode::Recording; } @@ -129,6 +129,7 @@ void Movie::CheckInputEnd() { if (current_byte + sizeof(ControllerState) > recorded_input.size()) { LOG_INFO(Movie, "Playback finished"); play_mode = PlayMode::None; + playback_completion_callback(); } } @@ -343,33 +344,35 @@ void Movie::Record(const Service::IR::ExtraHIDResponse& extra_hid_response) { Record(s); } -bool Movie::ValidateHeader(const CTMHeader& header) { +Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const { if (header_magic_bytes != header.filetype) { LOG_ERROR(Movie, "Playback file does not have valid header"); - return false; + return ValidationResult::Invalid; } std::string revision = Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false); revision = Common::ToLower(revision); - if (revision != Common::g_scm_rev) { - LOG_WARNING(Movie, - "This movie was created on a different version of Citra, playback may desync"); - } - u64 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; } - return true; + if (revision != Common::g_scm_rev) { + LOG_WARNING(Movie, + "This movie was created on a different version of Citra, playback may desync"); + return ValidationResult::RevisionDismatch; + } + + return ValidationResult::OK; } void Movie::SaveMovie() { - LOG_INFO(Movie, "Saving movie"); - FileUtil::IOFile save_record(Settings::values.movie_record, "wb"); + LOG_INFO(Movie, "Saving recorded movie to '{}'", record_movie_file); + FileUtil::IOFile save_record(record_movie_file, "wb"); if (!save_record.IsGood()) { LOG_ERROR(Movie, "Unable to open file to save movie"); @@ -394,31 +397,45 @@ void Movie::SaveMovie() { } } -void Movie::Init() { - if (!Settings::values.movie_play.empty()) { - LOG_INFO(Movie, "Loading Movie for playback"); - FileUtil::IOFile save_record(Settings::values.movie_play, "rb"); - u64 size = save_record.GetSize(); +void Movie::StartPlayback(const std::string& movie_file, + std::function completion_callback) { + LOG_INFO(Movie, "Loading Movie for playback"); + FileUtil::IOFile save_record(movie_file, "rb"); + const u64 size = save_record.GetSize(); - if (save_record.IsGood() && size > sizeof(CTMHeader)) { - CTMHeader header; - save_record.ReadArray(&header, 1); - if (ValidateHeader(header)) { - play_mode = PlayMode::Playing; - recorded_input.resize(size - sizeof(CTMHeader)); - save_record.ReadArray(recorded_input.data(), recorded_input.size()); - current_byte = 0; - } - } else { - LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", - Settings::values.movie_play); + if (save_record.IsGood() && size > sizeof(CTMHeader)) { + CTMHeader header; + save_record.ReadArray(&header, 1); + if (ValidateHeader(header) != ValidationResult::Invalid) { + play_mode = PlayMode::Playing; + recorded_input.resize(size - sizeof(CTMHeader)); + save_record.ReadArray(recorded_input.data(), recorded_input.size()); + current_byte = 0; + playback_completion_callback = completion_callback; } + } else { + LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file); + } +} + +void Movie::StartRecording(const std::string& movie_file) { + LOG_INFO(Movie, "Enabling Movie recording"); + play_mode = PlayMode::Recording; + record_movie_file = movie_file; +} + +Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) const { + LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); + FileUtil::IOFile save_record(movie_file, "rb"); + const u64 size = save_record.GetSize(); + + if (!save_record || size <= sizeof(CTMHeader)) { + return ValidationResult::Invalid; } - if (!Settings::values.movie_record.empty()) { - LOG_INFO(Movie, "Enabling Movie recording"); - play_mode = PlayMode::Recording; - } + CTMHeader header; + save_record.ReadArray(&header, 1); + return ValidateHeader(header); } void Movie::Shutdown() { @@ -428,6 +445,7 @@ void Movie::Shutdown() { play_mode = PlayMode::None; recorded_input.resize(0); + record_movie_file.clear(); current_byte = 0; } diff --git a/src/core/movie.h b/src/core/movie.h index 1f1ed6158..4a0d96b81 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -4,6 +4,7 @@ #pragma once +#include #include "common/common_types.h" namespace Service { @@ -26,6 +27,12 @@ enum class PlayMode; class Movie { public: + enum class ValidationResult { + OK, + RevisionDismatch, + GameDismatch, + Invalid, + }; /** * Gets the instance of the Movie singleton class. * @returns Reference to the instance of the Movie singleton class. @@ -34,7 +41,10 @@ public: return s_instance; } - void Init(); + 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; void Shutdown(); @@ -74,14 +84,12 @@ public: * When playing: Replaces the given input states with the ones stored in the playback file */ void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response); + bool IsPlayingInput() const; + bool IsRecordingInput() const; private: static Movie s_instance; - bool IsPlayingInput(); - - bool IsRecordingInput(); - void CheckInputEnd(); template @@ -103,12 +111,14 @@ 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); - bool ValidateHeader(const CTMHeader& header); + ValidationResult ValidateHeader(const CTMHeader& header) const; void SaveMovie(); PlayMode play_mode; + std::string record_movie_file; std::vector recorded_input; + std::function playback_completion_callback; size_t current_byte = 0; }; } // namespace Core \ No newline at end of file diff --git a/src/core/settings.h b/src/core/settings.h index 762246c33..90b5be6f8 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -156,10 +156,6 @@ struct Values { std::string log_filter; std::unordered_map lle_modules; - // Movie - std::string movie_play; - std::string movie_record; - // WebService bool enable_telemetry; std::string telemetry_endpoint_url; From a9ad8daf47b0bada921ef98000923743a957f0c2 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 8 Jul 2018 13:55:32 +0800 Subject: [PATCH 2/4] citra_qt: Add movie frontend --- src/citra_qt/main.cpp | 97 +++++++++++++++++++++++++++++++++++++++++++ src/citra_qt/main.h | 4 ++ src/citra_qt/main.ui | 33 +++++++++++++++ 3 files changed, 134 insertions(+) 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 @@ + + + Movie + + + + + true @@ -136,6 +144,7 @@ + @@ -243,6 +252,30 @@ Create Pica Surface Viewer + + + false + + + Record Movie + + + + + false + + + Play Movie + + + + + false + + + Stop Recording / Playback + + true From 3b459f6eb3409a2b4bcaddecd3a12a01a9299287 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Wed, 8 Aug 2018 23:42:23 +0800 Subject: [PATCH 3/4] 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(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"); 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 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 @@
- 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 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(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 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(); From 9d056282dfe789bc395972416b96aecbd756a80b Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Sun, 12 Aug 2018 20:23:59 +0800 Subject: [PATCH 4/4] citra_qt: record movie record/playback path --- src/citra_qt/configuration/config.cpp | 4 ++++ src/citra_qt/main.cpp | 18 +++++++----------- src/citra_qt/ui_settings.h | 2 ++ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index e1cced42d..cb0d58780 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -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); diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 0c5fe68c3..2130d4b6c 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -764,15 +764,7 @@ 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."); - ui.action_Record_Movie->setEnabled(true); - ui.action_Play_Movie->setEnabled(true); - ui.action_Stop_Recording_Playback->setEnabled(false); - } + OnStopRecordingPlayback(); emu_thread->RequestStop(); // Release emu threads from any breakpoints @@ -1252,9 +1244,11 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { void GMainWindow::OnRecordMovie() { const QString path = - QFileDialog::getSaveFileName(this, tr("Record Movie"), "", tr("Citra TAS Movie (*.ctm)")); + 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 { @@ -1311,9 +1305,11 @@ bool GMainWindow::ValidateMovie(const QString& path, u64 program_id) { void GMainWindow::OnPlayMovie() { const QString path = - QFileDialog::getOpenFileName(this, tr("Play Movie"), "", tr("Citra TAS Movie (*.ctm)")); + 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)) diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index 6b76a52f1..1cd94c99d 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -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 game_dirs;