From ebaa225bcbf823db358632bdc6f3662e9aaabc96 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Mon, 29 Jun 2020 22:32:24 +0800 Subject: [PATCH] core: Add read-only mode and separate savestate slots for movies The read-only mode switch affects how movies interact with savestates after you start a movie playback and load a savestate. When you are in read-only mode, the movie will resume playing from the loaded savestate. When you are in read+write mode however, your input will be recorded over the original movie ('rerecording'). If you wish to start rerecording immediately, you should switch to R+W mode, save a state and then load it. To make this more user-friendly, I also added a unique ID to the movies, which allows each movie to have an individual set of savestate slots (plus another set for when not doing any movies). This is as recommended by staff at TASVideos. --- src/core/movie.cpp | 71 ++++++++++++++++++++++++++++++++++++++++-- src/core/movie.h | 32 +++++++++++++------ src/core/savestate.cpp | 12 +++++-- 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 97c96ba3c..f680e9226 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -3,10 +3,12 @@ // Refer to the license.txt file included. #include +#include #include #include #include #include +#include #include "common/bit_field.h" #include "common/common_types.h" #include "common/file_util.h" @@ -117,12 +119,61 @@ struct CTMHeader { u64_le program_id; /// ID of the ROM being executed. Also called title_id std::array revision; /// Git hash of the revision this movie was created with u64_le clock_init_time; /// The init time of the system clock + // Unique identifier of the movie, used to support separate savestate slots for TASing + u64_le id; - std::array reserved; /// Make heading 256 bytes so it has consistent size + std::array reserved; /// Make heading 256 bytes so it has consistent size }; static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) +template +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(current_byte); + ar& _current_byte; + current_byte = static_cast(_current_byte); + + std::vector 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; + } + } + + if (Archive::is_loading::value && id != 0) { + if (read_only) { // Do not replace the previously recorded input. + if (play_mode == PlayMode::Recording) { + SaveMovie(); + } + if (current_byte >= recorded_input.size()) { + throw std::runtime_error( + "This savestate was created at a later point and must be loaded in R+W mode"); + } + play_mode = PlayMode::Playing; + } else { + recorded_input = std::move(recorded_input_); + play_mode = PlayMode::Recording; + } + } +} + +SERIALIZE_IMPL(Movie) + bool Movie::IsPlayingInput() const { return play_mode == PlayMode::Playing; } @@ -135,6 +186,7 @@ void Movie::CheckInputEnd() { LOG_INFO(Movie, "Playback finished"); play_mode = PlayMode::None; init_time = 0; + id = 0; playback_completion_callback(); } } @@ -394,6 +446,7 @@ void Movie::SaveMovie() { CTMHeader header = {}; header.filetype = header_magic_bytes; header.clock_init_time = init_time; + header.id = id; Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); @@ -421,10 +474,14 @@ void Movie::StartPlayback(const std::string& movie_file, save_record.ReadArray(&header, 1); if (ValidateHeader(header) != ValidationResult::Invalid) { play_mode = PlayMode::Playing; + record_movie_file = movie_file; recorded_input.resize(size - sizeof(CTMHeader)); save_record.ReadArray(recorded_input.data(), recorded_input.size()); current_byte = 0; + id = header.id; playback_completion_callback = completion_callback; + + LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id); } } else { LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file); @@ -432,9 +489,18 @@ void Movie::StartPlayback(const std::string& movie_file, } void Movie::StartRecording(const std::string& movie_file) { - LOG_INFO(Movie, "Enabling Movie recording"); play_mode = PlayMode::Recording; record_movie_file = movie_file; + + // Generate a random ID + CryptoPP::AutoSeededRandomPool rng; + rng.GenerateBlock(reinterpret_cast(&id), sizeof(id)); + + LOG_INFO(Movie, "Enabling Movie recording, ID: {:016X}", id); +} + +void Movie::SetReadOnly(bool read_only_) { + read_only = read_only_; } static boost::optional ReadHeader(const std::string& movie_file) { @@ -496,6 +562,7 @@ void Movie::Shutdown() { record_movie_file.clear(); current_byte = 0; init_time = 0; + id = 0; } template diff --git a/src/core/movie.h b/src/core/movie.h index e578b909c..0a4ab410a 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -46,6 +46,18 @@ public: const std::string& movie_file, std::function completion_callback = [] {}); void StartRecording(const std::string& movie_file); + /** + * 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 void PrepareForPlayback(const std::string& movie_file); @@ -58,6 +70,11 @@ public: u64 GetOverrideInitTime() const; u64 GetMovieProgramID(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(); /** @@ -133,16 +150,13 @@ private: u64 init_time; std::function playback_completion_callback; std::size_t current_byte = 0; + u64 id = 0; // ID of the current movie loaded + bool read_only = true; template - void serialize(Archive& ar, const unsigned int) { - // Only serialize what's needed to make savestates useful for TAS: - u64 _current_byte = static_cast(current_byte); - ar& _current_byte; - current_byte = static_cast(_current_byte); - ar& recorded_input; - ar& init_time; - } + void serialize(Archive& ar, const unsigned int file_version); friend class boost::serialization::access; }; -} // namespace Core \ No newline at end of file +} // namespace Core + +BOOST_CLASS_VERSION(Core::Movie, 1) diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index 26fc0cbed..a5bdb49d8 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -11,6 +11,7 @@ #include "common/zstd_compression.h" #include "core/cheats/cheats.h" #include "core/core.h" +#include "core/movie.h" #include "core/savestate.h" #include "network/network.h" #include "video_core/video_core.h" @@ -37,8 +38,15 @@ static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes"); constexpr std::array header_magic_bytes{{'C', 'S', 'T', 0x1B}}; std::string GetSaveStatePath(u64 program_id, u32 slot) { - return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), - program_id, slot); + const u64 movie_id = Movie::GetInstance().GetCurrentMovieID(); + 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 ListSaveStates(u64 program_id) {