From 500bf0f3f5422b31ea4c2b70e2b0b19329deac7f Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Mon, 1 Jul 2024 16:43:07 +0100 Subject: [PATCH] sys_usbd: Emulate Dimensions Toypad --- rpcs3/Emu/CMakeLists.txt | 1 + rpcs3/Emu/Cell/lv2/sys_usbd.cpp | 14 +- rpcs3/Emu/Io/Dimensions.cpp | 618 ++++++++++++++++++++++ rpcs3/Emu/Io/Dimensions.h | 76 +++ rpcs3/emucore.vcxproj | 2 + rpcs3/emucore.vcxproj.filters | 6 + rpcs3/rpcs3.vcxproj | 17 + rpcs3/rpcs3.vcxproj.filters | 15 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/dimensions_dialog.cpp | 768 ++++++++++++++++++++++++++++ rpcs3/rpcs3qt/dimensions_dialog.h | 63 +++ rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 6 + 13 files changed, 1593 insertions(+), 1 deletion(-) create mode 100644 rpcs3/Emu/Io/Dimensions.cpp create mode 100644 rpcs3/Emu/Io/Dimensions.h create mode 100644 rpcs3/rpcs3qt/dimensions_dialog.cpp create mode 100644 rpcs3/rpcs3qt/dimensions_dialog.h diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 526bec0e7a..fc2af93d1b 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -397,6 +397,7 @@ target_link_libraries(rpcs3_emu target_sources(rpcs3_emu PRIVATE Io/Buzz.cpp Io/camera_config.cpp + Io/Dimensions.cpp Io/GameTablet.cpp Io/GHLtar.cpp Io/GunCon3.cpp diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index 4787844bc4..acf0d6aa8c 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -17,6 +17,7 @@ #include "Emu/Io/usb_vfs.h" #include "Emu/Io/Skylander.h" #include "Emu/Io/Infinity.h" +#include "Emu/Io/Dimensions.h" #include "Emu/Io/GHLtar.h" #include "Emu/Io/ghltar_config.h" #include "Emu/Io/guncon3_config.h" @@ -239,6 +240,7 @@ usb_handler_thread::usb_handler_thread() bool found_skylander = false; bool found_infinity = false; + bool found_dimension = false; bool found_usj = false; for (ssize_t index = 0; index < ndev; index++) @@ -274,7 +276,11 @@ usb_handler_thread::usb_handler_thread() found_infinity = true; } - check_device(0x0E6F, 0x0241, 0x0241, "Lego Dimensions Portal"); + if (check_device(0x0E6F, 0x0241, 0x0241, "Lego Dimensions Portal")) + { + found_dimension = true; + } + check_device(0x0E6F, 0x200A, 0x200A, "Kamen Rider Summonride Portal"); // Cameras @@ -394,6 +400,12 @@ usb_handler_thread::usb_handler_thread() usb_devices.push_back(std::make_shared(get_new_location())); } + if (!found_dimension) + { + sys_usbd.notice("Adding emulated dimension toypad"); + usb_devices.push_back(std::make_shared(get_new_location())); + } + if (!found_usj) { if (!g_cfg_usio.load()) diff --git a/rpcs3/Emu/Io/Dimensions.cpp b/rpcs3/Emu/Io/Dimensions.cpp new file mode 100644 index 0000000000..afc527ecab --- /dev/null +++ b/rpcs3/Emu/Io/Dimensions.cpp @@ -0,0 +1,618 @@ +#include "stdafx.h" +#include "Dimensions.h" + +#include +#include + +#include "Crypto/aes.h" +#include "Crypto/sha1.h" +#include "util/asm.hpp" + +#include "Emu/Cell/lv2/sys_usbd.h" + +LOG_CHANNEL(dimensions_log, "dimensions"); + +dimensions_toypad g_dimensionstoypad; + +static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, + 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; + +static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, 0x3C, + 0xA8, 0xD8, 0x75, 0x47, 0x68, 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; + +static constexpr std::array PWD_CONSTANT = {0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, + 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4C, 0x45, 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; + +void dimensions_figure::save() +{ + if (!dim_file) + { + dimensions_log.error("Tried to save infinity figure to file but no infinity figure is active!"); + return; + } + dim_file.seek(0, fs::seek_set); + dim_file.write(data.data(), 0x2D * 0x04); +} + +u8 dimensions_toypad::generate_checksum(const std::array& data, u32 num_of_bytes) const +{ + int checksum = 0; + ensure(num_of_bytes <= data.size()); + for (u8 i = 0; i < num_of_bytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); +} + +void dimensions_toypad::get_blank_response(u8 type, u8 sequence, std::array& reply_buf) +{ + reply_buf[0] = 0x55; + reply_buf[1] = type; + reply_buf[2] = sequence; + reply_buf[3] = generate_checksum(reply_buf, 3); +} + +void dimensions_toypad::generate_random_number(const u8* buf, u8 sequence, std::array& reply_buf) +{ + // Decrypt payload into an 8 byte array + const std::array value = decrypt(buf, std::nullopt); + // Seed is the first 4 bytes (little endian) of the decrypted payload + const u32 seed = read_from_ptr>(value); + // Confirmation is the second 4 bytes (big endian) of the decrypted payload + const u32 conf = read_from_ptr>(value, 4); + // Initialize rng using the seed from decrypted payload + initialize_rng(seed); + std::array value_to_encrypt = {}; + // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are blank + write_to_ptr>(value_to_encrypt, conf); + const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + // Copy encrypted value to response data + std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); + reply_buf[11] = generate_checksum(reply_buf, 11); +} + +void dimensions_toypad::initialize_rng(u32 seed) +{ + random_a = 0xF1EA5EED; + random_b = seed; + random_c = seed; + random_d = seed; + + for (int i = 0; i < 42; i++) + { + get_next(); + } +} + +u32 dimensions_toypad::get_next() +{ + u32 e = random_a - std::rotl(random_b, 21); + random_a = random_b ^ std::rotl(random_c, 19); + random_b = random_c + std::rotl(random_d, 6); + random_c = random_d + e; + random_d = e + random_a; + return random_d; +} + +std::array dimensions_toypad::decrypt(const u8* buf, std::optional> key) +{ + // Value to decrypt is separated in to two little endian 32 bit unsigned integers + u32 data_one = read_from_ptr>(buf); + u32 data_two = read_from_ptr>(buf, 4); + + // Use the key as 4 32 bit little endian unsigned integers + u32 key_one; + u32 key_two; + u32 key_three; + u32 key_four; + + if (key) + { + key_one = read_from_ptr>(key.value()); + key_two = read_from_ptr>(key.value(), 4); + key_three = read_from_ptr>(key.value(), 8); + key_four = read_from_ptr>(key.value(), 12); + } + else + { + key_one = read_from_ptr>(COMMAND_KEY); + key_two = read_from_ptr>(COMMAND_KEY, 4); + key_three = read_from_ptr>(COMMAND_KEY, 8); + key_four = read_from_ptr>(COMMAND_KEY, 12); + } + + u32 sum = 0xC6EF3720; + constexpr u32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + data_two -= (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); + data_one -= (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); + sum -= delta; + } + + ensure(sum == 0, "Decryption failed, sum inequal to 0"); + + std::array decrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), + u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), + u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), + u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; + return decrypted; +} + +std::array dimensions_toypad::encrypt(const u8* buf, std::optional> key) +{ + // Value to encrypt is separated in to two little endian 32 bit unsigned integers + + u32 data_one = read_from_ptr>(buf); + u32 data_two = read_from_ptr>(buf, 4); + + // Use the key as 4 32 bit little endian unsigned integers + u32 key_one; + u32 key_two; + u32 key_three; + u32 key_four; + + if (key) + { + key_one = read_from_ptr>(key.value()); + key_two = read_from_ptr>(key.value(), 4); + key_three = read_from_ptr>(key.value(), 8); + key_four = read_from_ptr>(key.value(), 12); + } + else + { + key_one = read_from_ptr>(COMMAND_KEY); + key_two = read_from_ptr>(COMMAND_KEY, 4); + key_three = read_from_ptr>(COMMAND_KEY, 8); + key_four = read_from_ptr>(COMMAND_KEY, 12); + } + + u32 sum = 0; + constexpr u32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + sum += delta; + data_one += (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); + data_two += (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); + } + + ensure(sum == 0xC6EF3720, "Encryption failed, sum inequal to 0xC6EF3720"); + + std::array encrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), + u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), + u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), + u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; + return encrypted; +} + +std::array dimensions_toypad::generate_figure_key(const std::array& buf) +{ + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + std::array figure_key = {}; + + write_to_ptr>(figure_key, scramble(uid, 3)); + write_to_ptr>(figure_key, 4, scramble(uid, 4)); + write_to_ptr>(figure_key, 8, scramble(uid, 5)); + write_to_ptr>(figure_key, 12, scramble(uid, 6)); + + return figure_key; +} + +u32 dimensions_toypad::scramble(const std::array& uid, u8 count) +{ + std::vector to_scramble; + to_scramble.reserve(uid.size() + CHAR_CONSTANT.size()); + for (u8 x : uid) + { + to_scramble.push_back(x); + } + for (u8 c : CHAR_CONSTANT) + { + to_scramble.push_back(c); + } + ::at32(to_scramble, count * 4 - 1) = 0xaa; + + return read_from_ptr>(dimensions_randomize(to_scramble, count).data()); +} + +std::array dimensions_toypad::dimensions_randomize(const std::vector key, u8 count) +{ + u32 scrambled = 0; + for (u8 i = 0; i < count; i++) + { + const u32 v4 = std::rotr(scrambled, 25); + const u32 v5 = std::rotr(scrambled, 10); + const u32 b = read_from_ptr>(key, i * 4); + scrambled = b + v4 + v5 - scrambled; + } + return {u8(scrambled & 0xFF), u8(scrambled >> 8 & 0xFF), u8(scrambled >> 16 & 0xFF), u8(scrambled >> 24 & 0xFF)}; +} + +u32 dimensions_toypad::get_figure_id(const std::array& buf) +{ + std::array figure_key = generate_figure_key(buf); + + std::array decrypted = decrypt(&buf[36 * 4], figure_key); + + const u32 fig_num = read_from_ptr>(decrypted); + // Characters have their model number encrypted in page 36 + if (fig_num < 1000) + { + return fig_num; + } + // Vehicles/Gadgets have their model number written as little endian in page 36 + return read_from_ptr>(buf, 36 * 4); +} + +dimensions_figure& dimensions_toypad::get_figure_by_index(u8 index) +{ + return ::at32(m_figures, index); +} + +void dimensions_toypad::random_uid(u8* uid_buffer) +{ + uid_buffer[0] = 0x04; + uid_buffer[6] = 0x80; + + for (u8 i = 1; i < 6; i++) + { + u8 random = rand() % 255; + uid_buffer[i] = random; + } +} + +void dimensions_toypad::get_challenge_response(const u8* buf, u8 sequence, std::array& reply_buf) +{ + // Decrypt payload into an 8 byte array + const std::array value = decrypt(buf, std::nullopt); + // Confirmation is the first 4 bytes of the decrypted payload + const u32 conf = read_from_ptr>(value); + // Generate next random number based on RNG + const u32 next_random = get_next(); + std::array value_to_encrypt = {}; + // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) + // followed by the confirmation from the decrypted payload + write_to_ptr>(value_to_encrypt, next_random); + write_to_ptr>(value_to_encrypt, 4, conf); + const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + // Copy encrypted value to response data + std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); + reply_buf[11] = generate_checksum(reply_buf, 11); +} + +void dimensions_toypad::query_block(u8 index, u8 page, std::array& reply_buf, u8 sequence) +{ + std::lock_guard lock(dimensions_mutex); + + // Index from game begins at 1 rather than 0, so minus 1 here + dimensions_figure& figure = get_figure_by_index(index - 1); + + reply_buf[0] = 0x55; + reply_buf[1] = 0x12; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + // Query 4 pages of 4 bytes from the figure, copy this to the response + if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) + { + std::memcpy(&reply_buf[4], figure.data.data() + (4 * page), 16); + } + reply_buf[20] = generate_checksum(reply_buf, 20); +} + +void dimensions_toypad::write_block(u8 index, u8 page, const u8* to_write_buf, std::array& reply_buf, u8 sequence) +{ + std::lock_guard lock(dimensions_mutex); + + // Index from game begins at 1 rather than 0, so minus 1 here + dimensions_figure& figure = get_figure_by_index(index - 1); + + reply_buf[0] = 0x55; + reply_buf[1] = 0x02; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + // Copy 4 bytes to the page on the figure requested by the game + if (figure.index != 255 && page < 0x2D) + { + // Id is written to page 36 + if (page == 36) + { + figure.id = read_from_ptr>(to_write_buf); + } + std::memcpy(figure.data.data() + (page * 4), to_write_buf, 4); + figure.save(); + } + reply_buf[4] = generate_checksum(reply_buf, 4); +} + +void dimensions_toypad::get_model(const u8* buf, u8 sequence, std::array& reply_buf) +{ + // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation + const std::array value = decrypt(buf, std::nullopt); + const u8 index = value[0]; + const u32 conf = read_from_ptr>(value, 4); + // Index from game begins at 1 rather than 0, so minus 1 here + dimensions_figure& figure = get_figure_by_index(index - 1); + std::array value_to_encrypt = {}; + // Response is the figure's id (little endian) followed by the confirmation from payload + write_to_ptr>(value_to_encrypt, figure.id); + write_to_ptr>(value_to_encrypt, 4, conf); + const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x0a; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + // Copy encrypted message to response + std::memcpy(&reply_buf[4], encrypted.data(), encrypted.size()); + reply_buf[12] = generate_checksum(reply_buf, 12); +} + +u32 dimensions_toypad::load_figure(const std::array& buf, fs::file in_file, u8 pad, u8 index) +{ + std::lock_guard lock(dimensions_mutex); + + const u32 id = get_figure_id(buf); + + dimensions_figure& figure = get_figure_by_index(index); + figure.dim_file = std::move(in_file); + figure.id = id; + figure.pad = pad; + figure.index = index + 1; + std::memcpy(figure.data.data(), buf.data(), buf.size()); + // When a figure is added to the toypad, respond to the game with the pad they were added to, their index, + // the direction (0x00 in byte 6 for added) and their UID + std::array figure_change_response = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00}; + std::memcpy(&figure_change_response[6], buf.data(), 7); + figure_change_response[13] = generate_checksum(figure_change_response, 13); + m_figure_added_removed_responses.push(figure_change_response); + return id; +} + +bool dimensions_toypad::remove_figure(u8 pad, u8 index, bool save) +{ + std::lock_guard lock(dimensions_mutex); + dimensions_figure& figure = get_figure_by_index(index); + if (figure.index == 255) + { + return false; + } + // When a figure is removed from the toypad, respond to the game with the pad they were removed from, their index, + // the direction (0x01 in byte 6 for removed) and their UID + std::array figure_change_response = {0x56, 0x0b, pad, 0x00, figure.index, 0x01}; + std::memcpy(&figure_change_response[6], figure.data.data(), 7); + if (save) + { + figure.save(); + figure.dim_file.close(); + } + figure.index = 255; + figure.pad = 255; + figure_change_response[13] = generate_checksum(figure_change_response, 13); + m_figure_added_removed_responses.push(figure_change_response); + return true; +} + +bool dimensions_toypad::move_figure(u8 pad, u8 index, u8 old_pad, u8 old_index) +{ + // When moving figures between spaces on the portal, remove any figure from the space they are moving to, + // then remove them from their current space, then load them to the space they are moving to + remove_figure(pad, index, true); + + dimensions_figure& figure = get_figure_by_index(old_index); + const std::array data = figure.data; + fs::file in_file = std::move(figure.dim_file); + + remove_figure(old_pad, old_index, false); + + load_figure(data, std::move(in_file), pad, index); + + return true; +} + +bool dimensions_toypad::create_blank_character(std::array& buf, u16 id) +{ + random_uid(buf.data()); + buf[7] = id & 0xFF; + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + // Only characters are created with their ID encrypted and stored in pages 36 and 37, + // as well as a password stored in page 43. Blank tags have their information populated + // by the game when it calls the write_block command. + if (id != 0) + { + const std::array figure_key = generate_figure_key(buf); + + std::array value_to_encrypt = {}; + write_to_ptr>(value_to_encrypt, id); + write_to_ptr>(value_to_encrypt, 4, id); + + std::array encrypted = encrypt(value_to_encrypt.data(), figure_key); + + std::memcpy(&buf[36 * 4], &encrypted[0], 4); + std::memcpy(&buf[37 * 4], &encrypted[4], 4); + + std::memcpy(&buf[43 * 4], pwd_generate(uid).data(), 4); + } + else + { + // Page 38 is used as verification for blank tags + write_to_ptr>(buf.data(), 38 * 4, 1); + } + + return true; +} + +std::array dimensions_toypad::pwd_generate(const std::array& uid) +{ + std::vector pwd_calc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; + for (u8 i = 0; i < uid.size(); i++) + { + pwd_calc.insert(pwd_calc.begin() + i, uid[i]); + } + + return dimensions_randomize(pwd_calc, 8); +} + +std::optional> dimensions_toypad::pop_added_removed_response() +{ + if (m_figure_added_removed_responses.empty()) + { + return std::nullopt; + } + else + { + std::array response = m_figure_added_removed_responses.front(); + m_figure_added_removed_responses.pop(); + return response; + } +} + +usb_device_dimensions::usb_device_dimensions(const std::array& location) + : usb_device_emulated(location) +{ + device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{0x200, 0x0, 0x0, 0x0, 0x20, 0x0E6F, 0x0241, 0x200, 0x1, 0x2, 0x3, 0x1}); + auto& config0 = device.add_node(UsbDescriptorNode(USB_DESCRIPTOR_CONFIG, UsbDeviceConfiguration{0x29, 0x1, 0x1, 0x0, 0x80, 0xFA})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_INTERFACE, UsbDeviceInterface{0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_HID, UsbDeviceHID{0x0111, 0x00, 0x01, 0x22, 0x001d})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x81, 0x03, 0x20, 0x1})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x01, 0x03, 0x20, 0x1})); +} + +usb_device_dimensions::~usb_device_dimensions() +{ +} + +void usb_device_dimensions::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); +} + +void usb_device_dimensions::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) +{ + ensure(buf_size == 0x20); + + transfer->fake = true; + transfer->expected_count = buf_size; + transfer->expected_result = HC_CC_NOERR; + + if (endpoint == 0x81) + { + // Read Endpoint, if a request has not been sent via the write endpoint, set expected result as + // EHCI_CC_HALTED so the game doesn't report the Toypad as being disconnected. + std::unique_lock lock(query_mutex); + std::optional> response = g_dimensionstoypad.pop_added_removed_response(); + if (response) + { + std::memcpy(buf, response.value().data(), 0x20); + } + else if (!m_queries.empty()) + { + std::memcpy(buf, m_queries.front().data(), 0x20); + m_queries.pop(); + } + else + { + transfer->expected_count = 0; + transfer->expected_result = EHCI_CC_HALTED; + } + lock.unlock(); + } + else if (endpoint == 0x01) + { + // Write endpoint, similar structure of request to the Infinity Base with a command for byte 3, + // sequence for byte 4, the payload after that, then a checksum for the final byte. + + const u8 command = buf[2]; + const u8 sequence = buf[3]; + + transfer->expected_time = get_timestamp() + 100; + std::array q_result{}; + + switch (command) + { + case 0xB0: // Wake + { + // Consistent device response to the wake command + q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, + 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x46}; + break; + } + case 0xB1: // Seed + { + // Initialise a random number generator using the seed provided + g_dimensionstoypad.generate_random_number(&buf[4], sequence, q_result); + break; + } + case 0xB3: // Challenge + { + // Get the next number in the sequence based on the RNG from 0xB1 command + g_dimensionstoypad.get_challenge_response(&buf[4], sequence, q_result); + break; + } + case 0xC0: // Color + case 0xC2: // Fade + case 0xC3: // Flash + case 0xC6: // Fade All + { + // Send a blank response to acknowledge color has been sent to toypad + g_dimensionstoypad.get_blank_response(0x01, sequence, q_result); + break; + } + case 0xD2: // Read + { + // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] + g_dimensionstoypad.query_block(buf[4], buf[5], q_result, sequence); + break; + } + case 0xD3: // Write + { + // Write 4 bytes to page buf[5] to the figure at index buf[4] + g_dimensionstoypad.write_block(buf[4], buf[5], &buf[6], q_result, sequence); + break; + } + case 0xD4: // Model + { + // Get the model id of the figure at index buf[4] + g_dimensionstoypad.get_model(&buf[4], sequence, q_result); + break; + } + case 0xC1: // Get Pad Color + case 0xC4: // Fade Random + case 0xC7: // Flash All + case 0xC8: // Color All + case 0xD0: // Tag List + case 0xE1: // PWD + case 0xE5: // Active + case 0xFF: // LEDS Query + { + // Further investigation required + dimensions_log.error("Unimplemented LD Function: 0x%x", command); + dimensions_log.error("Request: %s", fmt::buf_to_hexstring(buf, buf_size)); + break; + } + default: + { + dimensions_log.error("Unknown LD Function: 0x%x", command); + dimensions_log.error("Request: %s", fmt::buf_to_hexstring(buf, buf_size)); + break; + } + } + std::lock_guard lock(query_mutex); + m_queries.push(q_result); + } +} + +void usb_device_dimensions::isochronous_transfer(UsbTransfer* transfer) +{ + usb_device_emulated::isochronous_transfer(transfer); +} diff --git a/rpcs3/Emu/Io/Dimensions.h b/rpcs3/Emu/Io/Dimensions.h new file mode 100644 index 0000000000..34a88fa567 --- /dev/null +++ b/rpcs3/Emu/Io/Dimensions.h @@ -0,0 +1,76 @@ +#pragma once + +#include "Emu/Io/usb_device.h" +#include "Utilities/mutex.h" +#include +#include + +struct dimensions_figure +{ + fs::file dim_file; + std::array data{}; + u8 index = 255; + u8 pad = 255; + u32 id = 0; + void save(); +}; + +class dimensions_toypad +{ +public: + void get_blank_response(u8 type, u8 sequence, std::array& reply_buf); + void generate_random_number(const u8* buf, u8 sequence, std::array& reply_buf); + void initialize_rng(u32 seed); + void get_challenge_response(const u8* buf, u8 sequence, std::array& reply_buf); + void query_block(u8 index, u8 page, std::array& reply_buf, u8 sequence); + void write_block(u8 index, u8 page, const u8* to_write_buf, std::array& reply_buf, u8 sequence); + void get_model(const u8* buf, u8 sequence, std::array& reply_buf); + std::optional> pop_added_removed_response(); + + bool remove_figure(u8 pad, u8 index, bool save); + u32 load_figure(const std::array& buf, fs::file in_file, u8 pad, u8 index); + bool move_figure(u8 pad, u8 index, u8 old_pad, u8 old_index); + bool create_blank_character(std::array& buf, u16 id); + +protected: + shared_mutex dimensions_mutex; + std::array m_figures; + +private: + void random_uid(u8* uid_buffer); + u8 generate_checksum(const std::array& data, u32 num_of_bytes) const; + std::array decrypt(const u8* buf, std::optional> key); + std::array encrypt(const u8* buf, std::optional> key); + std::array generate_figure_key(const std::array& buf); + u32 scramble(const std::array& uid, u8 count); + std::array pwd_generate(const std::array& uid); + std::array dimensions_randomize(const std::vector key, u8 count); + u32 get_figure_id(const std::array& buf); + u32 get_next(); + dimensions_figure& get_figure_by_index(u8 index); + + u32 random_a; + u32 random_b; + u32 random_c; + u32 random_d; + + u8 m_figure_order = 0; + std::queue> m_figure_added_removed_responses; +}; + +extern dimensions_toypad g_dimensionstoypad; + +class usb_device_dimensions : public usb_device_emulated +{ +public: + usb_device_dimensions(const std::array& location); + ~usb_device_dimensions(); + + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; + void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; + void isochronous_transfer(UsbTransfer* transfer) override; + +protected: + shared_mutex query_mutex; + std::queue> m_queries; +}; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index e837fac56f..ff392cf7c6 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -412,6 +412,7 @@ + @@ -741,6 +742,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 129f3e7dcd..9b27d17378 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -861,6 +861,9 @@ Emu\Cell\lv2 + + Emu\Io + Emu\Io @@ -1986,6 +1989,9 @@ Emu\Io + + Emu\Io + Emu\Io diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 9dbd1ac114..fc644ae066 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -229,6 +229,9 @@ true + + true + true @@ -496,6 +499,9 @@ true + + true + true @@ -734,6 +740,7 @@ + @@ -1516,6 +1523,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + Moc%27ing %(Identity)... .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 7df94e5d64..d397f0edfd 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -101,6 +101,9 @@ {66e6027b-d3dd-4894-814c-cc4444a4c7df} + + {d138e7e2-861a-4078-badf-80ecb10b0d04} + {f5fcca0d-918b-46ba-bb91-2f2f9d9ddbba} @@ -576,6 +579,9 @@ rpcs3 + + Gui\dimensions + Gui\infinity @@ -681,6 +687,12 @@ Generated Files\Release + + Generated Files\Debug + + + Generated Files\Release + Generated Files\Debug @@ -1447,6 +1459,9 @@ Gui\screenshot manager + + Gui\dimensions + Gui\infinity diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 5bf23b3e2e..ea23b2b3a2 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(rpcs3_ui STATIC debugger_frame.cpp debugger_list.cpp downloader.cpp + dimensions_dialog.cpp _discord_utils.cpp emu_settings.cpp elf_memory_dumping_dialog.cpp diff --git a/rpcs3/rpcs3qt/dimensions_dialog.cpp b/rpcs3/rpcs3qt/dimensions_dialog.cpp new file mode 100644 index 0000000000..f4f8f05876 --- /dev/null +++ b/rpcs3/rpcs3qt/dimensions_dialog.cpp @@ -0,0 +1,768 @@ +#include "stdafx.h" +#include "Utilities/File.h" +#include "dimensions_dialog.h" +#include "Emu/Io/Dimensions.h" + +#include "util/asm.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +dimensions_dialog* dimensions_dialog::inst = nullptr; +std::array, 7> figure_slots = {}; +static QString s_last_figure_path; + +LOG_CHANNEL(dimensions_log, "dimensions"); + +const std::map list_minifigs = { + {0, "Blank Tag"}, + {1, "Batman"}, + {2, "Gandalf"}, + {3, "Wyldstyle"}, + {4, "Aquaman"}, + {5, "Bad Cop"}, + {6, "Bane"}, + {7, "Bart Simpson"}, + {8, "Benny"}, + {9, "Chell"}, + {10, "Cole"}, + {11, "Cragger"}, + {12, "Cyborg"}, + {13, "Cyberman"}, + {14, "Doc Brown"}, + {15, "The Doctor"}, + {16, "Emmet"}, + {17, "Eris"}, + {18, "Gimli"}, + {19, "Gollum"}, + {20, "Harley Quinn"}, + {21, "Homer Simpson"}, + {22, "Jay"}, + {23, "Joker"}, + {24, "Kai"}, + {25, "ACU Trooper"}, + {26, "Gamer Kid"}, + {27, "Krusty the Clown"}, + {28, "Laval"}, + {29, "Legolas"}, + {30, "Lloyd"}, + {31, "Marty McFly"}, + {32, "Nya"}, + {33, "Owen Grady"}, + {34, "Peter Venkman"}, + {35, "Slimer"}, + {36, "Scooby-Doo"}, + {37, "Sensei Wu"}, + {38, "Shaggy"}, + {39, "Stay Puft"}, + {40, "Superman"}, + {41, "Unikitty"}, + {42, "Wicked Witch of the West"}, + {43, "Wonder Woman"}, + {44, "Zane"}, + {45, "Green Arrow"}, + {46, "Supergirl"}, + {47, "Abby Yates"}, + {48, "Finn the Human"}, + {49, "Ethan Hunt"}, + {50, "Lumpy Space Princess"}, + {51, "Jake the Dog"}, + {52, "Harry Potter"}, + {53, "Lord Voldemort"}, + {54, "Michael Knight"}, + {55, "B.A. Baracus"}, + {56, "Newt Scamander"}, + {57, "Sonic the Hedgehog"}, + {58, "Future Update (unreleased)"}, + {59, "Gizmo"}, + {60, "Stripe"}, + {61, "E.T."}, + {62, "Tina Goldstein"}, + {63, "Marceline the Vampire Queen"}, + {64, "Batgirl"}, + {65, "Robin"}, + {66, "Sloth"}, + {67, "Hermione Granger"}, + {68, "Chase McCain"}, + {69, "Excalibur Batman"}, + {70, "Raven"}, + {71, "Beast Boy"}, + {72, "Betelgeuse"}, + {73, "Lord Vortech (unreleased)"}, + {74, "Blossom"}, + {75, "Bubbles"}, + {76, "Buttercup"}, + {77, "Starfire"}, + {78, "World 15 (unreleased)"}, + {79, "World 16 (unreleased)"}, + {80, "World 17 (unreleased)"}, + {81, "World 18 (unreleased)"}, + {82, "World 19 (unreleased)"}, + {83, "World 20 (unreleased)"}, + {768, "Unknown 768"}, + {769, "Supergirl Red Lantern"}, + {770, "Unknown 770"}}; + +const std::map list_tokens = { + {1000, "Police Car"}, + {1001, "Aerial Squad Car"}, + {1002, "Missile Striker"}, + {1003, "Gravity Sprinter"}, + {1004, "Street Shredder"}, + {1005, "Sky Clobberer"}, + {1006, "Batmobile"}, + {1007, "Batblaster"}, + {1008, "Sonic Batray"}, + {1009, "Benny's Spaceship"}, + {1010, "Lasercraft"}, + {1011, "The Annihilator"}, + {1012, "DeLorean Time Machine"}, + {1013, "Electric Time Machine"}, + {1014, "Ultra Time Machine"}, + {1015, "Hoverboard"}, + {1016, "Cyclone Board"}, + {1017, "Ultimate Hoverjet"}, + {1018, "Eagle Interceptor"}, + {1019, "Eagle Sky Blazer"}, + {1020, "Eagle Swoop Diver"}, + {1021, "Swamp Skimmer"}, + {1022, "Cragger's Fireship"}, + {1023, "Croc Command Sub"}, + {1024, "Cyber-Guard"}, + {1025, "Cyber-Wrecker"}, + {1026, "Laser Robot Walker"}, + {1027, "K-9"}, + {1028, "K-9 Ruff Rover"}, + {1029, "K-9 Laser Cutter"}, + {1030, "TARDIS"}, + {1031, "Laser-Pulse TARDIS"}, + {1032, "Energy-Burst TARDIS"}, + {1033, "Emmet's Excavator"}, + {1034, "Destroy Dozer"}, + {1035, "Destruct-o-Mech"}, + {1036, "Winged Monkey"}, + {1037, "Battle Monkey"}, + {1038, "Commander Monkey"}, + {1039, "Axe Chariot"}, + {1040, "Axe Hurler"}, + {1041, "Soaring Chariot"}, + {1042, "Shelob the Great"}, + {1043, "8-Legged Stalker"}, + {1044, "Poison Slinger"}, + {1045, "Homer's Car"}, + {1047, "The SubmaHomer"}, + {1046, "The Homercraft"}, + {1048, "Taunt-o-Vision"}, + {1050, "The MechaHomer"}, + {1049, "Blast Cam"}, + {1051, "Velociraptor"}, + {1053, "Venom Raptor"}, + {1052, "Spike Attack Raptor"}, + {1054, "Gyrosphere"}, + {1055, "Sonic Beam Gyrosphere"}, + {1056, " Gyrosphere"}, + {1057, "Clown Bike"}, + {1058, "Cannon Bike"}, + {1059, "Anti-Gravity Rocket Bike"}, + {1060, "Mighty Lion Rider"}, + {1061, "Lion Blazer"}, + {1062, "Fire Lion"}, + {1063, "Arrow Launcher"}, + {1064, "Seeking Shooter"}, + {1065, "Triple Ballista"}, + {1066, "Mystery Machine"}, + {1067, "Mystery Tow & Go"}, + {1068, "Mystery Monster"}, + {1069, "Boulder Bomber"}, + {1070, "Boulder Blaster"}, + {1071, "Cyclone Jet"}, + {1072, "Storm Fighter"}, + {1073, "Lightning Jet"}, + {1074, "Electro-Shooter"}, + {1075, "Blade Bike"}, + {1076, "Flight Fire Bike"}, + {1077, "Blades of Fire"}, + {1078, "Samurai Mech"}, + {1079, "Samurai Shooter"}, + {1080, "Soaring Samurai Mech"}, + {1081, "Companion Cube"}, + {1082, "Laser Deflector"}, + {1083, "Gold Heart Emitter"}, + {1084, "Sentry Turret"}, + {1085, "Turret Striker"}, + {1086, "Flight Turret Carrier"}, + {1087, "Scooby Snack"}, + {1088, "Scooby Fire Snack"}, + {1089, "Scooby Ghost Snack"}, + {1090, "Cloud Cuckoo Car"}, + {1091, "X-Stream Soaker"}, + {1092, "Rainbow Cannon"}, + {1093, "Invisible Jet"}, + {1094, "Laser Shooter"}, + {1095, "Torpedo Bomber"}, + {1096, "NinjaCopter"}, + {1097, "Glaciator"}, + {1098, "Freeze Fighter"}, + {1099, "Travelling Time Train"}, + {1100, "Flight Time Train"}, + {1101, "Missile Blast Time Train"}, + {1102, "Aqua Watercraft"}, + {1103, "Seven Seas Speeder"}, + {1104, "Trident of Fire"}, + {1105, "Drill Driver"}, + {1106, "Bane Dig 'n' Drill"}, + {1107, "Bane Drill 'n' Blast"}, + {1108, "Quinn Mobile"}, + {1109, "Quinn Ultra Racer"}, + {1110, "Missile Launcher"}, + {1111, "The Joker's Chopper"}, + {1112, "Mischievous Missile Blaster"}, + {1113, "Lock 'n' Laser Jet"}, + {1114, "Hover Pod"}, + {1115, "Krypton Striker"}, + {1116, "Super Stealth Pod"}, + {1117, "Dalek"}, + {1118, "Fire 'n' Ride Dalek"}, + {1119, "Silver Shooter Dalek"}, + {1120, "Ecto-1"}, + {1121, "Ecto-1 Blaster"}, + {1122, "Ecto-1 Water Diver"}, + {1123, "Ghost Trap"}, + {1124, "Ghost Stun 'n' Trap"}, + {1125, "Proton Zapper"}, + {1126, "Unknown"}, + {1127, "Unknown"}, + {1128, "Unknown"}, + {1129, "Unknown"}, + {1130, "Unknown"}, + {1131, "Unknown"}, + {1132, "Lloyd's Golden Dragon"}, + {1133, "Sword Projector Dragon"}, + {1134, "Unknown"}, + {1135, "Unknown"}, + {1136, "Unknown"}, + {1137, "Unknown"}, + {1138, "Unknown"}, + {1139, "Unknown"}, + {1140, "Unknown"}, + {1141, "Unknown"}, + {1142, "Unknown"}, + {1143, "Unknown"}, + {1144, "Mega Flight Dragon"}, + {1145, "Unknown"}, + {1146, "Unknown"}, + {1147, "Unknown"}, + {1148, "Unknown"}, + {1149, "Unknown"}, + {1150, "Unknown"}, + {1151, "Unknown"}, + {1152, "Unknown"}, + {1153, "Unknown"}, + {1154, "Unknown"}, + {1155, "Flying White Dragon"}, + {1156, "Golden Fire Dragon"}, + {1157, "Ultra Destruction Dragon"}, + {1158, "Arcade Machine"}, + {1159, "8-Bit Shooter"}, + {1160, "The Pixelator"}, + {1161, "G-6155 Spy Hunter"}, + {1162, "Interdiver"}, + {1163, "Aerial Spyhunter"}, + {1164, "Slime Shooter"}, + {1165, "Slime Exploder"}, + {1166, "Slime Streamer"}, + {1167, "Terror Dog"}, + {1168, "Terror Dog Destroyer"}, + {1169, "Soaring Terror Dog"}, + {1170, "Ancient Psychic Tandem War Elephant"}, + {1171, "Cosmic Squid"}, + {1172, "Psychic Submarine"}, + {1173, "BMO"}, + {1174, "DOGMO"}, + {1175, "SNAKEMO"}, + {1176, "Jakemobile"}, + {1177, "Snail Dude Jake"}, + {1178, "Hover Jake"}, + {1179, "Lumpy Car"}, + {1181, "Lumpy Land Whale"}, + {1180, "Lumpy Truck"}, + {1182, "Lunatic Amp"}, + {1183, "Shadow Scorpion"}, + {1184, "Heavy Metal Monster"}, + {1185, "B.A.'s Van"}, + {1186, "Fool Smasher"}, + {1187, "Pain Plane"}, + {1188, "Phone Home"}, + {1189, "Mobile Uplink"}, + {1190, "Super-Charged Satellite"}, + {1191, "Niffler"}, + {1192, "Sinister Scorpion"}, + {1193, "Vicious Vulture"}, + {1194, "Swooping Evil"}, + {1195, "Brutal Bloom"}, + {1196, "Crawling Creeper"}, + {1197, "Ecto-1 (2016)"}, + {1198, "Ectozer"}, + {1199, "PerfEcto"}, + {1200, "Flash 'n' Finish"}, + {1201, "Rampage Record Player"}, + {1202, "Stripe's Throne"}, + {1203, "R.C. Racer"}, + {1204, "Gadget-O-Matic"}, + {1205, "Scarlet Scorpion"}, + {1206, "Hogwarts Express"}, + {1208, "Steam Warrior"}, + {1207, "Soaring Steam Plane"}, + {1209, "Enchanted Car"}, + {1210, "Shark Sub"}, + {1211, "Monstrous Mouth"}, + {1212, "IMF Scrambler"}, + {1213, "Shock Cycle"}, + {1214, "IMF Covert Jet"}, + {1215, "IMF Sports Car"}, + {1216, "IMF Tank"}, + {1217, "IMF Splorer"}, + {1218, "Sonic Speedster"}, + {1219, "Blue Typhoon"}, + {1220, "Moto Bug"}, + {1221, "The Tornado"}, + {1222, "Crabmeat"}, + {1223, "Eggcatcher"}, + {1224, "K.I.T.T."}, + {1225, "Goliath Armored Semi"}, + {1226, "K.I.T.T. Jet"}, + {1227, "Police Helicopter"}, + {1228, "Police Hovercraft"}, + {1229, "Police Plane"}, + {1230, "Bionic Steed"}, + {1231, "Bat-Raptor"}, + {1232, "Ultrabat"}, + {1233, "Batwing"}, + {1234, "The Black Thunder"}, + {1235, "Bat-Tank"}, + {1236, "Skeleton Organ"}, + {1237, "Skeleton Jukebox"}, + {1238, "Skele-Turkey"}, + {1239, "One-Eyed Willy's Pirate Ship"}, + {1240, "Fanged Fortune"}, + {1241, "Inferno Cannon"}, + {1242, "Buckbeak"}, + {1243, "Giant Owl"}, + {1244, "Fierce Falcon"}, + {1245, "Saturn's Sandworm"}, + {1247, "Haunted Vacuum"}, + {1246, "Spooky Spider"}, + {1248, "PPG Smartphone"}, + {1249, "PPG Hotline"}, + {1250, "Powerpuff Mag-Net"}, + {1253, "Mega Blast Bot"}, + {1251, "Ka-Pow Cannon"}, + {1252, "Slammin' Guitar"}, + {1254, "Octi"}, + {1255, "Super Skunk"}, + {1256, "Sonic Squid"}, + {1257, "T-Car"}, + {1258, "T-Forklift"}, + {1259, "T-Plane"}, + {1260, "Spellbook of Azarath"}, + {1261, "Raven Wings"}, + {1262, "Giant Hand"}, + {1263, "Titan Robot"}, + {1264, "T-Rocket"}, + {1265, "Robot Retriever"}}; + +minifig_creator_dialog::minifig_creator_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Figure Creator")); + setObjectName("figure_creator"); + setMinimumSize(QSize(500, 150)); + + QVBoxLayout* vbox_panel = new QVBoxLayout(); + + QComboBox* combo_figlist = new QComboBox(); + QStringList filterlist; + + for (const auto& [figure, figure_name] : list_minifigs) + { + const QString name = QString::fromStdString(figure_name); + combo_figlist->addItem(name, QVariant(figure)); + filterlist << name; + } + + combo_figlist->addItem(tr("--Unknown--"), QVariant(0xFFFF)); + combo_figlist->setEditable(true); + combo_figlist->setInsertPolicy(QComboBox::NoInsert); + + QCompleter* co_compl = new QCompleter(filterlist, this); + co_compl->setCaseSensitivity(Qt::CaseInsensitive); + co_compl->setCompletionMode(QCompleter::PopupCompletion); + co_compl->setFilterMode(Qt::MatchContains); + combo_figlist->setCompleter(co_compl); + + vbox_panel->addWidget(combo_figlist); + + QFrame* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + vbox_panel->addWidget(line); + + QHBoxLayout* hbox_number = new QHBoxLayout(); + QLabel* label_number = new QLabel(tr("Figure Number:")); + QLineEdit* edit_number = new QLineEdit(QString::fromStdString(std::to_string(0))); + QRegularExpressionValidator* rxv = new QRegularExpressionValidator(QRegularExpression("\\d*"), this); + edit_number->setValidator(rxv); + hbox_number->addWidget(label_number); + hbox_number->addWidget(edit_number); + vbox_panel->addLayout(hbox_number); + + QHBoxLayout* hbox_buttons = new QHBoxLayout(); + QPushButton* btn_create = new QPushButton(tr("Create"), this); + QPushButton* btn_cancel = new QPushButton(tr("Cancel"), this); + hbox_buttons->addStretch(); + hbox_buttons->addWidget(btn_create); + hbox_buttons->addWidget(btn_cancel); + vbox_panel->addLayout(hbox_buttons); + + setLayout(vbox_panel); + + connect(combo_figlist, QOverload::of(&QComboBox::currentIndexChanged), [=](int index) + { + const u16 fig_info = combo_figlist->itemData(index).toUInt(); + if (fig_info != 0xFFFF) + { + edit_number->setText(QString::number(fig_info)); + } + }); + + connect(btn_create, &QAbstractButton::clicked, this, [=, this]() + { + bool ok_num = false; + const u16 fig_num = edit_number->text().toUInt(&ok_num) & 0xFFFF; + if (!ok_num) + { + QMessageBox::warning(this, tr("Error converting value"), tr("Figure number entered is invalid!"), QMessageBox::Ok); + return; + } + const auto found_figure = list_minifigs.find(fig_num); + if (found_figure != list_minifigs.end()) + { + s_last_figure_path += QString::fromStdString(found_figure->second + ".bin"); + } + else + { + s_last_figure_path += QString("Unknown(%1).bin").arg(fig_num); + } + + m_file_path = QFileDialog::getSaveFileName(this, tr("Create Figure File"), s_last_figure_path, tr("Dimensions Figure (*.bin);;")); + if (m_file_path.isEmpty()) + { + return; + } + + fs::file dim_file(m_file_path.toStdString(), fs::read + fs::write + fs::create); + if (!dim_file) + { + QMessageBox::warning(this, tr("Failed to create minifig file!"), tr("Failed to create minifig file:\n%1").arg(m_file_path), QMessageBox::Ok); + return; + } + + std::array file_data{}; + g_dimensionstoypad.create_blank_character(file_data, fig_num); + + dim_file.write(file_data.data(), file_data.size()); + dim_file.close(); + + s_last_figure_path = QFileInfo(m_file_path).absolutePath() + "/"; + accept(); + }); + + connect(btn_cancel, &QAbstractButton::clicked, this, &QDialog::reject); + + connect(co_compl, QOverload::of(&QCompleter::activated), [=](const QString& text) + { + combo_figlist->setCurrentIndex(combo_figlist->findText(text)); + }); +} + +QString minifig_creator_dialog::get_file_path() const +{ + return m_file_path; +} + +minifig_move_dialog::minifig_move_dialog(QWidget* parent, u8 old_index) + : QDialog(parent) +{ + setWindowTitle(tr("Figure Mover")); + setObjectName("figure_mover"); + setMinimumSize(QSize(500, 150)); + + auto* grid_panel = new QGridLayout(); + + add_minifig_position(grid_panel, 0, 0, 0, old_index); + grid_panel->addWidget(new QLabel(tr("")), 0, 1); + add_minifig_position(grid_panel, 1, 0, 2, old_index); + grid_panel->addWidget(new QLabel(tr(""), this), 0, 3); + add_minifig_position(grid_panel, 2, 0, 4, old_index); + + add_minifig_position(grid_panel, 3, 1, 0, old_index); + add_minifig_position(grid_panel, 4, 1, 1, old_index); + grid_panel->addWidget(new QLabel(tr("")), 1, 2); + add_minifig_position(grid_panel, 5, 1, 3, old_index); + add_minifig_position(grid_panel, 6, 1, 4, old_index); + + setLayout(grid_panel); +} + +void minifig_move_dialog::add_minifig_position(QGridLayout* grid_panel, u8 index, u8 row, u8 column, u8 old_index) +{ + ensure(index < figure_slots.size()); + + auto* vbox_panel = new QVBoxLayout(); + + if (figure_slots[index]) + { + const auto found_figure = list_minifigs.find(figure_slots[index].value()); + if (found_figure != list_minifigs.end()) + { + vbox_panel->addWidget(new QLabel(tr(found_figure->second.c_str()))); + } + } + else + { + vbox_panel->addWidget(new QLabel(tr("None"))); + } + if (old_index != index) + { + auto* btn_move = new QPushButton(tr("Move Here"), this); + vbox_panel->addWidget(btn_move); + connect(btn_move, &QAbstractButton::clicked, this, [this, index] + { + m_index = index; + m_pad = index == 1 ? 1 : + index == 0 || index == 3 || index == 4 ? 2 : + 3; + accept(); + }); + } + + grid_panel->addLayout(vbox_panel, row, column); +} + +u8 minifig_move_dialog::get_new_pad() const +{ + return m_pad; +} + +u8 minifig_move_dialog::get_new_index() const +{ + return m_index; +} + +dimensions_dialog::dimensions_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Dimensions Manager")); + setObjectName("dimensions_manager"); + setAttribute(Qt::WA_DeleteOnClose); + setMinimumSize(QSize(1200, 500)); + + QVBoxLayout* vbox_panel = new QVBoxLayout(); + + QGroupBox* group_figures = new QGroupBox(tr("Active Dimensions Figures:")); + QGridLayout* grid_group = new QGridLayout(); + + add_minifig_slot(grid_group, 2, 0, 0, 0); + grid_group->addWidget(new QLabel(tr("")), 0, 1); + add_minifig_slot(grid_group, 1, 1, 0, 2); + grid_group->addWidget(new QLabel(tr("")), 0, 1); + add_minifig_slot(grid_group, 3, 2, 0, 4); + + add_minifig_slot(grid_group, 2, 3, 1, 0); + add_minifig_slot(grid_group, 2, 4, 1, 1); + grid_group->addWidget(new QLabel(tr("")), 0, 1); + add_minifig_slot(grid_group, 3, 5, 1, 3); + add_minifig_slot(grid_group, 3, 6, 1, 4); + + group_figures->setLayout(grid_group); + vbox_panel->addWidget(group_figures); + setLayout(vbox_panel); +} + +dimensions_dialog::~dimensions_dialog() +{ + inst = nullptr; +} + +dimensions_dialog* dimensions_dialog::get_dlg(QWidget* parent) +{ + if (inst == nullptr) + inst = new dimensions_dialog(parent); + + return inst; +} + +void dimensions_dialog::add_minifig_slot(QGridLayout* grid_group, u8 pad, u8 index, u8 row, u8 column) +{ + ensure(index < figure_slots.size()); + + QVBoxLayout* vbox_layout = new QVBoxLayout(); + + QHBoxLayout* hbox_name_move = new QHBoxLayout(); + QHBoxLayout* hbox_actions = new QHBoxLayout(); + + QPushButton* clear_btn = new QPushButton(tr("Clear")); + QPushButton* create_btn = new QPushButton(tr("Create")); + QPushButton* load_btn = new QPushButton(tr("Load")); + QPushButton* move_btn = new QPushButton(tr("Move")); + + m_edit_figures[index] = new QLineEdit(); + m_edit_figures[index]->setEnabled(false); + if (figure_slots[index]) + { + const auto found_figure = list_minifigs.find(figure_slots[index].value()); + if (found_figure != list_minifigs.end()) + { + m_edit_figures[index]->setText(QString::fromStdString(found_figure->second)); + } + else + { + m_edit_figures[index]->setText(tr("Unknown Figure")); + } + } + else + { + m_edit_figures[index]->setText(tr("None")); + } + + connect(clear_btn, &QAbstractButton::clicked, this, [this, pad, index] + { + clear_figure(pad, index); + }); + connect(create_btn, &QAbstractButton::clicked, this, [this, pad, index] + { + create_figure(pad, index); + }); + connect(load_btn, &QAbstractButton::clicked, this, [this, pad, index] + { + load_figure(pad, index); + }); + connect(move_btn, &QAbstractButton::clicked, this, [this, pad, index] + { + if (figure_slots[index]) + { + move_figure(pad, index); + } + }); + + hbox_name_move->addWidget(m_edit_figures[index]); + hbox_name_move->addWidget(move_btn); + hbox_actions->addWidget(clear_btn); + hbox_actions->addWidget(create_btn); + hbox_actions->addWidget(load_btn); + + vbox_layout->addLayout(hbox_name_move); + vbox_layout->addLayout(hbox_actions); + + grid_group->addLayout(vbox_layout, row, column); +} + +void dimensions_dialog::clear_figure(u8 pad, u8 index) +{ + ensure(index < figure_slots.size()); + + if (figure_slots[index]) + { + g_dimensionstoypad.remove_figure(pad, index, true); + figure_slots[index] = std::nullopt; + m_edit_figures[index]->setText(tr("None")); + } +} + +void dimensions_dialog::create_figure(u8 pad, u8 index) +{ + ensure(index < figure_slots.size()); + minifig_creator_dialog create_dlg(this); + if (create_dlg.exec() == Accepted) + { + load_figure_path(pad, index, create_dlg.get_file_path()); + } +} + +void dimensions_dialog::load_figure(u8 pad, u8 index) +{ + ensure(index < figure_slots.size()); + const QString file_path = QFileDialog::getOpenFileName(this, tr("Select Dimensions File"), s_last_figure_path, tr("Dimensions Figure (*.bin);;")); + if (file_path.isEmpty()) + { + return; + } + + s_last_figure_path = QFileInfo(file_path).absolutePath() + "/"; + + load_figure_path(pad, index, file_path); +} + +void dimensions_dialog::move_figure(u8 pad, u8 index) +{ + ensure(index < figure_slots.size()); + minifig_move_dialog move_dlg(this, index); + if (move_dlg.exec() == Accepted) + { + g_dimensionstoypad.move_figure(move_dlg.get_new_pad(), move_dlg.get_new_index(), pad, index); + figure_slots[move_dlg.get_new_index()] = figure_slots[index]; + m_edit_figures[move_dlg.get_new_index()]->setText(m_edit_figures[index]->text()); + figure_slots[index] = std::nullopt; + m_edit_figures[index]->setText(tr("None")); + } +} + +void dimensions_dialog::load_figure_path(u8 pad, u8 index, const QString& path) +{ + fs::file dim_file(path.toStdString(), fs::read + fs::write + fs::lock); + if (!dim_file) + { + QMessageBox::warning(this, tr("Failed to open the figure file!"), tr("Failed to open the figure file(%1)!\nFile may already be in use on the base.").arg(path), QMessageBox::Ok); + return; + } + + std::array data; + if (dim_file.read(data.data(), data.size()) != data.size()) + { + QMessageBox::warning(this, tr("Failed to read the figure file!"), tr("Failed to read the figure file(%1)!\nFile was too small.").arg(path), QMessageBox::Ok); + return; + } + + clear_figure(pad, index); + + const u32 fig_num = g_dimensionstoypad.load_figure(data, std::move(dim_file), pad, index); + + figure_slots[index] = fig_num; + auto name = list_minifigs.find(fig_num); + if (name != list_minifigs.end()) + { + m_edit_figures[index]->setText(QString::fromStdString(name->second)); + } + else + { + auto blank_name = list_tokens.find(fig_num); + if (blank_name != list_tokens.end()) + { + m_edit_figures[index]->setText(QString::fromStdString(blank_name->second)); + } + else + { + m_edit_figures[index]->setText(tr("Blank Tag")); + } + } +} diff --git a/rpcs3/rpcs3qt/dimensions_dialog.h b/rpcs3/rpcs3qt/dimensions_dialog.h new file mode 100644 index 0000000000..7866441d6c --- /dev/null +++ b/rpcs3/rpcs3qt/dimensions_dialog.h @@ -0,0 +1,63 @@ +#pragma once + +#include "util/types.hpp" + +#include +#include +#include + +class minifig_creator_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit minifig_creator_dialog(QWidget* parent); + QString get_file_path() const; + +protected: + QString m_file_path; +}; + +class minifig_move_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit minifig_move_dialog(QWidget* parent, u8 old_index); + u8 get_new_pad() const; + u8 get_new_index() const; + +protected: + u8 m_pad = 0; + u8 m_index = 0; + +private: + void add_minifig_position(QGridLayout* grid_panel, u8 index, u8 row, u8 column, u8 old_index); +}; + +class dimensions_dialog : public QDialog +{ + Q_OBJECT + +public: + explicit dimensions_dialog(QWidget* parent); + ~dimensions_dialog(); + static dimensions_dialog* get_dlg(QWidget* parent); + + dimensions_dialog(dimensions_dialog const&) = delete; + void operator=(dimensions_dialog const&) = delete; + +protected: + void clear_figure(u8 pad, u8 index); + void create_figure(u8 pad, u8 index); + void load_figure(u8 pad, u8 index); + void move_figure(u8 pad, u8 index); + void load_figure_path(u8 pad, u8 index, const QString& path); + +protected: + std::array m_edit_figures{}; + +private: + void add_minifig_slot(QGridLayout* grid_group, u8 pad, u8 index, u8 row, u8 column); + static dimensions_dialog* inst; +}; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 5d83a01aa7..e9dfa7d104 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -21,6 +21,7 @@ #include "progress_dialog.h" #include "skylander_dialog.h" #include "infinity_dialog.h" +#include "dimensions_dialog.h" #include "cheat_manager.h" #include "patch_manager_dialog.h" #include "patch_creator_dialog.h" @@ -2851,6 +2852,12 @@ void main_window::CreateConnects() inf_dlg->show(); }); + connect(ui->actionManage_Dimensions_ToyPad, &QAction::triggered, this, [this] + { + dimensions_dialog* dim_dlg = dimensions_dialog::get_dlg(this); + dim_dlg->show(); + }); + connect(ui->actionManage_Cheats, &QAction::triggered, this, [this] { cheat_manager_dialog* cheat_manager = cheat_manager_dialog::get_dlg(this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 117521987a..4eb73547aa 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -282,6 +282,7 @@ + @@ -1159,6 +1160,11 @@ Infinity Base + + + Dimensions Toypad + + Cheats