mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	Movie (recorded inputs) playback and recording. SDL has command lines to control it.
This commit is contained in:
		
							parent
							
								
									6e2a4ba665
								
							
						
					
					
						commit
						04541150b1
					
				
					 13 changed files with 625 additions and 41 deletions
				
			
		|  | @ -43,13 +43,16 @@ | |||
| #include "network/network.h" | ||||
| 
 | ||||
| static void PrintHelp(const char* argv0) { | ||||
|     std::cout << "Usage: " << argv0 << " [options] <filename>\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] <filename>\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<u32>(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<EmuWindow_SDL2> emu_window{std::make_unique<EmuWindow_SDL2>()}; | ||||
|  |  | |||
|  | @ -74,6 +74,7 @@ namespace Log { | |||
|     SUB(Audio, Sink)                                                                               \ | ||||
|     CLS(Input)                                                                                     \ | ||||
|     CLS(Network)                                                                                   \ | ||||
|     CLS(Movie)                                                                                     \ | ||||
|     CLS(Loader)                                                                                    \ | ||||
|     CLS(WebService) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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
 | ||||
| }; | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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<s16>(circle_pad_x_f * MAX_CIRCLEPAD_POS); | ||||
|     s16 circle_pad_y = static_cast<s16>(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<u16>(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<s16>(accel.y); | ||||
|     accelerometer_entry.z = static_cast<s16>(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<s16>(gyro.y); | ||||
|     gyroscope_entry.z = static_cast<s16>(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; | ||||
|  |  | |||
|  | @ -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<u8>(ResponseID::PollHID)); | ||||
|     response.c_stick.c_stick_x.Assign(static_cast<u32>(C_STICK_CENTER + C_STICK_RADIUS * x)); | ||||
|     response.c_stick.c_stick_y.Assign(static_cast<u32>(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<u8> response_buffer(sizeof(response)); | ||||
|     memcpy(response_buffer.data(), &response, sizeof(response)); | ||||
|     Send(response_buffer); | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ | |||
| 
 | ||||
| #include <array> | ||||
| #include <atomic> | ||||
| #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 | ||||
|  |  | |||
|  | @ -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<s16>(c_stick_x_f * MAX_CSTICK_RADIUS); | ||||
|     const s16 c_stick_y = static_cast<s16>(c_stick_y_f * MAX_CSTICK_RADIUS); | ||||
|     s16 c_stick_x = static_cast<s16>(c_stick_x_f * MAX_CSTICK_RADIUS); | ||||
|     s16 c_stick_y = static_cast<s16>(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); | ||||
|  |  | |||
|  | @ -6,6 +6,9 @@ | |||
| 
 | ||||
| #include <atomic> | ||||
| #include <memory> | ||||
| #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<IR_RST> { | ||||
| public: | ||||
|  |  | |||
							
								
								
									
										469
									
								
								src/core/movie.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										469
									
								
								src/core/movie.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,469 @@ | |||
| // Copyright 2017 Citra Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <cstring> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include <cryptopp/hex.h> | ||||
| #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<u8, 4> header_magic_bytes{{'C', 'T', 'M', 0x1B}}; | ||||
| 
 | ||||
| #pragma pack(push, 1) | ||||
| struct CTMHeader { | ||||
|     std::array<u8, 4> 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<u8, 20> revision; /// Git hash of the revision this movie was created with
 | ||||
| 
 | ||||
|     std::array<u8, 224> 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<u8> 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<u16>(pad_state.a)); | ||||
|     s.pad_and_circle.b.Assign(static_cast<u16>(pad_state.b)); | ||||
|     s.pad_and_circle.select.Assign(static_cast<u16>(pad_state.select)); | ||||
|     s.pad_and_circle.start.Assign(static_cast<u16>(pad_state.start)); | ||||
|     s.pad_and_circle.right.Assign(static_cast<u16>(pad_state.right)); | ||||
|     s.pad_and_circle.left.Assign(static_cast<u16>(pad_state.left)); | ||||
|     s.pad_and_circle.up.Assign(static_cast<u16>(pad_state.up)); | ||||
|     s.pad_and_circle.down.Assign(static_cast<u16>(pad_state.down)); | ||||
|     s.pad_and_circle.r.Assign(static_cast<u16>(pad_state.r)); | ||||
|     s.pad_and_circle.l.Assign(static_cast<u16>(pad_state.l)); | ||||
|     s.pad_and_circle.x.Assign(static_cast<u16>(pad_state.x)); | ||||
|     s.pad_and_circle.y.Assign(static_cast<u16>(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<u8>(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<u8>(pad_state.zl); | ||||
|     s.ir_rst.zr = static_cast<u8>(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 <typename... Targs> | ||||
| 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); | ||||
| } | ||||
| } | ||||
							
								
								
									
										64
									
								
								src/core/movie.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/core/movie.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
| } | ||||
|  | @ -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; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue