diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index cbf3cf5cd7..a8345f91fc 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -405,6 +405,8 @@ add_library(core IOS/USB/Bluetooth/WiimoteHIDAttr.h IOS/USB/Common.cpp IOS/USB/Common.h + IOS/USB/Emulated/Infinity.cpp + IOS/USB/Emulated/Infinity.h IOS/USB/Emulated/Skylander.cpp IOS/USB/Emulated/Skylander.h IOS/USB/Host.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 1a7c523d6e..3da8b874c6 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -555,6 +555,9 @@ void SetUSBDeviceWhitelist(const std::set>& devices) const Info MAIN_EMULATE_SKYLANDER_PORTAL{ {System::Main, "EmulatedUSBDevices", "EmulateSkylanderPortal"}, false}; +const Info MAIN_EMULATE_INFINITY_BASE{ + {System::Main, "EmulatedUSBDevices", "EmulateInfinityBase"}, false}; + // The reason we need this function is because some memory card code // expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii. DiscIO::Region ToGameCubeRegion(DiscIO::Region region) diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 546696476f..7caba3f581 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -344,6 +344,7 @@ void SetUSBDeviceWhitelist(const std::set>& devices); // Main.EmulatedUSBDevices extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; +extern const Info MAIN_EMULATE_INFINITY_BASE; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp b/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp new file mode 100644 index 0000000000..34cde3c46c --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Infinity.cpp @@ -0,0 +1,924 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/Infinity.h" + +#include +#include +#include +#include +#include +#include + +#include "Common/BitUtils.h" +#include "Common/Crypto/SHA1.h" +#include "Common/Logging/Log.h" +#include "Common/Random.h" +#include "Common/StringUtil.h" +#include "Common/Timer.h" +#include "Core/Core.h" +#include "Core/HW/Memmap.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ +// Information taken from https://disneyinfinity.fandom.com/wiki/Model_Numbers +const std::array, 104> list_infinity_figures = { + {{"The Incredibles - Mr. Incredible", 0x0F4241}, + {"Monsters University - Sulley", 0x0F4242}, + {"Pirates of the Caribbean - Jack Sparrow", 0x0F4243}, + {"The Lone Ranger - The Lone Ranger", 0x0F4244}, + {"The Lone Ranger - Tonto", 0x0F4245}, + {"Cars - Lightning McQueen", 0x0F4246}, + {"Cars - Holley Shiftwell", 0x0F4247}, + {"Toy Story - Buzz Lightyear", 0x0F4248}, + {"Toy Story - Jessie", 0x0F4249}, + {"Monsters University - Mike Wazowski", 0x0F424A}, + {"The Incredibles - Mrs. Incredible", 0x0F424B}, + {"Pirates of the Caribbean - Barbossa", 0x0F424C}, + {"Pirates of the Caribbean - Davy Jones", 0x0F424D}, + {"Monsters University - Randy", 0x0F424E}, + {"The Incredibles - Syndrome", 0x0F424F}, + {"Toy Story - Woody", 0x0F4250}, + {"Cars - Mater", 0x0F4251}, + {"The Incredibles - Dash", 0x0F4252}, + {"The Incredibles - Violet", 0x0F4253}, + {"Cars - Francesco Bernoulli", 0x0F4254}, + {"Fantasia - Sorcerer's Apprentice Mickey", 0x0F4255}, + {"The Nightmare Before Christmas - Jack Skellington", 0x0F4256}, + {"Tangled - Rapunzel", 0x0F4257}, + {"Frozen - Anna", 0x0F4258}, + {"Frozen - Elsa", 0x0F4259}, + {"Phineas and Ferb - Phineas Flynn", 0x0F425A}, + {"Phineas and Ferb - Agent P", 0x0F425B}, + {"Wreck-It Ralph - Wreck-It Ralph", 0x0F425C}, + {"Wreck-It Ralph - Vanellope von Schweetz", 0x0F425D}, + {"The Incredibles - Mr. Incredible (Crystal)", 0x0F425E}, + {"Pirates of the Caribbean - Jack Sparrow (Crystal)", 0x0F425F}, + {"Monsters University - Sulley (Crystal)", 0x0F4260}, + {"Cars - Lightning McQueen (Crystal)", 0x0F4261}, + {"The Lone Ranger - The Lone Ranger (Crystal)", 0x0F4262}, + {"Toy Story - Buzz Lightyear (Crystal)", 0x0F4263}, + {"Phineas and Ferb - Agent P (Crystal)", 0x0F4264}, + {"Fantasia - Sorcerer's Apprentice Mickey (Crystal)", 0x0F4265}, + {"Toy Story - Buzz Lightyear (Glowing)", 0x0F4266}, + {"The Incredibles - Pirates of the Caribbean - Monsters University Play Set", 0x1E8481}, + {"The Lone Ranger Play Set", 0x1E8482}, + {"Cars Play Set", 0x1E8483}, + {"Toy Story in Space Play Set", 0x1E8484}, + {"Bolt - Bolt's Super Strength - Ability", 0x2DC6C3}, + {"Wreck-it Ralph - Ralph's Power of Destruction - Ability", 0x2DC6C4}, + {"Fantasia - Chernabog's Power - Ability", 0x2DC6C5}, + {"Cars - C.H.R.O.M.E. Damage Increaser - Ability", 0x2DC6C6}, + {"Phineas and Ferb - Dr. Doofenshmirtz's Damage-Inator! - Ability", 0x2DC6C7}, + {"Frankenweenie - Electro-Charge - Ability", 0x2DC6C8}, + {"Wreck-It Ralph - Fix-It Felix's Repair Power - Ability", 0x2DC6C9}, + {"Tangled - Rapunzel's Healing - Ability", 0x2DC6CA}, + {"Cars - C.H.R.O.M.E. Armor Shield - Ability", 0x2DC6CB}, + {"Toy Story - Star Command Shield - Ability", 0x2DC6CC}, + {"The Incredibles - Violet's Force Field - Ability", 0x2DC6CD}, + {"Pirates of the Caribbean - Pieces of Eight - Ability", 0x2DC6CE}, + {"DuckTales - Scrooge McDuck's Lucky Dime - Ability", 0x2DC6CF}, + {"TRON - User Control Disc - Ability", 0x2DC6D0}, + {"Fantasia - Mickey's Sorcerer Hat - Ability", 0x2DC6D1}, + {"Toy Story - Emperor Zurg's Wrath - Ability", 0x2DC6FE}, + {"The Sword in the Stone - Merlin's Summon - Ability", 0x2DC6FF}, + {"Mickey Mouse Universe - Mickey's Car - Toy (Vehicle)", 0x3D0912}, + {"Cinderella - Cinderella's Coach - Toy (Vehicle)", 0x3D0913}, + {"The Muppets - Electric Mayhem Bus - Toy (Vehicle)", 0x3D0914}, + {"101 Dalmatians - Cruella De Vil's Car - Toy (Vehicle)", 0x3D0915}, + {"Toy Story - Pizza Planet Delivery Truck - Toy (Vehicle)", 0x3D0916}, + {"Monsters, Inc. - Mike's New Car - Toy (Vehicle)", 0x3D0917}, + {"Disney Parks - Disney Parks Parking Lot Tram - Toy (Vehicle)", 0x3D0919}, + {"Peter Pan, Disney Parks - Jolly Roger - Toy (Aircraft)", 0x3D091A}, + {"Dumbo, Disney Parks - Dumbo the Flying Elephant - Toy (Aircraft)", 0x3D091B}, + {"Bolt - Calico Helicopter - Toy (Aircraft)", 0x3D091C}, + {"Tangled - Maximus - Toy (Mount)", 0x3D091D}, + {"Brave - Angus - Toy (Mount)", 0x3D091E}, + {"Aladdin - Abu the Elephant - Toy (Mount)", 0x3D091F}, + {"The Adventures of Ichabod and Mr. Toad - Headless Horseman's Horse - Toy (Mount)", 0x3D0920}, + {"Beauty and the Beast - Phillipe - Toy (Mount)", 0x3D0921}, + {"Mulan - Khan - Toy (Mount)", 0x3D0922}, + {"Tarzan - Tantor - Toy (Mount)", 0x3D0923}, + {"Mulan - Dragon Firework Cannon - Toy (Weapon)", 0x3D0924}, + {"Lilo & Stitch - Stitch's Blaster - Toy (Weapon)", 0x3D0925}, + {"Toy Story, Disney Parks - Toy Story Mania Blaster - Toy (Weapon)", 0x3D0926}, + {"Alice in Wonderland - Flamingo Croquet Mallet - Toy (Weapon)", 0x3D0927}, + {"Up - Carl Fredricksen's Cane - Toy (Weapon)", 0x3D0928}, + {"Lilo & Stitch - Hangin' Ten Stitch With Surfboard - Toy (Hoverboard)", 0x3D0929}, + {"Condorman - Condorman Glider - Toy (Glider)", 0x3D092A}, + {"WALL-E - WALL-E's Fire Extinguisher - Toy (Jetpack)", 0x3D092B}, + {"TRON - On the Grid - Customization (Terrain)", 0x3D092C}, + {"WALL-E - WALL-E's Collection - Customization (Terrain)", 0x3D092D}, + {"Wreck-It Ralph - King Candy's Dessert Toppings - Customization (Terrain)", 0x3D092E}, + {"Frankenweenie - Victor's Experiments - Customization (Terrain)", 0x3D0930}, + {"The Nightmare Before Christmas - Jack's Scary Decorations - Customization (Terrain)", + 0x3D0931}, + {"Frozen - Frozen Flourish - Customization (Terrain)", 0x3D0933}, + {"Tangled - Rapunzel's Kingdom - Customization (Terrain)", 0x3D0934}, + {"TRON - TRON Interface - Customization (Skydome)", 0x3D0935}, + {"WALL-E - Buy N Large Atmosphere - Customization (Skydome)", 0x3D0936}, + {"Wreck-It Ralph - Sugar Rush Sky - Customization (Skydome)", 0x3D0937}, + {"The Nightmare Before Christmas - Halloween Town Sky - Customization (Skydome)", 0x3D093A}, + {"Frozen - Chill in the Air - Customization (Skydome)", 0x3D093C}, + {"Tangled - Rapunzel's Birthday Sky - Customization (Skydome)", 0x3D093D}, + {"Toy Story, Disney Parks - Astro Blasters Space Cruiser - Toy (Vehicle)", 0x3D0940}, + {"Finding Nemo - Marlin's Reef - Customization (Terrain)", 0x3D0941}, + {"Finding Nemo - Nemo's Seascape - Customization (Skydome)", 0x3D0942}, + {"Alice in Wonderland - Alice's Wonderland - Customization (Terrain)", 0x3D0943}, + {"Alice in Wonderland - Tulgey Wood - Customization (Skydome)", 0x3D0944}, + {"Phineas and Ferb - Tri-State Area Terrain - Customization (Terrain)", 0x3D0945}, + {"Phineas and Ferb - Danville Sky - Customization (Skydome)", 0x3D0946}}}; + +static constexpr std::array SHA1_CONSTANT = { + 0xAF, 0x62, 0xD2, 0xEC, 0x04, 0x91, 0x96, 0x8C, 0xC5, 0x2A, 0x1A, 0x71, 0x65, 0xF8, 0x65, 0xFE, + 0x28, 0x63, 0x29, 0x20, 0x44, 0x69, 0x73, 0x6e, 0x65, 0x79, 0x20, 0x32, 0x30, 0x31, 0x33}; + +static constexpr std::array BLANK_BLOCK = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +InfinityUSB::InfinityUSB(EmulationKernel& ios, const std::string& device_name) : m_ios(ios) +{ + m_vid = 0x0E6F; + m_pid = 0x0129; + m_id = (u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1)); + m_device_descriptor = DeviceDescriptor{0x12, 0x1, 0x200, 0x0, 0x0, 0x0, 0x20, + 0x0E6F, 0x0129, 0x200, 0x1, 0x2, 0x3, 0x1}; + m_config_descriptor.emplace_back(ConfigDescriptor{0x9, 0x2, 0x29, 0x1, 0x1, 0x0, 0x80, 0xFA}); + m_interface_descriptor.emplace_back( + InterfaceDescriptor{0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x81, 0x3, 0x20, 0x1}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x1, 0x3, 0x20, 0x1}); +} + +InfinityUSB::~InfinityUSB() = default; + +DeviceDescriptor InfinityUSB::GetDeviceDescriptor() const +{ + return m_device_descriptor; +} + +std::vector InfinityUSB::GetConfigurations() const +{ + return m_config_descriptor; +} + +std::vector InfinityUSB::GetInterfaces(u8 config) const +{ + return m_interface_descriptor; +} + +std::vector InfinityUSB::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + return m_endpoint_descriptor; +} + +bool InfinityUSB::Attach() +{ + if (m_device_attached) + { + return true; + } + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + m_device_attached = true; + return true; +} + +bool InfinityUSB::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int InfinityUSB::CancelTransfer(const u8 endpoint) +{ + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + + return IPC_SUCCESS; +} + +int InfinityUSB::ChangeInterface(const u8 interface) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid, + m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int InfinityUSB::GetNumberOfAltSettings(u8 interface) +{ + return 0; +} + +int InfinityUSB::SetAltSetting(u8 alt_setting) +{ + return 0; +} + +int InfinityUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x}" + " wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, + cmd->index, cmd->length); + return 0; +} +int InfinityUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={:04x} endpoint={:02x}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + return 0; +} +int InfinityUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Interrupt: length={:04x} endpoint={:02x}", m_vid, + m_pid, m_active_interface, cmd->length, cmd->endpoint); + + auto& system = m_ios.GetSystem(); + auto& memory = system.GetMemory(); + auto& infinity_base = system.GetInfinityBase(); + u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); + if (cmd->length != 32 || buf == nullptr) + { + ERROR_LOG_FMT(IOS_USB, "Infinity Base command invalid"); + return IPC_EINVAL; + } + + std::array data = {}; + std::array response_data = {}; + // First call - constant device response + if (buf[0] == 0x00) + { + m_response_list.push(std::move(cmd)); + } + // Respond with data requested from FF command + else if (buf[0] == 0xAA || buf[0] == 0xAB) + { + // If a new figure has been added or removed, get the updated figure response + if (infinity_base.HasFigureBeenAddedRemoved()) + { + ScheduleTransfer(std::move(cmd), infinity_base.PopAddedRemovedResponse(), 1000); + } + else if (m_queries.empty()) + { + m_response_list.push(std::move(cmd)); + } + else + { + ScheduleTransfer(std::move(cmd), m_queries.front(), 1000); + m_queries.pop(); + } + } + else if (buf[0] == 0xFF) + { + // FF + // is the sum of all the bytes preceding it (including the FF and ). + // + // consists of: + // + // + // is an auto-incrementing sequence, so that the game can match up the command + // packet with the response it goes with. + // includes , and , but + // not FF or + + u8 command = buf[2]; + u8 sequence = buf[3]; + + switch (command) + { + case 0x80: + { + // Activate Base, constant response (might be specific based on device type) + response_data = {0xaa, 0x15, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x03, 0x02, 0x09, 0x09, 0x43, + 0x20, 0x32, 0x62, 0x36, 0x36, 0x4b, 0x34, 0x99, 0x67, 0x31, 0x93, 0x8c}; + break; + } + case 0x81: + { + // Initiate random number generation, combine the 8 big endian aligned bytes sent in the + // command in to a u64, and descramble this number, which is then used to generate + // the seed for the random number generator to align with the game. Finally, respond with a + // blank packet as this command doesn't need a response. + infinity_base.DescrambleAndSeed(buf, sequence, response_data); + break; + } + case 0x83: + { + // Respond with random generated numbers based on the seed from the 0x81 command. The next + // random number is 4 bytes, which then needs to be scrambled in to an 8 byte unsigned + // integer, and then passed back as 8 big endian aligned bytes. + infinity_base.GetNextAndScramble(sequence, response_data); + break; + } + case 0x90: + case 0x92: + case 0x93: + case 0x95: + case 0x96: + { + // Set Colors. Needs further input to actually emulate the 'colors', but none of these + // commands expect a response. Potential commands could include turning the lights on + // individual sections of the base, flashing lights, setting colors, initiating a color + // sequence etc. + infinity_base.GetBlankResponse(sequence, response_data); + break; + } + case 0xA1: + { + // A1 - Get Presence Status: + // Returns what figures, if any, are present and the order they were added. + // For each figure on the infinity base, 2 blocks are sent in the response: + // + // is 20 for player 1, 30 for player 2, and 10 for the hexagonal position. + // This is also added with the order the figure was added, so 22 would indicate player one, + // which was added 3rd to the base. + // is (for me) always 09. If nothing is on the + // infinity base, no data is sent in the response. + infinity_base.GetPresentFigures(sequence, response_data); + break; + } + case 0xA2: + { + // A2 - Read Figure Data: + // This reads a block of 16 bytes from a figure on the infinity base. + // Attached data: II BB UU + // II is the order the figure was added (00 for first, 01 for second and 02 for third etc). + // BB is the block number. 00 is block 1, 01 is block 4, 02 is block 8, and 03 is block 12). + // UU is either 00 or 01 -- not exactly sure what it indicates. + infinity_base.QueryBlock(buf[4], buf[5], response_data, sequence); + break; + } + case 0xA3: + { + // A3 - Write Figure Data: + // This writes a block of 16 bytes to a figure on the infinity base. + // Attached data: II BB UU <16 bytes> + // II is the order the figure was added (00 for first, 01 for second and 02 for third etc). + // BB is the block number. 00 is block 1, 01 is block 4, 02 is block 8, and 03 is block 12). + // UU is either 00 or 01 -- not exactly sure what it indicates. + // <16 bytes> is the raw (encrypted) 16 bytes to be sent to the figure. + infinity_base.WriteBlock(buf[4], buf[5], &buf[7], response_data, sequence); + break; + } + case 0xB4: + { + // B4 - Get Tag ID: + // This gets the tag's unique ID for a figure on the infinity base. + // The first byte is the order the figure was added (00 for first, 01 for second and 02 for + // third etc). The infinity base will respond with 7 bytes (the tag ID). + infinity_base.GetFigureIdentifier(buf[4], sequence, response_data); + break; + } + case 0xB5: + { + // Get status? + infinity_base.GetBlankResponse(sequence, response_data); + break; + } + default: + ERROR_LOG_FMT(IOS_USB, "Unhandled Infinity Base Command: {}", command); + break; + } + + memcpy(data.data(), buf, 32); + ScheduleTransfer(std::move(cmd), data, 500); + if (m_response_list.empty()) + { + m_queries.push(response_data); + } + else + { + ScheduleTransfer(std::move(m_response_list.front()), response_data, 1000); + m_response_list.pop(); + } + } + + return 0; +} + +int InfinityUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Isochronous: length={:04x} endpoint={:02x} num_packets={:02x}", + m_vid, m_pid, m_active_interface, cmd->length, cmd->endpoint, cmd->num_packets); + return 0; +} + +void InfinityUSB::ScheduleTransfer(std::unique_ptr command, + const std::array& data, u64 expected_time_us) +{ + command->FillBuffer(data.data(), 32); + command->ScheduleTransferCompletion(32, expected_time_us); +} + +bool InfinityBase::HasFigureBeenAddedRemoved() const +{ + return !m_figure_added_removed_response.empty(); +} + +std::array InfinityBase::PopAddedRemovedResponse() +{ + std::array response = m_figure_added_removed_response.front(); + m_figure_added_removed_response.pop(); + return response; +} + +u8 InfinityBase::GenerateChecksum(const std::array& data, int num_of_bytes) const +{ + int checksum = 0; + for (int i = 0; i < num_of_bytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); +} + +void InfinityBase::GetBlankResponse(u8 sequence, std::array& reply_buf) +{ + reply_buf[0] = 0xaa; + reply_buf[1] = 0x01; + reply_buf[2] = sequence; + reply_buf[3] = GenerateChecksum(reply_buf, 3); +} + +void InfinityBase::GetPresentFigures(u8 sequence, std::array& reply_buf) +{ + int x = 3; + for (u8 i = 0; i < m_figures.size(); i++) + { + u8 slot = i == 0 ? 0x10 : (i > 0 && i < 4) ? 0x20 : 0x30; + if (m_figures[i].present) + { + reply_buf[x] = slot + m_figures[i].order_added; + reply_buf[x + 1] = 0x09; + x = x + 2; + } + } + reply_buf[0] = 0xaa; + reply_buf[1] = x - 2; + reply_buf[2] = sequence; + reply_buf[x] = GenerateChecksum(reply_buf, x); +} + +void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf) +{ + std::lock_guard lock(m_infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + + if (figure.present) + { + memcpy(&reply_buf[4], figure.data.data(), 7); + } + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void InfinityBase::QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence) +{ + std::lock_guard lock(m_infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x12; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + u8 file_block = 0; + if (block == 0) + { + file_block = 1; + } + else + { + file_block = block * 4; + } + if (figure.present && file_block < 20) + { + memcpy(&reply_buf[4], figure.data.data() + (16 * file_block), 16); + } + reply_buf[20] = GenerateChecksum(reply_buf, 20); +} + +void InfinityBase::WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, + std::array& reply_buf, u8 sequence) +{ + std::lock_guard lock(m_infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x02; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + u8 file_block = 0; + if (block == 0) + { + file_block = 1; + } + else + { + file_block = block * 4; + } + if (figure.present && file_block < 20) + { + memcpy(figure.data.data() + (file_block * 16), to_write_buf, 16); + figure.Save(); + } + reply_buf[4] = GenerateChecksum(reply_buf, 4); +} + +static u32 InfinityCRC32(const std::array& buffer) +{ + static constexpr std::array CRC32_TABLE{ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d}; + + // Infinity figures calculate their CRC32 based on 12 bytes in the block of 16 + u32 ret = 0; + for (u32 i = 0; i < 12; ++i) + { + u8 index = u8(ret & 0xFF) ^ buffer[i]; + ret = ((ret >> 8) ^ CRC32_TABLE[index]); + } + + return ret; +} + +std::string +InfinityBase::LoadFigure(const std::array& buf, + File::IOFile in_file, u8 position) +{ + std::lock_guard lock(m_infinity_mutex); + u8 order_added; + + std::vector sha1_calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + for (int i = 0; i < 7; i++) + { + sha1_calc.push_back(buf[i]); + } + + std::array key = GenerateInfinityFigureKey(sha1_calc); + + std::unique_ptr ctx = Common::AES::CreateContextDecrypt(key.data()); + std::array infinity_decrypted_block = {}; + ctx->CryptIvZero(&buf[16], infinity_decrypted_block.data(), 16); + + u32 number = u32(infinity_decrypted_block[1]) << 16 | u32(infinity_decrypted_block[2]) << 8 | + u32(infinity_decrypted_block[3]); + DEBUG_LOG_FMT(IOS_USB, "Toy Number: {}", number); + + InfinityFigure& figure = m_figures[position]; + + figure.inf_file = std::move(in_file); + memcpy(figure.data.data(), buf.data(), figure.data.size()); + figure.present = true; + if (figure.order_added == 255) + { + figure.order_added = m_figure_order; + m_figure_order++; + } + order_added = figure.order_added; + + position = DeriveFigurePosition(position); + + std::array figure_change_response = {0xab, 0x04, position, 0x09, order_added, 0x00}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_response.push(figure_change_response); + + return FindFigure(number); +} + +void InfinityBase::RemoveFigure(u8 position) +{ + std::lock_guard lock(m_infinity_mutex); + InfinityFigure& figure = m_figures[position]; + + if (figure.inf_file.IsOpen()) + { + figure.Save(); + figure.inf_file.Close(); + } + + if (figure.present) + { + figure.present = false; + + position = DeriveFigurePosition(position); + + std::array figure_change_response = {0xab, 0x04, position, 0x09, figure.order_added, + 0x01}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_response.push(figure_change_response); + } +} + +bool InfinityBase::CreateFigure(const std::string& file_path, u32 figure_num) +{ + File::IOFile inf_file(file_path, "w+b"); + if (!inf_file) + { + return false; + } + + // Create a 320 byte file with standard NFC read/write permissions + std::array file_data{}; + u32 first_block = 0x17878E; + u32 other_blocks = 0x778788; + for (s8 i = 2; i >= 0; i--) + { + file_data[0x38 - i] = u8((first_block >> i * 8) & 0xFF); + } + for (u32 index = 1; index < 0x05; index++) + { + for (s8 i = 2; i >= 0; i--) + { + file_data[((index * 0x40) + 0x38) - i] = u8((other_blocks >> i * 8) & 0xFF); + } + } + // Create the vector to calculate the SHA1 hash with + std::vector sha1_calc = {SHA1_CONSTANT.begin(), SHA1_CONSTANT.end() - 1}; + + // Generate random UID, used for AES encrypt/decrypt + std::array uid_data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89, 0x44, 0x00, 0xC2}; + Common::Random::Generate(&uid_data[0], 7); + for (s8 i = 0; i < 7; i++) + { + sha1_calc.push_back(uid_data[i]); + } + std::array figure_data = GenerateBlankFigureData(figure_num); + + if (figure_data[1] == 0x00) + return false; + + std::array key = GenerateInfinityFigureKey(sha1_calc); + + // Create AES Encrypt context based on AES key, use this to encrypt the character data and 4 blank + // blocks + std::unique_ptr ctx = Common::AES::CreateContextEncrypt(key.data()); + std::array encrypted_block = {}; + std::array encrypted_blank = {}; + + ctx->CryptIvZero(figure_data.data(), encrypted_block.data(), figure_data.size()); + ctx->CryptIvZero(BLANK_BLOCK.data(), encrypted_blank.data(), BLANK_BLOCK.size()); + + // Copy encrypted data and UID data to the Figure File + memcpy(&file_data[0], uid_data.data(), uid_data.size()); + memcpy(&file_data[16], encrypted_block.data(), encrypted_block.size()); + memcpy(&file_data[16 * 0x04], encrypted_blank.data(), encrypted_blank.size()); + memcpy(&file_data[16 * 0x08], encrypted_blank.data(), encrypted_blank.size()); + memcpy(&file_data[16 * 0x0C], encrypted_blank.data(), encrypted_blank.size()); + memcpy(&file_data[16 * 0x0D], encrypted_blank.data(), encrypted_blank.size()); + + DEBUG_LOG_FMT(IOS_USB, "File Data: \n{}", HexDump(file_data.data(), file_data.size())); + + inf_file.WriteBytes(file_data.data(), file_data.size()); + + return true; +} + +std::span> InfinityBase::GetFigureList() +{ + return list_infinity_figures; +} + +std::string InfinityBase::FindFigure(u32 number) const +{ + for (auto it = list_infinity_figures.begin(); it != list_infinity_figures.end(); it++) + { + if (it->second == number) + { + return it->first; + } + } + return ""; +} + +u8 InfinityBase::DeriveFigurePosition(u8 position) +{ + // In the added/removed response, position needs to be 1 for the hexagon, 2 for Player 1 and + // Player 1's abilities, and 3 for Player 2 and Player 2's abilities. Abilities are in positions + // > 2 in the UI (3/5 for player 1, 4/6 for player 2) so decrement the position until < 2. + + while (position > 2) + position -= 2; + + position++; + return position; +} + +InfinityFigure& InfinityBase::GetFigureByOrder(u8 order_added) +{ + for (u8 i = 0; i < m_figures.size(); i++) + { + if (m_figures[i].order_added == order_added) + { + return m_figures[i]; + } + } + // This should never reach this statement, but return 0 reference to supress warnings + ASSERT_MSG(IOS_USB, false, "Unable to find Disney Figure requested by game"); + return m_figures[0]; +} + +std::array InfinityBase::GenerateInfinityFigureKey(const std::vector& sha1_data) +{ + Common::SHA1::Digest digest = Common::SHA1::CalculateDigest(sha1_data); + // Infinity AES keys are the first 16 bytes of the SHA1 Digest, every set of 4 bytes need to be + // reversed due to endianness + std::array key = {}; + for (int i = 0; i < 4; i++) + { + for (int x = 3; x >= 0; x--) + { + key[(3 - x) + (i * 4)] = digest[x + (i * 4)]; + } + } + return key; +} + +std::array InfinityBase::GenerateBlankFigureData(u32 figure_num) +{ + std::array figure_data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xD1, 0x1F}; + + // Figure Number, input by end user + figure_data[1] = u8((figure_num >> 16) & 0xFF); + figure_data[2] = u8((figure_num >> 8) & 0xFF); + figure_data[3] = u8(figure_num & 0xFF); + + // Manufacture date, formatted as YY/MM/DD. Set to Infinity 1.0 release date + figure_data[4] = 0x0D; + figure_data[5] = 0x08; + figure_data[6] = 0x12; + + u32 checksum = InfinityCRC32(figure_data); + for (s8 i = 3; i >= 0; i--) + { + figure_data[15 - i] = u8((checksum >> i * 8) & 0xFF); + } + DEBUG_LOG_FMT(IOS_USB, "Block 1: \n{}", HexDump(figure_data.data(), 16)); + return figure_data; +} + +void InfinityBase::DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf) +{ + u64 value = u64(buf[4]) << 56 | u64(buf[5]) << 48 | u64(buf[6]) << 40 | u64(buf[7]) << 32 | + u64(buf[8]) << 24 | u64(buf[9]) << 16 | u64(buf[10]) << 8 | u64(buf[11]); + u32 seed = Descramble(value); + GenerateSeed(seed); + GetBlankResponse(sequence, reply_buf); +} + +void InfinityBase::GetNextAndScramble(u8 sequence, std::array& reply_buf) +{ + u32 next_random = GetNext(); + u64 scrambled_next_random = Scramble(next_random, 0); + reply_buf = {0xAA, 0x09, sequence}; + reply_buf[3] = u8((scrambled_next_random >> 56) & 0xFF); + reply_buf[4] = u8((scrambled_next_random >> 48) & 0xFF); + reply_buf[5] = u8((scrambled_next_random >> 40) & 0xFF); + reply_buf[6] = u8((scrambled_next_random >> 32) & 0xFF); + reply_buf[7] = u8((scrambled_next_random >> 24) & 0xFF); + reply_buf[8] = u8((scrambled_next_random >> 16) & 0xFF); + reply_buf[9] = u8((scrambled_next_random >> 8) & 0xFF); + reply_buf[10] = u8(scrambled_next_random & 0xFF); + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void InfinityBase::GenerateSeed(u32 seed) +{ + m_random_a = 0xF1EA5EED; + m_random_b = seed; + m_random_c = seed; + m_random_d = seed; + + for (int i = 0; i < 23; i++) + { + GetNext(); + } +} + +u32 InfinityBase::GetNext() +{ + u32 a = m_random_a; + u32 b = m_random_b; + u32 c = m_random_c; + u32 ret = std::rotl(m_random_b, 27); + + u32 temp = (a + ((ret ^ 0xFFFFFFFF) + 1)); + b ^= std::rotl(c, 17); + a = m_random_d; + c += a; + ret = b + temp; + a += temp; + + m_random_c = a; + m_random_a = b; + m_random_b = c; + m_random_d = ret; + + return ret; +} + +u64 InfinityBase::Scramble(u32 num_to_scramble, u32 garbage) +{ + u64 mask = 0x8E55AA1B3999E8AA; + u64 ret = 0; + + for (int i = 0; i < 64; i++) + { + ret <<= 1; + + if ((mask & 1) != 0) + { + ret |= (num_to_scramble & 1); + num_to_scramble >>= 1; + } + else + { + ret |= (garbage & 1); + garbage >>= 1; + } + + mask >>= 1; + } + + return ret; +} + +u32 InfinityBase::Descramble(u64 num_to_descramble) +{ + u64 mask = 0x8E55AA1B3999E8AA; + u32 ret = 0; + + for (int i = 0; i < 64; i++) + { + if (Common::ExtractBit(mask, 63)) + { + ret = (ret << 1) | (num_to_descramble & 0x01); + } + + num_to_descramble >>= 1; + mask <<= 1; + } + + return ret; +} + +void InfinityFigure::Save() +{ + if (!inf_file) + return; + + inf_file.Seek(0, File::SeekOrigin::Begin); + inf_file.WriteBytes(data.data(), 0x40 * 0x05); +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Infinity.h b/Source/Core/Core/IOS/USB/Emulated/Infinity.h new file mode 100644 index 0000000000..7d5ae4a8f7 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Infinity.h @@ -0,0 +1,115 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/IOFile.h" +#include "Core/IOS/USB/Common.h" + +namespace IOS::HLE::USB +{ +constexpr u8 INFINITY_NUM_BLOCKS = 0x14; +constexpr u8 INFINITY_BLOCK_SIZE = 0x10; + +struct InfinityFigure final +{ + void Save(); + + File::IOFile inf_file; + std::array data{}; + bool present = false; + u8 order_added = 255; +}; + +class InfinityUSB final : public Device +{ +public: + InfinityUSB(EmulationKernel& ios, const std::string& device_name); + ~InfinityUSB() override; + DeviceDescriptor GetDeviceDescriptor() const override; + std::vector GetConfigurations() const override; + std::vector GetInterfaces(u8 config) const override; + std::vector GetEndpoints(u8 config, u8 interface, u8 alt) const override; + bool Attach() override; + bool AttachAndChangeInterface(u8 interface) override; + int CancelTransfer(u8 endpoint) override; + int ChangeInterface(u8 interface) override; + int GetNumberOfAltSettings(u8 interface) override; + int SetAltSetting(u8 alt_setting) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + +private: + void ScheduleTransfer(std::unique_ptr command, const std::array& data, + u64 expected_time_us); + + EmulationKernel& m_ios; + u16 m_vid = 0; + u16 m_pid = 0; + u8 m_active_interface = 0; + bool m_device_attached = false; + DeviceDescriptor m_device_descriptor; + std::vector m_config_descriptor; + std::vector m_interface_descriptor; + std::vector m_endpoint_descriptor; + std::queue> m_queries; + std::queue> m_response_list; +}; + +class InfinityBase final +{ +public: + bool HasFigureBeenAddedRemoved() const; + std::array PopAddedRemovedResponse(); + void GetBlankResponse(u8 sequence, std::array& reply_buf); + void GetPresentFigures(u8 sequence, std::array& reply_buf); + void GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf); + void QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence); + void WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, std::array& reply_buf, + u8 sequence); + void DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf); + void GetNextAndScramble(u8 sequence, std::array& reply_buf); + void RemoveFigure(u8 position); + // Returns Infinity Figure name based on data from in_file param + std::string LoadFigure(const std::array& buf, + File::IOFile in_file, u8 position); + bool CreateFigure(const std::string& file_path, u32 character); + static std::span> GetFigureList(); + std::string FindFigure(u32 character) const; + +protected: + std::mutex m_infinity_mutex; + std::array m_figures; + +private: + InfinityFigure& GetFigureByOrder(u8 order_added); + std::array GenerateInfinityFigureKey(const std::vector& sha1_data); + std::array GenerateBlankFigureData(u32 figure_num); + u8 DeriveFigurePosition(u8 position); + void GenerateSeed(u32 seed); + u32 GetNext(); + u64 Scramble(u32 num_to_scramble, u32 garbage); + u32 Descramble(u64 num_to_descramble); + u8 GenerateChecksum(const std::array& data, int num_of_bytes) const; + + // These 4 variables are state variables used during the seeding and use of the random number + // generator. + u32 m_random_a; + u32 m_random_b; + u32 m_random_c; + u32 m_random_d; + + u8 m_figure_order = 0; + std::queue> m_figure_added_removed_response; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index a8a5182254..42deee2c62 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -22,6 +22,7 @@ #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/Infinity.h" #include "Core/IOS/USB/Emulated/Skylander.h" #include "Core/IOS/USB/LibusbDevice.h" #include "Core/NetPlayProto.h" @@ -140,13 +141,7 @@ bool USBHost::AddNewDevices(std::set& new_devices, DeviceChangeHooks& hooks auto usb_device = std::make_unique(GetEmulationKernel(), device, descriptor); - if (!ShouldAddDevice(*usb_device)) - return true; - - const u64 id = usb_device->GetId(); - new_devices.insert(id); - if (AddDevice(std::move(usb_device)) || always_add_hooks) - hooks.emplace(GetDeviceById(id), ChangeEvent::Inserted); + CheckAndAddDevice(std::move(usb_device), new_devices, hooks, always_add_hooks); return true; }); if (ret != LIBUSB_SUCCESS) @@ -194,14 +189,25 @@ void USBHost::AddEmulatedDevices(std::set& new_devices, DeviceChangeHooks& { auto skylanderportal = std::make_unique(GetEmulationKernel(), "Skylander Portal"); - if (ShouldAddDevice(*skylanderportal)) + CheckAndAddDevice(std::move(skylanderportal), new_devices, hooks, always_add_hooks); + } + if (Config::Get(Config::MAIN_EMULATE_INFINITY_BASE) && !NetPlay::IsNetPlayRunning()) + { + auto infinity_base = std::make_unique(GetEmulationKernel(), "Infinity Base"); + CheckAndAddDevice(std::move(infinity_base), new_devices, hooks, always_add_hooks); + } +} + +void USBHost::CheckAndAddDevice(std::unique_ptr device, std::set& new_devices, + DeviceChangeHooks& hooks, bool always_add_hooks) +{ + if (ShouldAddDevice(*device)) + { + const u64 deviceid = device->GetId(); + new_devices.insert(deviceid); + if (AddDevice(std::move(device)) || always_add_hooks) { - const u64 skyid = skylanderportal->GetId(); - new_devices.insert(skyid); - if (AddDevice(std::move(skylanderportal)) || always_add_hooks) - { - hooks.emplace(GetDeviceById(skyid), ChangeEvent::Inserted); - } + hooks.emplace(GetDeviceById(deviceid), ChangeEvent::Inserted); } } } diff --git a/Source/Core/Core/IOS/USB/Host.h b/Source/Core/Core/IOS/USB/Host.h index 70de628df0..8342159cb5 100644 --- a/Source/Core/Core/IOS/USB/Host.h +++ b/Source/Core/Core/IOS/USB/Host.h @@ -83,6 +83,8 @@ private: void DispatchHooks(const DeviceChangeHooks& hooks); void AddEmulatedDevices(std::set& new_devices, DeviceChangeHooks& hooks, bool always_add_hooks); + void CheckAndAddDevice(std::unique_ptr device, std::set& new_devices, + DeviceChangeHooks& hooks, bool always_add_hooks); bool m_has_initialised = false; LibusbUtils::Context m_context; diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index 33fb4a862f..eecb590d5c 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -25,6 +25,7 @@ #include "Core/PowerPC/Interpreter/Interpreter.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/PowerPC.h" +#include "IOS/USB/Emulated/Infinity.h" #include "IOS/USB/Emulated/Skylander.h" #include "VideoCommon/CommandProcessor.h" #include "VideoCommon/Fifo.h" @@ -63,6 +64,7 @@ struct System::Impl GeometryShaderManager m_geometry_shader_manager; GPFifo::GPFifoManager m_gp_fifo; HSP::HSPManager m_hsp; + IOS::HLE::USB::InfinityBase m_infinity_base; IOS::HLE::USB::SkylanderPortal m_skylander_portal; Memory::MemoryManager m_memory; MemoryInterface::MemoryInterfaceManager m_memory_interface; @@ -197,6 +199,11 @@ IOS::HLE::USB::SkylanderPortal& System::GetSkylanderPortal() const return m_impl->m_skylander_portal; } +IOS::HLE::USB::InfinityBase& System::GetInfinityBase() const +{ + return m_impl->m_infinity_base; +} + Memory::MemoryManager& System::GetMemory() const { return m_impl->m_memory; diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h index 630be2abe7..8d00a3ec5e 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -57,7 +57,8 @@ class HSPManager; namespace IOS::HLE::USB { class SkylanderPortal; -}; +class InfinityBase; +}; // namespace IOS::HLE::USB namespace Memory { class MemoryManager; @@ -138,6 +139,7 @@ public: Interpreter& GetInterpreter() const; JitInterface& GetJitInterface() const; IOS::HLE::USB::SkylanderPortal& GetSkylanderPortal() const; + IOS::HLE::USB::InfinityBase& GetInfinityBase() const; Memory::MemoryManager& GetMemory() const; MemoryInterface::MemoryInterfaceManager& GetMemoryInterface() const; PowerPC::MMU& GetMMU() const; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index ded5f4664b..01b74f3c06 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -377,6 +377,7 @@ + @@ -1005,6 +1006,7 @@ + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 0704dd54c2..a902eef0bd 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -234,6 +234,8 @@ add_executable(dolphin-emu Host.h HotkeyScheduler.cpp HotkeyScheduler.h + InfinityBase/InfinityBaseWindow.cpp + InfinityBase/InfinityBaseWindow.h Main.cpp MainWindow.cpp MainWindow.h diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 8517068622..3698f36c0e 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -155,6 +155,7 @@ + @@ -348,6 +349,7 @@ + diff --git a/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.cpp b/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.cpp new file mode 100644 index 0000000000..630ed4cfd0 --- /dev/null +++ b/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.cpp @@ -0,0 +1,321 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/InfinityBase/InfinityBaseWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/IOFile.h" + +#include "Core/Config/MainSettings.h" +#include "Core/System.h" + +#include "DolphinQt/QtUtils/DolphinFileDialog.h" +#include "DolphinQt/Settings.h" + +// Qt is not guaranteed to keep track of file paths using native file pickers, so we use this +// static variable to ensure we open at the most recent figure file location +static QString s_last_figure_path; + +InfinityBaseWindow::InfinityBaseWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(tr("Infinity Manager")); + setObjectName(QStringLiteral("infinity_manager")); + setMinimumSize(QSize(700, 200)); + + CreateMainWindow(); + + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &InfinityBaseWindow::OnEmulationStateChanged); + + installEventFilter(this); + + OnEmulationStateChanged(Core::GetState()); +}; + +InfinityBaseWindow::~InfinityBaseWindow() = default; + +void InfinityBaseWindow::CreateMainWindow() +{ + auto* main_layout = new QVBoxLayout(); + + auto* checkbox_group = new QGroupBox(); + auto* checkbox_layout = new QHBoxLayout(); + checkbox_layout->setAlignment(Qt::AlignHCenter); + m_checkbox = new QCheckBox(tr("Emulate Infinity Base"), this); + m_checkbox->setChecked(Config::Get(Config::MAIN_EMULATE_INFINITY_BASE)); + connect(m_checkbox, &QCheckBox::toggled, [=](bool checked) { EmulateBase(checked); }); + checkbox_layout->addWidget(m_checkbox); + checkbox_group->setLayout(checkbox_layout); + main_layout->addWidget(checkbox_group); + + auto add_line = [](QVBoxLayout* vbox) { + auto* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + vbox->addWidget(line); + }; + + m_group_figures = new QGroupBox(tr("Active Infinity Figures:")); + auto* vbox_group = new QVBoxLayout(); + auto* scroll_area = new QScrollArea(); + + AddFigureSlot(vbox_group, QString(tr("Play Set/Power Disc")), 0); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player One")), 1); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player One Ability One")), 3); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player One Ability Two")), 5); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player Two")), 2); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player Two Ability One")), 4); + add_line(vbox_group); + AddFigureSlot(vbox_group, QString(tr("Player Two Ability Two")), 6); + + m_group_figures->setLayout(vbox_group); + scroll_area->setWidget(m_group_figures); + scroll_area->setWidgetResizable(true); + m_group_figures->setVisible(Config::Get(Config::MAIN_EMULATE_INFINITY_BASE)); + main_layout->addWidget(scroll_area); + setLayout(main_layout); +} + +void InfinityBaseWindow::AddFigureSlot(QVBoxLayout* vbox_group, QString name, u8 slot) +{ + auto* hbox_infinity = new QHBoxLayout(); + + auto* label_skyname = new QLabel(name); + + auto* clear_btn = new QPushButton(tr("Clear")); + auto* create_btn = new QPushButton(tr("Create")); + auto* load_btn = new QPushButton(tr("Load")); + m_edit_figures[slot] = new QLineEdit(); + m_edit_figures[slot]->setEnabled(false); + m_edit_figures[slot]->setText(tr("None")); + + connect(clear_btn, &QAbstractButton::clicked, this, [this, slot] { ClearFigure(slot); }); + connect(create_btn, &QAbstractButton::clicked, this, [this, slot] { CreateFigure(slot); }); + connect(load_btn, &QAbstractButton::clicked, this, [this, slot] { LoadFigure(slot); }); + + hbox_infinity->addWidget(label_skyname); + hbox_infinity->addWidget(m_edit_figures[slot]); + hbox_infinity->addWidget(clear_btn); + hbox_infinity->addWidget(create_btn); + hbox_infinity->addWidget(load_btn); + + vbox_group->addLayout(hbox_infinity); +} + +void InfinityBaseWindow::ClearFigure(u8 slot) +{ + auto& system = Core::System::GetInstance(); + m_edit_figures[slot]->setText(tr("None")); + + system.GetInfinityBase().RemoveFigure(slot); +} + +void InfinityBaseWindow::LoadFigure(u8 slot) +{ + const QString file_path = + DolphinFileDialog::getOpenFileName(this, tr("Select Figure File"), s_last_figure_path, + QStringLiteral("Infinity Figure (*.bin);;")); + if (file_path.isEmpty()) + { + return; + } + + s_last_figure_path = QFileInfo(file_path).absolutePath() + QLatin1Char('/'); + + m_edit_figures[slot]->setText(QFileInfo(file_path).fileName()); + + LoadFigurePath(slot, file_path); +} + +void InfinityBaseWindow::CreateFigure(u8 slot) +{ + CreateFigureDialog create_dlg(this, slot); + if (create_dlg.exec() == CreateFigureDialog::Accepted) + { + LoadFigurePath(slot, create_dlg.GetFilePath()); + } +} + +void InfinityBaseWindow::LoadFigurePath(u8 slot, const QString& path) +{ + File::IOFile inf_file(path.toStdString(), "r+b"); + if (!inf_file) + { + QMessageBox::warning( + this, tr("Failed to open the Infinity file!"), + tr("Failed to open the Infinity file(%1)!\nFile may already be in use on the base.") + .arg(path), + QMessageBox::Ok); + return; + } + std::array file_data; + if (!inf_file.ReadBytes(file_data.data(), file_data.size())) + { + QMessageBox::warning(this, tr("Failed to read the Infinity file!"), + tr("Failed to read the Infinity file(%1)!\nFile was too small.").arg(path), + QMessageBox::Ok); + return; + } + + auto& system = Core::System::GetInstance(); + + system.GetInfinityBase().RemoveFigure(slot); + m_edit_figures[slot]->setText(QString::fromStdString( + system.GetInfinityBase().LoadFigure(file_data, std::move(inf_file), slot))); +} + +CreateFigureDialog::CreateFigureDialog(QWidget* parent, u8 slot) : QDialog(parent) +{ + setWindowTitle(tr("Infinity Figure Creator")); + setObjectName(QStringLiteral("infinity_creator")); + setMinimumSize(QSize(500, 150)); + auto* layout = new QVBoxLayout; + + auto* combo_figlist = new QComboBox(); + QStringList filterlist; + u32 first_entry = 0; + for (const auto& entry : Core::System::GetInstance().GetInfinityBase().GetFigureList()) + { + const auto figure = entry.second; + // Only display entry if it is a piece appropriate for the slot + if ((slot == 0 && + ((figure > 0x1E8480 && figure < 0x2DC6BF) || (figure > 0x3D0900 && figure < 0x4C4B3F))) || + ((slot == 1 || slot == 2) && figure < 0x1E847F) || + ((slot == 3 || slot == 4 || slot == 5 || slot == 6) && + (figure > 0x2DC6C0 && figure < 0x3D08FF))) + { + combo_figlist->addItem(QString::fromStdString(entry.first), QVariant(figure)); + filterlist << QString::fromStdString(entry.first); + if (first_entry == 0) + { + first_entry = figure; + } + } + } + combo_figlist->addItem(tr("--Unknown--"), QVariant(0xFFFF)); + combo_figlist->setEditable(true); + combo_figlist->setInsertPolicy(QComboBox::NoInsert); + + auto* 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); + + layout->addWidget(combo_figlist); + + auto* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + layout->addWidget(line); + + auto* hbox_idvar = new QHBoxLayout(); + auto* label_id = new QLabel(tr("Figure Number:")); + auto* edit_num = new QLineEdit(QString::fromStdString(std::to_string(first_entry))); + auto* rxv = new QRegularExpressionValidator(QRegularExpression(QStringLiteral("\\d*")), this); + edit_num->setValidator(rxv); + hbox_idvar->addWidget(label_id); + hbox_idvar->addWidget(edit_num); + layout->addLayout(hbox_idvar); + + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Create")); + layout->addWidget(buttons); + + setLayout(layout); + + connect(combo_figlist, QOverload::of(&QComboBox::currentIndexChanged), [=](int index) { + const u32 char_info = combo_figlist->itemData(index).toUInt(); + if (char_info != 0xFFFFFFFF) + { + edit_num->setText(QString::number(char_info)); + } + }); + + connect(buttons, &QDialogButtonBox::accepted, this, [=, this]() { + bool ok_char = false; + const u32 char_number = edit_num->text().toULong(&ok_char); + if (!ok_char) + { + QMessageBox::warning(this, tr("Error converting value"), tr("Character entered is invalid!"), + QMessageBox::Ok); + return; + } + + QString predef_name = s_last_figure_path; + + auto& system = Core::System::GetInstance(); + const auto found_fig = system.GetInfinityBase().FindFigure(char_number); + if (!found_fig.empty()) + { + predef_name += QString::fromStdString(std::string(found_fig) + ".bin"); + } + else + { + QString str = tr("Unknown(%1).bin"); + predef_name += str.arg(char_number); + } + + m_file_path = DolphinFileDialog::getSaveFileName(this, tr("Create Infinity File"), predef_name, + tr("Infinity Object (*.bin);;")); + if (m_file_path.isEmpty()) + { + return; + } + + if (!system.GetInfinityBase().CreateFigure(m_file_path.toStdString(), char_number)) + { + QMessageBox::warning( + this, tr("Failed to create Infinity file"), + tr("Blank figure creation failed at:\n%1, try again with a different character") + .arg(m_file_path), + QMessageBox::Ok); + return; + } + s_last_figure_path = QFileInfo(m_file_path).absolutePath() + QLatin1Char('/'); + accept(); + }); + + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(co_compl, QOverload::of(&QCompleter::activated), + [=](const QString& text) { + combo_figlist->setCurrentText(text); + combo_figlist->setCurrentIndex(combo_figlist->findText(text)); + }); +} + +QString CreateFigureDialog::GetFilePath() const +{ + return m_file_path; +} + +void InfinityBaseWindow::EmulateBase(bool emulate) +{ + Config::SetBaseOrCurrent(Config::MAIN_EMULATE_INFINITY_BASE, emulate); + m_group_figures->setVisible(emulate); +} + +void InfinityBaseWindow::OnEmulationStateChanged(Core::State state) +{ + const bool running = state != Core::State::Uninitialized; + + m_checkbox->setEnabled(!running); +} diff --git a/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.h b/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.h new file mode 100644 index 0000000000..bdb9c41501 --- /dev/null +++ b/Source/Core/DolphinQt/InfinityBase/InfinityBaseWindow.h @@ -0,0 +1,52 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include +#include +#include + +#include "Core/Core.h" +#include "Core/IOS/USB/Emulated/Infinity.h" + +class QCheckBox; +class QGroupBox; +class QLineEdit; + +class InfinityBaseWindow : public QWidget +{ + Q_OBJECT +public: + explicit InfinityBaseWindow(QWidget* parent = nullptr); + ~InfinityBaseWindow() override; + +protected: + std::array m_edit_figures; + +private: + void CreateMainWindow(); + void AddFigureSlot(QVBoxLayout* vbox_group, QString name, u8 slot); + void OnEmulationStateChanged(Core::State state); + void EmulateBase(bool emulate); + void ClearFigure(u8 slot); + void LoadFigure(u8 slot); + void CreateFigure(u8 slot); + void LoadFigurePath(u8 slot, const QString& path); + + QCheckBox* m_checkbox; + QGroupBox* m_group_figures; +}; + +class CreateFigureDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateFigureDialog(QWidget* parent, u8 slot); + QString GetFilePath() const; + +protected: + QString m_file_path; +}; diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 23c67e0306..c5142fd12d 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -95,6 +95,7 @@ #include "DolphinQt/GameList/GameList.h" #include "DolphinQt/Host.h" #include "DolphinQt/HotkeyScheduler.h" +#include "DolphinQt/InfinityBase/InfinityBaseWindow.h" #include "DolphinQt/MenuBar.h" #include "DolphinQt/NKitWarningDialog.h" #include "DolphinQt/NetPlay/NetPlayBrowser.h" @@ -540,6 +541,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::BrowseNetPlay, this, &MainWindow::ShowNetPlayBrowser); connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer); connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal); + connect(m_menu_bar, &MenuBar::ShowInfinityBase, this, &MainWindow::ShowInfinityBase); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); // Movie @@ -1325,7 +1327,7 @@ void MainWindow::ShowSkylanderPortal() { if (!m_skylander_window) { - m_skylander_window = new SkylanderPortalWindow; + m_skylander_window = new SkylanderPortalWindow(); } m_skylander_window->show(); @@ -1333,6 +1335,18 @@ void MainWindow::ShowSkylanderPortal() m_skylander_window->activateWindow(); } +void MainWindow::ShowInfinityBase() +{ + if (!m_infinity_window) + { + m_infinity_window = new InfinityBaseWindow(); + } + + m_infinity_window->show(); + m_infinity_window->raise(); + m_infinity_window->activateWindow(); +} + void MainWindow::StateLoad() { QString path = diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index fde91a8ae2..869d77cc07 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -30,6 +30,7 @@ class GBATASInputWindow; class GCTASInputWindow; class GraphicsWindow; class HotkeyScheduler; +class InfinityBaseWindow; class JITWidget; class LogConfigWidget; class LogWidget; @@ -161,6 +162,7 @@ private: void ShowNetPlayBrowser(); void ShowFIFOPlayer(); void ShowSkylanderPortal(); + void ShowInfinityBase(); void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); @@ -225,6 +227,7 @@ private: GraphicsWindow* m_graphics_window = nullptr; FIFOPlayerWindow* m_fifo_window = nullptr; SkylanderPortalWindow* m_skylander_window = nullptr; + InfinityBaseWindow* m_infinity_window = nullptr; MappingWindow* m_hotkey_window = nullptr; FreeLookWindow* m_freelook_window = nullptr; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 917935ec50..2744095f66 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -224,7 +224,10 @@ void MenuBar::AddToolsMenu() tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); - tools_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); + auto* usb_device_menu = new QMenu(tr("Emulated USB Devices"), tools_menu); + usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); + usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase); + tools_menu->addMenu(usb_device_menu); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 14418460f4..ba6490e1d9 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -90,6 +90,7 @@ signals: void ShowCheatsManager(); void ShowResourcePackManager(); void ShowSkylanderPortal(); + void ShowInfinityBase(); void ConnectWiiRemote(int id); // Options