mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30: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
				
			
		|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -179,7 +179,6 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) { | |||
|     Kernel::Init(system_mode); | ||||
|     Service::Init(service_manager); | ||||
|     GDBStub::Init(); | ||||
|     Movie::GetInstance().Init(); | ||||
| 
 | ||||
|     ResultStatus result = VideoCore::Init(emu_window); | ||||
|     if (result != ResultStatus::Success) { | ||||
|  | @ -218,7 +217,6 @@ void System::Shutdown() { | |||
|                          perf_results.frametime * 1000.0); | ||||
| 
 | ||||
|     // Shutdown emulation session
 | ||||
|     Movie::GetInstance().Shutdown(); | ||||
|     GDBStub::Shutdown(); | ||||
|     VideoCore::Shutdown(); | ||||
|     Service::Shutdown(); | ||||
|  |  | |||
|  | @ -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, u64 program_id) 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 (!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; | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|     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 true; | ||||
|     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,63 @@ 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<void()> 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, 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(); | ||||
| 
 | ||||
|     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, 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() { | ||||
|  | @ -428,6 +463,7 @@ void Movie::Shutdown() { | |||
| 
 | ||||
|     play_mode = PlayMode::None; | ||||
|     recorded_input.resize(0); | ||||
|     record_movie_file.clear(); | ||||
|     current_byte = 0; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <functional> | ||||
| #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,11 @@ public: | |||
|         return s_instance; | ||||
|     } | ||||
| 
 | ||||
|     void Init(); | ||||
|     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, u64 program_id = 0) const; | ||||
|     u64 GetMovieProgramID(const std::string& movie_file) const; | ||||
| 
 | ||||
|     void Shutdown(); | ||||
| 
 | ||||
|  | @ -74,14 +85,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 <typename... Targs> | ||||
|  | @ -103,12 +112,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, u64 program_id = 0) const; | ||||
| 
 | ||||
|     void SaveMovie(); | ||||
| 
 | ||||
|     PlayMode play_mode; | ||||
|     std::string record_movie_file; | ||||
|     std::vector<u8> recorded_input; | ||||
|     std::function<void()> playback_completion_callback; | ||||
|     size_t current_byte = 0; | ||||
| }; | ||||
| } // namespace Core
 | ||||
|  | @ -156,10 +156,6 @@ struct Values { | |||
|     std::string log_filter; | ||||
|     std::unordered_map<std::string, bool> lle_modules; | ||||
| 
 | ||||
|     // Movie
 | ||||
|     std::string movie_play; | ||||
|     std::string movie_record; | ||||
| 
 | ||||
|     // WebService
 | ||||
|     bool enable_telemetry; | ||||
|     std::string telemetry_endpoint_url; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue