From 04541150b1ee00644dfab79f93cc25ab063c496f Mon Sep 17 00:00:00 2001 From: danzel Date: Sun, 17 Dec 2017 16:43:09 +1300 Subject: [PATCH] Movie (recorded inputs) playback and recording. SDL has command lines to control it. --- src/citra/citra.cpp | 37 +- src/common/logging/backend.cpp | 1 + src/common/logging/log.h | 1 + src/core/CMakeLists.txt | 2 + src/core/core.cpp | 3 + src/core/hle/service/hid/hid.cpp | 11 + src/core/hle/service/ir/extra_hid.cpp | 21 +- src/core/hle/service/ir/extra_hid.h | 18 + src/core/hle/service/ir/ir_rst.cpp | 20 +- src/core/hle/service/ir/ir_rst.h | 15 + src/core/movie.cpp | 469 ++++++++++++++++++++++++++ src/core/movie.h | 64 ++++ src/core/settings.h | 4 + 13 files changed, 625 insertions(+), 41 deletions(-) create mode 100644 src/core/movie.cpp create mode 100644 src/core/movie.h diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index 85d7ab099..474b42a60 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -43,13 +43,16 @@ #include "network/network.h" static void PrintHelp(const char* argv0) { - std::cout << "Usage: " << argv0 << " [options] \n" - "-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\n" - "-i, --install=FILE Installs a specified CIA file\n" - "-m, --multiplayer=nick:password@address:port" - " Nickname, password, address and port for multiplayer\n" - "-h, --help Display this help and exit\n" - "-v, --version Output version information and exit\n"; + std::cout << "Usage: " << argv0 + << " [options] \n" + "-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\n" + "-i, --install=FILE Installs a specified CIA file\n" + "-m, --multiplayer=nick:password@address:port" + " Nickname, password, address and port for multiplayer\n" + "-r, --movie-record=[file] Record a movie (game inputs) to the given file\n" + "-p, --movie-play=[file] Playback the movie (game inputs) from the given file\n" + "-h, --help Display this help and exit\n" + "-v, --version Output version information and exit\n"; } static void PrintVersion() { @@ -109,6 +112,9 @@ int main(int argc, char** argv) { int option_index = 0; bool use_gdbstub = Settings::values.use_gdbstub; u32 gdb_port = static_cast(Settings::values.gdbstub_port); + std::string movie_record; + std::string movie_play; + char* endarg; #ifdef _WIN32 int argc_w; @@ -129,12 +135,13 @@ int main(int argc, char** argv) { static struct option long_options[] = { {"gdbport", required_argument, 0, 'g'}, {"install", required_argument, 0, 'i'}, - {"multiplayer", required_argument, 0, 'm'}, {"help", no_argument, 0, 'h'}, + {"multiplayer", required_argument, 0, 'm'}, {"movie-record", required_argument, 0, 'r'}, + {"movie-play", required_argument, 0, 'p'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {0, 0, 0, 0}, }; while (optind < argc) { - char arg = getopt_long(argc, argv, "g:i:m:hv", long_options, &option_index); + char arg = getopt_long(argc, argv, "g:i:m:r:p:hv", long_options, &option_index); if (arg != -1) { switch (arg) { case 'g': @@ -194,6 +201,12 @@ int main(int argc, char** argv) { } break; } + case 'r': + movie_record = optarg; + break; + case 'p': + movie_play = optarg; + break; case 'h': PrintHelp(argv[0]); return 0; @@ -226,11 +239,17 @@ int main(int argc, char** argv) { return -1; } + if (!movie_record.empty() && !movie_play.empty()) { + LOG_CRITICAL(Frontend, "Cannot both play and record a movie"); + } + log_filter.ParseFilterString(Settings::values.log_filter); // Apply the command line arguments Settings::values.gdbstub_port = gdb_port; Settings::values.use_gdbstub = use_gdbstub; + Settings::values.movie_play = std::move(movie_play); + Settings::values.movie_record = std::move(movie_record); Settings::Apply(); std::unique_ptr emu_window{std::make_unique()}; diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 0bc611649..7e58314a3 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -74,6 +74,7 @@ namespace Log { SUB(Audio, Sink) \ CLS(Input) \ CLS(Network) \ + CLS(Movie) \ CLS(Loader) \ CLS(WebService) diff --git a/src/common/logging/log.h b/src/common/logging/log.h index f36642c38..f602c3ee6 100644 --- a/src/common/logging/log.h +++ b/src/common/logging/log.h @@ -92,6 +92,7 @@ enum class Class : ClassType { Loader, ///< ROM loader Input, ///< Input emulation Network, ///< Network emulation + Movie, ///< Movie (Input Recording) Playback WebService, ///< Interface to Citra Web Services Count ///< Total number of logging classes }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8d22bd1a..8d3641e4f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -388,6 +388,8 @@ add_library(core STATIC memory.h memory_setup.h mmio.h + movie.cpp + movie.h perf_stats.cpp perf_stats.h settings.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index 0c658d1ff..653e33c42 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -19,6 +19,7 @@ #include "core/hw/hw.h" #include "core/loader/loader.h" #include "core/memory_setup.h" +#include "core/movie.h" #include "core/settings.h" #include "network/network.h" #include "video_core/video_core.h" @@ -160,6 +161,7 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) { Service::Init(); AudioCore::Init(); GDBStub::Init(); + Movie::Init(); if (!VideoCore::Init(emu_window)) { return ResultStatus::ErrorVideoCore; @@ -185,6 +187,7 @@ void System::Shutdown() { perf_results.frametime * 1000.0); // Shutdown emulation session + Movie::Shutdown(); GDBStub::Shutdown(); AudioCore::Shutdown(); VideoCore::Shutdown(); diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index 9074cd2b0..85bc54c4d 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -19,6 +19,8 @@ #include "core/hle/service/hid/hid_spvr.h" #include "core/hle/service/hid/hid_user.h" #include "core/hle/service/service.h" +#include "core/movie.h" +#include "video_core/video_core.h" namespace Service { namespace HID { @@ -135,6 +137,9 @@ static void UpdatePadCallback(u64 userdata, int cycles_late) { constexpr int MAX_CIRCLEPAD_POS = 0x9C; // Max value for a circle pad position s16 circle_pad_x = static_cast(circle_pad_x_f * MAX_CIRCLEPAD_POS); s16 circle_pad_y = static_cast(circle_pad_y_f * MAX_CIRCLEPAD_POS); + + Movie::HandlePadAndCircleStatus(state, circle_pad_x, circle_pad_y); + const DirectionState direction = GetStickDirectionState(circle_pad_x, circle_pad_y); state.circle_up.Assign(direction.up); state.circle_down.Assign(direction.down); @@ -180,6 +185,8 @@ static void UpdatePadCallback(u64 userdata, int cycles_late) { touch_entry.y = static_cast(y * Core::kScreenBottomHeight); touch_entry.valid.Assign(pressed ? 1 : 0); + Movie::HandleTouchStatus(touch_entry); + // TODO(bunnei): We're not doing anything with offset 0xA8 + 0x18 of HID SharedMemory, which // supposedly is "Touch-screen entry, which contains the raw coordinate data prior to being // converted to pixel coordinates." (http://3dbrew.org/wiki/HID_Shared_Memory#Offset_0xA8). @@ -218,6 +225,8 @@ static void UpdateAccelerometerCallback(u64 userdata, int cycles_late) { accelerometer_entry.y = static_cast(accel.y); accelerometer_entry.z = static_cast(accel.z); + Movie::HandleAccelerometerStatus(accelerometer_entry); + // Make up "raw" entry // TODO(wwylele): // From hardware testing, the raw_entry values are approximately, but not exactly, as twice as @@ -256,6 +265,8 @@ static void UpdateGyroscopeCallback(u64 userdata, int cycles_late) { gyroscope_entry.y = static_cast(gyro.y); gyroscope_entry.z = static_cast(gyro.z); + Movie::HandleGyroscopeStatus(gyroscope_entry); + // Make up "raw" entry mem->gyroscope.raw_entry.x = gyroscope_entry.x; mem->gyroscope.raw_entry.z = -gyroscope_entry.y; diff --git a/src/core/hle/service/ir/extra_hid.cpp b/src/core/hle/service/ir/extra_hid.cpp index e7acc17a5..1d3df1a4b 100644 --- a/src/core/hle/service/ir/extra_hid.cpp +++ b/src/core/hle/service/ir/extra_hid.cpp @@ -3,10 +3,10 @@ // Refer to the license.txt file included. #include "common/alignment.h" -#include "common/bit_field.h" #include "common/string_util.h" #include "core/core_timing.h" #include "core/hle/service/ir/extra_hid.h" +#include "core/movie.h" #include "core/settings.h" namespace Service { @@ -176,22 +176,6 @@ void ExtraHID::SendHIDStatus() { if (is_device_reload_pending.exchange(false)) LoadInputDevices(); - struct { - union { - BitField<0, 8, u32_le> header; - BitField<8, 12, u32_le> c_stick_x; - BitField<20, 12, u32_le> c_stick_y; - } c_stick; - union { - BitField<0, 5, u8> battery_level; - BitField<5, 1, u8> zl_not_held; - BitField<6, 1, u8> zr_not_held; - BitField<7, 1, u8> r_not_held; - } buttons; - u8 unknown; - } response; - static_assert(sizeof(response) == 6, "HID status response has wrong size!"); - constexpr int C_STICK_CENTER = 0x800; // TODO(wwylele): this value is not accurately measured. We currently assume that the axis can // take values in the whole range of a 12-bit integer. @@ -200,6 +184,7 @@ void ExtraHID::SendHIDStatus() { float x, y; std::tie(x, y) = c_stick->GetStatus(); + ExtraHIDResponse response; response.c_stick.header.Assign(static_cast(ResponseID::PollHID)); response.c_stick.c_stick_x.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * x)); response.c_stick.c_stick_y.Assign(static_cast(C_STICK_CENTER + C_STICK_RADIUS * y)); @@ -209,6 +194,8 @@ void ExtraHID::SendHIDStatus() { response.buttons.r_not_held.Assign(1); response.unknown = 0; + Movie::HandleExtraHidResponse(response); + std::vector response_buffer(sizeof(response)); memcpy(response_buffer.data(), &response, sizeof(response)); Send(response_buffer); diff --git a/src/core/hle/service/ir/extra_hid.h b/src/core/hle/service/ir/extra_hid.h index 8f4cf5010..0949334bf 100644 --- a/src/core/hle/service/ir/extra_hid.h +++ b/src/core/hle/service/ir/extra_hid.h @@ -6,6 +6,8 @@ #include #include +#include "common/bit_field.h" +#include "common/swap.h" #include "core/frontend/input.h" #include "core/hle/service/ir/ir_user.h" @@ -16,6 +18,22 @@ struct EventType; namespace Service { namespace IR { +struct ExtraHIDResponse { + union { + BitField<0, 8, u32_le> header; + BitField<8, 12, u32_le> c_stick_x; + BitField<20, 12, u32_le> c_stick_y; + } c_stick; + union { + BitField<0, 5, u8> battery_level; + BitField<5, 1, u8> zl_not_held; + BitField<6, 1, u8> zr_not_held; + BitField<7, 1, u8> r_not_held; + } buttons; + u8 unknown; +}; +static_assert(sizeof(ExtraHIDResponse) == 6, "HID status response has wrong size!"); + /** * An IRDevice emulating Circle Pad Pro or New 3DS additional HID hardware. * This device sends periodic udates at a rate configured by the 3DS, and sends calibration data if diff --git a/src/core/hle/service/ir/ir_rst.cpp b/src/core/hle/service/ir/ir_rst.cpp index 84690aecf..4f5af8b02 100644 --- a/src/core/hle/service/ir/ir_rst.cpp +++ b/src/core/hle/service/ir/ir_rst.cpp @@ -2,30 +2,18 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "common/bit_field.h" #include "core/core_timing.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/event.h" #include "core/hle/kernel/shared_memory.h" #include "core/hle/service/hid/hid.h" #include "core/hle/service/ir/ir_rst.h" +#include "core/movie.h" #include "core/settings.h" namespace Service { namespace IR { -union PadState { - u32_le hex{}; - - BitField<14, 1, u32_le> zl; - BitField<15, 1, u32_le> zr; - - BitField<24, 1, u32_le> c_stick_right; - BitField<25, 1, u32_le> c_stick_left; - BitField<26, 1, u32_le> c_stick_up; - BitField<27, 1, u32_le> c_stick_down; -}; - struct PadDataEntry { PadState current_state; PadState delta_additions; @@ -74,8 +62,10 @@ void IR_RST::UpdateCallback(u64 userdata, int cycles_late) { float c_stick_x_f, c_stick_y_f; std::tie(c_stick_x_f, c_stick_y_f) = c_stick->GetStatus(); constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius - const s16 c_stick_x = static_cast(c_stick_x_f * MAX_CSTICK_RADIUS); - const s16 c_stick_y = static_cast(c_stick_y_f * MAX_CSTICK_RADIUS); + s16 c_stick_x = static_cast(c_stick_x_f * MAX_CSTICK_RADIUS); + s16 c_stick_y = static_cast(c_stick_y_f * MAX_CSTICK_RADIUS); + + Movie::HandleIrRst(state, c_stick_x, c_stick_y); if (!raw_c_stick) { const HID::DirectionState direction = HID::GetStickDirectionState(c_stick_x, c_stick_y); diff --git a/src/core/hle/service/ir/ir_rst.h b/src/core/hle/service/ir/ir_rst.h index 621c1b51c..882b3e4c1 100644 --- a/src/core/hle/service/ir/ir_rst.h +++ b/src/core/hle/service/ir/ir_rst.h @@ -6,6 +6,9 @@ #include #include +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/swap.h" #include "core/frontend/input.h" #include "core/hle/kernel/kernel.h" #include "core/hle/service/service.h" @@ -22,6 +25,18 @@ class EventType; namespace Service { namespace IR { +union PadState { + u32_le hex{}; + + BitField<14, 1, u32_le> zl; + BitField<15, 1, u32_le> zr; + + BitField<24, 1, u32_le> c_stick_right; + BitField<25, 1, u32_le> c_stick_left; + BitField<26, 1, u32_le> c_stick_up; + BitField<27, 1, u32_le> c_stick_down; +}; + /// Interface to "ir:rst" service class IR_RST final : public ServiceFramework { public: diff --git a/src/core/movie.cpp b/src/core/movie.cpp new file mode 100644 index 000000000..0a6e82b63 --- /dev/null +++ b/src/core/movie.cpp @@ -0,0 +1,469 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "common/bit_field.h" +#include "common/common_types.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/scm_rev.h" +#include "common/string_util.h" +#include "common/swap.h" +#include "core/core.h" +#include "core/hle/service/hid/hid.h" +#include "core/hle/service/ir/extra_hid.h" +#include "core/hle/service/ir/ir_rst.h" +#include "core/movie.h" + +namespace Movie { + +enum class PlayMode { None, Recording, Playing }; + +enum class ControllerStateType : u8 { + PadAndCircle, + Touch, + Accelerometer, + Gyroscope, + IrRst, + ExtraHidResponse +}; + +#pragma pack(push, 1) +struct ControllerState { + ControllerStateType type; + + union { + struct { + union { + u16_le hex; + + BitField<0, 1, u16_le> a; + BitField<1, 1, u16_le> b; + BitField<2, 1, u16_le> select; + BitField<3, 1, u16_le> start; + BitField<4, 1, u16_le> right; + BitField<5, 1, u16_le> left; + BitField<6, 1, u16_le> up; + BitField<7, 1, u16_le> down; + BitField<8, 1, u16_le> r; + BitField<9, 1, u16_le> l; + BitField<10, 1, u16_le> x; + BitField<11, 1, u16_le> y; + // Bits 12-15 are currently unused + }; + s16_le circle_pad_x; + s16_le circle_pad_y; + } pad_and_circle; + + struct { + u16_le x; + u16_le y; + // This is a bool, u8 for platform compatibility + u8 valid; + } touch; + + struct { + s16_le x; + s16_le y; + s16_le z; + } accelerometer; + + struct { + s16_le x; + s16_le y; + s16_le z; + } gyroscope; + + struct { + s16_le x; + s16_le y; + // These are bool, u8 for platform compatibility + u8 zl; + u8 zr; + } ir_rst; + + struct { + union { + u32_le hex; + + BitField<0, 5, u32_le> battery_level; + BitField<5, 1, u32_le> zl_not_held; + BitField<6, 1, u32_le> zr_not_held; + BitField<7, 1, u32_le> r_not_held; + BitField<8, 12, u32_le> c_stick_x; + BitField<20, 12, u32_le> c_stick_y; + }; + } extra_hid_response; + }; +}; +static_assert(sizeof(ControllerState) == 7, "ControllerState should be 7 bytes"); +#pragma pack(pop) + +constexpr std::array header_magic_bytes{{'C', 'T', 'M', 0x1B}}; + +#pragma pack(push, 1) +struct CTMHeader { + std::array filetype; /// Unique Identifier to check the file type (always "CTM"0x1B) + 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 + + 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) + +static PlayMode play_mode = PlayMode::None; +static std::vector recorded_input; +static size_t current_byte = 0; + +static bool IsPlayingInput() { + return play_mode == PlayMode::Playing; +} +static bool IsRecordingInput() { + return play_mode == PlayMode::Recording; +} + +static void CheckInputEnd() { + if (current_byte + sizeof(ControllerState) > recorded_input.size()) { + LOG_INFO(Movie, "Playback finished"); + play_mode = PlayMode::None; + } +} + +static void Play(Service::HID::PadState& pad_state, s16& circle_pad_x, s16& circle_pad_y) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::PadAndCircle) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::PadAndCircle, s.type); + return; + } + + pad_state.a.Assign(s.pad_and_circle.a); + pad_state.b.Assign(s.pad_and_circle.b); + pad_state.select.Assign(s.pad_and_circle.select); + pad_state.start.Assign(s.pad_and_circle.start); + pad_state.right.Assign(s.pad_and_circle.right); + pad_state.left.Assign(s.pad_and_circle.left); + pad_state.up.Assign(s.pad_and_circle.up); + pad_state.down.Assign(s.pad_and_circle.down); + pad_state.r.Assign(s.pad_and_circle.r); + pad_state.l.Assign(s.pad_and_circle.l); + pad_state.x.Assign(s.pad_and_circle.x); + pad_state.y.Assign(s.pad_and_circle.y); + + circle_pad_x = s.pad_and_circle.circle_pad_x; + circle_pad_y = s.pad_and_circle.circle_pad_y; +} + +static void Play(Service::HID::TouchDataEntry& touch_data) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::Touch) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::Touch, s.type); + return; + } + + touch_data.x = s.touch.x; + touch_data.y = s.touch.y; + touch_data.valid.Assign(s.touch.valid); +} + +static void Play(Service::HID::AccelerometerDataEntry& accelerometer_data) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::Accelerometer) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::Accelerometer, s.type); + return; + } + + accelerometer_data.x = s.accelerometer.x; + accelerometer_data.y = s.accelerometer.y; + accelerometer_data.z = s.accelerometer.z; +} + +static void Play(Service::HID::GyroscopeDataEntry& gyroscope_data) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::Gyroscope) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::Gyroscope, s.type); + return; + } + + gyroscope_data.x = s.gyroscope.x; + gyroscope_data.y = s.gyroscope.y; + gyroscope_data.z = s.gyroscope.z; +} + +static void Play(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::IrRst) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::IrRst, s.type); + return; + } + + c_stick_x = s.ir_rst.x; + c_stick_y = s.ir_rst.y; + pad_state.zl.Assign(s.ir_rst.zl); + pad_state.zr.Assign(s.ir_rst.zr); +} + +static void Play(Service::IR::ExtraHIDResponse& extra_hid_response) { + ControllerState s; + std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState)); + current_byte += sizeof(ControllerState); + + if (s.type != ControllerStateType::ExtraHidResponse) { + LOG_ERROR(Movie, + "Expected to read type %d, but found %d. Your playback will be out of sync", + ControllerStateType::ExtraHidResponse, s.type); + return; + } + + extra_hid_response.buttons.battery_level.Assign(s.extra_hid_response.battery_level); + extra_hid_response.c_stick.c_stick_x.Assign(s.extra_hid_response.c_stick_x); + extra_hid_response.c_stick.c_stick_y.Assign(s.extra_hid_response.c_stick_y); + extra_hid_response.buttons.r_not_held.Assign(s.extra_hid_response.r_not_held); + extra_hid_response.buttons.zl_not_held.Assign(s.extra_hid_response.zl_not_held); + extra_hid_response.buttons.zr_not_held.Assign(s.extra_hid_response.zr_not_held); +} + +static void Record(const ControllerState& controller_state) { + recorded_input.resize(current_byte + sizeof(ControllerState)); + std::memcpy(&recorded_input[current_byte], &controller_state, sizeof(ControllerState)); + current_byte += sizeof(ControllerState); +} + +static void Record(const Service::HID::PadState& pad_state, const s16& circle_pad_x, + const s16& circle_pad_y) { + ControllerState s; + s.type = ControllerStateType::PadAndCircle; + + s.pad_and_circle.a.Assign(static_cast(pad_state.a)); + s.pad_and_circle.b.Assign(static_cast(pad_state.b)); + s.pad_and_circle.select.Assign(static_cast(pad_state.select)); + s.pad_and_circle.start.Assign(static_cast(pad_state.start)); + s.pad_and_circle.right.Assign(static_cast(pad_state.right)); + s.pad_and_circle.left.Assign(static_cast(pad_state.left)); + s.pad_and_circle.up.Assign(static_cast(pad_state.up)); + s.pad_and_circle.down.Assign(static_cast(pad_state.down)); + s.pad_and_circle.r.Assign(static_cast(pad_state.r)); + s.pad_and_circle.l.Assign(static_cast(pad_state.l)); + s.pad_and_circle.x.Assign(static_cast(pad_state.x)); + s.pad_and_circle.y.Assign(static_cast(pad_state.y)); + + s.pad_and_circle.circle_pad_x = circle_pad_x; + s.pad_and_circle.circle_pad_y = circle_pad_y; + + Record(s); +} + +static void Record(const Service::HID::TouchDataEntry& touch_data) { + ControllerState s; + s.type = ControllerStateType::Touch; + + s.touch.x = touch_data.x; + s.touch.y = touch_data.y; + s.touch.valid = static_cast(touch_data.valid); + + Record(s); +} + +static void Record(const Service::HID::AccelerometerDataEntry& accelerometer_data) { + ControllerState s; + s.type = ControllerStateType::Accelerometer; + + s.accelerometer.x = accelerometer_data.x; + s.accelerometer.y = accelerometer_data.y; + s.accelerometer.z = accelerometer_data.z; + + Record(s); +} + +static void Record(const Service::HID::GyroscopeDataEntry& gyroscope_data) { + ControllerState s; + s.type = ControllerStateType::Gyroscope; + + s.gyroscope.x = gyroscope_data.x; + s.gyroscope.y = gyroscope_data.y; + s.gyroscope.z = gyroscope_data.z; + + Record(s); +} + +static void Record(const Service::IR::PadState& pad_state, const s16& c_stick_x, + const s16& c_stick_y) { + ControllerState s; + s.type = ControllerStateType::IrRst; + + s.ir_rst.x = c_stick_x; + s.ir_rst.y = c_stick_y; + s.ir_rst.zl = static_cast(pad_state.zl); + s.ir_rst.zr = static_cast(pad_state.zr); + + Record(s); +} + +static void Record(const Service::IR::ExtraHIDResponse& extra_hid_response) { + ControllerState s; + s.type = ControllerStateType::ExtraHidResponse; + + s.extra_hid_response.battery_level.Assign(extra_hid_response.buttons.battery_level); + s.extra_hid_response.c_stick_x.Assign(extra_hid_response.c_stick.c_stick_x); + s.extra_hid_response.c_stick_y.Assign(extra_hid_response.c_stick.c_stick_y); + s.extra_hid_response.r_not_held.Assign(extra_hid_response.buttons.r_not_held); + s.extra_hid_response.zl_not_held.Assign(extra_hid_response.buttons.zl_not_held); + s.extra_hid_response.zr_not_held.Assign(extra_hid_response.buttons.zr_not_held); + + Record(s); +} + +static bool ValidateHeader(const CTMHeader& header) { + if (header_magic_bytes != header.filetype) { + LOG_ERROR(Movie, "Playback file does not have valid header"); + return false; + } + + std::string revision = + Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false); + revision = Common::ToLower(revision); + + if (revision != Common::g_scm_rev) { + LOG_WARNING(Movie, + "This movie was created on a different version of Citra, playback may desync"); + } + + u64 program_id; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + if (program_id != header.program_id) { + LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id"); + } + + return true; +} + +static void SaveMovie() { + LOG_INFO(Movie, "Saving movie"); + FileUtil::IOFile save_record(Settings::values.movie_record, "wb"); + + if (!save_record.IsGood()) { + LOG_ERROR(Movie, "Unable to open file to save movie"); + return; + } + + CTMHeader header = {}; + header.filetype = header_magic_bytes; + + Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id); + + std::string rev_bytes; + CryptoPP::StringSource(Common::g_scm_rev, true, + new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes))); + std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(CTMHeader::revision)); + + save_record.WriteBytes(&header, sizeof(CTMHeader)); + save_record.WriteBytes(recorded_input.data(), recorded_input.size()); + + if (!save_record.IsGood()) { + LOG_ERROR(Movie, "Error saving movie"); + } +} + +void Init() { + if (!Settings::values.movie_play.empty()) { + LOG_INFO(Movie, "Loading Movie for playback"); + FileUtil::IOFile save_record(Settings::values.movie_play, "rb"); + u64 size = save_record.GetSize(); + + if (save_record.IsGood() && size > sizeof(CTMHeader)) { + CTMHeader header; + save_record.ReadArray(&header, 1); + if (ValidateHeader(header)) { + play_mode = PlayMode::Playing; + recorded_input.resize(size - sizeof(CTMHeader)); + save_record.ReadArray(recorded_input.data(), recorded_input.size()); + current_byte = 0; + } + } else { + LOG_ERROR(Movie, "Failed to playback movie: Unable to open '%s'", + Settings::values.movie_play.c_str()); + } + } + + if (!Settings::values.movie_record.empty()) { + LOG_INFO(Movie, "Enabling Movie recording"); + play_mode = PlayMode::Recording; + } +} + +void Shutdown() { + if (!IsRecordingInput()) { + return; + } + + SaveMovie(); + + play_mode = PlayMode::None; + recorded_input.resize(0); + current_byte = 0; +} + +template +static void Handle(Targs&... Fargs) { + if (IsPlayingInput()) { + Play(Fargs...); + CheckInputEnd(); + } else if (IsRecordingInput()) { + Record(Fargs...); + } +} + +void HandlePadAndCircleStatus(Service::HID::PadState& pad_state, s16& circle_pad_x, + s16& circle_pad_y) { + Handle(pad_state, circle_pad_x, circle_pad_y); +} + +void HandleTouchStatus(Service::HID::TouchDataEntry& touch_data) { + Handle(touch_data); +} + +void HandleAccelerometerStatus(Service::HID::AccelerometerDataEntry& accelerometer_data) { + Handle(accelerometer_data); +} + +void HandleGyroscopeStatus(Service::HID::GyroscopeDataEntry& gyroscope_data) { + Handle(gyroscope_data); +} + +void HandleIrRst(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y) { + Handle(pad_state, c_stick_x, c_stick_y); +} + +void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response) { + Handle(extra_hid_response); +} +} diff --git a/src/core/movie.h b/src/core/movie.h new file mode 100644 index 000000000..44b1978a2 --- /dev/null +++ b/src/core/movie.h @@ -0,0 +1,64 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" + +namespace Service { +namespace HID { +struct AccelerometerDataEntry; +struct GyroscopeDataEntry; +struct PadState; +struct TouchDataEntry; +} +namespace IR { +struct ExtraHIDResponse; +union PadState; +} +} + +namespace Movie { + +void Init(); + +void Shutdown(); + +/** + * When recording: Takes a copy of the given input states so they can be used for playback + * When playing: Replaces the given input states with the ones stored in the playback file + */ +void HandlePadAndCircleStatus(Service::HID::PadState& pad_state, s16& circle_pad_x, + s16& circle_pad_y); + +/** +* When recording: Takes a copy of the given input states so they can be used for playback +* When playing: Replaces the given input states with the ones stored in the playback file +*/ +void HandleTouchStatus(Service::HID::TouchDataEntry& touch_data); + +/** +* When recording: Takes a copy of the given input states so they can be used for playback +* When playing: Replaces the given input states with the ones stored in the playback file +*/ +void HandleAccelerometerStatus(Service::HID::AccelerometerDataEntry& accelerometer_data); + +/** +* When recording: Takes a copy of the given input states so they can be used for playback +* When playing: Replaces the given input states with the ones stored in the playback file +*/ +void HandleGyroscopeStatus(Service::HID::GyroscopeDataEntry& gyroscope_data); + +/** +* When recording: Takes a copy of the given input states so they can be used for playback +* When playing: Replaces the given input states with the ones stored in the playback file +*/ +void HandleIrRst(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y); + +/** +* When recording: Takes a copy of the given input states so they can be used for playback +* When playing: Replaces the given input states with the ones stored in the playback file +*/ +void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response); +} diff --git a/src/core/settings.h b/src/core/settings.h index 8d78cb424..b8fa3f05a 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -130,6 +130,10 @@ struct Values { bool use_gdbstub; u16 gdbstub_port; + // Movie + std::string movie_play; + std::string movie_record; + // WebService bool enable_telemetry; std::string telemetry_endpoint_url;