mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +00:00 
			
		
		
		
	Merge pull request #5448 from zhaowenlan1779/rerecording
Implement basic rerecording features
This commit is contained in:
		
						commit
						62753e882e
					
				
					 25 changed files with 971 additions and 241 deletions
				
			
		|  | @ -66,6 +66,7 @@ static void PrintHelp(const char* argv0) { | ||||||
|                  "-m, --multiplayer=nick:password@address:port" |                  "-m, --multiplayer=nick:password@address:port" | ||||||
|                  " Nickname, password, address and port for multiplayer\n" |                  " Nickname, password, address and port for multiplayer\n" | ||||||
|                  "-r, --movie-record=[file]  Record a movie (game inputs) to the given file\n" |                  "-r, --movie-record=[file]  Record a movie (game inputs) to the given file\n" | ||||||
|  |                  "-a, --movie-record-author=AUTHOR Sets the author of the movie to be recorded\n" | ||||||
|                  "-p, --movie-play=[file]    Playback the movie (game inputs) from the given file\n" |                  "-p, --movie-play=[file]    Playback the movie (game inputs) from the given file\n" | ||||||
|                  "-d, --dump-video=[file]    Dumps audio and video to the given video file\n" |                  "-d, --dump-video=[file]    Dumps audio and video to the given video file\n" | ||||||
|                  "-f, --fullscreen     Start in fullscreen mode\n" |                  "-f, --fullscreen     Start in fullscreen mode\n" | ||||||
|  | @ -192,6 +193,7 @@ int main(int argc, char** argv) { | ||||||
|     bool use_gdbstub = Settings::values.use_gdbstub; |     bool use_gdbstub = Settings::values.use_gdbstub; | ||||||
|     u32 gdb_port = static_cast<u32>(Settings::values.gdbstub_port); |     u32 gdb_port = static_cast<u32>(Settings::values.gdbstub_port); | ||||||
|     std::string movie_record; |     std::string movie_record; | ||||||
|  |     std::string movie_record_author; | ||||||
|     std::string movie_play; |     std::string movie_play; | ||||||
|     std::string dump_video; |     std::string dump_video; | ||||||
| 
 | 
 | ||||||
|  | @ -217,11 +219,17 @@ int main(int argc, char** argv) { | ||||||
|     u16 port = Network::DefaultRoomPort; |     u16 port = Network::DefaultRoomPort; | ||||||
| 
 | 
 | ||||||
|     static struct option long_options[] = { |     static struct option long_options[] = { | ||||||
|         {"gdbport", required_argument, 0, 'g'},     {"install", required_argument, 0, 'i'}, |         {"gdbport", required_argument, 0, 'g'}, | ||||||
|         {"multiplayer", required_argument, 0, 'm'}, {"movie-record", required_argument, 0, 'r'}, |         {"install", required_argument, 0, 'i'}, | ||||||
|         {"movie-play", required_argument, 0, 'p'},  {"dump-video", required_argument, 0, 'd'}, |         {"multiplayer", required_argument, 0, 'm'}, | ||||||
|         {"fullscreen", no_argument, 0, 'f'},        {"help", no_argument, 0, 'h'}, |         {"movie-record", required_argument, 0, 'r'}, | ||||||
|         {"version", no_argument, 0, 'v'},           {0, 0, 0, 0}, |         {"movie-record-author", required_argument, 0, 'a'}, | ||||||
|  |         {"movie-play", required_argument, 0, 'p'}, | ||||||
|  |         {"dump-video", required_argument, 0, 'd'}, | ||||||
|  |         {"fullscreen", no_argument, 0, 'f'}, | ||||||
|  |         {"help", no_argument, 0, 'h'}, | ||||||
|  |         {"version", no_argument, 0, 'v'}, | ||||||
|  |         {0, 0, 0, 0}, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     while (optind < argc) { |     while (optind < argc) { | ||||||
|  | @ -285,6 +293,9 @@ int main(int argc, char** argv) { | ||||||
|             case 'r': |             case 'r': | ||||||
|                 movie_record = optarg; |                 movie_record = optarg; | ||||||
|                 break; |                 break; | ||||||
|  |             case 'a': | ||||||
|  |                 movie_record_author = optarg; | ||||||
|  |                 break; | ||||||
|             case 'p': |             case 'p': | ||||||
|                 movie_play = optarg; |                 movie_play = optarg; | ||||||
|                 break; |                 break; | ||||||
|  | @ -401,10 +412,14 @@ int main(int argc, char** argv) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!movie_play.empty()) { |     if (!movie_play.empty()) { | ||||||
|  |         auto metadata = Core::Movie::GetInstance().GetMovieMetadata(movie_play); | ||||||
|  |         LOG_INFO(Movie, "Author: {}", metadata.author); | ||||||
|  |         LOG_INFO(Movie, "Rerecord count: {}", metadata.rerecord_count); | ||||||
|  |         LOG_INFO(Movie, "Input count: {}", metadata.input_count); | ||||||
|         Core::Movie::GetInstance().StartPlayback(movie_play); |         Core::Movie::GetInstance().StartPlayback(movie_play); | ||||||
|     } |     } | ||||||
|     if (!movie_record.empty()) { |     if (!movie_record.empty()) { | ||||||
|         Core::Movie::GetInstance().StartRecording(movie_record); |         Core::Movie::GetInstance().StartRecording(movie_record, movie_record_author); | ||||||
|     } |     } | ||||||
|     if (!dump_video.empty()) { |     if (!dump_video.empty()) { | ||||||
|         Layout::FramebufferLayout layout{ |         Layout::FramebufferLayout layout{ | ||||||
|  |  | ||||||
|  | @ -128,6 +128,12 @@ add_executable(citra-qt | ||||||
|     main.cpp |     main.cpp | ||||||
|     main.h |     main.h | ||||||
|     main.ui |     main.ui | ||||||
|  |     movie/movie_play_dialog.cpp | ||||||
|  |     movie/movie_play_dialog.h | ||||||
|  |     movie/movie_play_dialog.ui | ||||||
|  |     movie/movie_record_dialog.cpp | ||||||
|  |     movie/movie_record_dialog.h | ||||||
|  |     movie/movie_record_dialog.ui | ||||||
|     multiplayer/chat_room.cpp |     multiplayer/chat_room.cpp | ||||||
|     multiplayer/chat_room.h |     multiplayer/chat_room.h | ||||||
|     multiplayer/chat_room.ui |     multiplayer/chat_room.ui | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ | ||||||
| #include "core/3ds.h" | #include "core/3ds.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
| #include "core/frontend/scope_acquire_context.h" | #include "core/frontend/scope_acquire_context.h" | ||||||
|  | #include "core/perf_stats.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
| #include "input_common/keyboard.h" | #include "input_common/keyboard.h" | ||||||
| #include "input_common/main.h" | #include "input_common/main.h" | ||||||
|  | @ -52,6 +53,13 @@ void EmuThread::run() { | ||||||
| 
 | 
 | ||||||
|     emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); |     emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); | ||||||
| 
 | 
 | ||||||
|  |     if (Core::System::GetInstance().frame_limiter.IsFrameAdvancing()) { | ||||||
|  |         // Usually the loading screen is hidden after the first frame is drawn. In this case
 | ||||||
|  |         // we hide it immediately as we need to wait for user input to start the emulation.
 | ||||||
|  |         emit HideLoadingScreen(); | ||||||
|  |         Core::System::GetInstance().frame_limiter.WaitOnce(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // Holds whether the cpu was running during the last iteration,
 |     // Holds whether the cpu was running during the last iteration,
 | ||||||
|     // so that the DebugModeLeft signal can be emitted before the
 |     // so that the DebugModeLeft signal can be emitted before the
 | ||||||
|     // next execution step.
 |     // next execution step.
 | ||||||
|  |  | ||||||
|  | @ -122,6 +122,8 @@ signals: | ||||||
|     void ErrorThrown(Core::System::ResultStatus, std::string); |     void ErrorThrown(Core::System::ResultStatus, std::string); | ||||||
| 
 | 
 | ||||||
|     void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); |     void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); | ||||||
|  | 
 | ||||||
|  |     void HideLoadingScreen(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class OpenGLWindow : public QWindow { | class OpenGLWindow : public QWindow { | ||||||
|  |  | ||||||
|  | @ -722,17 +722,17 @@ void GameList::RefreshGameDirectory() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| QString GameList::FindGameByProgramID(u64 program_id) { | QString GameList::FindGameByProgramID(u64 program_id, int role) { | ||||||
|     return FindGameByProgramID(item_model->invisibleRootItem(), program_id); |     return FindGameByProgramID(item_model->invisibleRootItem(), program_id, role); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_id) { | QString GameList::FindGameByProgramID(QStandardItem* current_item, u64 program_id, int role) { | ||||||
|     if (current_item->type() == static_cast<int>(GameListItemType::Game) && |     if (current_item->type() == static_cast<int>(GameListItemType::Game) && | ||||||
|         current_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { |         current_item->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) { | ||||||
|         return current_item->data(GameListItemPath::FullPathRole).toString(); |         return current_item->data(role).toString(); | ||||||
|     } else if (current_item->hasChildren()) { |     } else if (current_item->hasChildren()) { | ||||||
|         for (int child_id = 0; child_id < current_item->rowCount(); child_id++) { |         for (int child_id = 0; child_id < current_item->rowCount(); child_id++) { | ||||||
|             QString path = FindGameByProgramID(current_item->child(child_id, 0), program_id); |             QString path = FindGameByProgramID(current_item->child(child_id, 0), program_id, role); | ||||||
|             if (!path.isEmpty()) |             if (!path.isEmpty()) | ||||||
|                 return path; |                 return path; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -70,7 +70,7 @@ public: | ||||||
| 
 | 
 | ||||||
|     QStandardItemModel* GetModel() const; |     QStandardItemModel* GetModel() const; | ||||||
| 
 | 
 | ||||||
|     QString FindGameByProgramID(u64 program_id); |     QString FindGameByProgramID(u64 program_id, int role); | ||||||
| 
 | 
 | ||||||
|     void RefreshGameDirectory(); |     void RefreshGameDirectory(); | ||||||
| 
 | 
 | ||||||
|  | @ -105,7 +105,7 @@ 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); |     QString FindGameByProgramID(QStandardItem* current_item, u64 program_id, int role); | ||||||
| 
 | 
 | ||||||
|     GameListSearchField* search_field; |     GameListSearchField* search_field; | ||||||
|     GMainWindow* main_window = nullptr; |     GMainWindow* main_window = nullptr; | ||||||
|  |  | ||||||
|  | @ -51,6 +51,8 @@ | ||||||
| #include "citra_qt/hotkeys.h" | #include "citra_qt/hotkeys.h" | ||||||
| #include "citra_qt/loading_screen.h" | #include "citra_qt/loading_screen.h" | ||||||
| #include "citra_qt/main.h" | #include "citra_qt/main.h" | ||||||
|  | #include "citra_qt/movie/movie_play_dialog.h" | ||||||
|  | #include "citra_qt/movie/movie_record_dialog.h" | ||||||
| #include "citra_qt/multiplayer/state.h" | #include "citra_qt/multiplayer/state.h" | ||||||
| #include "citra_qt/qt_image_interface.h" | #include "citra_qt/qt_image_interface.h" | ||||||
| #include "citra_qt/uisettings.h" | #include "citra_qt/uisettings.h" | ||||||
|  | @ -174,6 +176,10 @@ GMainWindow::GMainWindow() | ||||||
| 
 | 
 | ||||||
|     Network::Init(); |     Network::Init(); | ||||||
| 
 | 
 | ||||||
|  |     Core::Movie::GetInstance().SetPlaybackCompletionCallback([this] { | ||||||
|  |         QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     InitializeWidgets(); |     InitializeWidgets(); | ||||||
|     InitializeDebugWidgets(); |     InitializeDebugWidgets(); | ||||||
|     InitializeRecentFileMenuActions(); |     InitializeRecentFileMenuActions(); | ||||||
|  | @ -755,8 +761,10 @@ void GMainWindow::ConnectMenuEvents() { | ||||||
|     // Movie
 |     // Movie
 | ||||||
|     connect(ui->action_Record_Movie, &QAction::triggered, this, &GMainWindow::OnRecordMovie); |     connect(ui->action_Record_Movie, &QAction::triggered, this, &GMainWindow::OnRecordMovie); | ||||||
|     connect(ui->action_Play_Movie, &QAction::triggered, this, &GMainWindow::OnPlayMovie); |     connect(ui->action_Play_Movie, &QAction::triggered, this, &GMainWindow::OnPlayMovie); | ||||||
|     connect(ui->action_Stop_Recording_Playback, &QAction::triggered, this, |     connect(ui->action_Close_Movie, &QAction::triggered, this, &GMainWindow::OnCloseMovie); | ||||||
|             &GMainWindow::OnStopRecordingPlayback); |     connect(ui->action_Save_Movie, &QAction::triggered, this, &GMainWindow::OnSaveMovie); | ||||||
|  |     connect(ui->action_Movie_Read_Only_Mode, &QAction::toggled, this, | ||||||
|  |             [this](bool checked) { Core::Movie::GetInstance().SetReadOnly(checked); }); | ||||||
|     connect(ui->action_Enable_Frame_Advancing, &QAction::triggered, this, [this] { |     connect(ui->action_Enable_Frame_Advancing, &QAction::triggered, this, [this] { | ||||||
|         if (emulation_running) { |         if (emulation_running) { | ||||||
|             Core::System::GetInstance().frame_limiter.SetFrameAdvancing( |             Core::System::GetInstance().frame_limiter.SetFrameAdvancing( | ||||||
|  | @ -1025,6 +1033,9 @@ void GMainWindow::BootGame(const QString& filename) { | ||||||
|     if (movie_record_on_start) { |     if (movie_record_on_start) { | ||||||
|         Core::Movie::GetInstance().PrepareForRecording(); |         Core::Movie::GetInstance().PrepareForRecording(); | ||||||
|     } |     } | ||||||
|  |     if (movie_playback_on_start) { | ||||||
|  |         Core::Movie::GetInstance().PrepareForPlayback(movie_playback_path.toStdString()); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // Save configurations
 |     // Save configurations
 | ||||||
|     UpdateUISettings(); |     UpdateUISettings(); | ||||||
|  | @ -1034,6 +1045,42 @@ void GMainWindow::BootGame(const QString& filename) { | ||||||
|     if (!LoadROM(filename)) |     if (!LoadROM(filename)) | ||||||
|         return; |         return; | ||||||
| 
 | 
 | ||||||
|  |     // Set everything up
 | ||||||
|  |     if (movie_record_on_start) { | ||||||
|  |         Core::Movie::GetInstance().StartRecording(movie_record_path.toStdString(), | ||||||
|  |                                                   movie_record_author.toStdString()); | ||||||
|  |         movie_record_on_start = false; | ||||||
|  |         movie_record_path.clear(); | ||||||
|  |         movie_record_author.clear(); | ||||||
|  |     } | ||||||
|  |     if (movie_playback_on_start) { | ||||||
|  |         Core::Movie::GetInstance().StartPlayback(movie_playback_path.toStdString()); | ||||||
|  |         movie_playback_on_start = false; | ||||||
|  |         movie_playback_path.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (ui->action_Enable_Frame_Advancing->isChecked()) { | ||||||
|  |         ui->action_Advance_Frame->setEnabled(true); | ||||||
|  |         Core::System::GetInstance().frame_limiter.SetFrameAdvancing(true); | ||||||
|  |     } else { | ||||||
|  |         ui->action_Advance_Frame->setEnabled(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (video_dumping_on_start) { | ||||||
|  |         Layout::FramebufferLayout layout{ | ||||||
|  |             Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; | ||||||
|  |         if (!Core::System::GetInstance().VideoDumper().StartDumping( | ||||||
|  |                 video_dumping_path.toStdString(), layout)) { | ||||||
|  | 
 | ||||||
|  |             QMessageBox::critical( | ||||||
|  |                 this, tr("Citra"), | ||||||
|  |                 tr("Could not start video dumping.<br>Refer to the log for details.")); | ||||||
|  |             ui->action_Dump_Video->setChecked(false); | ||||||
|  |         } | ||||||
|  |         video_dumping_on_start = false; | ||||||
|  |         video_dumping_path.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // Create and start the emulation thread
 |     // Create and start the emulation thread
 | ||||||
|     emu_thread = std::make_unique<EmuThread>(*render_window); |     emu_thread = std::make_unique<EmuThread>(*render_window); | ||||||
|     emit EmulationStarting(emu_thread.get()); |     emit EmulationStarting(emu_thread.get()); | ||||||
|  | @ -1055,6 +1102,8 @@ void GMainWindow::BootGame(const QString& filename) { | ||||||
| 
 | 
 | ||||||
|     connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, |     connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, | ||||||
|             &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); |             &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); | ||||||
|  |     connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, | ||||||
|  |             &LoadingScreen::OnLoadComplete); | ||||||
| 
 | 
 | ||||||
|     // Update the GUI
 |     // Update the GUI
 | ||||||
|     registersWidget->OnDebugModeEntered(); |     registersWidget->OnDebugModeEntered(); | ||||||
|  | @ -1062,7 +1111,7 @@ void GMainWindow::BootGame(const QString& filename) { | ||||||
|         game_list->hide(); |         game_list->hide(); | ||||||
|         game_list_placeholder->hide(); |         game_list_placeholder->hide(); | ||||||
|     } |     } | ||||||
|     status_bar_update_timer.start(2000); |     status_bar_update_timer.start(1000); | ||||||
| 
 | 
 | ||||||
|     if (UISettings::values.hide_mouse) { |     if (UISettings::values.hide_mouse) { | ||||||
|         mouse_hide_timer.start(); |         mouse_hide_timer.start(); | ||||||
|  | @ -1081,20 +1130,6 @@ void GMainWindow::BootGame(const QString& filename) { | ||||||
|         ShowFullscreen(); |         ShowFullscreen(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (video_dumping_on_start) { |  | ||||||
|         Layout::FramebufferLayout layout{ |  | ||||||
|             Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; |  | ||||||
|         if (!Core::System::GetInstance().VideoDumper().StartDumping( |  | ||||||
|                 video_dumping_path.toStdString(), layout)) { |  | ||||||
| 
 |  | ||||||
|             QMessageBox::critical( |  | ||||||
|                 this, tr("Citra"), |  | ||||||
|                 tr("Could not start video dumping.<br>Refer to the log for details.")); |  | ||||||
|             ui->action_Dump_Video->setChecked(false); |  | ||||||
|         } |  | ||||||
|         video_dumping_on_start = false; |  | ||||||
|         video_dumping_path.clear(); |  | ||||||
|     } |  | ||||||
|     OnStartGame(); |     OnStartGame(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1118,7 +1153,6 @@ void GMainWindow::ShutdownGame() { | ||||||
|     AllowOSSleep(); |     AllowOSSleep(); | ||||||
| 
 | 
 | ||||||
|     discord_rpc->Pause(); |     discord_rpc->Pause(); | ||||||
|     OnStopRecordingPlayback(); |  | ||||||
|     emu_thread->RequestStop(); |     emu_thread->RequestStop(); | ||||||
| 
 | 
 | ||||||
|     // Release emu threads from any breakpoints
 |     // Release emu threads from any breakpoints
 | ||||||
|  | @ -1137,6 +1171,8 @@ void GMainWindow::ShutdownGame() { | ||||||
|     emu_thread->wait(); |     emu_thread->wait(); | ||||||
|     emu_thread = nullptr; |     emu_thread = nullptr; | ||||||
| 
 | 
 | ||||||
|  |     OnCloseMovie(); | ||||||
|  | 
 | ||||||
|     discord_rpc->Update(); |     discord_rpc->Update(); | ||||||
| 
 | 
 | ||||||
|     Camera::QtMultimediaCameraHandler::ReleaseHandlers(); |     Camera::QtMultimediaCameraHandler::ReleaseHandlers(); | ||||||
|  | @ -1154,8 +1190,6 @@ void GMainWindow::ShutdownGame() { | ||||||
|     ui->action_Load_Amiibo->setEnabled(false); |     ui->action_Load_Amiibo->setEnabled(false); | ||||||
|     ui->action_Remove_Amiibo->setEnabled(false); |     ui->action_Remove_Amiibo->setEnabled(false); | ||||||
|     ui->action_Report_Compatibility->setEnabled(false); |     ui->action_Report_Compatibility->setEnabled(false); | ||||||
|     ui->action_Enable_Frame_Advancing->setEnabled(false); |  | ||||||
|     ui->action_Enable_Frame_Advancing->setChecked(false); |  | ||||||
|     ui->action_Advance_Frame->setEnabled(false); |     ui->action_Advance_Frame->setEnabled(false); | ||||||
|     ui->action_Capture_Screenshot->setEnabled(false); |     ui->action_Capture_Screenshot->setEnabled(false); | ||||||
|     render_window->hide(); |     render_window->hide(); | ||||||
|  | @ -1172,6 +1206,7 @@ void GMainWindow::ShutdownGame() { | ||||||
|     // Disable status bar updates
 |     // Disable status bar updates
 | ||||||
|     status_bar_update_timer.stop(); |     status_bar_update_timer.stop(); | ||||||
|     message_label->setVisible(false); |     message_label->setVisible(false); | ||||||
|  |     message_label_used_for_movie = false; | ||||||
|     emu_speed_label->setVisible(false); |     emu_speed_label->setVisible(false); | ||||||
|     game_fps_label->setVisible(false); |     game_fps_label->setVisible(false); | ||||||
|     emu_frametime_label->setVisible(false); |     emu_frametime_label->setVisible(false); | ||||||
|  | @ -1545,12 +1580,6 @@ 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(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     PreventOSSleep(); |     PreventOSSleep(); | ||||||
| 
 | 
 | ||||||
|     emu_thread->SetRunning(true); |     emu_thread->SetRunning(true); | ||||||
|  | @ -1567,7 +1596,6 @@ void GMainWindow::OnStartGame() { | ||||||
|     ui->action_Cheats->setEnabled(true); |     ui->action_Cheats->setEnabled(true); | ||||||
|     ui->action_Load_Amiibo->setEnabled(true); |     ui->action_Load_Amiibo->setEnabled(true); | ||||||
|     ui->action_Report_Compatibility->setEnabled(true); |     ui->action_Report_Compatibility->setEnabled(true); | ||||||
|     ui->action_Enable_Frame_Advancing->setEnabled(true); |  | ||||||
|     ui->action_Capture_Screenshot->setEnabled(true); |     ui->action_Capture_Screenshot->setEnabled(true); | ||||||
| 
 | 
 | ||||||
|     discord_rpc->Update(); |     discord_rpc->Update(); | ||||||
|  | @ -1851,144 +1879,81 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnRecordMovie() { | void GMainWindow::OnRecordMovie() { | ||||||
|     if (emulation_running) { |     MovieRecordDialog dialog(this); | ||||||
|         QMessageBox::StandardButton answer = QMessageBox::warning( |     if (dialog.exec() != QDialog::Accepted) { | ||||||
|             this, tr("Record Movie"), |  | ||||||
|             tr("To keep consistency with the RNG, it is recommended to record the movie from game " |  | ||||||
|                "start.<br>Are you sure you still want to record movies now?"), |  | ||||||
|             QMessageBox::Yes | QMessageBox::No); |  | ||||||
|         if (answer == QMessageBox::No) |  | ||||||
|             return; |  | ||||||
|     } |  | ||||||
|     const QString path = |  | ||||||
|         QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path, |  | ||||||
|                                      tr("Citra TAS Movie (*.ctm)")); |  | ||||||
|     if (path.isEmpty()) |  | ||||||
|         return; |         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) { |     movie_record_on_start = true; | ||||||
|     using namespace Core; |     movie_record_path = dialog.GetPath(); | ||||||
|     Movie::ValidationResult result = |     movie_record_author = dialog.GetAuthor(); | ||||||
|         Core::Movie::GetInstance().ValidateMovie(path.toStdString(), program_id); | 
 | ||||||
|     const QString revision_dismatch_text = |     if (emulation_running) { // Restart game
 | ||||||
|         tr("The movie file you are trying to load was created on a different revision of Citra." |         BootGame(QString(game_path)); | ||||||
|            "<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; |     ui->action_Close_Movie->setEnabled(true); | ||||||
|  |     ui->action_Save_Movie->setEnabled(true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnPlayMovie() { | void GMainWindow::OnPlayMovie() { | ||||||
|     if (emulation_running) { |     MoviePlayDialog dialog(this, game_list); | ||||||
|         QMessageBox::StandardButton answer = QMessageBox::warning( |     if (dialog.exec() != QDialog::Accepted) { | ||||||
|             this, tr("Play Movie"), |  | ||||||
|             tr("To keep consistency with the RNG, it is recommended to play the movie from game " |  | ||||||
|                "start.<br>Are you sure you still want to play movies now?"), |  | ||||||
|             QMessageBox::Yes | QMessageBox::No); |  | ||||||
|         if (answer == QMessageBox::No) |  | ||||||
|             return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const QString path = |  | ||||||
|         QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path, |  | ||||||
|                                      tr("Citra TAS Movie (*.ctm)")); |  | ||||||
|     if (path.isEmpty()) |  | ||||||
|         return; |         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; |  | ||||||
|         Core::Movie::GetInstance().PrepareForPlayback(path.toStdString()); |  | ||||||
|         BootGame(game_path); |  | ||||||
|     } |     } | ||||||
|     Core::Movie::GetInstance().StartPlayback(path.toStdString(), [this] { | 
 | ||||||
|         QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted"); |     movie_playback_on_start = true; | ||||||
|     }); |     movie_playback_path = dialog.GetMoviePath(); | ||||||
|     ui->action_Record_Movie->setEnabled(false); |     BootGame(dialog.GetGamePath()); | ||||||
|     ui->action_Play_Movie->setEnabled(false); | 
 | ||||||
|     ui->action_Stop_Recording_Playback->setEnabled(true); |     ui->action_Close_Movie->setEnabled(true); | ||||||
|  |     ui->action_Save_Movie->setEnabled(false); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnStopRecordingPlayback() { | void GMainWindow::OnCloseMovie() { | ||||||
|     if (movie_record_on_start) { |     if (movie_record_on_start) { | ||||||
|         QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); |         QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); | ||||||
|         movie_record_on_start = false; |         movie_record_on_start = false; | ||||||
|         movie_record_path.clear(); |         movie_record_path.clear(); | ||||||
|  |         movie_record_author.clear(); | ||||||
|     } else { |     } else { | ||||||
|         const bool was_recording = Core::Movie::GetInstance().IsRecordingInput(); |         const bool was_running = emu_thread && emu_thread->IsRunning(); | ||||||
|  |         if (was_running) { | ||||||
|  |             OnPauseGame(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const bool was_recording = | ||||||
|  |             Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording; | ||||||
|         Core::Movie::GetInstance().Shutdown(); |         Core::Movie::GetInstance().Shutdown(); | ||||||
|         if (was_recording) { |         if (was_recording) { | ||||||
|             QMessageBox::information(this, tr("Movie Saved"), |             QMessageBox::information(this, tr("Movie Saved"), | ||||||
|                                      tr("The movie is successfully saved.")); |                                      tr("The movie is successfully saved.")); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (was_running) { | ||||||
|  |             OnStartGame(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ui->action_Close_Movie->setEnabled(false); | ||||||
|  |     ui->action_Save_Movie->setEnabled(false); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void GMainWindow::OnSaveMovie() { | ||||||
|  |     const bool was_running = emu_thread && emu_thread->IsRunning(); | ||||||
|  |     if (was_running) { | ||||||
|  |         OnPauseGame(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { | ||||||
|  |         Core::Movie::GetInstance().SaveMovie(); | ||||||
|  |         QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); | ||||||
|  |     } else { | ||||||
|  |         LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (was_running) { | ||||||
|  |         OnStartGame(); | ||||||
|     } |     } | ||||||
|     ui->action_Record_Movie->setEnabled(true); |  | ||||||
|     ui->action_Play_Movie->setEnabled(true); |  | ||||||
|     ui->action_Stop_Recording_Playback->setEnabled(false); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnCaptureScreenshot() { | void GMainWindow::OnCaptureScreenshot() { | ||||||
|  | @ -2067,6 +2032,32 @@ void GMainWindow::UpdateStatusBar() { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Update movie status
 | ||||||
|  |     const u64 current = Core::Movie::GetInstance().GetCurrentInputIndex(); | ||||||
|  |     const u64 total = Core::Movie::GetInstance().GetTotalInputCount(); | ||||||
|  |     const auto play_mode = Core::Movie::GetInstance().GetPlayMode(); | ||||||
|  |     if (play_mode == Core::Movie::PlayMode::Recording) { | ||||||
|  |         message_label->setText(tr("Recording %1").arg(current)); | ||||||
|  |         message_label->setVisible(true); | ||||||
|  |         message_label_used_for_movie = true; | ||||||
|  |         ui->action_Save_Movie->setEnabled(true); | ||||||
|  |     } else if (play_mode == Core::Movie::PlayMode::Playing) { | ||||||
|  |         message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); | ||||||
|  |         message_label->setVisible(true); | ||||||
|  |         message_label_used_for_movie = true; | ||||||
|  |         ui->action_Save_Movie->setEnabled(false); | ||||||
|  |     } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { | ||||||
|  |         message_label->setText(tr("Movie Finished")); | ||||||
|  |         message_label->setVisible(true); | ||||||
|  |         message_label_used_for_movie = true; | ||||||
|  |         ui->action_Save_Movie->setEnabled(false); | ||||||
|  |     } else if (message_label_used_for_movie) { // Clear the label if movie was just closed
 | ||||||
|  |         message_label->setText(QString{}); | ||||||
|  |         message_label->setVisible(false); | ||||||
|  |         message_label_used_for_movie = false; | ||||||
|  |         ui->action_Save_Movie->setEnabled(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     auto results = Core::System::GetInstance().GetAndResetPerfStats(); |     auto results = Core::System::GetInstance().GetAndResetPerfStats(); | ||||||
| 
 | 
 | ||||||
|     if (Settings::values.use_frame_limit_alternate) { |     if (Settings::values.use_frame_limit_alternate) { | ||||||
|  | @ -2178,6 +2169,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det | ||||||
|             emu_thread->SetRunning(true); |             emu_thread->SetRunning(true); | ||||||
|             message_label->setText(status_message); |             message_label->setText(status_message); | ||||||
|             message_label->setVisible(true); |             message_label->setVisible(true); | ||||||
|  |             message_label_used_for_movie = false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -2356,10 +2348,8 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnMoviePlaybackCompleted() { | void GMainWindow::OnMoviePlaybackCompleted() { | ||||||
|  |     OnPauseGame(); | ||||||
|     QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); |     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::UpdateWindowTitle() { | void GMainWindow::UpdateWindowTitle() { | ||||||
|  |  | ||||||
|  | @ -208,7 +208,8 @@ private slots: | ||||||
|     void OnCreateGraphicsSurfaceViewer(); |     void OnCreateGraphicsSurfaceViewer(); | ||||||
|     void OnRecordMovie(); |     void OnRecordMovie(); | ||||||
|     void OnPlayMovie(); |     void OnPlayMovie(); | ||||||
|     void OnStopRecordingPlayback(); |     void OnCloseMovie(); | ||||||
|  |     void OnSaveMovie(); | ||||||
|     void OnCaptureScreenshot(); |     void OnCaptureScreenshot(); | ||||||
| #ifdef ENABLE_FFMPEG_VIDEO_DUMPER | #ifdef ENABLE_FFMPEG_VIDEO_DUMPER | ||||||
|     void OnStartVideoDumping(); |     void OnStartVideoDumping(); | ||||||
|  | @ -224,7 +225,6 @@ private slots: | ||||||
|     void OnMouseActivity(); |     void OnMouseActivity(); | ||||||
| 
 | 
 | ||||||
| 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(); | ||||||
|  | @ -249,6 +249,7 @@ private: | ||||||
|     QLabel* game_fps_label = nullptr; |     QLabel* game_fps_label = nullptr; | ||||||
|     QLabel* emu_frametime_label = nullptr; |     QLabel* emu_frametime_label = nullptr; | ||||||
|     QTimer status_bar_update_timer; |     QTimer status_bar_update_timer; | ||||||
|  |     bool message_label_used_for_movie = false; | ||||||
| 
 | 
 | ||||||
|     MultiplayerState* multiplayer_state = nullptr; |     MultiplayerState* multiplayer_state = nullptr; | ||||||
|     std::unique_ptr<Config> config; |     std::unique_ptr<Config> config; | ||||||
|  | @ -267,6 +268,10 @@ private: | ||||||
|     // Movie
 |     // Movie
 | ||||||
|     bool movie_record_on_start = false; |     bool movie_record_on_start = false; | ||||||
|     QString movie_record_path; |     QString movie_record_path; | ||||||
|  |     QString movie_record_author; | ||||||
|  | 
 | ||||||
|  |     bool movie_playback_on_start = false; | ||||||
|  |     QString movie_playback_path; | ||||||
| 
 | 
 | ||||||
|     // Video dumping
 |     // Video dumping
 | ||||||
|     bool video_dumping_on_start = false; |     bool video_dumping_on_start = false; | ||||||
|  |  | ||||||
|  | @ -163,7 +163,10 @@ | ||||||
|      </property> |      </property> | ||||||
|      <addaction name="action_Record_Movie"/> |      <addaction name="action_Record_Movie"/> | ||||||
|      <addaction name="action_Play_Movie"/> |      <addaction name="action_Play_Movie"/> | ||||||
|      <addaction name="action_Stop_Recording_Playback"/> |      <addaction name="action_Close_Movie"/> | ||||||
|  |      <addaction name="separator"/> | ||||||
|  |      <addaction name="action_Movie_Read_Only_Mode"/> | ||||||
|  |      <addaction name="action_Save_Movie"/> | ||||||
|     </widget> |     </widget> | ||||||
|     <widget class="QMenu" name="menu_Frame_Advance"> |     <widget class="QMenu" name="menu_Frame_Advance"> | ||||||
|      <property name="title"> |      <property name="title"> | ||||||
|  | @ -318,36 +321,43 @@ | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Record_Movie"> |   <action name="action_Record_Movie"> | ||||||
|    <property name="enabled"> |  | ||||||
|     <bool>true</bool> |  | ||||||
|    </property> |  | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Record Movie</string> |     <string>Record...</string> | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Play_Movie"> |   <action name="action_Play_Movie"> | ||||||
|    <property name="enabled"> |  | ||||||
|     <bool>true</bool> |  | ||||||
|    </property> |  | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Play Movie</string> |     <string>Play...</string> | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Stop_Recording_Playback"> |   <action name="action_Close_Movie"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Close</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_Save_Movie"> | ||||||
|    <property name="enabled"> |    <property name="enabled"> | ||||||
|     <bool>false</bool> |     <bool>false</bool> | ||||||
|    </property> |    </property> | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Stop Recording / Playback</string> |     <string>Save without Closing</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|  |   <action name="action_Movie_Read_Only_Mode"> | ||||||
|  |    <property name="checkable"> | ||||||
|  |     <bool>true</bool> | ||||||
|  |    </property> | ||||||
|  |    <property name="checked"> | ||||||
|  |     <bool>true</bool> | ||||||
|  |    </property> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Read-Only Mode</string> | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|   <action name="action_Enable_Frame_Advancing"> |   <action name="action_Enable_Frame_Advancing"> | ||||||
|    <property name="checkable"> |    <property name="checkable"> | ||||||
|     <bool>true</bool> |     <bool>true</bool> | ||||||
|    </property> |    </property> | ||||||
|    <property name="enabled"> |  | ||||||
|     <bool>false</bool> |  | ||||||
|    </property> |  | ||||||
|    <property name="text"> |    <property name="text"> | ||||||
|     <string>Enable Frame Advancing</string> |     <string>Enable Frame Advancing</string> | ||||||
|    </property> |    </property> | ||||||
|  |  | ||||||
							
								
								
									
										130
									
								
								src/citra_qt/movie/movie_play_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/citra_qt/movie/movie_play_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <QFileDialog> | ||||||
|  | #include <QPushButton> | ||||||
|  | #include <QTime> | ||||||
|  | #include "citra_qt/game_list.h" | ||||||
|  | #include "citra_qt/game_list_p.h" | ||||||
|  | #include "citra_qt/movie/movie_play_dialog.h" | ||||||
|  | #include "citra_qt/uisettings.h" | ||||||
|  | #include "core/core.h" | ||||||
|  | #include "core/core_timing.h" | ||||||
|  | #include "core/hle/service/hid/hid.h" | ||||||
|  | #include "core/movie.h" | ||||||
|  | #include "ui_movie_play_dialog.h" | ||||||
|  | 
 | ||||||
|  | MoviePlayDialog::MoviePlayDialog(QWidget* parent, GameList* game_list_) | ||||||
|  |     : QDialog(parent), ui(std::make_unique<Ui::MoviePlayDialog>()), game_list(game_list_) { | ||||||
|  |     ui->setupUi(this); | ||||||
|  | 
 | ||||||
|  |     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); | ||||||
|  | 
 | ||||||
|  |     connect(ui->filePathButton, &QToolButton::clicked, this, &MoviePlayDialog::OnToolButtonClicked); | ||||||
|  |     connect(ui->filePath, &QLineEdit::editingFinished, this, &MoviePlayDialog::UpdateUIDisplay); | ||||||
|  |     connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MoviePlayDialog::accept); | ||||||
|  |     connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MoviePlayDialog::reject); | ||||||
|  | 
 | ||||||
|  |     if (Core::System::GetInstance().IsPoweredOn()) { | ||||||
|  |         QString note_text; | ||||||
|  |         note_text = tr("Current running game will be stopped."); | ||||||
|  |         if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { | ||||||
|  |             note_text.append(tr("<br>Current recording will be discarded.")); | ||||||
|  |         } | ||||||
|  |         ui->note2Label->setText(note_text); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | MoviePlayDialog::~MoviePlayDialog() = default; | ||||||
|  | 
 | ||||||
|  | QString MoviePlayDialog::GetMoviePath() const { | ||||||
|  |     return ui->filePath->text(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | QString MoviePlayDialog::GetGamePath() const { | ||||||
|  |     const auto metadata = Core::Movie::GetInstance().GetMovieMetadata(GetMoviePath().toStdString()); | ||||||
|  |     return game_list->FindGameByProgramID(metadata.program_id, GameListItemPath::FullPathRole); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void MoviePlayDialog::OnToolButtonClicked() { | ||||||
|  |     const QString path = | ||||||
|  |         QFileDialog::getOpenFileName(this, tr("Play Movie"), UISettings::values.movie_playback_path, | ||||||
|  |                                      tr("Citra TAS Movie (*.ctm)")); | ||||||
|  |     if (path.isEmpty()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     ui->filePath->setText(path); | ||||||
|  |     UISettings::values.movie_playback_path = path; | ||||||
|  |     UpdateUIDisplay(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void MoviePlayDialog::UpdateUIDisplay() { | ||||||
|  |     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); | ||||||
|  |     ui->gameLineEdit->clear(); | ||||||
|  |     ui->authorLineEdit->clear(); | ||||||
|  |     ui->rerecordCountLineEdit->clear(); | ||||||
|  |     ui->lengthLineEdit->clear(); | ||||||
|  |     ui->note1Label->setVisible(true); | ||||||
|  | 
 | ||||||
|  |     const auto path = GetMoviePath().toStdString(); | ||||||
|  | 
 | ||||||
|  |     const auto validation_result = Core::Movie::GetInstance().ValidateMovie(path); | ||||||
|  |     if (validation_result == Core::Movie::ValidationResult::Invalid) { | ||||||
|  |         ui->note1Label->setText(tr("Invalid movie file.")); | ||||||
|  |         ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ui->note2Label->setVisible(true); | ||||||
|  |     ui->infoGroupBox->setVisible(true); | ||||||
|  | 
 | ||||||
|  |     switch (validation_result) { | ||||||
|  |     case Core::Movie::ValidationResult::OK: | ||||||
|  |         ui->note1Label->setText(QString{}); | ||||||
|  |         break; | ||||||
|  |     case Core::Movie::ValidationResult::RevisionDismatch: | ||||||
|  |         ui->note1Label->setText(tr("Revision dismatch, playback may desync.")); | ||||||
|  |         break; | ||||||
|  |     case Core::Movie::ValidationResult::InputCountDismatch: | ||||||
|  |         ui->note1Label->setText(tr("Indicated length is incorrect, file may be corrupted.")); | ||||||
|  |         ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); | ||||||
|  |         break; | ||||||
|  |     default: | ||||||
|  |         UNREACHABLE(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const auto metadata = Core::Movie::GetInstance().GetMovieMetadata(path); | ||||||
|  | 
 | ||||||
|  |     // Format game title
 | ||||||
|  |     const auto title = | ||||||
|  |         game_list->FindGameByProgramID(metadata.program_id, GameListItemPath::TitleRole); | ||||||
|  |     if (title.isEmpty()) { | ||||||
|  |         ui->gameLineEdit->setText(tr("(unknown)")); | ||||||
|  |         ui->note1Label->setText(tr("Game used in this movie is not in game list.")); | ||||||
|  |         ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); | ||||||
|  |     } else { | ||||||
|  |         ui->gameLineEdit->setText(title); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ui->authorLineEdit->setText(metadata.author.empty() ? tr("(unknown)") | ||||||
|  |                                                         : QString::fromStdString(metadata.author)); | ||||||
|  |     ui->rerecordCountLineEdit->setText( | ||||||
|  |         metadata.rerecord_count == 0 ? tr("(unknown)") : QString::number(metadata.rerecord_count)); | ||||||
|  | 
 | ||||||
|  |     // Format length
 | ||||||
|  |     if (metadata.input_count == 0) { | ||||||
|  |         ui->lengthLineEdit->setText(tr("(unknown)")); | ||||||
|  |     } else { | ||||||
|  |         if (metadata.input_count > | ||||||
|  |             BASE_CLOCK_RATE_ARM11 * 24 * 60 * 60 / Service::HID::Module::pad_update_ticks) { | ||||||
|  |             // More than a day
 | ||||||
|  |             ui->lengthLineEdit->setText(tr("(>1 day)")); | ||||||
|  |         } else { | ||||||
|  |             const u64 msecs = Service::HID::Module::pad_update_ticks * metadata.input_count * 1000 / | ||||||
|  |                               BASE_CLOCK_RATE_ARM11; | ||||||
|  |             ui->lengthLineEdit->setText( | ||||||
|  |                 QTime::fromMSecsSinceStartOfDay(msecs).toString(QStringLiteral("hh:mm:ss.zzz"))); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								src/citra_qt/movie/movie_play_dialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/citra_qt/movie/movie_play_dialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | #include <QDialog> | ||||||
|  | 
 | ||||||
|  | class GameList; | ||||||
|  | 
 | ||||||
|  | namespace Ui { | ||||||
|  | class MoviePlayDialog; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class MoviePlayDialog : public QDialog { | ||||||
|  |     Q_OBJECT | ||||||
|  | 
 | ||||||
|  | public: | ||||||
|  |     explicit MoviePlayDialog(QWidget* parent, GameList* game_list); | ||||||
|  |     ~MoviePlayDialog() override; | ||||||
|  | 
 | ||||||
|  |     QString GetMoviePath() const; | ||||||
|  |     QString GetGamePath() const; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     void OnToolButtonClicked(); | ||||||
|  |     void UpdateUIDisplay(); | ||||||
|  | 
 | ||||||
|  |     std::unique_ptr<Ui::MoviePlayDialog> ui; | ||||||
|  |     GameList* game_list; | ||||||
|  | }; | ||||||
							
								
								
									
										136
									
								
								src/citra_qt/movie/movie_play_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/citra_qt/movie/movie_play_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,136 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <ui version="4.0"> | ||||||
|  |  <class>MoviePlayDialog</class> | ||||||
|  |  <widget class="QDialog" name="MoviePlayDialog"> | ||||||
|  |   <property name="geometry"> | ||||||
|  |    <rect> | ||||||
|  |     <x>0</x> | ||||||
|  |     <y>0</y> | ||||||
|  |     <width>600</width> | ||||||
|  |     <height>100</height> | ||||||
|  |    </rect> | ||||||
|  |   </property> | ||||||
|  |   <property name="windowTitle"> | ||||||
|  |    <string>Play Movie</string> | ||||||
|  |   </property> | ||||||
|  |   <layout class="QVBoxLayout"> | ||||||
|  |    <item> | ||||||
|  |     <layout class="QHBoxLayout"> | ||||||
|  |      <item> | ||||||
|  |       <widget class="QLabel"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>File:</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |      <item> | ||||||
|  |       <widget class="QLineEdit" name="filePath"/> | ||||||
|  |      </item> | ||||||
|  |      <item> | ||||||
|  |       <widget class="QToolButton" name="filePathButton"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>...</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |     </layout> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QLabel" name="note1Label"> | ||||||
|  |      <property name="visible"> | ||||||
|  |       <bool>false</bool> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QGroupBox" name="infoGroupBox"> | ||||||
|  |      <property name="title"> | ||||||
|  |       <string>Info</string> | ||||||
|  |      </property> | ||||||
|  |      <property name="visible"> | ||||||
|  |       <bool>false</bool> | ||||||
|  |      </property> | ||||||
|  |      <layout class="QFormLayout"> | ||||||
|  |       <item row="0" column="0"> | ||||||
|  |        <widget class="QLabel"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Game:</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="0" column="1"> | ||||||
|  |        <widget class="QLineEdit" name="gameLineEdit"> | ||||||
|  |         <property name="readOnly"> | ||||||
|  |          <bool>true</bool> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="1" column="0"> | ||||||
|  |        <widget class="QLabel"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Author:</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="1" column="1"> | ||||||
|  |        <widget class="QLineEdit" name="authorLineEdit"> | ||||||
|  |         <property name="readOnly"> | ||||||
|  |          <bool>true</bool> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="2" column="0"> | ||||||
|  |        <widget class="QLabel" name="rerecordCountLabel"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Rerecord Count:</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="2" column="1"> | ||||||
|  |        <widget class="QLineEdit" name="rerecordCountLineEdit"> | ||||||
|  |         <property name="readOnly"> | ||||||
|  |          <bool>true</bool> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="3" column="0"> | ||||||
|  |        <widget class="QLabel" name="lengthLabel"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Length:</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |       <item row="3" column="1"> | ||||||
|  |        <widget class="QLineEdit" name="lengthLineEdit"> | ||||||
|  |         <property name="readOnly"> | ||||||
|  |          <bool>true</bool> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|  |      </layout> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <spacer> | ||||||
|  |      <property name="orientation"> | ||||||
|  |       <enum>Qt::Vertical</enum> | ||||||
|  |      </property> | ||||||
|  |     </spacer> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QLabel" name="note2Label"> | ||||||
|  |      <property name="visible"> | ||||||
|  |       <bool>false</bool> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QDialogButtonBox" name="buttonBox"> | ||||||
|  |      <property name="standardButtons"> | ||||||
|  |       <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |   </layout> | ||||||
|  |  </widget> | ||||||
|  | </ui> | ||||||
							
								
								
									
										61
									
								
								src/citra_qt/movie/movie_record_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/citra_qt/movie/movie_record_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <QFileDialog> | ||||||
|  | #include <QPushButton> | ||||||
|  | #include "citra_qt/movie/movie_record_dialog.h" | ||||||
|  | #include "citra_qt/uisettings.h" | ||||||
|  | #include "core/core.h" | ||||||
|  | #include "core/movie.h" | ||||||
|  | #include "ui_movie_record_dialog.h" | ||||||
|  | 
 | ||||||
|  | MovieRecordDialog::MovieRecordDialog(QWidget* parent) | ||||||
|  |     : QDialog(parent), ui(std::make_unique<Ui::MovieRecordDialog>()) { | ||||||
|  |     ui->setupUi(this); | ||||||
|  | 
 | ||||||
|  |     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); | ||||||
|  | 
 | ||||||
|  |     connect(ui->filePathButton, &QToolButton::clicked, this, | ||||||
|  |             &MovieRecordDialog::OnToolButtonClicked); | ||||||
|  |     connect(ui->filePath, &QLineEdit::editingFinished, this, &MovieRecordDialog::UpdateUIDisplay); | ||||||
|  |     connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MovieRecordDialog::accept); | ||||||
|  |     connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MovieRecordDialog::reject); | ||||||
|  | 
 | ||||||
|  |     QString note_text; | ||||||
|  |     if (Core::System::GetInstance().IsPoweredOn()) { | ||||||
|  |         note_text = tr("Current running game will be restarted."); | ||||||
|  |         if (Core::Movie::GetInstance().GetPlayMode() == Core::Movie::PlayMode::Recording) { | ||||||
|  |             note_text.append(tr("<br>Current recording will be discarded.")); | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         note_text = tr("Recording will start once you boot a game."); | ||||||
|  |     } | ||||||
|  |     ui->noteLabel->setText(note_text); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | MovieRecordDialog::~MovieRecordDialog() = default; | ||||||
|  | 
 | ||||||
|  | QString MovieRecordDialog::GetPath() const { | ||||||
|  |     return ui->filePath->text(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | QString MovieRecordDialog::GetAuthor() const { | ||||||
|  |     return ui->authorLineEdit->text(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void MovieRecordDialog::OnToolButtonClicked() { | ||||||
|  |     const QString path = | ||||||
|  |         QFileDialog::getSaveFileName(this, tr("Record Movie"), UISettings::values.movie_record_path, | ||||||
|  |                                      tr("Citra TAS Movie (*.ctm)")); | ||||||
|  |     if (path.isEmpty()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     ui->filePath->setText(path); | ||||||
|  |     UISettings::values.movie_record_path = path; | ||||||
|  |     UpdateUIDisplay(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void MovieRecordDialog::UpdateUIDisplay() { | ||||||
|  |     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!ui->filePath->text().isEmpty()); | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								src/citra_qt/movie/movie_record_dialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/citra_qt/movie/movie_record_dialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | #include <QDialog> | ||||||
|  | 
 | ||||||
|  | namespace Ui { | ||||||
|  | class MovieRecordDialog; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class MovieRecordDialog : public QDialog { | ||||||
|  |     Q_OBJECT | ||||||
|  | 
 | ||||||
|  | public: | ||||||
|  |     explicit MovieRecordDialog(QWidget* parent); | ||||||
|  |     ~MovieRecordDialog() override; | ||||||
|  | 
 | ||||||
|  |     QString GetPath() const; | ||||||
|  |     QString GetAuthor() const; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     void OnToolButtonClicked(); | ||||||
|  |     void UpdateUIDisplay(); | ||||||
|  | 
 | ||||||
|  |     std::unique_ptr<Ui::MovieRecordDialog> ui; | ||||||
|  | }; | ||||||
							
								
								
									
										71
									
								
								src/citra_qt/movie/movie_record_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/citra_qt/movie/movie_record_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <ui version="4.0"> | ||||||
|  |  <class>MovieRecordDialog</class> | ||||||
|  |  <widget class="QDialog" name="MovieRecordDialog"> | ||||||
|  |   <property name="geometry"> | ||||||
|  |    <rect> | ||||||
|  |     <x>0</x> | ||||||
|  |     <y>0</y> | ||||||
|  |     <width>600</width> | ||||||
|  |     <height>150</height> | ||||||
|  |    </rect> | ||||||
|  |   </property> | ||||||
|  |   <property name="windowTitle"> | ||||||
|  |    <string>Record Movie</string> | ||||||
|  |   </property> | ||||||
|  |   <layout class="QVBoxLayout"> | ||||||
|  |    <item> | ||||||
|  |     <layout class="QGridLayout"> | ||||||
|  |      <item row="0" column="0"> | ||||||
|  |       <widget class="QLabel"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>File:</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |      <item row="0" column="1"> | ||||||
|  |       <widget class="QLineEdit" name="filePath"/> | ||||||
|  |      </item> | ||||||
|  |      <item row="0" column="2"> | ||||||
|  |       <widget class="QToolButton" name="filePathButton"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>...</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |      <item row="1" column="0"> | ||||||
|  |       <widget class="QLabel"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>Author:</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |      <item row="1" column="1"> | ||||||
|  |       <widget class="QLineEdit" name="authorLineEdit"> | ||||||
|  |        <property name="maxLength"> | ||||||
|  |         <number>32</number> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |     </layout> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <spacer> | ||||||
|  |      <property name="orientation"> | ||||||
|  |       <enum>Qt::Vertical</enum> | ||||||
|  |      </property> | ||||||
|  |     </spacer> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QLabel" name="noteLabel"/> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QDialogButtonBox" name="buttonBox"> | ||||||
|  |      <property name="standardButtons"> | ||||||
|  |       <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |   </layout> | ||||||
|  |  </widget> | ||||||
|  | </ui> | ||||||
|  | @ -630,6 +630,7 @@ void System::serialize(Archive& ar, const unsigned int file_version) { | ||||||
| 
 | 
 | ||||||
|     // This needs to be set from somewhere - might as well be here!
 |     // This needs to be set from somewhere - might as well be here!
 | ||||||
|     if (Archive::is_loading::value) { |     if (Archive::is_loading::value) { | ||||||
|  |         timing->UnlockEventQueue(); | ||||||
|         Service::GSP::SetGlobalModule(*this); |         Service::GSP::SetGlobalModule(*this); | ||||||
|         memory->SetDSP(*dsp_core); |         memory->SetDSP(*dsp_core); | ||||||
|         cheat_engine->Connect(); |         cheat_engine->Connect(); | ||||||
|  |  | ||||||
|  | @ -49,6 +49,10 @@ TimingEventType* Timing::RegisterEvent(const std::string& name, TimedCallback ca | ||||||
| 
 | 
 | ||||||
| void Timing::ScheduleEvent(s64 cycles_into_future, const TimingEventType* event_type, u64 userdata, | void Timing::ScheduleEvent(s64 cycles_into_future, const TimingEventType* event_type, u64 userdata, | ||||||
|                            std::size_t core_id) { |                            std::size_t core_id) { | ||||||
|  |     if (event_queue_locked) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     ASSERT(event_type != nullptr); |     ASSERT(event_type != nullptr); | ||||||
|     Timing::Timer* timer = nullptr; |     Timing::Timer* timer = nullptr; | ||||||
|     if (core_id == std::numeric_limits<std::size_t>::max()) { |     if (core_id == std::numeric_limits<std::size_t>::max()) { | ||||||
|  | @ -74,6 +78,9 @@ void Timing::ScheduleEvent(s64 cycles_into_future, const TimingEventType* event_ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Timing::UnscheduleEvent(const TimingEventType* event_type, u64 userdata) { | void Timing::UnscheduleEvent(const TimingEventType* event_type, u64 userdata) { | ||||||
|  |     if (event_queue_locked) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|     for (auto timer : timers) { |     for (auto timer : timers) { | ||||||
|         auto itr = std::remove_if( |         auto itr = std::remove_if( | ||||||
|             timer->event_queue.begin(), timer->event_queue.end(), |             timer->event_queue.begin(), timer->event_queue.end(), | ||||||
|  | @ -89,6 +96,9 @@ void Timing::UnscheduleEvent(const TimingEventType* event_type, u64 userdata) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Timing::RemoveEvent(const TimingEventType* event_type) { | void Timing::RemoveEvent(const TimingEventType* event_type) { | ||||||
|  |     if (event_queue_locked) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|     for (auto timer : timers) { |     for (auto timer : timers) { | ||||||
|         auto itr = std::remove_if(timer->event_queue.begin(), timer->event_queue.end(), |         auto itr = std::remove_if(timer->event_queue.begin(), timer->event_queue.end(), | ||||||
|                                   [&](const Event& e) { return e.type == event_type; }); |                                   [&](const Event& e) { return e.type == event_type; }); | ||||||
|  |  | ||||||
|  | @ -280,6 +280,11 @@ public: | ||||||
| 
 | 
 | ||||||
|     std::shared_ptr<Timer> GetTimer(std::size_t cpu_id); |     std::shared_ptr<Timer> GetTimer(std::size_t cpu_id); | ||||||
| 
 | 
 | ||||||
|  |     // Used after deserializing to unprotect the event queue.
 | ||||||
|  |     void UnlockEventQueue() { | ||||||
|  |         event_queue_locked = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|     // unordered_map stores each element separately as a linked list node so pointers to
 |     // unordered_map stores each element separately as a linked list node so pointers to
 | ||||||
|     // elements remain stable regardless of rehashes/resizing.
 |     // elements remain stable regardless of rehashes/resizing.
 | ||||||
|  | @ -292,6 +297,10 @@ private: | ||||||
|     // under/overclocking the guest cpu
 |     // under/overclocking the guest cpu
 | ||||||
|     double cpu_clock_scale = 1.0; |     double cpu_clock_scale = 1.0; | ||||||
| 
 | 
 | ||||||
|  |     // When true, the event queue can't be modified. Used while deserializing to workaround
 | ||||||
|  |     // destructor side effects.
 | ||||||
|  |     bool event_queue_locked = false; | ||||||
|  | 
 | ||||||
|     template <class Archive> |     template <class Archive> | ||||||
|     void serialize(Archive& ar, const unsigned int file_version) { |     void serialize(Archive& ar, const unsigned int file_version) { | ||||||
|         // event_types set during initialization of other things
 |         // event_types set during initialization of other things
 | ||||||
|  | @ -303,6 +312,9 @@ private: | ||||||
|         } else { |         } else { | ||||||
|             ar& current_timer; |             ar& current_timer; | ||||||
|         } |         } | ||||||
|  |         if (Archive::is_loading::value) { | ||||||
|  |             event_queue_locked = true; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     friend class boost::serialization::access; |     friend class boost::serialization::access; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -12,7 +12,6 @@ | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
| #include "core/3ds.h" | #include "core/3ds.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
| #include "core/core_timing.h" |  | ||||||
| #include "core/hle/ipc_helpers.h" | #include "core/hle/ipc_helpers.h" | ||||||
| #include "core/hle/kernel/event.h" | #include "core/hle/kernel/event.h" | ||||||
| #include "core/hle/kernel/handle_table.h" | #include "core/hle/kernel/handle_table.h" | ||||||
|  | @ -55,11 +54,6 @@ void Module::serialize(Archive& ar, const unsigned int file_version) { | ||||||
| } | } | ||||||
| SERIALIZE_IMPL(Module) | SERIALIZE_IMPL(Module) | ||||||
| 
 | 
 | ||||||
| // Updating period for each HID device. These empirical values are measured from a 11.2 3DS.
 |  | ||||||
| constexpr u64 pad_update_ticks = BASE_CLOCK_RATE_ARM11 / 234; |  | ||||||
| constexpr u64 accelerometer_update_ticks = BASE_CLOCK_RATE_ARM11 / 104; |  | ||||||
| constexpr u64 gyroscope_update_ticks = BASE_CLOCK_RATE_ARM11 / 101; |  | ||||||
| 
 |  | ||||||
| constexpr float accelerometer_coef = 512.0f; // measured from hw test result
 | constexpr float accelerometer_coef = 512.0f; // measured from hw test result
 | ||||||
| constexpr float gyroscope_coef = 14.375f; // got from hwtest GetGyroscopeLowRawToDpsCoefficient call
 | constexpr float gyroscope_coef = 14.375f; // got from hwtest GetGyroscopeLowRawToDpsCoefficient call
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
| #include "common/bit_field.h" | #include "common/bit_field.h" | ||||||
| #include "common/common_funcs.h" | #include "common/common_funcs.h" | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
|  | #include "core/core_timing.h" | ||||||
| #include "core/frontend/input.h" | #include "core/frontend/input.h" | ||||||
| #include "core/hle/service/service.h" | #include "core/hle/service/service.h" | ||||||
| #include "core/settings.h" | #include "core/settings.h" | ||||||
|  | @ -299,6 +300,11 @@ public: | ||||||
| 
 | 
 | ||||||
|     const PadState& GetState() const; |     const PadState& GetState() const; | ||||||
| 
 | 
 | ||||||
|  |     // Updating period for each HID device. These empirical values are measured from a 11.2 3DS.
 | ||||||
|  |     static constexpr u64 pad_update_ticks = BASE_CLOCK_RATE_ARM11 / 234; | ||||||
|  |     static constexpr u64 accelerometer_update_ticks = BASE_CLOCK_RATE_ARM11 / 104; | ||||||
|  |     static constexpr u64 gyroscope_update_ticks = BASE_CLOCK_RATE_ARM11 / 101; | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|     void LoadInputDevices(); |     void LoadInputDevices(); | ||||||
|     void UpdatePadCallback(u64 userdata, s64 cycles_late); |     void UpdatePadCallback(u64 userdata, s64 cycles_late); | ||||||
|  |  | ||||||
|  | @ -2,11 +2,14 @@ | ||||||
| // Licensed under GPLv2 or any later version
 | // Licensed under GPLv2 or any later version
 | ||||||
| // Refer to the license.txt file included.
 | // Refer to the license.txt file included.
 | ||||||
| 
 | 
 | ||||||
|  | #include <algorithm> | ||||||
| #include <cstring> | #include <cstring> | ||||||
|  | #include <stdexcept> | ||||||
| #include <string> | #include <string> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <boost/optional.hpp> | #include <boost/optional.hpp> | ||||||
| #include <cryptopp/hex.h> | #include <cryptopp/hex.h> | ||||||
|  | #include <cryptopp/osrng.h> | ||||||
| #include "common/bit_field.h" | #include "common/bit_field.h" | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/file_util.h" | #include "common/file_util.h" | ||||||
|  | @ -25,8 +28,6 @@ namespace Core { | ||||||
| 
 | 
 | ||||||
| /*static*/ Movie Movie::s_instance; | /*static*/ Movie Movie::s_instance; | ||||||
| 
 | 
 | ||||||
| enum class PlayMode { None, Recording, Playing }; |  | ||||||
| 
 |  | ||||||
| enum class ControllerStateType : u8 { | enum class ControllerStateType : u8 { | ||||||
|     PadAndCircle, |     PadAndCircle, | ||||||
|     Touch, |     Touch, | ||||||
|  | @ -117,24 +118,120 @@ struct CTMHeader { | ||||||
|     u64_le program_id;           /// ID of the ROM being executed. Also called title_id
 |     u64_le program_id;           /// ID of the ROM being executed. Also called title_id
 | ||||||
|     std::array<u8, 20> revision; /// Git hash of the revision this movie was created with
 |     std::array<u8, 20> revision; /// Git hash of the revision this movie was created with
 | ||||||
|     u64_le clock_init_time;      /// The init time of the system clock
 |     u64_le clock_init_time;      /// The init time of the system clock
 | ||||||
|  |     u64_le id; /// Unique identifier of the movie, used to support separate savestate slots
 | ||||||
|  |     std::array<char, 32> author; /// Author of the movie
 | ||||||
|  |     u32_le rerecord_count;       /// Number of rerecords when making the movie
 | ||||||
|  |     u64_le input_count;          /// Number of inputs (button and pad states) when making the movie
 | ||||||
| 
 | 
 | ||||||
|     std::array<u8, 216> reserved; /// Make heading 256 bytes so it has consistent size
 |     std::array<u8, 164> reserved; /// Make heading 256 bytes so it has consistent size
 | ||||||
| }; | }; | ||||||
| static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); | static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); | ||||||
| #pragma pack(pop) | #pragma pack(pop) | ||||||
| 
 | 
 | ||||||
| bool Movie::IsPlayingInput() const { | static u64 GetInputCount(const std::vector<u8>& input) { | ||||||
|     return play_mode == PlayMode::Playing; |     u64 input_count = 0; | ||||||
|  |     for (std::size_t pos = 0; pos < input.size(); pos += sizeof(ControllerState)) { | ||||||
|  |         if (input.size() < pos + sizeof(ControllerState)) { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ControllerState state; | ||||||
|  |         std::memcpy(&state, input.data() + pos, sizeof(ControllerState)); | ||||||
|  |         if (state.type == ControllerStateType::PadAndCircle) { | ||||||
|  |             input_count++; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return input_count; | ||||||
| } | } | ||||||
| bool Movie::IsRecordingInput() const { | 
 | ||||||
|     return play_mode == PlayMode::Recording; | template <class Archive> | ||||||
|  | void Movie::serialize(Archive& ar, const unsigned int file_version) { | ||||||
|  |     // Only serialize what's needed to make savestates useful for TAS:
 | ||||||
|  |     u64 _current_byte = static_cast<u64>(current_byte); | ||||||
|  |     ar& _current_byte; | ||||||
|  |     current_byte = static_cast<std::size_t>(_current_byte); | ||||||
|  | 
 | ||||||
|  |     if (file_version > 0) { | ||||||
|  |         ar& current_input; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::vector<u8> recorded_input_ = recorded_input; | ||||||
|  |     ar& recorded_input_; | ||||||
|  | 
 | ||||||
|  |     ar& init_time; | ||||||
|  | 
 | ||||||
|  |     if (file_version > 0) { | ||||||
|  |         if (Archive::is_loading::value) { | ||||||
|  |             u64 savestate_movie_id; | ||||||
|  |             ar& savestate_movie_id; | ||||||
|  |             if (id != savestate_movie_id) { | ||||||
|  |                 if (savestate_movie_id == 0) { | ||||||
|  |                     throw std::runtime_error("You must close your movie to load this state"); | ||||||
|  |                 } else { | ||||||
|  |                     throw std::runtime_error("You must load the same movie to load this state"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             ar& id; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Whether the state was made in MovieFinished state
 | ||||||
|  |     bool post_movie = play_mode == PlayMode::MovieFinished; | ||||||
|  |     if (file_version > 0) { | ||||||
|  |         ar& post_movie; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Archive::is_loading::value && id != 0) { | ||||||
|  |         if (!read_only) { | ||||||
|  |             recorded_input = std::move(recorded_input_); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (post_movie) { | ||||||
|  |             play_mode = PlayMode::MovieFinished; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (read_only) { | ||||||
|  |             if (play_mode == PlayMode::Recording) { | ||||||
|  |                 SaveMovie(); | ||||||
|  |             } | ||||||
|  |             if (recorded_input_.size() >= recorded_input.size()) { | ||||||
|  |                 throw std::runtime_error("Future event savestate not allowed in R/O mode"); | ||||||
|  |             } | ||||||
|  |             // Ensure that the current movie and savestate movie are in the same timeline
 | ||||||
|  |             if (std::mismatch(recorded_input_.begin(), recorded_input_.end(), | ||||||
|  |                               recorded_input.begin()) | ||||||
|  |                     .first != recorded_input_.end()) { | ||||||
|  |                 throw std::runtime_error("Timeline mismatch not allowed in R/O mode"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             play_mode = PlayMode::Playing; | ||||||
|  |             total_input = GetInputCount(recorded_input); | ||||||
|  |         } else { | ||||||
|  |             play_mode = PlayMode::Recording; | ||||||
|  |             rerecord_count++; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | SERIALIZE_IMPL(Movie) | ||||||
|  | 
 | ||||||
|  | Movie::PlayMode Movie::GetPlayMode() const { | ||||||
|  |     return play_mode; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u64 Movie::GetCurrentInputIndex() const { | ||||||
|  |     return current_input; | ||||||
|  | } | ||||||
|  | u64 Movie::GetTotalInputCount() const { | ||||||
|  |     return total_input; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Movie::CheckInputEnd() { | void Movie::CheckInputEnd() { | ||||||
|     if (current_byte + sizeof(ControllerState) > recorded_input.size()) { |     if (current_byte + sizeof(ControllerState) > recorded_input.size()) { | ||||||
|         LOG_INFO(Movie, "Playback finished"); |         LOG_INFO(Movie, "Playback finished"); | ||||||
|         play_mode = PlayMode::None; |         play_mode = PlayMode::MovieFinished; | ||||||
|         init_time = 0; |  | ||||||
|         playback_completion_callback(); |         playback_completion_callback(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -143,6 +240,7 @@ void Movie::Play(Service::HID::PadState& pad_state, s16& circle_pad_x, s16& circ | ||||||
|     ControllerState s; |     ControllerState s; | ||||||
|     std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); |     std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); | ||||||
|     current_byte += sizeof(ControllerState); |     current_byte += sizeof(ControllerState); | ||||||
|  |     current_input++; | ||||||
| 
 | 
 | ||||||
|     if (s.type != ControllerStateType::PadAndCircle) { |     if (s.type != ControllerStateType::PadAndCircle) { | ||||||
|         LOG_ERROR(Movie, |         LOG_ERROR(Movie, | ||||||
|  | @ -270,6 +368,8 @@ void Movie::Record(const ControllerState& controller_state) { | ||||||
| 
 | 
 | ||||||
| void Movie::Record(const Service::HID::PadState& pad_state, const s16& circle_pad_x, | void Movie::Record(const Service::HID::PadState& pad_state, const s16& circle_pad_x, | ||||||
|                    const s16& circle_pad_y) { |                    const s16& circle_pad_y) { | ||||||
|  |     current_input++; | ||||||
|  | 
 | ||||||
|     ControllerState s; |     ControllerState s; | ||||||
|     s.type = ControllerStateType::PadAndCircle; |     s.type = ControllerStateType::PadAndCircle; | ||||||
| 
 | 
 | ||||||
|  | @ -358,21 +458,13 @@ u64 Movie::GetOverrideInitTime() const { | ||||||
|     return init_time; |     return init_time; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 program_id) const { | Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) 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; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); |     std::string revision = fmt::format("{:02x}", fmt::join(header.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) { |     if (revision != Common::g_scm_rev) { | ||||||
|         LOG_WARNING(Movie, |         LOG_WARNING(Movie, | ||||||
|                     "This movie was created on a different version of Citra, playback may desync"); |                     "This movie was created on a different version of Citra, playback may desync"); | ||||||
|  | @ -382,6 +474,12 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 progr | ||||||
|     return ValidationResult::OK; |     return ValidationResult::OK; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Movie::ValidationResult Movie::ValidateInput(const std::vector<u8>& input, | ||||||
|  |                                              u64 expected_count) const { | ||||||
|  |     return GetInputCount(input) == expected_count ? ValidationResult::OK | ||||||
|  |                                                   : ValidationResult::InputCountDismatch; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void Movie::SaveMovie() { | void Movie::SaveMovie() { | ||||||
|     LOG_INFO(Movie, "Saving recorded movie to '{}'", record_movie_file); |     LOG_INFO(Movie, "Saving recorded movie to '{}'", record_movie_file); | ||||||
|     FileUtil::IOFile save_record(record_movie_file, "wb"); |     FileUtil::IOFile save_record(record_movie_file, "wb"); | ||||||
|  | @ -393,9 +491,15 @@ void Movie::SaveMovie() { | ||||||
| 
 | 
 | ||||||
|     CTMHeader header = {}; |     CTMHeader header = {}; | ||||||
|     header.filetype = header_magic_bytes; |     header.filetype = header_magic_bytes; | ||||||
|  |     header.program_id = program_id; | ||||||
|     header.clock_init_time = init_time; |     header.clock_init_time = init_time; | ||||||
|  |     header.id = id; | ||||||
| 
 | 
 | ||||||
|     Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); |     std::memcpy(header.author.data(), record_movie_author.data(), | ||||||
|  |                 std::min(header.author.size(), record_movie_author.size())); | ||||||
|  | 
 | ||||||
|  |     header.rerecord_count = rerecord_count; | ||||||
|  |     header.input_count = GetInputCount(recorded_input); | ||||||
| 
 | 
 | ||||||
|     std::string rev_bytes; |     std::string rev_bytes; | ||||||
|     CryptoPP::StringSource(Common::g_scm_rev, true, |     CryptoPP::StringSource(Common::g_scm_rev, true, | ||||||
|  | @ -410,8 +514,11 @@ void Movie::SaveMovie() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Movie::StartPlayback(const std::string& movie_file, | void Movie::SetPlaybackCompletionCallback(std::function<void()> completion_callback) { | ||||||
|                           std::function<void()> completion_callback) { |     playback_completion_callback = completion_callback; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Movie::StartPlayback(const std::string& movie_file) { | ||||||
|     LOG_INFO(Movie, "Loading Movie for playback"); |     LOG_INFO(Movie, "Loading Movie for playback"); | ||||||
|     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(); | ||||||
|  | @ -421,20 +528,49 @@ void Movie::StartPlayback(const std::string& movie_file, | ||||||
|         save_record.ReadArray(&header, 1); |         save_record.ReadArray(&header, 1); | ||||||
|         if (ValidateHeader(header) != ValidationResult::Invalid) { |         if (ValidateHeader(header) != ValidationResult::Invalid) { | ||||||
|             play_mode = PlayMode::Playing; |             play_mode = PlayMode::Playing; | ||||||
|  |             record_movie_file = movie_file; | ||||||
|  | 
 | ||||||
|  |             std::array<char, 33> author{}; // Add a null terminator
 | ||||||
|  |             std::memcpy(author.data(), header.author.data(), header.author.size()); | ||||||
|  |             record_movie_author = author.data(); | ||||||
|  | 
 | ||||||
|  |             rerecord_count = header.rerecord_count; | ||||||
|  |             total_input = header.input_count; | ||||||
|  | 
 | ||||||
|             recorded_input.resize(size - sizeof(CTMHeader)); |             recorded_input.resize(size - sizeof(CTMHeader)); | ||||||
|             save_record.ReadArray(recorded_input.data(), recorded_input.size()); |             save_record.ReadArray(recorded_input.data(), recorded_input.size()); | ||||||
|  | 
 | ||||||
|             current_byte = 0; |             current_byte = 0; | ||||||
|             playback_completion_callback = completion_callback; |             current_input = 0; | ||||||
|  |             id = header.id; | ||||||
|  |             program_id = header.program_id; | ||||||
|  | 
 | ||||||
|  |             LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); | ||||||
|         } |         } | ||||||
|     } else { |     } else { | ||||||
|         LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file); |         LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Movie::StartRecording(const std::string& movie_file) { | void Movie::StartRecording(const std::string& movie_file, const std::string& author) { | ||||||
|     LOG_INFO(Movie, "Enabling Movie recording"); |  | ||||||
|     play_mode = PlayMode::Recording; |     play_mode = PlayMode::Recording; | ||||||
|     record_movie_file = movie_file; |     record_movie_file = movie_file; | ||||||
|  |     record_movie_author = author; | ||||||
|  |     rerecord_count = 1; | ||||||
|  | 
 | ||||||
|  |     // Generate a random ID
 | ||||||
|  |     CryptoPP::AutoSeededRandomPool rng; | ||||||
|  |     rng.GenerateBlock(reinterpret_cast<CryptoPP::byte*>(&id), sizeof(id)); | ||||||
|  | 
 | ||||||
|  |     // Get program ID
 | ||||||
|  |     program_id = 0; | ||||||
|  |     Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); | ||||||
|  | 
 | ||||||
|  |     LOG_INFO(Movie, "Enabling Movie recording, ID: {:016X}", id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Movie::SetReadOnly(bool read_only_) { | ||||||
|  |     read_only = read_only_; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static boost::optional<CTMHeader> ReadHeader(const std::string& movie_file) { | static boost::optional<CTMHeader> ReadHeader(const std::string& movie_file) { | ||||||
|  | @ -469,25 +605,51 @@ void Movie::PrepareForRecording() { | ||||||
|                      : Settings::values.init_time); |                      : Settings::values.init_time); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const { | Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) const { | ||||||
|     LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); |     LOG_INFO(Movie, "Validating Movie file '{}'", movie_file); | ||||||
|     auto header = ReadHeader(movie_file); |  | ||||||
|     if (header == boost::none) |  | ||||||
|         return ValidationResult::Invalid; |  | ||||||
| 
 | 
 | ||||||
|     return ValidateHeader(header.value(), program_id); |     FileUtil::IOFile save_record(movie_file, "rb"); | ||||||
|  |     const u64 size = save_record.GetSize(); | ||||||
|  | 
 | ||||||
|  |     if (!save_record || size <= sizeof(CTMHeader)) { | ||||||
|  |         return ValidationResult::Invalid; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CTMHeader header; | ||||||
|  |     save_record.ReadArray(&header, 1); | ||||||
|  | 
 | ||||||
|  |     if (header_magic_bytes != header.filetype) { | ||||||
|  |         return ValidationResult::Invalid; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     auto result = ValidateHeader(header); | ||||||
|  |     if (result != ValidationResult::OK) { | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!header.input_count) { // Probably created by an older version.
 | ||||||
|  |         return ValidationResult::OK; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::vector<u8> input(size - sizeof(header)); | ||||||
|  |     save_record.ReadArray(input.data(), input.size()); | ||||||
|  |     return ValidateInput(input, header.input_count); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| u64 Movie::GetMovieProgramID(const std::string& movie_file) const { | Movie::MovieMetadata Movie::GetMovieMetadata(const std::string& movie_file) const { | ||||||
|     auto header = ReadHeader(movie_file); |     auto header = ReadHeader(movie_file); | ||||||
|     if (header == boost::none) |     if (header == boost::none) | ||||||
|         return 0; |         return {}; | ||||||
| 
 | 
 | ||||||
|     return static_cast<u64>(header.value().program_id); |     std::array<char, 33> author{}; // Add a null terminator
 | ||||||
|  |     std::memcpy(author.data(), header->author.data(), header->author.size()); | ||||||
|  | 
 | ||||||
|  |     return {header->program_id, std::string{author.data()}, header->rerecord_count, | ||||||
|  |             header->input_count}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Movie::Shutdown() { | void Movie::Shutdown() { | ||||||
|     if (IsRecordingInput()) { |     if (play_mode == PlayMode::Recording) { | ||||||
|         SaveMovie(); |         SaveMovie(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -495,16 +657,18 @@ void Movie::Shutdown() { | ||||||
|     recorded_input.resize(0); |     recorded_input.resize(0); | ||||||
|     record_movie_file.clear(); |     record_movie_file.clear(); | ||||||
|     current_byte = 0; |     current_byte = 0; | ||||||
|  |     current_input = 0; | ||||||
|     init_time = 0; |     init_time = 0; | ||||||
|  |     id = 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| template <typename... Targs> | template <typename... Targs> | ||||||
| void Movie::Handle(Targs&... Fargs) { | void Movie::Handle(Targs&... Fargs) { | ||||||
|     if (IsPlayingInput()) { |     if (play_mode == PlayMode::Playing) { | ||||||
|         ASSERT(current_byte + sizeof(ControllerState) <= recorded_input.size()); |         ASSERT(current_byte + sizeof(ControllerState) <= recorded_input.size()); | ||||||
|         Play(Fargs...); |         Play(Fargs...); | ||||||
|         CheckInputEnd(); |         CheckInputEnd(); | ||||||
|     } else if (IsRecordingInput()) { |     } else if (play_mode == PlayMode::Recording) { | ||||||
|         Record(Fargs...); |         Record(Fargs...); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -24,14 +24,14 @@ union PadState; | ||||||
| namespace Core { | namespace Core { | ||||||
| struct CTMHeader; | struct CTMHeader; | ||||||
| struct ControllerState; | struct ControllerState; | ||||||
| enum class PlayMode; |  | ||||||
| 
 | 
 | ||||||
| class Movie { | class Movie { | ||||||
| public: | public: | ||||||
|  |     enum class PlayMode { None, Recording, Playing, MovieFinished }; | ||||||
|     enum class ValidationResult { |     enum class ValidationResult { | ||||||
|         OK, |         OK, | ||||||
|         RevisionDismatch, |         RevisionDismatch, | ||||||
|         GameDismatch, |         InputCountDismatch, | ||||||
|         Invalid, |         Invalid, | ||||||
|     }; |     }; | ||||||
|     /**
 |     /**
 | ||||||
|  | @ -42,9 +42,21 @@ public: | ||||||
|         return s_instance; |         return s_instance; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     void StartPlayback( |     void SetPlaybackCompletionCallback(std::function<void()> completion_callback); | ||||||
|         const std::string& movie_file, std::function<void()> completion_callback = [] {}); |     void StartPlayback(const std::string& movie_file); | ||||||
|     void StartRecording(const std::string& movie_file); |     void StartRecording(const std::string& movie_file, const std::string& author); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Sets the read-only status. | ||||||
|  |      * When true, movies will be opened in read-only mode. Loading a state will resume playback | ||||||
|  |      * from that state. | ||||||
|  |      * When false, movies will be opened in read/write mode. Loading a state will start recording | ||||||
|  |      * from that state (rerecording). To start rerecording without loading a state, one can save | ||||||
|  |      * and then immediately load while in R/W. | ||||||
|  |      * | ||||||
|  |      * The default is true. | ||||||
|  |      */ | ||||||
|  |     void SetReadOnly(bool read_only); | ||||||
| 
 | 
 | ||||||
|     /// Prepare to override the clock before playing back movies
 |     /// Prepare to override the clock before playing back movies
 | ||||||
|     void PrepareForPlayback(const std::string& movie_file); |     void PrepareForPlayback(const std::string& movie_file); | ||||||
|  | @ -52,11 +64,23 @@ public: | ||||||
|     /// Prepare to override the clock before recording movies
 |     /// Prepare to override the clock before recording movies
 | ||||||
|     void PrepareForRecording(); |     void PrepareForRecording(); | ||||||
| 
 | 
 | ||||||
|     ValidationResult ValidateMovie(const std::string& movie_file, u64 program_id = 0) const; |     ValidationResult ValidateMovie(const std::string& movie_file) const; | ||||||
| 
 | 
 | ||||||
|     /// Get the init time that would override the one in the settings
 |     /// Get the init time that would override the one in the settings
 | ||||||
|     u64 GetOverrideInitTime() const; |     u64 GetOverrideInitTime() const; | ||||||
|     u64 GetMovieProgramID(const std::string& movie_file) const; | 
 | ||||||
|  |     struct MovieMetadata { | ||||||
|  |         u64 program_id; | ||||||
|  |         std::string author; | ||||||
|  |         u32 rerecord_count; | ||||||
|  |         u64 input_count; | ||||||
|  |     }; | ||||||
|  |     MovieMetadata GetMovieMetadata(const std::string& movie_file) const; | ||||||
|  | 
 | ||||||
|  |     /// Get the current movie's unique ID. Used to provide separate savestate slots for movies.
 | ||||||
|  |     u64 GetCurrentMovieID() const { | ||||||
|  |         return id; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     void Shutdown(); |     void Shutdown(); | ||||||
| 
 | 
 | ||||||
|  | @ -96,8 +120,16 @@ public: | ||||||
|      * When playing: Replaces the given input states with the ones stored in the playback file |      * When playing: Replaces the given input states with the ones stored in the playback file | ||||||
|      */ |      */ | ||||||
|     void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response); |     void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response); | ||||||
|     bool IsPlayingInput() const; |     PlayMode GetPlayMode() const; | ||||||
|     bool IsRecordingInput() const; | 
 | ||||||
|  |     u64 GetCurrentInputIndex() const; | ||||||
|  |     u64 GetTotalInputCount() const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Saves the movie immediately, in its current state. | ||||||
|  |      * This is called in Shutdown. | ||||||
|  |      */ | ||||||
|  |     void SaveMovie(); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     static Movie s_instance; |     static Movie s_instance; | ||||||
|  | @ -123,26 +155,33 @@ 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, u64 program_id = 0) const; |     ValidationResult ValidateHeader(const CTMHeader& header) const; | ||||||
| 
 |     ValidationResult ValidateInput(const std::vector<u8>& input, u64 expected_count) const; | ||||||
|     void SaveMovie(); |  | ||||||
| 
 | 
 | ||||||
|     PlayMode play_mode; |     PlayMode play_mode; | ||||||
|  | 
 | ||||||
|     std::string record_movie_file; |     std::string record_movie_file; | ||||||
|  |     std::string record_movie_author; | ||||||
|  | 
 | ||||||
|  |     u64 init_time; // Clock init time override for RNG consistency
 | ||||||
|  | 
 | ||||||
|     std::vector<u8> recorded_input; |     std::vector<u8> recorded_input; | ||||||
|     u64 init_time; |  | ||||||
|     std::function<void()> playback_completion_callback; |  | ||||||
|     std::size_t current_byte = 0; |     std::size_t current_byte = 0; | ||||||
|  |     u64 current_input = 0; | ||||||
|  |     // Total input count of the current movie being played. Not used for recording.
 | ||||||
|  |     u64 total_input = 0; | ||||||
|  | 
 | ||||||
|  |     u64 id = 0; // ID of the current movie loaded
 | ||||||
|  |     u64 program_id = 0; | ||||||
|  |     u32 rerecord_count = 1; | ||||||
|  |     bool read_only = true; | ||||||
|  | 
 | ||||||
|  |     std::function<void()> playback_completion_callback = [] {}; | ||||||
| 
 | 
 | ||||||
|     template <class Archive> |     template <class Archive> | ||||||
|     void serialize(Archive& ar, const unsigned int) { |     void serialize(Archive& ar, const unsigned int file_version); | ||||||
|         // Only serialize what's needed to make savestates useful for TAS:
 |  | ||||||
|         u64 _current_byte = static_cast<u64>(current_byte); |  | ||||||
|         ar& _current_byte; |  | ||||||
|         current_byte = static_cast<std::size_t>(_current_byte); |  | ||||||
|         ar& recorded_input; |  | ||||||
|         ar& init_time; |  | ||||||
|     } |  | ||||||
|     friend class boost::serialization::access; |     friend class boost::serialization::access; | ||||||
| }; | }; | ||||||
| } // namespace Core
 | } // namespace Core
 | ||||||
|  | 
 | ||||||
|  | BOOST_CLASS_VERSION(Core::Movie, 1) | ||||||
|  |  | ||||||
|  | @ -169,6 +169,10 @@ void FrameLimiter::DoFrameLimiting(microseconds current_system_time_us) { | ||||||
|     previous_walltime = now; |     previous_walltime = now; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | bool FrameLimiter::IsFrameAdvancing() const { | ||||||
|  |     return frame_advancing_enabled; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void FrameLimiter::SetFrameAdvancing(bool value) { | void FrameLimiter::SetFrameAdvancing(bool value) { | ||||||
|     const bool was_enabled = frame_advancing_enabled.exchange(value); |     const bool was_enabled = frame_advancing_enabled.exchange(value); | ||||||
|     if (was_enabled && !value) { |     if (was_enabled && !value) { | ||||||
|  |  | ||||||
|  | @ -90,6 +90,7 @@ public: | ||||||
| 
 | 
 | ||||||
|     void DoFrameLimiting(std::chrono::microseconds current_system_time_us); |     void DoFrameLimiting(std::chrono::microseconds current_system_time_us); | ||||||
| 
 | 
 | ||||||
|  |     bool IsFrameAdvancing() const; | ||||||
|     /**
 |     /**
 | ||||||
|      * Sets whether frame advancing is enabled or not. |      * Sets whether frame advancing is enabled or not. | ||||||
|      * Note: The frontend must cancel frame advancing before shutting down in order |      * Note: The frontend must cancel frame advancing before shutting down in order | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| #include "common/zstd_compression.h" | #include "common/zstd_compression.h" | ||||||
| #include "core/cheats/cheats.h" | #include "core/cheats/cheats.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  | #include "core/movie.h" | ||||||
| #include "core/savestate.h" | #include "core/savestate.h" | ||||||
| #include "network/network.h" | #include "network/network.h" | ||||||
| #include "video_core/video_core.h" | #include "video_core/video_core.h" | ||||||
|  | @ -37,8 +38,15 @@ static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes"); | ||||||
| constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}}; | constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}}; | ||||||
| 
 | 
 | ||||||
| std::string GetSaveStatePath(u64 program_id, u32 slot) { | std::string GetSaveStatePath(u64 program_id, u32 slot) { | ||||||
|     return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), |     const u64 movie_id = Movie::GetInstance().GetCurrentMovieID(); | ||||||
|                        program_id, slot); |     if (movie_id) { | ||||||
|  |         return fmt::format("{}{:016X}.movie{:016X}.{:02d}.cst", | ||||||
|  |                            FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, | ||||||
|  |                            movie_id, slot); | ||||||
|  |     } else { | ||||||
|  |         return fmt::format("{}{:016X}.{:02d}.cst", | ||||||
|  |                            FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, slot); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| std::vector<SaveStateInfo> ListSaveStates(u64 program_id) { | std::vector<SaveStateInfo> ListSaveStates(u64 program_id) { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue