From bd8866724754338f5e3fb986271e241ec00f17c5 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 30 Jun 2020 22:48:10 +0800 Subject: [PATCH] core/movie: Add a few fields These fields are included in most emulators and required by TASVideos. `input_count` is implemented by counting the number of 'PadAndCircle' states, as this is always polled regularly and can act as a time/length indicator. TASVideos also require the input count/frame count to be verified by the emulator before playback, which is also implemented in this commit. --- src/core/movie.cpp | 85 ++++++++++++++++++++++++++++++++++++++++------ src/core/movie.h | 27 +++++++++++---- 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/src/core/movie.cpp b/src/core/movie.cpp index f680e9226..35157508b 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -119,10 +119,12 @@ 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; + u64_le id; /// Unique identifier of the movie, used to support separate savestate slots + std::array 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 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) @@ -168,6 +170,7 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { } else { recorded_input = std::move(recorded_input_); play_mode = PlayMode::Recording; + rerecord_count++; } } } @@ -434,6 +437,28 @@ Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header, u64 progr return ValidationResult::OK; } +static u64 GetInputCount(const std::vector& input) { + 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; +} + +Movie::ValidationResult Movie::ValidateInput(const std::vector& input, + u64 expected_count) const { + return GetInputCount(input) == expected_count ? ValidationResult::OK + : ValidationResult::InputCountDismatch; +} + void Movie::SaveMovie() { LOG_INFO(Movie, "Saving recorded movie to '{}'", record_movie_file); FileUtil::IOFile save_record(record_movie_file, "wb"); @@ -448,6 +473,12 @@ void Movie::SaveMovie() { header.clock_init_time = init_time; header.id = 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); + Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); std::string rev_bytes; @@ -475,8 +506,16 @@ void Movie::StartPlayback(const std::string& movie_file, if (ValidateHeader(header) != ValidationResult::Invalid) { play_mode = PlayMode::Playing; record_movie_file = movie_file; + + std::array 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; + 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; @@ -488,9 +527,11 @@ void Movie::StartPlayback(const std::string& movie_file, } } -void Movie::StartRecording(const std::string& movie_file) { +void Movie::StartRecording(const std::string& movie_file, const std::string& author) { play_mode = PlayMode::Recording; record_movie_file = movie_file; + record_movie_author = author; + rerecord_count = 1; // Generate a random ID CryptoPP::AutoSeededRandomPool rng; @@ -537,19 +578,41 @@ void Movie::PrepareForRecording() { Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file, u64 program_id) const { 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, program_id); + if (result != ValidationResult::OK) { + return result; + } + + std::vector 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); if (header == boost::none) - return 0; + return {}; - return static_cast(header.value().program_id); + std::array 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() { diff --git a/src/core/movie.h b/src/core/movie.h index 0a4ab410a..7093290c2 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -32,6 +32,7 @@ public: OK, RevisionDismatch, GameDismatch, + InputCountDismatch, Invalid, }; /** @@ -42,9 +43,9 @@ public: return s_instance; } - void StartPlayback( - const std::string& movie_file, std::function completion_callback = [] {}); - void StartRecording(const std::string& movie_file); + void StartPlayback(const std::string& movie_file, + std::function completion_callback = [] {}); + void StartRecording(const std::string& movie_file, const std::string& author); /** * Sets the read-only status. @@ -68,7 +69,14 @@ public: /// Get the init time that would override the one in the settings 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 { @@ -141,18 +149,25 @@ private: void Record(const Service::IR::ExtraHIDResponse& extra_hid_response); ValidationResult ValidateHeader(const CTMHeader& header, u64 program_id = 0) const; + ValidationResult ValidateInput(const std::vector& input, u64 expected_count) const; void SaveMovie(); PlayMode play_mode; + std::string record_movie_file; + std::string record_movie_author; + std::vector recorded_input; - u64 init_time; - std::function playback_completion_callback; std::size_t current_byte = 0; + u64 id = 0; // ID of the current movie loaded + u64 init_time; + u32 rerecord_count = 1; bool read_only = true; + std::function playback_completion_callback; + template void serialize(Archive& ar, const unsigned int file_version); friend class boost::serialization::access;