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) {