From f76a6789a0906dcc9a65ff69b16f62774f3c0563 Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Sun, 8 Jan 2023 11:54:36 +1300 Subject: [PATCH 1/3] Emulate Skylanders within Dolphin Ported the code from RPCS3, with improvements made to the handling of control messages and audio transfers, Co-Authored with @mandar1jn Missing new line chars Co-Authored-By: mandar1jn <49076509+mandar1jn@users.noreply.github.com> --- Source/Core/Core/CMakeLists.txt | 4 + Source/Core/Core/Config/MainSettings.cpp | 10 + Source/Core/Core/Config/MainSettings.h | 5 + Source/Core/Core/IOS/USB/Common.h | 4 + .../Core/Core/IOS/USB/Emulated/Skylander.cpp | 721 ++++++++++++++ Source/Core/Core/IOS/USB/Emulated/Skylander.h | 102 ++ .../Core/Core/IOS/USB/EmulatedUSBDevice.cpp | 82 ++ Source/Core/Core/IOS/USB/EmulatedUSBDevice.h | 43 + Source/Core/Core/IOS/USB/Host.cpp | 13 + Source/Core/DolphinLib.props | 4 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + Source/Core/DolphinQt/MainWindow.cpp | 14 + Source/Core/DolphinQt/MainWindow.h | 3 + Source/Core/DolphinQt/MenuBar.cpp | 2 + Source/Core/DolphinQt/MenuBar.h | 1 + .../SkylanderPortal/SkylanderPortalWindow.cpp | 922 ++++++++++++++++++ .../SkylanderPortal/SkylanderPortalWindow.h | 60 ++ 18 files changed, 1994 insertions(+) create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylander.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylander.h create mode 100644 Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp create mode 100644 Source/Core/Core/IOS/USB/EmulatedUSBDevice.h create mode 100644 Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp create mode 100644 Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index b5f1dfdb87..16bc0b1b0b 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -399,6 +399,10 @@ add_library(core IOS/USB/Bluetooth/WiimoteHIDAttr.h IOS/USB/Common.cpp IOS/USB/Common.h + IOS/USB/EmulatedUSBDevice.cpp + IOS/USB/EmulatedUSBDevice.h + IOS/USB/Emulated/Skylander.cpp + IOS/USB/Emulated/Skylander.h IOS/USB/Host.cpp IOS/USB/Host.h IOS/USB/OH0/OH0.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index c3f1456f60..09cfcabb55 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -551,6 +551,16 @@ void SetUSBDeviceWhitelist(const std::set>& devices) Config::SetBase(Config::MAIN_USB_PASSTHROUGH_DEVICES, SaveUSBWhitelistToString(devices)); } +// Main.EmulatedUSBDevices + +const Info MAIN_EMULATE_SKYLANDER_PORTAL{ + {System::Main, "EmulatedUSBDevices", "EmulateSkylanderPortal"}, false}; + +bool EmulateSkylanderPortal() +{ + return Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL); +} + // 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 d6a8d058f5..e2a4549e49 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -342,6 +342,11 @@ extern const Info MAIN_USB_PASSTHROUGH_DEVICES; std::set> GetUSBDeviceWhitelist(); void SetUSBDeviceWhitelist(const std::set>& devices); +// Main.EmulatedUSBDevices + +extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; +bool EmulateSkylanderPortal(); + // GameCube path utility functions // Replaces NTSC-K with some other region, and doesn't replace non-NTSC-K regions diff --git a/Source/Core/Core/IOS/USB/Common.h b/Source/Core/Core/IOS/USB/Common.h index 9531660264..eb506dd64f 100644 --- a/Source/Core/Core/IOS/USB/Common.h +++ b/Source/Core/Core/IOS/USB/Common.h @@ -112,6 +112,10 @@ struct TransferCommand std::unique_ptr MakeBuffer(size_t size) const; void FillBuffer(const u8* src, size_t size) const; + // Fake Transfers + u64 expected_time; + u32 expected_count; + private: Kernel& m_ios; }; diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp new file mode 100644 index 0000000000..9a09299607 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp @@ -0,0 +1,721 @@ +// Copyright 2017 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/Skylander.h" + +#include +#include +#include + +#include "Common/Logging/Log.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 +{ +SkylanderPortal g_skyportal; + +SkylanderUSB::SkylanderUSB(Kernel& ios, const std::string& device_name) + : EmulatedUSBDevice(ios, device_name) +{ + m_vid = 0x1430; + m_pid = 0x150; + m_id = (static_cast(m_vid) << 32 | static_cast(m_pid) << 16 | static_cast(9) << 8 | + static_cast(1)); + deviceDesc = DeviceDescriptor{18, 1, 512, 0, 0, 0, 64, 5168, 336, 256, 1, 2, 0, 1}; + configDesc.emplace_back(ConfigDescriptor{9, 2, 41, 1, 1, 0, 128, 250}); + interfaceDesc.emplace_back(InterfaceDescriptor{9, 4, 0, 0, 2, 3, 0, 0, 0}); + endpointDesc.emplace_back(EndpointDescriptor{7, 5, 129, 3, 64, 1}); + endpointDesc.emplace_back(EndpointDescriptor{7, 5, 2, 3, 64, 1}); +} + +SkylanderUSB::~SkylanderUSB() +{ +} + +DeviceDescriptor SkylanderUSB::GetDeviceDescriptor() const +{ + return deviceDesc; +} + +std::vector SkylanderUSB::GetConfigurations() const +{ + return configDesc; +} + +std::vector SkylanderUSB::GetInterfaces(u8 config) const +{ + return interfaceDesc; +} + +std::vector SkylanderUSB::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + return endpointDesc; +} + +bool SkylanderUSB::Attach() +{ + if (m_device_attached) + { + return true; + } + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + m_device_attached = true; + if (!m_has_initialised && !Core::WantsDeterminism()) + { + GetTransferThread().Start(); + m_has_initialised = true; + } + return true; +} + +bool SkylanderUSB::AttachAndChangeInterface(const u8 interface) +{ + return true; +} + +int SkylanderUSB::CancelTransfer(const u8 endpoint) +{ + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + if (GetTransferThread().GetTransfers()) + { + return IPC_ENOENT; + } + GetTransferThread().ClearTransfers(); + return IPC_SUCCESS; +} + +int SkylanderUSB::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 SkylanderUSB::GetNumberOfAltSettings(u8 interface) +{ + return 0; +} + +int SkylanderUSB::SetAltSetting(u8 alt_setting) +{ + return 0; +} + +// Skylander Portal control transfers are handled via this method, if a transfer requires a +// response, then we request that data from the "Portal" and queue it to be responded to via the +// Interrupt Response, references can be found via: +// https://marijnkneppers.dev/posts +// https://github.com/tresni/PoweredPortals/wiki/USB-Protocols +// https://pastebin.com/EqtTRzeF +int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={} wValue={:04x}" + " wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, + cmd->index, cmd->length); + + cmd->expected_time = Common::Timer::NowUs() + 100; + auto& system = Core::System::GetInstance(); + auto& memory = system.GetMemory(); + u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); + std::array q_result = {}; + std::array q_data = {}; + // Control transfers are instantaneous + switch (cmd->request_type) + { + // HID host to device type + case 0x21: + switch (cmd->request) + { + case 0x09: + switch (buf[0]) + { + case 'A': + { + // Activation + // Command { 'A', (00 | 01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'A', (00 | 01), + // ff, 77, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00 } + // The 3rd byte of the command is whether to activate (0x01) or deactivate (0x00) the + // portal. The response echos back the activation byte as the 2nd byte of the response. The + // 3rd and 4th bytes of the response appear to vary from wired to wireless. On wired + // portals, the bytes appear to always be ff 77. On wireless portals, during activation the + // 3rd byte appears to count down from ff (possibly a battery power indication) and during + // deactivation ed and eb responses have been observed. The 4th byte appears to always be 00 + // for wireless portals. + + // Wii U Wireless: 41 01 f4 00 41 00 ed 00 41 01 f4 00 41 00 eb 00 41 01 f3 00 41 00 ed 00 + if (cmd->length == 2 || cmd->length == 32) + { + q_data = {buf[0], buf[1]}; + q_result = {0x41, buf[1], 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + q_queries.push(q_result); + cmd->expected_count = 10; + g_skyportal.Activate(); + } + break; + } + case 'C': + { + // Color + // Command { 'C', 12, 34, 56, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'C', 12, 34, 56, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // The 3 bytes {12, 34, 56} are RGB values. + + // This command should set the color of the LED in the portal, however this appears + // deprecated in most of the recent portals. On portals that do not have LEDs, this command + // is silently ignored and do not require a response. + if (cmd->length == 4 || cmd->length == 32) + { + g_skyportal.SetLEDs(0x01, buf[1], buf[2], buf[3]); + q_data = {0x43, buf[1], buf[2], buf[3]}; + cmd->expected_count = 12; + } + break; + } + case 'J': + { + // Sided color + // buf[1] is the side + // 0x00: right + // 0x01: left and right + // 0x02: left + + // buf[2], buf[3] and buf[4] are red, green and blue + + // buf[5] is unknown. Observed values are 0x00, 0x0D and 0xF4 + + // buf[6] is the fade duration. Exact value-time corrolation unknown. Observed values are + // 0x00, 0x01 and 0x07. Custom commands show that the higher this value the longer the + // duration. + + // Empty J response is send after the fade is completed. Immeditately sending it is fine + // as long as we don't show the fade happening + if (cmd->length == 7) + { + q_data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]}; + cmd->expected_count = 15; + q_result = {buf[0]}; + q_queries.push(q_result); + g_skyportal.SetLEDs(buf[1], buf[2], buf[3], buf[4]); + } + break; + } + case 'L': + { + // Light + // This command is used while playing audio through the portal + + // buf[1] is the position + // 0x00: right + // 0x01: trap led + // 0x02: left + + // buf[2], buf[3] and buf[4] are red, green and blue + // the trap led is white-only + // increasing or decreasing the values results in a birghter or dimmer light + + // buf[5] is unknown. + // A range of values have been observed + if (cmd->length == 5) + { + q_data = {buf[0], buf[1], buf[2], buf[3], buf[4]}; + cmd->expected_count = 13; + + u8 side = buf[1]; + if (side == 0x02) + { + side = 0x04; + } + g_skyportal.SetLEDs(side, buf[2], buf[3], buf[4]); + } + break; + } + case 'M': + { + // Audio Firmware version + if (cmd->length == 2) + { + q_data = {buf[0], buf[1]}; + cmd->expected_count = 10; + q_result = {buf[0], buf[1], 0x00, 0x19}; + q_queries.push(q_result); + } + break; + } + // Query + // Command { 'Q', 10, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'Q', 10, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // In the command the 3rd byte indicates which Skylander to query data + // from. Index starts at 0x10 for the 1st Skylander (as reported in the Status command.) The + // 16th Skylander indexed would be 0x20. + + // A response with the 2nd byte of 0x01 indicates an error in the read. Otherwise, the + // response indicates the Skylander's index in the 2nd byte, the block read in the 3rd byte, + // data (16 bytes) is contained in bytes 4-20. + + // A Skylander has 64 blocks of data indexed from 0x00 to 0x3f. SwapForce characters have 2 + // character indexes, these may not be sequential. + case 'Q': + { + // Queries a block + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + g_skyportal.QueryBlock(sky_num, block, q_result.data()); + q_queries.push(q_result); + q_data = {buf[0], buf[1], buf[2]}; + cmd->expected_count = 11; + break; + } + case 'R': + { + // Ready + // Command { 'R', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'R', 02, 0a, 03, 02, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // The 4 byte sequence after the R (0x52) is unknown, but appears consistent based on device + // type. + if (cmd->length == 2 || cmd->length == 32) + { + q_data = {0x52, 0x00}; + q_result = {0x52, 0x02, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + q_queries.push(q_result); + cmd->expected_count = 10; + } + break; + } + // Status + // Command { 'S', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'S', 55, 00, 00, 55, 3e, + // (00|01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00 } + // Status is the default command. If you open the HID device and + // activate the portal, you will get status outputs. + + // The 4 bytes {55, 00, 00, 55} are the status of characters on the portal. The 4 bytes are + // treated as a 32-bit binary array. Each unique Skylander placed on a board is represented + // by 2 bits starting with the first Skylander in the least significant bit. This bit is + // present whenever the Skylandar is added or present on the portal. When the Skylander is + // added to the board, both bits are set in the next status message as a one-time signal. + // When a Skylander is removed from the board, only the most significant bit of the 2 bits + // is set. + + // Different portals can track a different number of RFID tags. The Wii Wireless portal + // tracks 4, the Wired portal can track 8. The maximum number of unique Skylanders tracked + // at any time is 16, after which new Skylanders appear to cycle unused bits. + + // Certain Skylanders, e.g. SwapForce Skylanders, are represented as 2 ID in the bit array. + // This may be due to the presence of 2 RFIDs, one for each half of the Skylander. + + // The 6th byte {3e} is a counter and increments by one. It will roll over when reaching + // {ff}. + + // The purpose of the (00\|01) byte at the 7th position appear to indicate if the portal has + // been activated: {01} when active and {00} when deactivated. + case 'S': + { + q_data = {buf[0]}; + cmd->expected_count = 9; + break; + } + case 'V': + { + q_data = {buf[0], buf[1], buf[2], buf[3]}; + cmd->expected_count = 12; + break; + } + // Write + // Command { 'W', 10, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'W', 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // In the command the 3rd byte indicates which Skylander to query data from. Index starts at + // 0x10 for the 1st Skylander (as reported in the Status command.) The 16th Skylander + // indexed would be 0x20. + + // 4th byte is the block to write to. + + // Bytes 5 - 20 ({ 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f }) are the + // data to write. + + // The response does not appear to return the id of the Skylander being written, the 2nd + // byte is 0x00; however, the 3rd byte echos the block that was written (0x00 in example + // above.) + + case 'W': + { + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + g_skyportal.WriteBlock(sky_num, block, &buf[3], q_result.data()); + q_queries.push(q_result); + q_data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], + buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], + buf[14], buf[15], buf[16], buf[17], buf[18]}; + cmd->expected_count = 19; + break; + } + default: + ERROR_LOG_FMT(IOS_USB, "Unhandled Skylander Portal Query: {}", buf[0]); + break; + } + break; + case 0x0A: + cmd->expected_count = 8; + break; + case 0x0B: + cmd->expected_count = 8; + break; + default: + ERROR_LOG_FMT(IOS_USB, "Unhandled Request {}", cmd->request); + break; + } + break; + + default: + break; + } + cmd->expected_time = Common::Timer::NowUs() + 100; + GetTransferThread().AddTransfer(std::move(cmd), q_data); + return 0; +} +int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={} endpoint={}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + return 0; +} + +// When an Interrupt Message is received by the Skylander Portal from the console, +// it needs to respond with either the status of the console if there are no Control Messages that +// require a response, or with the relevant response data that is requested from the Control +// Message. +int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Interrupt: length={} endpoint={}", m_vid, m_pid, + m_active_interface, cmd->length, cmd->endpoint); + + auto& system = Core::System::GetInstance(); + auto& memory = system.GetMemory(); + u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); + std::array q_result = {}; + // Audio requests are 64 bytes long, are the only Interrupt requests longer than 32 bytes, + // echo the request as the response and respond after 1ms + if (cmd->length > 32) + { + std::array q_audio_result = {}; + u8* audio_buf = q_audio_result.data(); + memcpy(audio_buf, buf, cmd->length); + cmd->expected_time = Common::Timer::NowUs() + 1000; + cmd->expected_count = cmd->length; + GetTransferThread().AddTransfer(std::move(cmd), q_audio_result); + return 0; + } + // If some data was requested from the Control Message, then the Interrupt message needs to + // respond with that data. Check if the queries queue is empty + if (!q_queries.empty()) + { + q_result = q_queries.front(); + q_queries.pop(); + // This needs to happen after ~22 milliseconds + cmd->expected_time = Common::Timer::NowUs() + 22000; + } + // If there is no relevant data to respond with, respond with the currentstatus of the Portal + else + { + q_result = g_skyportal.GetStatus(); + cmd->expected_time = Common::Timer::NowUs() + 2000; + } + cmd->expected_count = 32; + GetTransferThread().AddTransfer(std::move(cmd), q_result); + return 0; +} +int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Isochronous: length={} endpoint={} num_packets={}", + m_vid, m_pid, m_active_interface, cmd->length, cmd->endpoint, cmd->num_packets); + return 0; +} + +void Skylander::save() +{ + if (!sky_file) + { + return; + } + + { + sky_file.Seek(0, File::SeekOrigin::Begin); + sky_file.WriteBytes(data.data(), 0x40 * 0x10); + } +} + +void SkylanderPortal::Activate() +{ + std::lock_guard lock(sky_mutex); + if (activated) + { + // If the portal was already active no change is needed + return; + } + + // If not we need to advertise change to all the figures present on the portal + for (auto& s : skylanders) + { + if (s.status & 1) + { + s.queued_status.push(3); + s.queued_status.push(1); + } + } + + activated = true; +} + +void SkylanderPortal::Deactivate() +{ + std::lock_guard lock(sky_mutex); + + for (auto& s : skylanders) + { + // check if at the end of the updates there would be a figure on the portal + if (!s.queued_status.empty()) + { + s.status = s.queued_status.back(); + s.queued_status = std::queue(); + } + + s.status &= 1; + } + + activated = false; +} + +bool SkylanderPortal::IsActivated() +{ + std::lock_guard lock(sky_mutex); + + return activated; +} + +void SkylanderPortal::UpdateStatus() +{ + std::lock_guard lock(sky_mutex); + + if (!status_updated) + { + for (auto& s : skylanders) + { + if (s.status & 1) + { + s.queued_status.push(0); + s.queued_status.push(3); + s.queued_status.push(1); + } + } + status_updated = true; + } +} + +// Side: +// 0x00 = right +// 0x01 = left and right +// 0x02 = left +// 0x03 = trap +void SkylanderPortal::SetLEDs(u8 side, u8 red, u8 green, u8 blue) +{ + std::lock_guard lock(sky_mutex); + if (side == 0x00) + { + this->color_right.r = red; + this->color_right.g = green; + this->color_right.b = blue; + } + else if (side == 0x01) + { + this->color_right.r = red; + this->color_right.g = green; + this->color_right.b = blue; + + this->color_left.r = red; + this->color_left.g = green; + this->color_left.b = blue; + } + else if (side == 0x02) + { + this->color_left.r = red; + this->color_left.g = green; + this->color_left.b = blue; + } + else if (side == 0x03) + { + this->color_trap.r = red; + this->color_trap.g = green; + this->color_trap.b = blue; + } +} + +std::array SkylanderPortal::GetStatus() +{ + std::lock_guard lock(sky_mutex); + + u32 status = 0; + u8 active = 0x00; + + if (activated) + { + active = 0x01; + } + + for (int i = MAX_SKYLANDERS - 1; i >= 0; i--) + { + auto& s = skylanders[i]; + + if (!s.queued_status.empty()) + { + s.status = s.queued_status.front(); + s.queued_status.pop(); + } + status <<= 2; + status |= s.status; + } + + std::array q_result = {0x53, 0x00, 0x00, 0x00, 0x00, interrupt_counter++, + active, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + memcpy(&q_result.data()[1], &status, sizeof(status)); + return q_result; +} + +void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) +{ + std::lock_guard lock(sky_mutex); + + const auto& skylander = skylanders[sky_num]; + + reply_buf[0] = 'Q'; + reply_buf[2] = block; + if (skylander.status & 1) + { + reply_buf[1] = (0x10 | sky_num); + memcpy(reply_buf + 3, skylander.data.data() + (16 * block), 16); + } + else + { + reply_buf[1] = sky_num; + } +} + +void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf) +{ + std::lock_guard lock(sky_mutex); + + auto& skylander = skylanders[sky_num]; + + reply_buf[0] = 'W'; + reply_buf[2] = block; + + if (skylander.status & 1) + { + reply_buf[1] = (0x10 | sky_num); + memcpy(skylander.data.data() + (block * 16), to_write_buf, 16); + skylander.save(); + } + else + { + reply_buf[1] = sky_num; + } +} + +bool SkylanderPortal::RemoveSkylander(u8 sky_num) +{ + DEBUG_LOG_FMT(IOS_USB, "Cleared Skylander from slot {}", sky_num); + std::lock_guard lock(sky_mutex); + auto& skylander = skylanders[sky_num]; + + if (skylander.status & 1) + { + skylander.status = 2; + skylander.queued_status.push(2); + skylander.queued_status.push(0); + skylander.sky_file.Close(); + return true; + } + + return false; +} + +u8 SkylanderPortal::LoadSkylander(u8* buf, File::IOFile in_file) +{ + std::lock_guard lock(sky_mutex); + + u32 sky_serial = 0; + for (int i = 3; i > -1; i--) + { + sky_serial <<= 8; + sky_serial |= buf[i]; + } + u8 found_slot = 0xFF; + + // mimics spot retaining on the portal + for (auto i = 0; i < MAX_SKYLANDERS; i++) + { + if ((skylanders[i].status & 1) == 0) + { + if (skylanders[i].last_id == sky_serial) + { + DEBUG_LOG_FMT(IOS_USB, "Last Id: {}", skylanders[i].last_id); + + found_slot = i; + break; + } + + if (i < found_slot) + { + DEBUG_LOG_FMT(IOS_USB, "Last Id: {}", skylanders[i].last_id); + found_slot = i; + } + } + } + + if (found_slot != 0xFF) + { + auto& skylander = skylanders[found_slot]; + memcpy(skylander.data.data(), buf, skylander.data.size()); + DEBUG_LOG_FMT(IOS_USB, "Skylander Data: \n{}", + HexDump(skylander.data.data(), skylander.data.size())); + skylander.sky_file = std::move(in_file); + skylander.status = 3; + skylander.queued_status.push(3); + skylander.queued_status.push(1); + skylander.last_id = sky_serial; + } + return found_slot; +} + +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.h b/Source/Core/Core/IOS/USB/Emulated/Skylander.h new file mode 100644 index 0000000000..3e16d25a95 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.h @@ -0,0 +1,102 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/IOFile.h" +#include "Core/IOS/USB/EmulatedUSBDevice.h" + +// The maximum possible characters the portal can handle. +// The status array is 32 bits and every character takes 2 bits. +// 32/2 = 16 +constexpr u8 MAX_SKYLANDERS = 16; + +namespace IOS::HLE::USB +{ +class SkylanderUSB final : public EmulatedUSBDevice +{ +public: + SkylanderUSB(Kernel& ios, const std::string& device_name); + ~SkylanderUSB(); + 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; + +protected: + std::queue> q_queries; + +private: + u16 m_vid = 0; + u16 m_pid = 0; + u8 m_active_interface = 0; + bool m_device_attached = false; + DeviceDescriptor deviceDesc; + std::vector configDesc; + std::vector interfaceDesc; + std::vector endpointDesc; + bool m_has_initialised = false; +}; + +struct Skylander final +{ + File::IOFile sky_file; + u8 status = 0; + std::queue queued_status; + std::array data{}; + u32 last_id = 0; + void save(); +}; + +struct LedColor final +{ + u8 r = 0, g = 0, b = 0; +}; + +class SkylanderPortal final +{ +public: + void Activate(); + void Deactivate(); + bool IsActivated(); + void UpdateStatus(); + void SetLEDs(u8 side, u8 r, u8 g, u8 b); + + std::array GetStatus(); + void QueryBlock(u8 sky_num, u8 block, u8* reply_buf); + void WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf); + + bool RemoveSkylander(u8 sky_num); + u8 LoadSkylander(u8* buf, File::IOFile in_file); + +protected: + std::mutex sky_mutex; + + bool activated = true; + bool status_updated = false; + u8 interrupt_counter = 0; + LedColor color_right = {}; + LedColor color_left = {}; + LedColor color_trap = {}; + + Skylander skylanders[MAX_SKYLANDERS]; +}; + +extern SkylanderPortal g_skyportal; + +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp new file mode 100644 index 0000000000..8f271745d0 --- /dev/null +++ b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp @@ -0,0 +1,82 @@ +#include "Core/IOS/USB/EmulatedUSBDevice.h" + +#include + +#include "Common/Thread.h" +#include "Common/Timer.h" +#include "Core/Core.h" + +namespace IOS::HLE::USB +{ +EmulatedUSBDevice::EmulatedUSBDevice(Kernel& ios, const std::string& device_name) : m_ios(ios) +{ +} + +EmulatedUSBDevice::~EmulatedUSBDevice() +{ +} + +EmulatedUSBDevice::FakeTransferThread::~FakeTransferThread() +{ + Stop(); +} + +void EmulatedUSBDevice::FakeTransferThread::Start() +{ + if (Core::WantsDeterminism()) + return; + + if (m_thread_running.TestAndSet()) + { + m_thread = std::thread([this] { + Common::SetCurrentThreadName("Fake Transfer Thread"); + while (m_thread_running.IsSet()) + { + if (!m_transfers.empty()) + { + std::lock_guard lk{m_transfers_mutex}; + u64 timestamp = Common::Timer::NowUs(); + for (auto iterator = m_transfers.begin(); iterator != m_transfers.end();) + { + auto* command = iterator->second.get(); + if (command->expected_time > timestamp) + { + ++iterator; + continue; + } + command->FillBuffer(iterator->first.data(), command->expected_count); + command->OnTransferComplete(command->expected_count); + iterator = m_transfers.erase(iterator); + } + } + } + }); + } +} + +void EmulatedUSBDevice::FakeTransferThread::Stop() +{ + if (m_thread_running.TestAndClear()) + m_thread.join(); +} + +void EmulatedUSBDevice::FakeTransferThread::AddTransfer(std::unique_ptr command, + std::array data) +{ + std::lock_guard lk{m_transfers_mutex}; + m_transfers.emplace(data, std::move(command)); +} + +void EmulatedUSBDevice::FakeTransferThread::ClearTransfers() +{ + std::lock_guard lk{m_transfers_mutex}; + m_transfers.clear(); +} + +bool EmulatedUSBDevice::FakeTransferThread::GetTransfers() +{ + std::lock_guard lk{m_transfers_mutex}; + return m_transfers.empty(); +} + +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h new file mode 100644 index 0000000000..60e469f7ce --- /dev/null +++ b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/IOS/USB/Common.h" + +namespace IOS::HLE::USB +{ +class EmulatedUSBDevice : public Device +{ +public: + EmulatedUSBDevice(Kernel& ios, const std::string& device_name); + virtual ~EmulatedUSBDevice(); + +protected: + class FakeTransferThread final + { + public: + explicit FakeTransferThread(EmulatedUSBDevice* device) : m_device(device) {} + ~FakeTransferThread(); + void Start(); + void Stop(); + void AddTransfer(std::unique_ptr command, std::array data); + void ClearTransfers(); + bool GetTransfers(); + + private: + EmulatedUSBDevice* m_device = nullptr; + Common::Flag m_thread_running; + std::thread m_thread; + Common::Flag m_is_initialized; + std::map, std::unique_ptr> m_transfers; + std::mutex m_transfers_mutex; + }; + FakeTransferThread m_transfer_thread{this}; + FakeTransferThread& GetTransferThread() { return m_transfer_thread; } + +private: + Kernel& m_ios; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index bd5debb809..a7378efcc2 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/Skylander.h" #include "Core/IOS/USB/LibusbDevice.h" namespace IOS::HLE @@ -115,6 +116,18 @@ bool USBHost::UpdateDevices(const bool always_add_hooks) bool USBHost::AddNewDevices(std::set& new_devices, DeviceChangeHooks& hooks, const bool always_add_hooks) { + if (Config::EmulateSkylanderPortal()) + { + auto skylanderportal = std::make_unique(m_ios, "Skylander Portal"); + if (!ShouldAddDevice(*skylanderportal)) + return true; + const u64 skyid = skylanderportal->GetId(); + new_devices.insert(skyid); + if (AddDevice(std::move(skylanderportal))) + { + hooks.emplace(GetDeviceById(skyid), ChangeEvent::Inserted); + } + } #ifdef __LIBUSB__ auto whitelist = Config::GetUSBDeviceWhitelist(); if (whitelist.empty()) diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 62a60526c4..729db0a46d 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -373,6 +373,8 @@ + + @@ -985,6 +987,8 @@ + + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 182bd9e6e5..a9e07afe0b 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -325,6 +325,8 @@ add_executable(dolphin-emu Settings/USBDeviceAddToWhitelistDialog.h Settings/WiiPane.cpp Settings/WiiPane.h + SkylanderPortal/SkylanderPortalWindow.cpp + SkylanderPortal/SkylanderPortalWindow.h TAS/GCTASInputWindow.cpp TAS/GCTASInputWindow.h TAS/GBATASInputWindow.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 85949f6a5f..f776feceed 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -200,6 +200,7 @@ + @@ -379,6 +380,7 @@ + diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index ac9ae287ce..f4d52c0fa5 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -111,6 +111,7 @@ #include "DolphinQt/RiivolutionBootWidget.h" #include "DolphinQt/SearchBar.h" #include "DolphinQt/Settings.h" +#include "DolphinQt/SkylanderPortal/SkylanderPortalWindow.h" #include "DolphinQt/TAS/GBATASInputWindow.h" #include "DolphinQt/TAS/GCTASInputWindow.h" #include "DolphinQt/TAS/WiiTASInputWindow.h" @@ -520,6 +521,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog); 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::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); // Movie @@ -1301,6 +1303,18 @@ void MainWindow::ShowFIFOPlayer() m_fifo_window->activateWindow(); } +void MainWindow::ShowSkylanderPortal() +{ + if (!m_skylander_window) + { + m_skylander_window = new SkylanderPortalWindow; + } + + m_skylander_window->show(); + m_skylander_window->raise(); + m_skylander_window->activateWindow(); +} + void MainWindow::StateLoad() { QString path = diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index 8d017606f3..953b6b1811 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -43,6 +43,7 @@ class RegisterWidget; class RenderWidget; class SearchBar; class SettingsWindow; +class SkylanderPortalWindow; class ThreadWidget; class ToolBar; class WatchWidget; @@ -159,6 +160,7 @@ private: void ShowNetPlaySetupDialog(); void ShowNetPlayBrowser(); void ShowFIFOPlayer(); + void ShowSkylanderPortal(); void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); @@ -222,6 +224,7 @@ private: SettingsWindow* m_settings_window = nullptr; GraphicsWindow* m_graphics_window = nullptr; FIFOPlayerWindow* m_fifo_window = nullptr; + SkylanderPortalWindow* m_skylander_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 d42bb959ab..ae72f12158 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -221,6 +221,8 @@ void MenuBar::AddToolsMenu() tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); + tools_menu->addAction(tr("&Skylanders Portal"), this, [this] { emit ShowSkylanderPortal(); }); + tools_menu->addSeparator(); tools_menu->addAction(tr("Start &NetPlay..."), this, &MenuBar::StartNetPlay); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 40c0b01261..14418460f4 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -89,6 +89,7 @@ signals: void ShowAboutDialog(); void ShowCheatsManager(); void ShowResourcePackManager(); + void ShowSkylanderPortal(); void ConnectWiiRemote(int id); // Options diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp new file mode 100644 index 0000000000..1e91c273ae --- /dev/null +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp @@ -0,0 +1,922 @@ +// DolphinQt code copied and modified for Dolphin from the RPCS3 Qt utility for Creating, Loading +// and Clearing skylanders + +#include "DolphinQt/SkylanderPortal/SkylanderPortalWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/IOFile.h" + +#include "Core/Config/MainSettings.h" + +#include "DolphinQt/QtUtils/DolphinFileDialog.h" +#include "DolphinQt/Settings.h" + +SkylanderPortalWindow* SkylanderPortalWindow::inst = nullptr; +std::optional> SkylanderPortalWindow::sky_slots[MAX_SKYLANDERS]; +QString last_skylander_path; + +const std::map, const std::string> list_skylanders = { + {{0, 0x0000}, "Whirlwind"}, + {{0, 0x1801}, "Series 2 Whirlwind"}, + {{0, 0x1C02}, "Polar Whirlwind"}, + {{0, 0x2805}, "Horn Blast Whirlwind"}, + {{0, 0x3810}, "Eon's Elite Whirlwind"}, + {{1, 0x0000}, "Sonic Boom"}, + {{1, 0x1801}, "Series 2 Sonic Boom"}, + {{2, 0x0000}, "Warnado"}, + {{2, 0x2206}, "LightCore Warnado"}, + {{3, 0x0000}, "Lightning Rod"}, + {{3, 0x1801}, "Series 2 Lightning Rod"}, + {{4, 0x0000}, "Bash"}, + {{4, 0x1801}, "Series 2 Bash"}, + {{5, 0x0000}, "Terrafin"}, + {{5, 0x1801}, "Series 2 Terrafin"}, + {{5, 0x2805}, "Knockout Terrafin"}, + {{5, 0x3810}, "Eon's Elite Terrafin"}, + {{6, 0x0000}, "Dino Rang"}, + {{6, 0x4810}, "Eon's Elite Dino Rang"}, + {{7, 0x0000}, "Prism Break"}, + {{7, 0x1801}, "Series 2 Prism Break"}, + {{7, 0x2805}, "Hyper Beam Prism Break"}, + {{7, 0x1206}, "LightCore Prism Break"}, + {{8, 0x0000}, "Sunburn"}, + {{9, 0x0000}, "Eruptor"}, + {{9, 0x1801}, "Series 2 Eruptor"}, + {{9, 0x2C02}, "Volcanic Eruptor"}, + {{9, 0x2805}, "Lava Barf Eruptor"}, + {{9, 0x1206}, "LightCore Eruptor"}, + {{9, 0x3810}, "Eon's Elite Eruptor"}, + {{10, 0x0000}, "Ignitor"}, + {{10, 0x1801}, "Series 2 Ignitor"}, + {{10, 0x1C03}, "Legendary Ignitor"}, + {{11, 0x0000}, "Flameslinger"}, + {{11, 0x1801}, "Series 2 Flameslinger"}, + {{12, 0x0000}, "Zap"}, + {{12, 0x1801}, "Series 2 Zap"}, + {{13, 0x0000}, "Wham Shell"}, + {{13, 0x2206}, "LightCore Wham Shell"}, + {{14, 0x0000}, "Gill Grunt"}, + {{14, 0x1801}, "Series 2 Gill Grunt"}, + {{14, 0x2805}, "Anchors Away Gill Grunt"}, + {{14, 0x3805}, "Tidal Wave Gill Grunt"}, + {{14, 0x3810}, "Eon's Elite Gill Grunt"}, + {{15, 0x0000}, "Slam Bam"}, + {{15, 0x1801}, "Series 2 Slam Bam"}, + {{15, 0x1C03}, "Legendary Slam Bam"}, + {{15, 0x4810}, "Eon's Elite Slam Bam"}, + {{16, 0x0000}, "Spyro"}, + {{16, 0x1801}, "Series 2 Spyro"}, + {{16, 0x2C02}, "Dark Mega Ram Spyro"}, + {{16, 0x2805}, "Mega Ram Spyro"}, + {{16, 0x3810}, "Eon's Elite Spyro"}, + {{17, 0x0000}, "Voodood"}, + {{17, 0x4810}, "Eon's Elite Voodood"}, + {{18, 0x0000}, "Double Trouble"}, + {{18, 0x1801}, "Series 2 Double Trouble"}, + {{18, 0x1C02}, "Royal Double Trouble"}, + {{19, 0x0000}, "Trigger Happy"}, + {{19, 0x1801}, "Series 2 Trigger Happy"}, + {{19, 0x2C02}, "Springtime Trigger Happy"}, + {{19, 0x2805}, "Big Bang Trigger Happy"}, + {{19, 0x3810}, "Eon's Elite Trigger Happy"}, + {{20, 0x0000}, "Drobot"}, + {{20, 0x1801}, "Series 2 Drobot"}, + {{20, 0x1206}, "LightCore Drobot"}, + {{21, 0x0000}, "Drill Seargeant"}, + {{21, 0x1801}, "Series 2 Drill Seargeant"}, + {{22, 0x0000}, "Boomer"}, + {{22, 0x4810}, "Eon's Elite Boomer"}, + {{23, 0x0000}, "Wrecking Ball"}, + {{23, 0x1801}, "Series 2 Wrecking Ball"}, + {{24, 0x0000}, "Camo"}, + {{24, 0x2805}, "Thorn Horn Camo"}, + {{25, 0x0000}, "Zook"}, + {{25, 0x1801}, "Series 2 Zook"}, + {{25, 0x4810}, "Eon's Elite Zook"}, + {{26, 0x0000}, "Stealth Elf"}, + {{26, 0x1801}, "Series 2 Stealth Elf"}, + {{26, 0x2C02}, "Dark Stealth Elf"}, + {{26, 0x1C03}, "Legendary Stealth Elf"}, + {{26, 0x2805}, "Ninja Stealth Elf"}, + {{26, 0x3810}, "Eon's Elite Stealth Elf"}, + {{27, 0x0000}, "Stump Smash"}, + {{27, 0x1801}, "Series 2 Stump Smash"}, + {{28, 0x0000}, "Dark Spyro"}, + {{29, 0x0000}, "Hex"}, + {{29, 0x1801}, "Series 2 Hex"}, + {{29, 0x1206}, "LightCore Hex"}, + {{30, 0x0000}, "Chop Chop"}, + {{30, 0x1801}, "Series 2 Chop Chop"}, + {{30, 0x2805}, "Twin Blade Chop Chop"}, + {{30, 0x3810}, "Eon's Elite Chop Chop"}, + {{31, 0x0000}, "Ghost Roaster"}, + {{31, 0x4810}, "Eon's Elite Ghost Roaster"}, + {{32, 0x0000}, "Cynder"}, + {{32, 0x1801}, "Series 2 Cynder"}, + {{32, 0x2805}, "Phantom Cynder"}, + {{100, 0x0000}, "Jet Vac"}, + {{100, 0x1403}, "Legendary Jet Vac"}, + {{100, 0x2805}, "Turbo Jet Vac"}, + {{100, 0x3805}, "Full Blast Jet Vac"}, + {{100, 0x1206}, "LightCore Jet Vac"}, + {{101, 0x0000}, "Swarm"}, + {{102, 0x0000}, "Crusher"}, + {{102, 0x1602}, "Granite Crusher"}, + {{103, 0x0000}, "Flashwing"}, + {{103, 0x1402}, "Jade Flash Wing"}, + {{103, 0x2206}, "LightCore Flashwing"}, + {{104, 0x0000}, "Hot Head"}, + {{105, 0x0000}, "Hot Dog"}, + {{105, 0x1402}, "Molten Hot Dog"}, + {{105, 0x2805}, "Fire Bone Hot Dog"}, + {{106, 0x0000}, "Chill"}, + {{106, 0x1603}, "Legendary Chill"}, + {{106, 0x2805}, "Blizzard Chill"}, + {{106, 0x1206}, "LightCore Chill"}, + {{107, 0x0000}, "Thumpback"}, + {{108, 0x0000}, "Pop Fizz"}, + {{108, 0x1402}, "Punch Pop Fizz"}, + {{108, 0x3C02}, "Love Potion Pop Fizz"}, + {{108, 0x2805}, "Super Gulp Pop Fizz"}, + {{108, 0x3805}, "Fizzy Frenzy Pop Fizz"}, + {{108, 0x1206}, "LightCore Pop Fizz"}, + {{109, 0x0000}, "Ninjini"}, + {{109, 0x1602}, "Scarlet Ninjini"}, + {{110, 0x0000}, "Bouncer"}, + {{110, 0x1603}, "Legendary Bouncer"}, + {{111, 0x0000}, "Sprocket"}, + {{111, 0x2805}, "Heavy Duty Sprocket"}, + {{112, 0x0000}, "Tree Rex"}, + {{112, 0x1602}, "Gnarly Tree Rex"}, + {{113, 0x0000}, "Shroomboom"}, + {{113, 0x3805}, "Sure Shot Shroomboom"}, + {{113, 0x1206}, "LightCore Shroomboom"}, + {{114, 0x0000}, "Eye Brawl"}, + {{115, 0x0000}, "Fright Rider"}, + {{200, 0x0000}, "Anvil Rain"}, + {{201, 0x0000}, "Hidden Treasure"}, + {{201, 0x2000}, "Platinum Hidden Treasure"}, + {{202, 0x0000}, "Healing Elixir"}, + {{203, 0x0000}, "Ghost Pirate Swords"}, + {{204, 0x0000}, "Time Twist Hourglass"}, + {{205, 0x0000}, "Sky Iron Shield"}, + {{206, 0x0000}, "Winged Boots"}, + {{207, 0x0000}, "Sparx the Dragonfly"}, + {{208, 0x0000}, "Dragonfire Cannon"}, + {{208, 0x1602}, "Golden Dragonfire Cannon"}, + {{209, 0x0000}, "Scorpion Striker"}, + {{210, 0x3002}, "Biter's Bane"}, + {{210, 0x3008}, "Sorcerous Skull"}, + {{210, 0x300B}, "Axe of Illusion"}, + {{210, 0x300E}, "Arcane Hourglass"}, + {{210, 0x3012}, "Spell Slapper"}, + {{210, 0x3014}, "Rune Rocket"}, + {{211, 0x3001}, "Tidal Tiki"}, + {{211, 0x3002}, "Wet Walter"}, + {{211, 0x3006}, "Flood Flask"}, + {{211, 0x3406}, "Legendary Flood Flask"}, + {{211, 0x3007}, "Soaking Staff"}, + {{211, 0x300B}, "Aqua Axe"}, + {{211, 0x3016}, "Frost Helm"}, + {{212, 0x3003}, "Breezy Bird"}, + {{212, 0x3006}, "Drafty Decanter"}, + {{212, 0x300D}, "Tempest Timer"}, + {{212, 0x3010}, "Cloudy Cobra"}, + {{212, 0x3011}, "Storm Warning"}, + {{212, 0x3018}, "Cyclone Saber"}, + {{213, 0x3004}, "Spirit Sphere"}, + {{213, 0x3404}, "Legendary Spirit Sphere"}, + {{213, 0x3008}, "Spectral Skull"}, + {{213, 0x3408}, "Legendary Spectral Skull"}, + {{213, 0x300B}, "Haunted Hatchet"}, + {{213, 0x300C}, "Grim Gripper"}, + {{213, 0x3010}, "Spooky Snake"}, + {{213, 0x3017}, "Dream Piercer"}, + {{214, 0x3000}, "Tech Totem"}, + {{214, 0x3007}, "Automatic Angel"}, + {{214, 0x3009}, "Factory Flower"}, + {{214, 0x300C}, "Grabbing Gadget"}, + {{214, 0x3016}, "Makers Mana"}, + {{214, 0x301A}, "Topsy Techy"}, + {{215, 0x3005}, "Eternal Flame"}, + {{215, 0x3009}, "Fire Flower"}, + {{215, 0x3011}, "Scorching Stopper"}, + {{215, 0x3012}, "Searing Spinner"}, + {{215, 0x3017}, "Spark Spear"}, + {{215, 0x301B}, "Blazing Belch"}, + {{216, 0x3000}, "Banded Boulder"}, + {{216, 0x3003}, "Rock Hawk"}, + {{216, 0x300A}, "Slag Hammer"}, + {{216, 0x300E}, "Dust Of Time"}, + {{216, 0x3013}, "Spinning Sandstorm"}, + {{216, 0x301A}, "Rubble Trouble"}, + {{217, 0x3003}, "Oak Eagle"}, + {{217, 0x3005}, "Emerald Energy"}, + {{217, 0x300A}, "Weed Whacker"}, + {{217, 0x3010}, "Seed Serpent"}, + {{217, 0x3018}, "Jade Blade"}, + {{217, 0x301B}, "Shrub Shrieker"}, + {{218, 0x3000}, "Dark Dagger"}, + {{218, 0x3014}, "Shadow Spider"}, + {{218, 0x301A}, "Ghastly Grimace"}, + {{219, 0x3000}, "Shining Ship"}, + {{219, 0x300F}, "Heavenly Hawk"}, + {{219, 0x301B}, "Beam Scream"}, + {{220, 0x301E}, "Kaos Trap"}, + {{220, 0x351F}, "Ultimate Kaos Trap"}, + {{230, 0x0000}, "Hand of Fate"}, + {{230, 0x3403}, "Legendary Hand of Fate"}, + {{231, 0x0000}, "Piggy Bank"}, + {{232, 0x0000}, "Rocket Ram"}, + {{233, 0x0000}, "Tiki Speaky"}, + {{300, 0x0000}, "Dragon’s Peak"}, + {{301, 0x0000}, "Empire of Ice"}, + {{302, 0x0000}, "Pirate Seas"}, + {{303, 0x0000}, "Darklight Crypt"}, + {{304, 0x0000}, "Volcanic Vault"}, + {{305, 0x0000}, "Mirror of Mystery"}, + {{306, 0x0000}, "Nightmare Express"}, + {{307, 0x0000}, "Sunscraper Spire"}, + {{308, 0x0000}, "Midnight Museum"}, + {{404, 0x0000}, "Legendary Bash"}, + {{416, 0x0000}, "Legendary Spyro"}, + {{419, 0x0000}, "Legendary Trigger Happy"}, + {{430, 0x0000}, "Legendary Chop Chop"}, + {{450, 0x0000}, "Gusto"}, + {{451, 0x0000}, "Thunderbolt"}, + {{452, 0x0000}, "Fling Kong"}, + {{453, 0x0000}, "Blades"}, + {{453, 0x3403}, "Legendary Blades"}, + {{454, 0x0000}, "Wallop"}, + {{455, 0x0000}, "Head Rush"}, + {{455, 0x3402}, "Nitro Head Rush"}, + {{456, 0x0000}, "Fist Bump"}, + {{457, 0x0000}, "Rocky Roll"}, + {{458, 0x0000}, "Wildfire"}, + {{458, 0x3402}, "Dark Wildfire"}, + {{459, 0x0000}, "Ka Boom"}, + {{460, 0x0000}, "Trail Blazer"}, + {{461, 0x0000}, "Torch"}, + {{462, 0x0000}, "Snap Shot"}, + {{462, 0x3402}, "Dark Snap Shot"}, + {{463, 0x0000}, "Lob Star"}, + {{463, 0x3402}, "Winterfest Lob-Star"}, + {{464, 0x0000}, "Flip Wreck"}, + {{465, 0x0000}, "Echo"}, + {{466, 0x0000}, "Blastermind"}, + {{467, 0x0000}, "Enigma"}, + {{468, 0x0000}, "Deja Vu"}, + {{468, 0x3403}, "Legendary Deja Vu"}, + {{469, 0x0000}, "Cobra Candabra"}, + {{469, 0x3402}, "King Cobra Cadabra"}, + {{470, 0x0000}, "Jawbreaker"}, + {{470, 0x3403}, "Legendary Jawbreaker"}, + {{471, 0x0000}, "Gearshift"}, + {{472, 0x0000}, "Chopper"}, + {{473, 0x0000}, "Tread Head"}, + {{474, 0x0000}, "Bushwack"}, + {{474, 0x3403}, "Legendary Bushwack"}, + {{475, 0x0000}, "Tuff Luck"}, + {{476, 0x0000}, "Food Fight"}, + {{476, 0x3402}, "Dark Food Fight"}, + {{477, 0x0000}, "High Five"}, + {{478, 0x0000}, "Krypt King"}, + {{478, 0x3402}, "Nitro Krypt King"}, + {{479, 0x0000}, "Short Cut"}, + {{480, 0x0000}, "Bat Spin"}, + {{481, 0x0000}, "Funny Bone"}, + {{482, 0x0000}, "Knight Light"}, + {{483, 0x0000}, "Spotlight"}, + {{484, 0x0000}, "Knight Mare"}, + {{485, 0x0000}, "Blackout"}, + {{502, 0x0000}, "Bop"}, + {{505, 0x0000}, "Terrabite"}, + {{506, 0x0000}, "Breeze"}, + {{508, 0x0000}, "Pet Vac"}, + {{508, 0x3402}, "Power Punch Pet Vac"}, + {{507, 0x0000}, "Weeruptor"}, + {{507, 0x3402}, "Eggcellent Weeruptor"}, + {{509, 0x0000}, "Small Fry"}, + {{510, 0x0000}, "Drobit"}, + {{519, 0x0000}, "Trigger Snappy"}, + {{526, 0x0000}, "Whisper Elf"}, + {{540, 0x0000}, "Barkley"}, + {{540, 0x3402}, "Gnarly Barkley"}, + {{541, 0x0000}, "Thumpling"}, + {{514, 0x0000}, "Gill Runt"}, + {{542, 0x0000}, "Mini-Jini"}, + {{503, 0x0000}, "Spry"}, + {{504, 0x0000}, "Hijinx"}, + {{543, 0x0000}, "Eye Small"}, + {{601, 0x0000}, "King Pen"}, + {{602, 0x0000}, "Tri-Tip"}, + {{603, 0x0000}, "Chopscotch"}, + {{604, 0x0000}, "Boom Bloom"}, + {{605, 0x0000}, "Pit Boss"}, + {{606, 0x0000}, "Barbella"}, + {{607, 0x0000}, "Air Strike"}, + {{608, 0x0000}, "Ember"}, + {{609, 0x0000}, "Ambush"}, + {{610, 0x0000}, "Dr. Krankcase"}, + {{611, 0x0000}, "Hood Sickle"}, + {{612, 0x0000}, "Tae Kwon Crow"}, + {{613, 0x0000}, "Golden Queen"}, + {{614, 0x0000}, "Wolfgang"}, + {{615, 0x0000}, "Pain-Yatta"}, + {{616, 0x0000}, "Mysticat"}, + {{617, 0x0000}, "Starcast"}, + {{618, 0x0000}, "Buckshot"}, + {{619, 0x0000}, "Aurora"}, + {{620, 0x0000}, "Flare Wolf"}, + {{621, 0x0000}, "Chompy Mage"}, + {{622, 0x0000}, "Bad Juju"}, + {{623, 0x0000}, "Grave Clobber"}, + {{624, 0x0000}, "Blaster-Tron"}, + {{625, 0x0000}, "Ro-Bow"}, + {{626, 0x0000}, "Chain Reaction"}, + {{627, 0x0000}, "Kaos"}, + {{628, 0x0000}, "Wild Storm"}, + {{629, 0x0000}, "Tidepool"}, + {{630, 0x0000}, "Crash Bandicoot"}, + {{631, 0x0000}, "Dr. Neo Cortex"}, + {{1000, 0x0000}, "Boom Jet (Bottom)"}, + {{1001, 0x0000}, "Free Ranger (Bottom)"}, + {{1001, 0x2403}, "Legendary Free Ranger (Bottom)"}, + {{1002, 0x0000}, "Rubble Rouser (Bottom)"}, + {{1003, 0x0000}, "Doom Stone (Bottom)"}, + {{1004, 0x0000}, "Blast Zone (Bottom)"}, + {{1004, 0x2402}, "Dark Blast Zone (Bottom)"}, + {{1005, 0x0000}, "Fire Kraken (Bottom)"}, + {{1005, 0x2402}, "Jade Fire Kraken (Bottom)"}, + {{1006, 0x0000}, "Stink Bomb (Bottom)"}, + {{1007, 0x0000}, "Grilla Drilla (Bottom)"}, + {{1008, 0x0000}, "Hoot Loop (Bottom)"}, + {{1008, 0x2402}, "Enchanted Hoot Loop (Bottom)"}, + {{1009, 0x0000}, "Trap Shadow (Bottom)"}, + {{1010, 0x0000}, "Magna Charge (Bottom)"}, + {{1010, 0x2402}, "Nitro Magna Charge (Bottom)"}, + {{1011, 0x0000}, "Spy Rise (Bottom)"}, + {{1012, 0x0000}, "Night Shift (Bottom)"}, + {{1012, 0x2403}, "Legendary Night Shift (Bottom)"}, + {{1013, 0x0000}, "Rattle Shake (Bottom)"}, + {{1013, 0x2402}, "Quick Draw Rattle Shake (Bottom)"}, + {{1014, 0x0000}, "Freeze Blade (Bottom)"}, + {{1014, 0x2402}, "Nitro Freeze Blade (Bottom)"}, + {{1015, 0x0000}, "Wash Buckler (Bottom)"}, + {{1015, 0x2402}, "Dark Wash Buckler (Bottom)"}, + {{2000, 0x0000}, "Boom Jet (Top)"}, + {{2001, 0x0000}, "Free Ranger (Top)"}, + {{2001, 0x2403}, "Legendary Free Ranger (Top)"}, + {{2002, 0x0000}, "Rubble Rouser (Top)"}, + {{2003, 0x0000}, "Doom Stone (Top)"}, + {{2004, 0x0000}, "Blast Zone (Top)"}, + {{2004, 0x2402}, "Dark Blast Zone (Top)"}, + {{2005, 0x0000}, "Fire Kraken (Top)"}, + {{2005, 0x2402}, "Jade Fire Kraken (Top)"}, + {{2006, 0x0000}, "Stink Bomb (Top)"}, + {{2007, 0x0000}, "Grilla Drilla (Top)"}, + {{2008, 0x0000}, "Hoot Loop (Top)"}, + {{2008, 0x2402}, "Enchanted Hoot Loop (Top)"}, + {{2009, 0x0000}, "Trap Shadow (Top)"}, + {{2010, 0x0000}, "Magna Charge (Top)"}, + {{2010, 0x2402}, "Nitro Magna Charge (Top)"}, + {{2011, 0x0000}, "Spy Rise (Top)"}, + {{2012, 0x0000}, "Night Shift (Top)"}, + {{2012, 0x2403}, "Legendary Night Shift (Top)"}, + {{2013, 0x0000}, "Rattle Shake (Top)"}, + {{2013, 0x2402}, "Quick Draw Rattle Shake (Top)"}, + {{2014, 0x0000}, "Freeze Blade (Top)"}, + {{2014, 0x2402}, "Nitro Freeze Blade (Top)"}, + {{2015, 0x0000}, "Wash Buckler (Top)"}, + {{2015, 0x2402}, "Dark Wash Buckler (Top)"}, + {{3000, 0x0000}, "Scratch"}, + {{3001, 0x0000}, "Pop Thorn"}, + {{3002, 0x0000}, "Slobber Tooth"}, + {{3002, 0x2402}, "Dark Slobber Tooth"}, + {{3003, 0x0000}, "Scorp"}, + {{3004, 0x0000}, "Fryno"}, + {{3004, 0x3805}, "Hog Wild Fryno"}, + {{3005, 0x0000}, "Smolderdash"}, + {{3005, 0x2206}, "LightCore Smolderdash"}, + {{3006, 0x0000}, "Bumble Blast"}, + {{3006, 0x2402}, "Jolly Bumble Blast"}, + {{3006, 0x2206}, "LightCore Bumble Blast"}, + {{3007, 0x0000}, "Zoo Lou"}, + {{3007, 0x2403}, "Legendary Zoo Lou"}, + {{3008, 0x0000}, "Dune Bug"}, + {{3009, 0x0000}, "Star Strike"}, + {{3009, 0x2602}, "Enchanted Star Strike"}, + {{3009, 0x2206}, "LightCore Star Strike"}, + {{3010, 0x0000}, "Countdown"}, + {{3010, 0x2402}, "Kickoff Countdown"}, + {{3010, 0x2206}, "LightCore Countdown"}, + {{3011, 0x0000}, "Wind Up"}, + {{3012, 0x0000}, "Roller Brawl"}, + {{3013, 0x0000}, "Grim Creeper"}, + {{3013, 0x2603}, "Legendary Grim Creeper"}, + {{3013, 0x2206}, "LightCore Grim Creeper"}, + {{3014, 0x0000}, "Rip Tide"}, + {{3015, 0x0000}, "Punk Shock"}, + {{3200, 0x0000}, "Battle Hammer"}, + {{3201, 0x0000}, "Sky Diamond"}, + {{3202, 0x0000}, "Platinum Sheep"}, + {{3203, 0x0000}, "Groove Machine"}, + {{3204, 0x0000}, "UFO Hat"}, + {{3300, 0x0000}, "Sheep Wreck Island"}, + {{3301, 0x0000}, "Tower of Time"}, + {{3302, 0x0000}, "Fiery Forge"}, + {{3303, 0x0000}, "Arkeyan Crossbow"}, + {{3220, 0x0000}, "Jet Stream"}, + {{3221, 0x0000}, "Tomb Buggy"}, + {{3222, 0x0000}, "Reef Ripper"}, + {{3223, 0x0000}, "Burn Cycle"}, + {{3224, 0x0000}, "Hot Streak"}, + {{3224, 0x4402}, "Dark Hot Streak"}, + {{3224, 0x4004}, "E3 Hot Streak"}, + {{3224, 0x441E}, "Golden Hot Streak"}, + {{3225, 0x0000}, "Shark Tank"}, + {{3226, 0x0000}, "Thump Truck"}, + {{3227, 0x0000}, "Crypt Crusher"}, + {{3228, 0x0000}, "Stealth Stinger"}, + {{3228, 0x4402}, "Nitro Stealth Stinger"}, + {{3231, 0x0000}, "Dive Bomber"}, + {{3231, 0x4402}, "Spring Ahead Dive Bomber"}, + {{3232, 0x0000}, "Sky Slicer"}, + {{3233, 0x0000}, "Clown Cruiser (Nintendo Only)"}, + {{3233, 0x4402}, "Dark Clown Cruiser (Nintendo Only)"}, + {{3234, 0x0000}, "Gold Rusher"}, + {{3234, 0x4402}, "Power Blue Gold Rusher"}, + {{3235, 0x0000}, "Shield Striker"}, + {{3236, 0x0000}, "Sun Runner"}, + {{3236, 0x4403}, "Legendary Sun Runner"}, + {{3237, 0x0000}, "Sea Shadow"}, + {{3237, 0x4402}, "Dark Sea Shadow"}, + {{3238, 0x0000}, "Splatter Splasher"}, + {{3238, 0x4402}, "Power Blue Splatter Splasher"}, + {{3239, 0x0000}, "Soda Skimmer"}, + {{3240, 0x0000}, "Barrel Blaster (Nintendo Only)"}, + {{3240, 0x4402}, "Dark Barrel Blaster (Nintendo Only)"}, + {{3239, 0x4402}, "Nitro Soda Skimmer"}, + {{3241, 0x0000}, "Buzz Wing"}, + {{3400, 0x0000}, "Fiesta"}, + {{3400, 0x4515}, "Frightful Fiesta"}, + {{3401, 0x0000}, "High Volt"}, + {{3402, 0x0000}, "Splat"}, + {{3402, 0x4502}, "Power Blue Splat"}, + {{3406, 0x0000}, "Stormblade"}, + {{3411, 0x0000}, "Smash Hit"}, + {{3411, 0x4502}, "Steel Plated Smash Hit"}, + {{3412, 0x0000}, "Spitfire"}, + {{3412, 0x4502}, "Dark Spitfire"}, + {{3413, 0x0000}, "Hurricane Jet Vac"}, + {{3413, 0x4503}, "Legendary Hurricane Jet Vac"}, + {{3414, 0x0000}, "Double Dare Trigger Happy"}, + {{3414, 0x4502}, "Power Blue Double Dare Trigger Happy"}, + {{3415, 0x0000}, "Super Shot Stealth Elf"}, + {{3415, 0x4502}, "Dark Super Shot Stealth Elf"}, + {{3416, 0x0000}, "Shark Shooter Terrafin"}, + {{3417, 0x0000}, "Bone Bash Roller Brawl"}, + {{3417, 0x4503}, "Legendary Bone Bash Roller Brawl"}, + {{3420, 0x0000}, "Big Bubble Pop Fizz"}, + {{3420, 0x450E}, "Birthday Bash Big Bubble Pop Fizz"}, + {{3421, 0x0000}, "Lava Lance Eruptor"}, + {{3422, 0x0000}, "Deep Dive Gill Grunt"}, + {{3423, 0x0000}, "Turbo Charge Donkey Kong (Nintendo Only)"}, + {{3423, 0x4502}, "Dark Turbo Charge Donkey Kong (Nintendo Only)"}, + {{3424, 0x0000}, "Hammer Slam Bowser (Nintendo Only)"}, + {{3424, 0x4502}, "Dark Hammer Slam Bowser (Nintendo Only)"}, + {{3425, 0x0000}, "Dive-Clops"}, + {{3425, 0x450E}, "Missile-Tow Dive-Clops"}, + {{3426, 0x0000}, "Astroblast"}, + {{3426, 0x4503}, "Legendary Astroblast"}, + {{3427, 0x0000}, "Nightfall"}, + {{3428, 0x0000}, "Thrillipede"}, + {{3428, 0x450D}, "Eggcited Thrillipede"}, + {{3500, 0x0000}, "Sky Trophy"}, + {{3501, 0x0000}, "Land Trophy"}, + {{3502, 0x0000}, "Sea Trophy"}, + {{3503, 0x0000}, "Kaos Trophy"}, +}; + +u16 skylander_crc16(u16 init_value, const u8* buffer, u32 size) +{ + const unsigned short CRC_CCITT_TABLE[256] = { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, + 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, + 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, + 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, + 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, + 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, + 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, + 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, + 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, + 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, + 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, + 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, + 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, + 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, + 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, + 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, + 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, + 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, + 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, + 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, + 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, + 0x3EB2, 0x0ED1, 0x1EF0}; + + u16 crc = init_value; + + for (u32 i = 0; i < size; i++) + { + const u16 tmp = (crc >> 8) ^ buffer[i]; + crc = (crc << 8) ^ CRC_CCITT_TABLE[tmp]; + } + + return crc; +} + +SkylanderPortalWindow::SkylanderPortalWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(tr("Skylanders Manager")); + setObjectName(QString::fromStdString("skylanders_manager")); + setMinimumSize(QSize(700, 200)); + + CreateMainWindow(); + + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &SkylanderPortalWindow::OnEmulationStateChanged); + + installEventFilter(this); + + OnEmulationStateChanged(Core::GetState()); +}; + +SkylanderPortalWindow::~SkylanderPortalWindow() +{ +} + +void SkylanderPortalWindow::CreateMainWindow() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(); + + QGroupBox* checkbox_group = new QGroupBox(); + QHBoxLayout* checkboxLayout = new QHBoxLayout(); + checkboxLayout->setAlignment(Qt::AlignHCenter); + checkbox = new QCheckBox(QString::fromStdString("Emulate Skylander Portal"), this); + connect(checkbox, &QCheckBox::toggled, [=](bool checked) { EmulatePortal(checked); }); + checkboxLayout->addWidget(checkbox); + checkbox_group->setLayout(checkboxLayout); + mainLayout->addWidget(checkbox_group); + + auto add_line = [](QVBoxLayout* vbox) { + QFrame* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + vbox->addWidget(line); + }; + + group_skylanders = new QGroupBox(tr("Active Portal Skylanders:")); + QVBoxLayout* vbox_group = new QVBoxLayout(); + QScrollArea* scroll_area = new QScrollArea(); + + for (auto i = 0; i < MAX_SKYLANDERS; i++) + { + if (i != 0) + { + add_line(vbox_group); + } + + QHBoxLayout* hbox_skylander = new QHBoxLayout(); + QLabel* label_skyname = new QLabel(QString(tr("Skylander %1")).arg(i + 1)); + edit_skylanders[i] = new QLineEdit(); + edit_skylanders[i]->setEnabled(false); + + QPushButton* clear_btn = new QPushButton(tr("Clear")); + QPushButton* create_btn = new QPushButton(tr("Create")); + QPushButton* load_btn = new QPushButton(tr("Load")); + + connect(clear_btn, &QAbstractButton::clicked, this, [this, i]() { ClearSkylander(i); }); + connect(create_btn, &QAbstractButton::clicked, this, [this, i]() { CreateSkylander(i); }); + connect(load_btn, &QAbstractButton::clicked, this, [this, i]() { LoadSkylander(i); }); + + hbox_skylander->addWidget(label_skyname); + hbox_skylander->addWidget(edit_skylanders[i]); + hbox_skylander->addWidget(clear_btn); + hbox_skylander->addWidget(create_btn); + hbox_skylander->addWidget(load_btn); + + vbox_group->addLayout(hbox_skylander); + } + + group_skylanders->setLayout(vbox_group); + scroll_area->setWidget(group_skylanders); + scroll_area->setWidgetResizable(true); + group_skylanders->setVisible(false); + mainLayout->addWidget(scroll_area); + setLayout(mainLayout); + + UpdateEdits(); +} + +void SkylanderPortalWindow::OnEmulationStateChanged(Core::State state) +{ + const bool running = state != Core::State::Uninitialized; + + checkbox->setEnabled(!running); +} + +CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("Skylander Creator")); + setObjectName(QString::fromStdString("skylanders_creator")); + setMinimumSize(QSize(500, 150)); + QVBoxLayout* layout = new QVBoxLayout; + + QComboBox* combo_skylist = new QComboBox(); + QStringList filterlist; + for (const auto& entry : list_skylanders) + { + const uint qvar = (entry.first.first << 16) | entry.first.second; + combo_skylist->addItem(QString::fromStdString(entry.second), QVariant(qvar)); + filterlist << QString::fromStdString(entry.second.c_str()); + } + combo_skylist->addItem(tr("--Unknown--"), QVariant(0xFFFFFFFF)); + combo_skylist->setEditable(true); + combo_skylist->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_skylist->setCompleter(co_compl); + + layout->addWidget(combo_skylist); + + QFrame* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + layout->addWidget(line); + + QHBoxLayout* hbox_idvar = new QHBoxLayout(); + QLabel* label_id = new QLabel(tr("ID:")); + QLabel* label_var = new QLabel(tr("Variant:")); + QLineEdit* edit_id = new QLineEdit(QString::fromStdString("0")); + QLineEdit* edit_var = new QLineEdit(QString::fromStdString("0")); + QRegularExpressionValidator* rxv = + new QRegularExpressionValidator(QRegularExpression(QString::fromStdString("\\d*")), this); + edit_id->setValidator(rxv); + edit_var->setValidator(rxv); + hbox_idvar->addWidget(label_id); + hbox_idvar->addWidget(edit_id); + hbox_idvar->addWidget(label_var); + hbox_idvar->addWidget(edit_var); + layout->addLayout(hbox_idvar); + + 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); + layout->addLayout(hbox_buttons); + + setLayout(layout); + + connect(combo_skylist, QOverload::of(&QComboBox::currentIndexChanged), [=](int index) { + const u32 sky_info = combo_skylist->itemData(index).toUInt(); + if (sky_info != 0xFFFFFFFF) + { + const u16 sky_id = sky_info >> 16; + const u16 sky_var = sky_info & 0xFFFF; + + edit_id->setText(QString::number(sky_id)); + edit_var->setText(QString::number(sky_var)); + } + }); + + connect(btn_create, &QAbstractButton::clicked, this, [=, this]() { + bool ok_id = false, ok_var = false; + const u16 sky_id = edit_id->text().toUShort(&ok_id); + if (!ok_id) + { + QMessageBox::warning(this, tr("Error converting value"), tr("ID entered is invalid!"), + QMessageBox::Ok); + return; + } + const u16 sky_var = edit_var->text().toUShort(&ok_var); + if (!ok_var) + { + QMessageBox::warning(this, tr("Error converting value"), tr("Variant entered is invalid!"), + QMessageBox::Ok); + return; + } + + QString predef_name = last_skylander_path; + const auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); + if (found_sky != list_skylanders.end()) + { + predef_name += QString::fromStdString(found_sky->second + ".sky"); + } + else + { + QString str = QString::fromStdString("Unknown(%1 %2).sky"); + predef_name += str.arg(sky_id, sky_var); + } + + file_path = QFileDialog::getSaveFileName(this, tr("Create Skylander File"), predef_name, + tr("Skylander Object (*.sky);;")); + if (file_path.isEmpty()) + { + return; + } + + File::IOFile sky_file(file_path.toStdString(), "w+b"); + if (!sky_file) + { + QMessageBox::warning(this, tr("Failed to create skylander file!"), + tr("Failed to create skylander file:\n%1").arg(file_path), + QMessageBox::Ok); + return; + } + + std::array buf{}; + const auto file_data = buf.data(); + // Set the block permissions + u32 first_block = 0x690F0F0F; + u32 other_blocks = 0x69080F7F; + memcpy(&file_data[0x36], &first_block, sizeof(first_block)); + for (u32 index = 1; index < 0x10; index++) + { + memcpy(&file_data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); + } + // Set the skylander info + u16 sky_info = (sky_id | sky_var) + 1; + memcpy(&file_data[0], &sky_info, sizeof(sky_info)); + memcpy(&file_data[0x10], &sky_id, sizeof(sky_id)); + memcpy(&file_data[0x1C], &sky_var, sizeof(sky_var)); + // Set checksum + u16 checksum = skylander_crc16(0xFFFF, file_data, 0x1E); + memcpy(&file_data[0x1E], &checksum, sizeof(checksum)); + + sky_file.WriteBytes(buf.data(), buf.size()); + sky_file.Close(); + + last_skylander_path = QFileInfo(file_path).absolutePath() + QString::fromStdString("/"); + accept(); + }); + + connect(btn_cancel, &QAbstractButton::clicked, this, &QDialog::reject); + + connect(co_compl, QOverload::of(&QCompleter::activated), + [=](const QString& text) { + combo_skylist->setCurrentText(text); + combo_skylist->setCurrentIndex(combo_skylist->findText(text)); + }); +} + +QString CreateSkylanderDialog::GetFilePath() const +{ + return file_path; +} + +void SkylanderPortalWindow::EmulatePortal(bool emulate) +{ + Config::SetBaseOrCurrent(Config::MAIN_EMULATE_SKYLANDER_PORTAL, emulate); + group_skylanders->setVisible(emulate); +} + +void SkylanderPortalWindow::CreateSkylander(u8 slot) +{ + CreateSkylanderDialog create_dlg(this); + if (create_dlg.exec() == CreateSkylanderDialog::Accepted) + { + LoadSkylanderPath(slot, create_dlg.GetFilePath()); + } +} + +void SkylanderPortalWindow::LoadSkylander(u8 slot) +{ + const QString file_path = DolphinFileDialog::getOpenFileName( + this, tr("Select Skylander File"), last_skylander_path, tr("Skylander (*.sky);;")); + if (file_path.isEmpty()) + { + return; + } + last_skylander_path = QFileInfo(file_path).absolutePath() + QString::fromStdString("/"); + + LoadSkylanderPath(slot, file_path); +} + +void SkylanderPortalWindow::LoadSkylanderPath(u8 slot, const QString& path) +{ + File::IOFile sky_file(path.toStdString(), "r+b"); + if (!sky_file) + { + QMessageBox::warning( + this, tr("Failed to open the skylander file!"), + tr("Failed to open the skylander file(%1)!\nFile may already be in use on the portal.") + .arg(path), + QMessageBox::Ok); + return; + } + std::array file_data; + if (!sky_file.ReadBytes(file_data.data(), file_data.size())) + { + QMessageBox::warning( + this, tr("Failed to read the skylander file!"), + tr("Failed to read the skylander file(%1)!\nFile was too small.").arg(path), + QMessageBox::Ok); + return; + } + + ClearSkylander(slot); + + u16 sky_id = file_data[0x11]; + u16 sky_var = file_data[0x1D]; + sky_id <<= 8; + sky_var <<= 8; + sky_id |= file_data[0x10]; + sky_var |= file_data[0x1C]; + + DEBUG_LOG_FMT(IOS_USB, "Sky Id: {}, 0x10: {} 0x11: {}", sky_id, file_data[0x10], file_data[0x11]); + DEBUG_LOG_FMT(IOS_USB, "Sky Var: {}, 0x1D: {} 0x1C: {}", sky_var, file_data[0x1D], + file_data[0x1C]); + + u8 portal_slot = IOS::HLE::USB::g_skyportal.LoadSkylander(file_data.data(), std::move(sky_file)); + sky_slots[slot] = std::tuple(portal_slot, sky_id, sky_var); + + UpdateEdits(); +} + +void SkylanderPortalWindow::ClearSkylander(u8 slot) +{ + if (auto slot_infos = sky_slots[slot]) + { + auto [cur_slot, id, var] = slot_infos.value(); + IOS::HLE::USB::g_skyportal.RemoveSkylander(cur_slot); + sky_slots[slot] = {}; + UpdateEdits(); + } +} + +void SkylanderPortalWindow::UpdateEdits() +{ + for (auto i = 0; i < MAX_SKYLANDERS; i++) + { + QString display_string; + if (auto sd = sky_slots[i]) + { + auto [portal_slot, sky_id, sky_var] = sd.value(); + auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); + if (found_sky != list_skylanders.end()) + { + display_string = QString::fromStdString(found_sky->second); + } + else + { + display_string = + QString(QString::fromStdString("Unknown (Id:%1 Var:%2)")).arg(sky_id).arg(sky_var); + } + } + else + { + display_string = QString::fromStdString("None"); + } + + edit_skylanders[i]->setText(display_string); + } +} + +bool SkylanderPortalWindow::eventFilter(QObject* object, QEvent* event) +{ + // Close when escape is pressed + if (event->type() == QEvent::KeyPress) + { + if (static_cast(event)->matches(QKeySequence::Cancel)) + hide(); + } + + return false; +} + +void SkylanderPortalWindow::closeEvent(QCloseEvent* event) +{ + hide(); +} diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h new file mode 100644 index 0000000000..1a3fda52a8 --- /dev/null +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "Core/Core.h" +#include "Core/IOS/USB/Emulated/Skylander.h" + +class QDialogButtonBox; +class QLabel; +class QPushButton; +class QSpinBox; +class QTabWidget; + +class SkylanderPortalWindow : public QWidget +{ + Q_OBJECT +public: + explicit SkylanderPortalWindow(QWidget* parent = nullptr); + ~SkylanderPortalWindow(); + +protected: + QLineEdit* edit_skylanders[MAX_SKYLANDERS]{}; + static std::optional> sky_slots[MAX_SKYLANDERS]; + +private: + void CreateMainWindow(); + void OnEmulationStateChanged(Core::State state); + void CreateSkylander(u8 slot); + void ClearSkylander(u8 slot); + void EmulatePortal(bool emulate); + void LoadSkylander(u8 slot); + void LoadSkylanderPath(u8 slot, const QString& path); + void UpdateEdits(); + void closeEvent(QCloseEvent* bar) override; + + static SkylanderPortalWindow* inst; + + QCheckBox* checkbox; + QGroupBox* group_skylanders; + + bool eventFilter(QObject* object, QEvent* event) final override; +}; + +class CreateSkylanderDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateSkylanderDialog(QWidget* parent); + QString GetFilePath() const; + +protected: + QString file_path; +}; From 18fd0d7dcdfee9929d659ac6577a644a3691bd7e Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Thu, 12 Jan 2023 14:21:29 +1300 Subject: [PATCH 2/3] Use Core timing instead of thread Skylander code tidy ups Convert c array to std::array and fix comments Formatting fixes/review changes Variable comment Migrate portal to System Impl and code tidy ups Use struct Restore review changes Minor fix to schedule transfer method Change descriptors to hex and fix comments --- Source/Core/Core/CMakeLists.txt | 2 - Source/Core/Core/Config/MainSettings.cpp | 5 - Source/Core/Core/Config/MainSettings.h | 1 - .../Core/ConfigLoaders/IsSettingSaveable.cpp | 2 +- Source/Core/Core/IOS/USB/Common.cpp | 7 + Source/Core/Core/IOS/USB/Common.h | 5 +- .../Core/Core/IOS/USB/Emulated/Skylander.cpp | 427 +++++++++++------- Source/Core/Core/IOS/USB/Emulated/Skylander.h | 54 ++- .../Core/Core/IOS/USB/EmulatedUSBDevice.cpp | 82 ---- Source/Core/Core/IOS/USB/EmulatedUSBDevice.h | 43 -- Source/Core/Core/IOS/USB/Host.cpp | 100 ++-- Source/Core/Core/IOS/USB/Host.h | 3 + Source/Core/Core/System.cpp | 7 + Source/Core/Core/System.h | 5 + Source/Core/DolphinLib.props | 2 - Source/Core/DolphinQt/MenuBar.cpp | 2 +- .../SkylanderPortal/SkylanderPortalWindow.cpp | 241 ++++------ .../SkylanderPortal/SkylanderPortalWindow.h | 40 +- 18 files changed, 487 insertions(+), 541 deletions(-) delete mode 100644 Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp delete mode 100644 Source/Core/Core/IOS/USB/EmulatedUSBDevice.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 16bc0b1b0b..682c686c7d 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -399,8 +399,6 @@ add_library(core IOS/USB/Bluetooth/WiimoteHIDAttr.h IOS/USB/Common.cpp IOS/USB/Common.h - IOS/USB/EmulatedUSBDevice.cpp - IOS/USB/EmulatedUSBDevice.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 09cfcabb55..552763c8d1 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -556,11 +556,6 @@ void SetUSBDeviceWhitelist(const std::set>& devices) const Info MAIN_EMULATE_SKYLANDER_PORTAL{ {System::Main, "EmulatedUSBDevices", "EmulateSkylanderPortal"}, false}; -bool EmulateSkylanderPortal() -{ - return Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL); -} - // 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 e2a4549e49..e47cb910ce 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -345,7 +345,6 @@ void SetUSBDeviceWhitelist(const std::set>& devices); // Main.EmulatedUSBDevices extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; -bool EmulateSkylanderPortal(); // GameCube path utility functions diff --git a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp index 86c0ffc5fb..69684786a7 100644 --- a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp +++ b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp @@ -29,7 +29,7 @@ bool IsSettingSaveable(const Config::Location& config_location) for (const std::string_view section : {"NetPlay", "General", "GBA", "Display", "Network", "Analytics", "AndroidOverlayButtons", "DSP", "GameList", "FifoPlayer", "AutoUpdate", "Movie", "Input", "Debug", - "BluetoothPassthrough", "USBPassthrough", "Interface"}) + "BluetoothPassthrough", "USBPassthrough", "Interface", "EmulatedUSBDevices"}) { if (config_location.section == section) return true; diff --git a/Source/Core/Core/IOS/USB/Common.cpp b/Source/Core/Core/IOS/USB/Common.cpp index 2c77371590..fda8e37129 100644 --- a/Source/Core/Core/IOS/USB/Common.cpp +++ b/Source/Core/Core/IOS/USB/Common.cpp @@ -38,6 +38,13 @@ void TransferCommand::OnTransferComplete(s32 return_value) const m_ios.EnqueueIPCReply(ios_request, return_value, 0, CoreTiming::FromThread::NON_CPU); } +void TransferCommand::ScheduleTransferCompletion(s32 return_value, u32 expected_time_us) const +{ + auto ticks = SystemTimers::GetTicksPerSecond(); + s64 cycles_in_future = static_cast((static_cast(ticks) * expected_time_us) / 1'000'000); + m_ios.EnqueueIPCReply(ios_request, return_value, cycles_in_future, CoreTiming::FromThread::ANY); +} + void IsoMessage::SetPacketReturnValue(const size_t packet_num, const u16 return_value) const { auto& system = Core::System::GetInstance(); diff --git a/Source/Core/Core/IOS/USB/Common.h b/Source/Core/Core/IOS/USB/Common.h index eb506dd64f..edf9a724e1 100644 --- a/Source/Core/Core/IOS/USB/Common.h +++ b/Source/Core/Core/IOS/USB/Common.h @@ -109,13 +109,10 @@ struct TransferCommand // Called after a transfer has completed to reply to the IPC request. // This can be overridden for additional processing before replying. virtual void OnTransferComplete(s32 return_value) const; + void ScheduleTransferCompletion(s32 return_value, u32 expected_time_us) const; std::unique_ptr MakeBuffer(size_t size) const; void FillBuffer(const u8* src, size_t size) const; - // Fake Transfers - u64 expected_time; - u32 expected_count; - private: Kernel& m_ios; }; diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp index 9a09299607..60c7302563 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Dolphin Emulator Project +// Copyright 2022 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Core/IOS/USB/Emulated/Skylander.h" @@ -16,44 +16,41 @@ namespace IOS::HLE::USB { -SkylanderPortal g_skyportal; - -SkylanderUSB::SkylanderUSB(Kernel& ios, const std::string& device_name) - : EmulatedUSBDevice(ios, device_name) +SkylanderUSB::SkylanderUSB(Kernel& ios, const std::string& device_name) : m_ios(ios) { m_vid = 0x1430; m_pid = 0x150; m_id = (static_cast(m_vid) << 32 | static_cast(m_pid) << 16 | static_cast(9) << 8 | static_cast(1)); - deviceDesc = DeviceDescriptor{18, 1, 512, 0, 0, 0, 64, 5168, 336, 256, 1, 2, 0, 1}; - configDesc.emplace_back(ConfigDescriptor{9, 2, 41, 1, 1, 0, 128, 250}); - interfaceDesc.emplace_back(InterfaceDescriptor{9, 4, 0, 0, 2, 3, 0, 0, 0}); - endpointDesc.emplace_back(EndpointDescriptor{7, 5, 129, 3, 64, 1}); - endpointDesc.emplace_back(EndpointDescriptor{7, 5, 2, 3, 64, 1}); + m_device_descriptor = DeviceDescriptor{0x12, 0x1, 0x200, 0x0, 0x0, 0x0, 0x40, + 0x1430, 0x150, 0x100, 0x1, 0x2, 0x0, 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, 0x40, 0x1}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x2, 0x3, 0x40, 0x1}); } -SkylanderUSB::~SkylanderUSB() -{ -} +SkylanderUSB::~SkylanderUSB() = default; DeviceDescriptor SkylanderUSB::GetDeviceDescriptor() const { - return deviceDesc; + return m_device_descriptor; } std::vector SkylanderUSB::GetConfigurations() const { - return configDesc; + return m_config_descriptor; } std::vector SkylanderUSB::GetInterfaces(u8 config) const { - return interfaceDesc; + return m_interface_descriptor; } std::vector SkylanderUSB::GetEndpoints(u8 config, u8 interface, u8 alt) const { - return endpointDesc; + return m_endpoint_descriptor; } bool SkylanderUSB::Attach() @@ -64,11 +61,6 @@ bool SkylanderUSB::Attach() } DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); m_device_attached = true; - if (!m_has_initialised && !Core::WantsDeterminism()) - { - GetTransferThread().Start(); - m_has_initialised = true; - } return true; } @@ -81,11 +73,7 @@ int SkylanderUSB::CancelTransfer(const u8 endpoint) { INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, m_active_interface, endpoint); - if (GetTransferThread().GetTransfers()) - { - return IPC_ENOENT; - } - GetTransferThread().ClearTransfers(); + return IPC_SUCCESS; } @@ -121,17 +109,23 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); - cmd->expected_time = Common::Timer::NowUs() + 100; auto& system = Core::System::GetInstance(); auto& memory = system.GetMemory(); u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); - std::array q_result = {}; - std::array q_data = {}; - // Control transfers are instantaneous - switch (cmd->request_type) + if ((cmd->length == 0 || buf == nullptr) && cmd->request == 0x09) { - // HID host to device type - case 0x21: + ERROR_LOG_FMT(IOS_USB, "Skylander command invalid"); + return IPC_EINVAL; + } + std::array result = {}; + std::array data = {}; + s32 expected_count = 0; + u64 expected_time_us = 100; + // Control transfers are instantaneous + u8 request_type = cmd->request_type; + if (request_type == 0x21) + { + // HID host to device type switch (cmd->request) { case 0x09: @@ -145,7 +139,7 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // Response { 'A', (00 | 01), // ff, 77, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, // 00, 00, 00, 00, 00, 00, 00, 00 } - // The 3rd byte of the command is whether to activate (0x01) or deactivate (0x00) the + // The 2nd byte of the command is whether to activate (0x01) or deactivate (0x00) the // portal. The response echos back the activation byte as the 2nd byte of the response. The // 3rd and 4th bytes of the response appear to vary from wired to wireless. On wired // portals, the bytes appear to always be ff 77. On wireless portals, during activation the @@ -154,15 +148,15 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // for wireless portals. // Wii U Wireless: 41 01 f4 00 41 00 ed 00 41 01 f4 00 41 00 eb 00 41 01 f3 00 41 00 ed 00 - if (cmd->length == 2 || cmd->length == 32) + if (cmd->length == 2) { - q_data = {buf[0], buf[1]}; - q_result = {0x41, buf[1], 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - q_queries.push(q_result); - cmd->expected_count = 10; - g_skyportal.Activate(); + data = {buf[0], buf[1]}; + result = {0x41, buf[1], 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + m_queries.push(result); + expected_count = 10; + system.GetSkylanderPortal().Activate(); } break; } @@ -179,39 +173,39 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // This command should set the color of the LED in the portal, however this appears // deprecated in most of the recent portals. On portals that do not have LEDs, this command // is silently ignored and do not require a response. - if (cmd->length == 4 || cmd->length == 32) + if (cmd->length == 4) { - g_skyportal.SetLEDs(0x01, buf[1], buf[2], buf[3]); - q_data = {0x43, buf[1], buf[2], buf[3]}; - cmd->expected_count = 12; + system.GetSkylanderPortal().SetLEDs(0x01, buf[1], buf[2], buf[3]); + data = {0x43, buf[1], buf[2], buf[3]}; + expected_count = 12; } break; } case 'J': { // Sided color - // buf[1] is the side + // The 2nd byte is the side // 0x00: right // 0x01: left and right // 0x02: left - // buf[2], buf[3] and buf[4] are red, green and blue + // The 3rd, 4th and 5th bytes are red, green and blue - // buf[5] is unknown. Observed values are 0x00, 0x0D and 0xF4 + // The 6th byte is unknown. Observed values are 0x00, 0x0D and 0xF4 - // buf[6] is the fade duration. Exact value-time corrolation unknown. Observed values are - // 0x00, 0x01 and 0x07. Custom commands show that the higher this value the longer the + // The 7th byte is the fade duration. Exact value-time corrolation unknown. Observed values + // are 0x00, 0x01 and 0x07. Custom commands show that the higher this value the longer the // duration. // Empty J response is send after the fade is completed. Immeditately sending it is fine // as long as we don't show the fade happening if (cmd->length == 7) { - q_data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]}; - cmd->expected_count = 15; - q_result = {buf[0]}; - q_queries.push(q_result); - g_skyportal.SetLEDs(buf[1], buf[2], buf[3], buf[4]); + data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]}; + expected_count = 15; + result = {buf[0]}; + m_queries.push(result); + system.GetSkylanderPortal().SetLEDs(buf[1], buf[2], buf[3], buf[4]); } break; } @@ -220,40 +214,38 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // Light // This command is used while playing audio through the portal - // buf[1] is the position + // The 2nd bytes is the position // 0x00: right // 0x01: trap led // 0x02: left - // buf[2], buf[3] and buf[4] are red, green and blue + // The 3rd, 4th and 5th bytes are red, green and blue // the trap led is white-only - // increasing or decreasing the values results in a birghter or dimmer light - - // buf[5] is unknown. - // A range of values have been observed + // increasing or decreasing the values results in a brighter or dimmer light if (cmd->length == 5) { - q_data = {buf[0], buf[1], buf[2], buf[3], buf[4]}; - cmd->expected_count = 13; + data = {buf[0], buf[1], buf[2], buf[3], buf[4]}; + expected_count = 13; u8 side = buf[1]; if (side == 0x02) { side = 0x04; } - g_skyportal.SetLEDs(side, buf[2], buf[3], buf[4]); + system.GetSkylanderPortal().SetLEDs(side, buf[2], buf[3], buf[4]); } break; } case 'M': { // Audio Firmware version + // Respond with version obtained from Trap Team wired portal if (cmd->length == 2) { - q_data = {buf[0], buf[1]}; - cmd->expected_count = 10; - q_result = {buf[0], buf[1], 0x00, 0x19}; - q_queries.push(q_result); + data = {buf[0], buf[1]}; + expected_count = 10; + result = {buf[0], buf[1], 0x00, 0x19}; + m_queries.push(result); } break; } @@ -263,25 +255,28 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // Response { 'Q', 10, 00, 00, 00, 00, // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, // 00, 00, 00, 00 } - // In the command the 3rd byte indicates which Skylander to query data + // In the command the 2nd byte indicates which Skylander to query data // from. Index starts at 0x10 for the 1st Skylander (as reported in the Status command.) The - // 16th Skylander indexed would be 0x20. + // 16th Skylander indexed would be 0x20. The 3rd byte indicate which block to read from. // A response with the 2nd byte of 0x01 indicates an error in the read. Otherwise, the // response indicates the Skylander's index in the 2nd byte, the block read in the 3rd byte, - // data (16 bytes) is contained in bytes 4-20. + // data (16 bytes) is contained in bytes 4-19. // A Skylander has 64 blocks of data indexed from 0x00 to 0x3f. SwapForce characters have 2 // character indexes, these may not be sequential. case 'Q': { // Queries a block - const u8 sky_num = buf[1] & 0xF; - const u8 block = buf[2]; - g_skyportal.QueryBlock(sky_num, block, q_result.data()); - q_queries.push(q_result); - q_data = {buf[0], buf[1], buf[2]}; - cmd->expected_count = 11; + if (cmd->length == 3) + { + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + system.GetSkylanderPortal().QueryBlock(sky_num, block, result.data()); + m_queries.push(result); + data = {buf[0], buf[1], buf[2]}; + expected_count = 11; + } break; } case 'R': @@ -294,14 +289,14 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // 00, 00, 00, 00 } // The 4 byte sequence after the R (0x52) is unknown, but appears consistent based on device // type. - if (cmd->length == 2 || cmd->length == 32) + if (cmd->length == 2) { - q_data = {0x52, 0x00}; - q_result = {0x52, 0x02, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - q_queries.push(q_result); - cmd->expected_count = 10; + data = {0x52, 0x00}; + result = {0x52, 0x02, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + m_queries.push(result); + expected_count = 10; } break; } @@ -336,14 +331,20 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // been activated: {01} when active and {00} when deactivated. case 'S': { - q_data = {buf[0]}; - cmd->expected_count = 9; + if (cmd->length == 1) + { + data = {buf[0]}; + expected_count = 9; + } break; } case 'V': { - q_data = {buf[0], buf[1], buf[2], buf[3]}; - cmd->expected_count = 12; + if (cmd->length == 4) + { + data = {buf[0], buf[1], buf[2], buf[3]}; + expected_count = 12; + } break; } // Write @@ -352,13 +353,13 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // Response { 'W', 00, 00, 00, 00, 00, // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, // 00, 00, 00, 00 } - // In the command the 3rd byte indicates which Skylander to query data from. Index starts at + // In the command the 2nd byte indicates which Skylander to query data from. Index starts at // 0x10 for the 1st Skylander (as reported in the Status command.) The 16th Skylander // indexed would be 0x20. - // 4th byte is the block to write to. + // 3rd byte is the block to write to. - // Bytes 5 - 20 ({ 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f }) are the + // Bytes 4 - 19 ({ 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f }) are the // data to write. // The response does not appear to return the id of the Skylander being written, the 2nd @@ -367,14 +368,17 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) case 'W': { - const u8 sky_num = buf[1] & 0xF; - const u8 block = buf[2]; - g_skyportal.WriteBlock(sky_num, block, &buf[3], q_result.data()); - q_queries.push(q_result); - q_data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], + if (cmd->length == 19) + { + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + system.GetSkylanderPortal().WriteBlock(sky_num, block, &buf[3], result.data()); + m_queries.push(result); + data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18]}; - cmd->expected_count = 19; + expected_count = 27; + } break; } default: @@ -383,24 +387,22 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) } break; case 0x0A: - cmd->expected_count = 8; + expected_count = 8; break; case 0x0B: - cmd->expected_count = 8; + expected_count = 8; break; default: ERROR_LOG_FMT(IOS_USB, "Unhandled Request {}", cmd->request); break; } - break; - - default: - break; } - cmd->expected_time = Common::Timer::NowUs() + 100; - GetTransferThread().AddTransfer(std::move(cmd), q_data); + if (expected_count == 0) + return IPC_EINVAL; + ScheduleTransfer(std::move(cmd), data, expected_count, expected_time_us); return 0; } + int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) { DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Bulk: length={} endpoint={}", m_vid, m_pid, @@ -420,38 +422,46 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) auto& system = Core::System::GetInstance(); auto& memory = system.GetMemory(); u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); - std::array q_result = {}; + if (cmd->length == 0 || buf == nullptr) + { + ERROR_LOG_FMT(IOS_USB, "Skylander command invalid"); + return IPC_EINVAL; + } + std::array result = {}; + s32 expected_count; + u64 expected_time_us; // Audio requests are 64 bytes long, are the only Interrupt requests longer than 32 bytes, // echo the request as the response and respond after 1ms - if (cmd->length > 32) + if (cmd->length > 32 && cmd->length <= 64) { - std::array q_audio_result = {}; - u8* audio_buf = q_audio_result.data(); + std::array audio_result = {}; + u8* audio_buf = audio_result.data(); memcpy(audio_buf, buf, cmd->length); - cmd->expected_time = Common::Timer::NowUs() + 1000; - cmd->expected_count = cmd->length; - GetTransferThread().AddTransfer(std::move(cmd), q_audio_result); + expected_time_us = 1000; + expected_count = cmd->length; + ScheduleTransfer(std::move(cmd), audio_result, expected_count, expected_time_us); return 0; } // If some data was requested from the Control Message, then the Interrupt message needs to // respond with that data. Check if the queries queue is empty - if (!q_queries.empty()) + if (!m_queries.empty()) { - q_result = q_queries.front(); - q_queries.pop(); + result = m_queries.front(); + m_queries.pop(); // This needs to happen after ~22 milliseconds - cmd->expected_time = Common::Timer::NowUs() + 22000; + expected_time_us = 22000; } // If there is no relevant data to respond with, respond with the currentstatus of the Portal else { - q_result = g_skyportal.GetStatus(); - cmd->expected_time = Common::Timer::NowUs() + 2000; + result = system.GetSkylanderPortal().GetStatus(); + expected_time_us = 2000; } - cmd->expected_count = 32; - GetTransferThread().AddTransfer(std::move(cmd), q_result); + expected_count = 32; + ScheduleTransfer(std::move(cmd), result, expected_count, expected_time_us); return 0; } + int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) { DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Isochronous: length={} endpoint={} num_packets={}", @@ -459,23 +469,27 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) return 0; } -void Skylander::save() +void SkylanderUSB::ScheduleTransfer(std::unique_ptr command, + const std::array& data, s32 expected_count, + u64 expected_time_us) +{ + command->FillBuffer(data.data(), expected_count); + command->ScheduleTransferCompletion(expected_count, expected_time_us); +} + +void Skylander::Save() { if (!sky_file) - { return; - } - { - sky_file.Seek(0, File::SeekOrigin::Begin); - sky_file.WriteBytes(data.data(), 0x40 * 0x10); - } + sky_file.Seek(0, File::SeekOrigin::Begin); + sky_file.WriteBytes(data.data(), 0x40 * 0x10); } void SkylanderPortal::Activate() { std::lock_guard lock(sky_mutex); - if (activated) + if (m_activated) { // If the portal was already active no change is needed return; @@ -486,12 +500,12 @@ void SkylanderPortal::Activate() { if (s.status & 1) { - s.queued_status.push(3); - s.queued_status.push(1); + s.queued_status.push(Skylander::ADDED); + s.queued_status.push(Skylander::READY); } } - activated = true; + m_activated = true; } void SkylanderPortal::Deactivate() @@ -510,32 +524,32 @@ void SkylanderPortal::Deactivate() s.status &= 1; } - activated = false; + m_activated = false; } bool SkylanderPortal::IsActivated() { std::lock_guard lock(sky_mutex); - return activated; + return m_activated; } void SkylanderPortal::UpdateStatus() { std::lock_guard lock(sky_mutex); - if (!status_updated) + if (!m_status_updated) { for (auto& s : skylanders) { if (s.status & 1) { - s.queued_status.push(0); - s.queued_status.push(3); - s.queued_status.push(1); + s.queued_status.push(Skylander::REMOVED); + s.queued_status.push(Skylander::ADDED); + s.queued_status.push(Skylander::READY); } } - status_updated = true; + m_status_updated = true; } } @@ -549,31 +563,31 @@ void SkylanderPortal::SetLEDs(u8 side, u8 red, u8 green, u8 blue) std::lock_guard lock(sky_mutex); if (side == 0x00) { - this->color_right.r = red; - this->color_right.g = green; - this->color_right.b = blue; + m_color_right.red = red; + m_color_right.green = green; + m_color_right.blue = blue; } else if (side == 0x01) { - this->color_right.r = red; - this->color_right.g = green; - this->color_right.b = blue; + m_color_right.red = red; + m_color_right.green = green; + m_color_right.blue = blue; - this->color_left.r = red; - this->color_left.g = green; - this->color_left.b = blue; + m_color_left.red = red; + m_color_left.green = green; + m_color_left.blue = blue; } else if (side == 0x02) { - this->color_left.r = red; - this->color_left.g = green; - this->color_left.b = blue; + m_color_left.red = red; + m_color_left.green = green; + m_color_left.blue = blue; } else if (side == 0x03) { - this->color_trap.r = red; - this->color_trap.g = green; - this->color_trap.b = blue; + m_color_trap.red = red; + m_color_trap.green = green; + m_color_trap.blue = blue; } } @@ -584,7 +598,7 @@ std::array SkylanderPortal::GetStatus() u32 status = 0; u8 active = 0x00; - if (activated) + if (m_activated) { active = 0x01; } @@ -602,14 +616,14 @@ std::array SkylanderPortal::GetStatus() status |= s.status; } - std::array q_result = {0x53, 0x00, 0x00, 0x00, 0x00, interrupt_counter++, - active, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00}; - memcpy(&q_result.data()[1], &status, sizeof(status)); - return q_result; + std::array result = {0x53, 0x00, 0x00, 0x00, 0x00, m_interrupt_counter++, + active, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + memcpy(&result[1], &status, sizeof(status)); + return result; } void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) @@ -644,7 +658,7 @@ void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u { reply_buf[1] = (0x10 | sky_num); memcpy(skylander.data.data() + (block * 16), to_write_buf, 16); - skylander.save(); + skylander.Save(); } else { @@ -652,6 +666,76 @@ void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u } } +u16 SkylanderCRC16(u16 init_value, const u8* buffer, u32 size) +{ + const unsigned short CRC_CCITT_TABLE[256] = { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, + 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, + 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, + 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, + 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, + 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, + 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, + 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, + 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, + 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, + 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, + 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, + 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, + 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, + 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, + 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, + 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, + 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, + 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, + 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, + 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, + 0x3EB2, 0x0ED1, 0x1EF0}; + + u16 crc = init_value; + + for (u32 i = 0; i < size; i++) + { + const u16 tmp = (crc >> 8) ^ buffer[i]; + crc = (crc << 8) ^ CRC_CCITT_TABLE[tmp]; + } + + return crc; +} + +bool SkylanderPortal::CreateSkylander(const std::string& file_path, u16 sky_id, u16 sky_var) +{ + File::IOFile sky_file(file_path, "w+b"); + if (!sky_file) + { + return false; + } + + std::array buf{}; + const auto file_data = buf.data(); + // Set the block permissions + u32 first_block = 0x690F0F0F; + u32 other_blocks = 0x69080F7F; + memcpy(&file_data[0x36], &first_block, sizeof(first_block)); + for (u32 index = 1; index < 0x10; index++) + { + memcpy(&file_data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); + } + // Set the skylander info + u16 sky_info = (sky_id | sky_var) + 1; + memcpy(&file_data[0], &sky_info, sizeof(sky_info)); + memcpy(&file_data[0x10], &sky_id, sizeof(sky_id)); + memcpy(&file_data[0x1C], &sky_var, sizeof(sky_var)); + // Set checksum + u16 checksum = SkylanderCRC16(0xFFFF, file_data, 0x1E); + memcpy(&file_data[0x1E], &checksum, sizeof(checksum)); + + sky_file.WriteBytes(buf.data(), buf.size()); + return true; +} + bool SkylanderPortal::RemoveSkylander(u8 sky_num) { DEBUG_LOG_FMT(IOS_USB, "Cleared Skylander from slot {}", sky_num); @@ -660,10 +744,9 @@ bool SkylanderPortal::RemoveSkylander(u8 sky_num) if (skylander.status & 1) { - skylander.status = 2; - skylander.queued_status.push(2); - skylander.queued_status.push(0); - skylander.sky_file.Close(); + skylander.status = Skylander::REMOVING; + skylander.queued_status.push(Skylander::REMOVING); + skylander.queued_status.push(Skylander::REMOVED); return true; } @@ -710,9 +793,9 @@ u8 SkylanderPortal::LoadSkylander(u8* buf, File::IOFile in_file) DEBUG_LOG_FMT(IOS_USB, "Skylander Data: \n{}", HexDump(skylander.data.data(), skylander.data.size())); skylander.sky_file = std::move(in_file); - skylander.status = 3; - skylander.queued_status.push(3); - skylander.queued_status.push(1); + skylander.status = Skylander::ADDED; + skylander.queued_status.push(Skylander::ADDED); + skylander.queued_status.push(Skylander::READY); skylander.last_id = sky_serial; } return found_slot; diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.h b/Source/Core/Core/IOS/USB/Emulated/Skylander.h index 3e16d25a95..cfd862f881 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylander.h +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.h @@ -8,8 +8,9 @@ #include #include +#include "Common/CommonTypes.h" #include "Common/IOFile.h" -#include "Core/IOS/USB/EmulatedUSBDevice.h" +#include "Core/IOS/USB/Common.h" // The maximum possible characters the portal can handle. // The status array is 32 bits and every character takes 2 bits. @@ -18,7 +19,7 @@ constexpr u8 MAX_SKYLANDERS = 16; namespace IOS::HLE::USB { -class SkylanderUSB final : public EmulatedUSBDevice +class SkylanderUSB final : public Device { public: SkylanderUSB(Kernel& ios, const std::string& device_name); @@ -37,20 +38,20 @@ public: int SubmitTransfer(std::unique_ptr message) override; int SubmitTransfer(std::unique_ptr message) override; int SubmitTransfer(std::unique_ptr message) override; - -protected: - std::queue> q_queries; + void ScheduleTransfer(std::unique_ptr command, const std::array& data, + s32 expected_count, u64 expected_time_us); private: + Kernel& m_ios; u16 m_vid = 0; u16 m_pid = 0; u8 m_active_interface = 0; bool m_device_attached = false; - DeviceDescriptor deviceDesc; - std::vector configDesc; - std::vector interfaceDesc; - std::vector endpointDesc; - bool m_has_initialised = false; + DeviceDescriptor m_device_descriptor; + std::vector m_config_descriptor; + std::vector m_interface_descriptor; + std::vector m_endpoint_descriptor; + std::queue> m_queries; }; struct Skylander final @@ -60,12 +61,22 @@ struct Skylander final std::queue queued_status; std::array data{}; u32 last_id = 0; - void save(); + void Save(); + + enum : u8 + { + REMOVED = 0, + READY = 1, + REMOVING = 2, + ADDED = 3 + }; }; -struct LedColor final +struct SkylanderLEDColor final { - u8 r = 0, g = 0, b = 0; + u8 red = 0; + u8 green = 0; + u8 blue = 0; }; class SkylanderPortal final @@ -81,22 +92,21 @@ public: void QueryBlock(u8 sky_num, u8 block, u8* reply_buf); void WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf); + bool CreateSkylander(const std::string& file_path, u16 sky_id, u16 sky_var); bool RemoveSkylander(u8 sky_num); u8 LoadSkylander(u8* buf, File::IOFile in_file); protected: std::mutex sky_mutex; - bool activated = true; - bool status_updated = false; - u8 interrupt_counter = 0; - LedColor color_right = {}; - LedColor color_left = {}; - LedColor color_trap = {}; + bool m_activated = true; + bool m_status_updated = false; + u8 m_interrupt_counter = 0; + SkylanderLEDColor m_color_right = {}; + SkylanderLEDColor m_color_left = {}; + SkylanderLEDColor m_color_trap = {}; - Skylander skylanders[MAX_SKYLANDERS]; + std::array skylanders; }; -extern SkylanderPortal g_skyportal; - } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp deleted file mode 100644 index 8f271745d0..0000000000 --- a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "Core/IOS/USB/EmulatedUSBDevice.h" - -#include - -#include "Common/Thread.h" -#include "Common/Timer.h" -#include "Core/Core.h" - -namespace IOS::HLE::USB -{ -EmulatedUSBDevice::EmulatedUSBDevice(Kernel& ios, const std::string& device_name) : m_ios(ios) -{ -} - -EmulatedUSBDevice::~EmulatedUSBDevice() -{ -} - -EmulatedUSBDevice::FakeTransferThread::~FakeTransferThread() -{ - Stop(); -} - -void EmulatedUSBDevice::FakeTransferThread::Start() -{ - if (Core::WantsDeterminism()) - return; - - if (m_thread_running.TestAndSet()) - { - m_thread = std::thread([this] { - Common::SetCurrentThreadName("Fake Transfer Thread"); - while (m_thread_running.IsSet()) - { - if (!m_transfers.empty()) - { - std::lock_guard lk{m_transfers_mutex}; - u64 timestamp = Common::Timer::NowUs(); - for (auto iterator = m_transfers.begin(); iterator != m_transfers.end();) - { - auto* command = iterator->second.get(); - if (command->expected_time > timestamp) - { - ++iterator; - continue; - } - command->FillBuffer(iterator->first.data(), command->expected_count); - command->OnTransferComplete(command->expected_count); - iterator = m_transfers.erase(iterator); - } - } - } - }); - } -} - -void EmulatedUSBDevice::FakeTransferThread::Stop() -{ - if (m_thread_running.TestAndClear()) - m_thread.join(); -} - -void EmulatedUSBDevice::FakeTransferThread::AddTransfer(std::unique_ptr command, - std::array data) -{ - std::lock_guard lk{m_transfers_mutex}; - m_transfers.emplace(data, std::move(command)); -} - -void EmulatedUSBDevice::FakeTransferThread::ClearTransfers() -{ - std::lock_guard lk{m_transfers_mutex}; - m_transfers.clear(); -} - -bool EmulatedUSBDevice::FakeTransferThread::GetTransfers() -{ - std::lock_guard lk{m_transfers_mutex}; - return m_transfers.empty(); -} - -} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h b/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h deleted file mode 100644 index 60e469f7ce..0000000000 --- a/Source/Core/Core/IOS/USB/EmulatedUSBDevice.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -#include "Common/CommonTypes.h" -#include "Core/IOS/USB/Common.h" - -namespace IOS::HLE::USB -{ -class EmulatedUSBDevice : public Device -{ -public: - EmulatedUSBDevice(Kernel& ios, const std::string& device_name); - virtual ~EmulatedUSBDevice(); - -protected: - class FakeTransferThread final - { - public: - explicit FakeTransferThread(EmulatedUSBDevice* device) : m_device(device) {} - ~FakeTransferThread(); - void Start(); - void Stop(); - void AddTransfer(std::unique_ptr command, std::array data); - void ClearTransfers(); - bool GetTransfers(); - - private: - EmulatedUSBDevice* m_device = nullptr; - Common::Flag m_thread_running; - std::thread m_thread; - Common::Flag m_is_initialized; - std::map, std::unique_ptr> m_transfers; - std::mutex m_transfers_mutex; - }; - FakeTransferThread m_transfer_thread{this}; - FakeTransferThread& GetTransferThread() { return m_transfer_thread; } - -private: - Kernel& m_ios; -}; -} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index a7378efcc2..8870f730b0 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -24,6 +24,8 @@ #include "Core/IOS/USB/Common.h" #include "Core/IOS/USB/Emulated/Skylander.h" #include "Core/IOS/USB/LibusbDevice.h" +#include "Core/NetPlayProto.h" +#include "Core/System.h" namespace IOS::HLE { @@ -35,7 +37,7 @@ USBHost::~USBHost() = default; std::optional USBHost::Open(const OpenRequest& request) { - if (!m_has_initialised && !Core::WantsDeterminism()) + if (!m_has_initialised) { GetScanThread().Start(); // Force a device scan to complete, because some games (including Your Shape) only care @@ -97,12 +99,15 @@ bool USBHost::ShouldAddDevice(const USB::Device& device) const return true; } +void USBHost::Update() +{ + if (Core::WantsDeterminism()) + UpdateDevices(); +} + // This is called from the scan thread. Returns false if we failed to update the device list. bool USBHost::UpdateDevices(const bool always_add_hooks) { - if (Core::WantsDeterminism()) - return true; - DeviceChangeHooks hooks; std::set plugged_devices; // If we failed to get a new, up-to-date list of devices, we cannot detect device removals. @@ -116,43 +121,35 @@ bool USBHost::UpdateDevices(const bool always_add_hooks) bool USBHost::AddNewDevices(std::set& new_devices, DeviceChangeHooks& hooks, const bool always_add_hooks) { - if (Config::EmulateSkylanderPortal()) - { - auto skylanderportal = std::make_unique(m_ios, "Skylander Portal"); - if (!ShouldAddDevice(*skylanderportal)) - return true; - const u64 skyid = skylanderportal->GetId(); - new_devices.insert(skyid); - if (AddDevice(std::move(skylanderportal))) - { - hooks.emplace(GetDeviceById(skyid), ChangeEvent::Inserted); - } - } + AddEmulatedDevices(new_devices, hooks, always_add_hooks); #ifdef __LIBUSB__ - auto whitelist = Config::GetUSBDeviceWhitelist(); - if (whitelist.empty()) - return true; - - if (m_context.IsValid()) + if (!Core::WantsDeterminism()) { - const int ret = m_context.GetDeviceList([&](libusb_device* device) { - libusb_device_descriptor descriptor; - libusb_get_device_descriptor(device, &descriptor); - if (whitelist.count({descriptor.idVendor, descriptor.idProduct}) == 0) - return true; - - auto usb_device = std::make_unique(m_ios, 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); + auto whitelist = Config::GetUSBDeviceWhitelist(); + if (whitelist.empty()) return true; - }); - if (ret != LIBUSB_SUCCESS) - WARN_LOG_FMT(IOS_USB, "GetDeviceList failed: {}", LibusbUtils::ErrorWrap(ret)); + + if (m_context.IsValid()) + { + const int ret = m_context.GetDeviceList([&](libusb_device* device) { + libusb_device_descriptor descriptor; + libusb_get_device_descriptor(device, &descriptor); + if (whitelist.count({descriptor.idVendor, descriptor.idProduct}) == 0) + return true; + + auto usb_device = std::make_unique(m_ios, 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); + return true; + }); + if (ret != LIBUSB_SUCCESS) + WARN_LOG_FMT(IOS_USB, "GetDeviceList failed: {}", LibusbUtils::ErrorWrap(ret)); + } } #endif return true; @@ -188,6 +185,24 @@ void USBHost::DispatchHooks(const DeviceChangeHooks& hooks) OnDeviceChangeEnd(); } +void USBHost::AddEmulatedDevices(std::set& new_devices, DeviceChangeHooks& hooks, + bool always_add_hooks) +{ + if (Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL) && !NetPlay::IsNetPlayRunning()) + { + auto skylanderportal = std::make_unique(m_ios, "Skylander Portal"); + if (ShouldAddDevice(*skylanderportal)) + { + const u64 skyid = skylanderportal->GetId(); + new_devices.insert(skyid); + if (AddDevice(std::move(skylanderportal)) || always_add_hooks) + { + hooks.emplace(GetDeviceById(skyid), ChangeEvent::Inserted); + } + } + } +} + USBHost::ScanThread::~ScanThread() { Stop(); @@ -195,14 +210,19 @@ USBHost::ScanThread::~ScanThread() void USBHost::ScanThread::WaitForFirstScan() { - m_first_scan_complete_event.Wait(); + if (m_thread_running.IsSet()) + { + m_first_scan_complete_event.Wait(); + } } void USBHost::ScanThread::Start() { if (Core::WantsDeterminism()) + { + m_host->UpdateDevices(); return; - + } if (m_thread_running.TestAndSet()) { m_thread = std::thread([this] { diff --git a/Source/Core/Core/IOS/USB/Host.h b/Source/Core/Core/IOS/USB/Host.h index 341cbaa628..8e146df9d1 100644 --- a/Source/Core/Core/IOS/USB/Host.h +++ b/Source/Core/Core/IOS/USB/Host.h @@ -76,10 +76,13 @@ protected: private: bool AddDevice(std::unique_ptr device); + void Update() override; bool UpdateDevices(bool always_add_hooks = false); bool AddNewDevices(std::set& new_devices, DeviceChangeHooks& hooks, bool always_add_hooks); void DetectRemovedDevices(const std::set& plugged_devices, DeviceChangeHooks& hooks); void DispatchHooks(const DeviceChangeHooks& hooks); + void AddEmulatedDevices(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 a03d3958be..fc4272ed12 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -20,6 +20,7 @@ #include "Core/HW/SI/SI.h" #include "Core/HW/Sram.h" #include "Core/HW/VideoInterface.h" +#include "IOS/USB/Emulated/Skylander.h" #include "VideoCommon/CommandProcessor.h" #include "VideoCommon/Fifo.h" #include "VideoCommon/GeometryShaderManager.h" @@ -47,6 +48,7 @@ struct System::Impl Fifo::FifoManager m_fifo; GeometryShaderManager m_geometry_shader_manager; GPFifo::GPFifoManager m_gp_fifo; + IOS::HLE::USB::SkylanderPortal m_skylander_portal; Memory::MemoryManager m_memory; MemoryInterface::MemoryInterfaceState m_memory_interface_state; PixelEngine::PixelEngineManager m_pixel_engine; @@ -151,6 +153,11 @@ GPFifo::GPFifoManager& System::GetGPFifo() const return m_impl->m_gp_fifo; } +IOS::HLE::USB::SkylanderPortal& System::GetSkylanderPortal() const +{ + return m_impl->m_skylander_portal; +} + 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 410da6d697..159a73bb99 100644 --- a/Source/Core/Core/System.h +++ b/Source/Core/Core/System.h @@ -47,6 +47,10 @@ namespace GPFifo { class GPFifoManager; } +namespace IOS::HLE::USB +{ +class SkylanderPortal; +}; namespace Memory { class MemoryManager; @@ -116,6 +120,7 @@ public: Fifo::FifoManager& GetFifo() const; GeometryShaderManager& GetGeometryShaderManager() const; GPFifo::GPFifoManager& GetGPFifo() const; + IOS::HLE::USB::SkylanderPortal& GetSkylanderPortal() const; Memory::MemoryManager& GetMemory() const; MemoryInterface::MemoryInterfaceState& GetMemoryInterfaceState() const; PixelEngine::PixelEngineManager& GetPixelEngine() const; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 729db0a46d..b720e736b7 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -373,7 +373,6 @@ - @@ -987,7 +986,6 @@ - diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index ae72f12158..0acc8a36b9 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -221,7 +221,7 @@ void MenuBar::AddToolsMenu() tools_menu->addAction(tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer); - tools_menu->addAction(tr("&Skylanders Portal"), this, [this] { emit ShowSkylanderPortal(); }); + tools_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp index 1e91c273ae..346612fa3b 100644 --- a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp @@ -1,3 +1,6 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + // DolphinQt code copied and modified for Dolphin from the RPCS3 Qt utility for Creating, Loading // and Clearing skylanders @@ -6,12 +9,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -22,15 +27,16 @@ #include "Common/IOFile.h" #include "Core/Config/MainSettings.h" +#include "Core/System.h" #include "DolphinQt/QtUtils/DolphinFileDialog.h" #include "DolphinQt/Settings.h" -SkylanderPortalWindow* SkylanderPortalWindow::inst = nullptr; -std::optional> SkylanderPortalWindow::sky_slots[MAX_SKYLANDERS]; -QString last_skylander_path; +// 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 Skylander file location +static QString s_last_skylander_path; -const std::map, const std::string> list_skylanders = { +const std::map, const char*> list_skylanders = { {{0, 0x0000}, "Whirlwind"}, {{0, 0x1801}, "Series 2 Whirlwind"}, {{0, 0x1C02}, "Polar Whirlwind"}, @@ -513,49 +519,10 @@ const std::map, const std::string> list_sk {{3503, 0x0000}, "Kaos Trophy"}, }; -u16 skylander_crc16(u16 init_value, const u8* buffer, u32 size) -{ - const unsigned short CRC_CCITT_TABLE[256] = { - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, - 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, - 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, - 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, - 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, - 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, - 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, - 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, - 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, - 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, - 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, - 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, - 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, - 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, - 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, - 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, - 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, - 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, - 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, - 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, - 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, - 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, - 0x3EB2, 0x0ED1, 0x1EF0}; - - u16 crc = init_value; - - for (u32 i = 0; i < size; i++) - { - const u16 tmp = (crc >> 8) ^ buffer[i]; - crc = (crc << 8) ^ CRC_CCITT_TABLE[tmp]; - } - - return crc; -} - SkylanderPortalWindow::SkylanderPortalWindow(QWidget* parent) : QWidget(parent) { setWindowTitle(tr("Skylanders Manager")); - setObjectName(QString::fromStdString("skylanders_manager")); + setObjectName(tr("skylanders_manager")); setMinimumSize(QSize(700, 200)); CreateMainWindow(); @@ -568,33 +535,32 @@ SkylanderPortalWindow::SkylanderPortalWindow(QWidget* parent) : QWidget(parent) OnEmulationStateChanged(Core::GetState()); }; -SkylanderPortalWindow::~SkylanderPortalWindow() -{ -} +SkylanderPortalWindow::~SkylanderPortalWindow() = default; void SkylanderPortalWindow::CreateMainWindow() { - QVBoxLayout* mainLayout = new QVBoxLayout(); + auto* main_layout = new QVBoxLayout(); - QGroupBox* checkbox_group = new QGroupBox(); - QHBoxLayout* checkboxLayout = new QHBoxLayout(); - checkboxLayout->setAlignment(Qt::AlignHCenter); - checkbox = new QCheckBox(QString::fromStdString("Emulate Skylander Portal"), this); - connect(checkbox, &QCheckBox::toggled, [=](bool checked) { EmulatePortal(checked); }); - checkboxLayout->addWidget(checkbox); - checkbox_group->setLayout(checkboxLayout); - mainLayout->addWidget(checkbox_group); + auto* checkbox_group = new QGroupBox(); + auto* checkbox_layout = new QHBoxLayout(); + checkbox_layout->setAlignment(Qt::AlignHCenter); + m_checkbox = new QCheckBox(tr("Emulate Skylander Portal"), this); + m_checkbox->setChecked(Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL)); + connect(m_checkbox, &QCheckBox::toggled, [=](bool checked) { EmulatePortal(checked); }); + checkbox_layout->addWidget(m_checkbox); + checkbox_group->setLayout(checkbox_layout); + main_layout->addWidget(checkbox_group); auto add_line = [](QVBoxLayout* vbox) { - QFrame* line = new QFrame(); + auto* line = new QFrame(); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); vbox->addWidget(line); }; - group_skylanders = new QGroupBox(tr("Active Portal Skylanders:")); - QVBoxLayout* vbox_group = new QVBoxLayout(); - QScrollArea* scroll_area = new QScrollArea(); + m_group_skylanders = new QGroupBox(tr("Active Portal Skylanders:")); + auto* vbox_group = new QVBoxLayout(); + auto* scroll_area = new QScrollArea(); for (auto i = 0; i < MAX_SKYLANDERS; i++) { @@ -603,21 +569,21 @@ void SkylanderPortalWindow::CreateMainWindow() add_line(vbox_group); } - QHBoxLayout* hbox_skylander = new QHBoxLayout(); - QLabel* label_skyname = new QLabel(QString(tr("Skylander %1")).arg(i + 1)); - edit_skylanders[i] = new QLineEdit(); - edit_skylanders[i]->setEnabled(false); + auto* hbox_skylander = new QHBoxLayout(); + auto* label_skyname = new QLabel(QString(tr("Skylander %1")).arg(i + 1)); + m_edit_skylanders[i] = new QLineEdit(); + m_edit_skylanders[i]->setEnabled(false); - QPushButton* clear_btn = new QPushButton(tr("Clear")); - QPushButton* create_btn = new QPushButton(tr("Create")); - QPushButton* load_btn = new QPushButton(tr("Load")); + auto* clear_btn = new QPushButton(tr("Clear")); + auto* create_btn = new QPushButton(tr("Create")); + auto* load_btn = new QPushButton(tr("Load")); connect(clear_btn, &QAbstractButton::clicked, this, [this, i]() { ClearSkylander(i); }); connect(create_btn, &QAbstractButton::clicked, this, [this, i]() { CreateSkylander(i); }); connect(load_btn, &QAbstractButton::clicked, this, [this, i]() { LoadSkylander(i); }); hbox_skylander->addWidget(label_skyname); - hbox_skylander->addWidget(edit_skylanders[i]); + hbox_skylander->addWidget(m_edit_skylanders[i]); hbox_skylander->addWidget(clear_btn); hbox_skylander->addWidget(create_btn); hbox_skylander->addWidget(load_btn); @@ -625,12 +591,12 @@ void SkylanderPortalWindow::CreateMainWindow() vbox_group->addLayout(hbox_skylander); } - group_skylanders->setLayout(vbox_group); - scroll_area->setWidget(group_skylanders); + m_group_skylanders->setLayout(vbox_group); + scroll_area->setWidget(m_group_skylanders); scroll_area->setWidgetResizable(true); - group_skylanders->setVisible(false); - mainLayout->addWidget(scroll_area); - setLayout(mainLayout); + m_group_skylanders->setVisible(Config::Get(Config::MAIN_EMULATE_SKYLANDER_PORTAL)); + main_layout->addWidget(scroll_area); + setLayout(main_layout); UpdateEdits(); } @@ -639,29 +605,29 @@ void SkylanderPortalWindow::OnEmulationStateChanged(Core::State state) { const bool running = state != Core::State::Uninitialized; - checkbox->setEnabled(!running); + m_checkbox->setEnabled(!running); } CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Skylander Creator")); - setObjectName(QString::fromStdString("skylanders_creator")); + setObjectName(tr("skylanders_creator")); setMinimumSize(QSize(500, 150)); - QVBoxLayout* layout = new QVBoxLayout; + auto* layout = new QVBoxLayout; - QComboBox* combo_skylist = new QComboBox(); + auto* combo_skylist = new QComboBox(); QStringList filterlist; for (const auto& entry : list_skylanders) { const uint qvar = (entry.first.first << 16) | entry.first.second; - combo_skylist->addItem(QString::fromStdString(entry.second), QVariant(qvar)); - filterlist << QString::fromStdString(entry.second.c_str()); + combo_skylist->addItem(tr(entry.second), QVariant(qvar)); + filterlist << tr(entry.second); } combo_skylist->addItem(tr("--Unknown--"), QVariant(0xFFFFFFFF)); combo_skylist->setEditable(true); combo_skylist->setInsertPolicy(QComboBox::NoInsert); - QCompleter* co_compl = new QCompleter(filterlist, this); + auto* co_compl = new QCompleter(filterlist, this); co_compl->setCaseSensitivity(Qt::CaseInsensitive); co_compl->setCompletionMode(QCompleter::PopupCompletion); co_compl->setFilterMode(Qt::MatchContains); @@ -669,18 +635,17 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) layout->addWidget(combo_skylist); - QFrame* line = new QFrame(); + auto* line = new QFrame(); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); layout->addWidget(line); - QHBoxLayout* hbox_idvar = new QHBoxLayout(); - QLabel* label_id = new QLabel(tr("ID:")); - QLabel* label_var = new QLabel(tr("Variant:")); - QLineEdit* edit_id = new QLineEdit(QString::fromStdString("0")); - QLineEdit* edit_var = new QLineEdit(QString::fromStdString("0")); - QRegularExpressionValidator* rxv = - new QRegularExpressionValidator(QRegularExpression(QString::fromStdString("\\d*")), this); + auto* hbox_idvar = new QHBoxLayout(); + auto* label_id = new QLabel(tr("ID:")); + auto* label_var = new QLabel(tr("Variant:")); + auto* edit_id = new QLineEdit(tr("0")); + auto* edit_var = new QLineEdit(tr("0")); + auto* rxv = new QRegularExpressionValidator(QRegularExpression(tr("\\d*")), this); edit_id->setValidator(rxv); edit_var->setValidator(rxv); hbox_idvar->addWidget(label_id); @@ -689,13 +654,9 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) hbox_idvar->addWidget(edit_var); layout->addLayout(hbox_idvar); - 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); - layout->addLayout(hbox_buttons); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Create")); + layout->addWidget(buttons); setLayout(layout); @@ -711,7 +672,7 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) } }); - connect(btn_create, &QAbstractButton::clicked, this, [=, this]() { + connect(buttons, &QDialogButtonBox::accepted, this, [=, this]() { bool ok_id = false, ok_var = false; const u16 sky_id = edit_id->text().toUShort(&ok_id); if (!ok_id) @@ -728,61 +689,40 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) return; } - QString predef_name = last_skylander_path; + QString predef_name = s_last_skylander_path; const auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); if (found_sky != list_skylanders.end()) { - predef_name += QString::fromStdString(found_sky->second + ".sky"); + std::string name = std::string(found_sky->second) + ".sky"; + predef_name += tr(name.c_str()); } else { - QString str = QString::fromStdString("Unknown(%1 %2).sky"); + QString str = tr("Unknown(%1 %2).sky"); predef_name += str.arg(sky_id, sky_var); } - file_path = QFileDialog::getSaveFileName(this, tr("Create Skylander File"), predef_name, - tr("Skylander Object (*.sky);;")); - if (file_path.isEmpty()) + m_file_path = QFileDialog::getSaveFileName(this, tr("Create Skylander File"), predef_name, + tr("Skylander Object (*.sky);;")); + if (m_file_path.isEmpty()) { return; } - File::IOFile sky_file(file_path.toStdString(), "w+b"); - if (!sky_file) + auto& system = Core::System::GetInstance(); + + if (!system.GetSkylanderPortal().CreateSkylander(m_file_path.toStdString(), sky_id, sky_var)) { QMessageBox::warning(this, tr("Failed to create skylander file!"), - tr("Failed to create skylander file:\n%1").arg(file_path), + tr("Failed to create skylander file:\n%1").arg(m_file_path), QMessageBox::Ok); return; } - - std::array buf{}; - const auto file_data = buf.data(); - // Set the block permissions - u32 first_block = 0x690F0F0F; - u32 other_blocks = 0x69080F7F; - memcpy(&file_data[0x36], &first_block, sizeof(first_block)); - for (u32 index = 1; index < 0x10; index++) - { - memcpy(&file_data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); - } - // Set the skylander info - u16 sky_info = (sky_id | sky_var) + 1; - memcpy(&file_data[0], &sky_info, sizeof(sky_info)); - memcpy(&file_data[0x10], &sky_id, sizeof(sky_id)); - memcpy(&file_data[0x1C], &sky_var, sizeof(sky_var)); - // Set checksum - u16 checksum = skylander_crc16(0xFFFF, file_data, 0x1E); - memcpy(&file_data[0x1E], &checksum, sizeof(checksum)); - - sky_file.WriteBytes(buf.data(), buf.size()); - sky_file.Close(); - - last_skylander_path = QFileInfo(file_path).absolutePath() + QString::fromStdString("/"); + s_last_skylander_path = QFileInfo(m_file_path).absolutePath() + tr("/"); accept(); }); - connect(btn_cancel, &QAbstractButton::clicked, this, &QDialog::reject); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(co_compl, QOverload::of(&QCompleter::activated), [=](const QString& text) { @@ -793,13 +733,13 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) QString CreateSkylanderDialog::GetFilePath() const { - return file_path; + return m_file_path; } void SkylanderPortalWindow::EmulatePortal(bool emulate) { Config::SetBaseOrCurrent(Config::MAIN_EMULATE_SKYLANDER_PORTAL, emulate); - group_skylanders->setVisible(emulate); + m_group_skylanders->setVisible(emulate); } void SkylanderPortalWindow::CreateSkylander(u8 slot) @@ -814,12 +754,12 @@ void SkylanderPortalWindow::CreateSkylander(u8 slot) void SkylanderPortalWindow::LoadSkylander(u8 slot) { const QString file_path = DolphinFileDialog::getOpenFileName( - this, tr("Select Skylander File"), last_skylander_path, tr("Skylander (*.sky);;")); + this, tr("Select Skylander File"), s_last_skylander_path, tr("Skylander (*.sky);;")); if (file_path.isEmpty()) { return; } - last_skylander_path = QFileInfo(file_path).absolutePath() + QString::fromStdString("/"); + s_last_skylander_path = QFileInfo(file_path).absolutePath() + tr("/"); LoadSkylanderPath(slot, file_path); } @@ -859,19 +799,26 @@ void SkylanderPortalWindow::LoadSkylanderPath(u8 slot, const QString& path) DEBUG_LOG_FMT(IOS_USB, "Sky Var: {}, 0x1D: {} 0x1C: {}", sky_var, file_data[0x1D], file_data[0x1C]); - u8 portal_slot = IOS::HLE::USB::g_skyportal.LoadSkylander(file_data.data(), std::move(sky_file)); - sky_slots[slot] = std::tuple(portal_slot, sky_id, sky_var); + auto& system = Core::System::GetInstance(); + u8 portal_slot = system.GetSkylanderPortal().LoadSkylander(file_data.data(), std::move(sky_file)); + if (portal_slot == 0xFF) + { + QMessageBox::warning(this, tr("Failed to load the skylander file!"), + tr("Failed to load the skylander file(%1)!\n").arg(path), QMessageBox::Ok); + return; + } + m_sky_slots[slot] = {portal_slot, sky_id, sky_var}; UpdateEdits(); } void SkylanderPortalWindow::ClearSkylander(u8 slot) { - if (auto slot_infos = sky_slots[slot]) + auto& system = Core::System::GetInstance(); + if (auto slot_infos = m_sky_slots[slot]) { - auto [cur_slot, id, var] = slot_infos.value(); - IOS::HLE::USB::g_skyportal.RemoveSkylander(cur_slot); - sky_slots[slot] = {}; + system.GetSkylanderPortal().RemoveSkylander(slot_infos->portal_slot); + m_sky_slots[slot].reset(); UpdateEdits(); } } @@ -881,26 +828,24 @@ void SkylanderPortalWindow::UpdateEdits() for (auto i = 0; i < MAX_SKYLANDERS; i++) { QString display_string; - if (auto sd = sky_slots[i]) + if (auto sd = m_sky_slots[i]) { - auto [portal_slot, sky_id, sky_var] = sd.value(); - auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); + auto found_sky = list_skylanders.find(std::make_pair(sd->sky_id, sd->sky_var)); if (found_sky != list_skylanders.end()) { - display_string = QString::fromStdString(found_sky->second); + display_string = tr(found_sky->second); } else { - display_string = - QString(QString::fromStdString("Unknown (Id:%1 Var:%2)")).arg(sky_id).arg(sky_var); + display_string = QString(tr("Unknown (Id:%1 Var:%2)")).arg(sd->sky_id).arg(sd->sky_var); } } else { - display_string = QString::fromStdString("None"); + display_string = tr("None"); } - edit_skylanders[i]->setText(display_string); + m_edit_skylanders[i]->setText(display_string); } } diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h index 1a3fda52a8..320d524710 100644 --- a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h @@ -1,32 +1,39 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #pragma once +#include #include -#include #include -#include -#include +#include #include #include "Core/Core.h" #include "Core/IOS/USB/Emulated/Skylander.h" -class QDialogButtonBox; -class QLabel; -class QPushButton; -class QSpinBox; -class QTabWidget; +class QCheckBox; +class QGroupBox; +class QLineEdit; + +struct Skylander +{ + u8 portal_slot; + u16 sky_id; + u16 sky_var; +}; class SkylanderPortalWindow : public QWidget { Q_OBJECT public: explicit SkylanderPortalWindow(QWidget* parent = nullptr); - ~SkylanderPortalWindow(); + ~SkylanderPortalWindow() override; protected: - QLineEdit* edit_skylanders[MAX_SKYLANDERS]{}; - static std::optional> sky_slots[MAX_SKYLANDERS]; + std::array m_edit_skylanders; + std::array, MAX_SKYLANDERS> m_sky_slots; private: void CreateMainWindow(); @@ -38,13 +45,10 @@ private: void LoadSkylanderPath(u8 slot, const QString& path); void UpdateEdits(); void closeEvent(QCloseEvent* bar) override; - - static SkylanderPortalWindow* inst; - - QCheckBox* checkbox; - QGroupBox* group_skylanders; - bool eventFilter(QObject* object, QEvent* event) final override; + + QCheckBox* m_checkbox; + QGroupBox* m_group_skylanders; }; class CreateSkylanderDialog : public QDialog @@ -56,5 +60,5 @@ public: QString GetFilePath() const; protected: - QString file_path; + QString m_file_path; }; From c76d2c16eb46d313b985cbb6244b8629e25eb4cb Mon Sep 17 00:00:00 2001 From: Joshua de Reeper Date: Sun, 22 Jan 2023 09:56:08 +1300 Subject: [PATCH 3/3] Valid block/num checks, rename variables, remove translatable strings Capitalize Skylander in tr strings Lint and validation method fixes Proper Attach and Change Interface method Re-jig code to exit early and read easier --- .../Core/Core/IOS/USB/Emulated/Skylander.cpp | 611 ++++++++++-------- Source/Core/Core/IOS/USB/Emulated/Skylander.h | 4 + .../SkylanderPortal/SkylanderPortalWindow.cpp | 55 +- 3 files changed, 359 insertions(+), 311 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp index 60c7302563..c799c3477e 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp @@ -66,6 +66,12 @@ bool SkylanderUSB::Attach() bool SkylanderUSB::AttachAndChangeInterface(const u8 interface) { + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + return true; } @@ -109,286 +115,25 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); - auto& system = Core::System::GetInstance(); - auto& memory = system.GetMemory(); - u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); - if ((cmd->length == 0 || buf == nullptr) && cmd->request == 0x09) - { - ERROR_LOG_FMT(IOS_USB, "Skylander command invalid"); + // If not HID Host to Device type, return invalid + if (cmd->request_type != 0x21) return IPC_EINVAL; - } - std::array result = {}; - std::array data = {}; + + // Data to be sent back via the control transfer immediately + std::array control_response = {}; s32 expected_count = 0; u64 expected_time_us = 100; - // Control transfers are instantaneous - u8 request_type = cmd->request_type; - if (request_type == 0x21) + + // Non 0x09 Requests are handled here - no portal data is requested + if (cmd->request != 0x09) { - // HID host to device type switch (cmd->request) { - case 0x09: - switch (buf[0]) - { - case 'A': - { - // Activation - // Command { 'A', (00 | 01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'A', (00 | 01), - // ff, 77, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00 } - // The 2nd byte of the command is whether to activate (0x01) or deactivate (0x00) the - // portal. The response echos back the activation byte as the 2nd byte of the response. The - // 3rd and 4th bytes of the response appear to vary from wired to wireless. On wired - // portals, the bytes appear to always be ff 77. On wireless portals, during activation the - // 3rd byte appears to count down from ff (possibly a battery power indication) and during - // deactivation ed and eb responses have been observed. The 4th byte appears to always be 00 - // for wireless portals. - - // Wii U Wireless: 41 01 f4 00 41 00 ed 00 41 01 f4 00 41 00 eb 00 41 01 f3 00 41 00 ed 00 - if (cmd->length == 2) - { - data = {buf[0], buf[1]}; - result = {0x41, buf[1], 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - m_queries.push(result); - expected_count = 10; - system.GetSkylanderPortal().Activate(); - } - break; - } - case 'C': - { - // Color - // Command { 'C', 12, 34, 56, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'C', 12, 34, 56, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00 } - // The 3 bytes {12, 34, 56} are RGB values. - - // This command should set the color of the LED in the portal, however this appears - // deprecated in most of the recent portals. On portals that do not have LEDs, this command - // is silently ignored and do not require a response. - if (cmd->length == 4) - { - system.GetSkylanderPortal().SetLEDs(0x01, buf[1], buf[2], buf[3]); - data = {0x43, buf[1], buf[2], buf[3]}; - expected_count = 12; - } - break; - } - case 'J': - { - // Sided color - // The 2nd byte is the side - // 0x00: right - // 0x01: left and right - // 0x02: left - - // The 3rd, 4th and 5th bytes are red, green and blue - - // The 6th byte is unknown. Observed values are 0x00, 0x0D and 0xF4 - - // The 7th byte is the fade duration. Exact value-time corrolation unknown. Observed values - // are 0x00, 0x01 and 0x07. Custom commands show that the higher this value the longer the - // duration. - - // Empty J response is send after the fade is completed. Immeditately sending it is fine - // as long as we don't show the fade happening - if (cmd->length == 7) - { - data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]}; - expected_count = 15; - result = {buf[0]}; - m_queries.push(result); - system.GetSkylanderPortal().SetLEDs(buf[1], buf[2], buf[3], buf[4]); - } - break; - } - case 'L': - { - // Light - // This command is used while playing audio through the portal - - // The 2nd bytes is the position - // 0x00: right - // 0x01: trap led - // 0x02: left - - // The 3rd, 4th and 5th bytes are red, green and blue - // the trap led is white-only - // increasing or decreasing the values results in a brighter or dimmer light - if (cmd->length == 5) - { - data = {buf[0], buf[1], buf[2], buf[3], buf[4]}; - expected_count = 13; - - u8 side = buf[1]; - if (side == 0x02) - { - side = 0x04; - } - system.GetSkylanderPortal().SetLEDs(side, buf[2], buf[3], buf[4]); - } - break; - } - case 'M': - { - // Audio Firmware version - // Respond with version obtained from Trap Team wired portal - if (cmd->length == 2) - { - data = {buf[0], buf[1]}; - expected_count = 10; - result = {buf[0], buf[1], 0x00, 0x19}; - m_queries.push(result); - } - break; - } - // Query - // Command { 'Q', 10, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'Q', 10, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00 } - // In the command the 2nd byte indicates which Skylander to query data - // from. Index starts at 0x10 for the 1st Skylander (as reported in the Status command.) The - // 16th Skylander indexed would be 0x20. The 3rd byte indicate which block to read from. - - // A response with the 2nd byte of 0x01 indicates an error in the read. Otherwise, the - // response indicates the Skylander's index in the 2nd byte, the block read in the 3rd byte, - // data (16 bytes) is contained in bytes 4-19. - - // A Skylander has 64 blocks of data indexed from 0x00 to 0x3f. SwapForce characters have 2 - // character indexes, these may not be sequential. - case 'Q': - { - // Queries a block - if (cmd->length == 3) - { - const u8 sky_num = buf[1] & 0xF; - const u8 block = buf[2]; - system.GetSkylanderPortal().QueryBlock(sky_num, block, result.data()); - m_queries.push(result); - data = {buf[0], buf[1], buf[2]}; - expected_count = 11; - } - break; - } - case 'R': - { - // Ready - // Command { 'R', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'R', 02, 0a, 03, 02, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00 } - // The 4 byte sequence after the R (0x52) is unknown, but appears consistent based on device - // type. - if (cmd->length == 2) - { - data = {0x52, 0x00}; - result = {0x52, 0x02, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - m_queries.push(result); - expected_count = 10; - } - break; - } - // Status - // Command { 'S', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'S', 55, 00, 00, 55, 3e, - // (00|01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00 } - // Status is the default command. If you open the HID device and - // activate the portal, you will get status outputs. - - // The 4 bytes {55, 00, 00, 55} are the status of characters on the portal. The 4 bytes are - // treated as a 32-bit binary array. Each unique Skylander placed on a board is represented - // by 2 bits starting with the first Skylander in the least significant bit. This bit is - // present whenever the Skylandar is added or present on the portal. When the Skylander is - // added to the board, both bits are set in the next status message as a one-time signal. - // When a Skylander is removed from the board, only the most significant bit of the 2 bits - // is set. - - // Different portals can track a different number of RFID tags. The Wii Wireless portal - // tracks 4, the Wired portal can track 8. The maximum number of unique Skylanders tracked - // at any time is 16, after which new Skylanders appear to cycle unused bits. - - // Certain Skylanders, e.g. SwapForce Skylanders, are represented as 2 ID in the bit array. - // This may be due to the presence of 2 RFIDs, one for each half of the Skylander. - - // The 6th byte {3e} is a counter and increments by one. It will roll over when reaching - // {ff}. - - // The purpose of the (00\|01) byte at the 7th position appear to indicate if the portal has - // been activated: {01} when active and {00} when deactivated. - case 'S': - { - if (cmd->length == 1) - { - data = {buf[0]}; - expected_count = 9; - } - break; - } - case 'V': - { - if (cmd->length == 4) - { - data = {buf[0], buf[1], buf[2], buf[3]}; - expected_count = 12; - } - break; - } - // Write - // Command { 'W', 10, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } - // Response { 'W', 00, 00, 00, 00, 00, - // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, - // 00, 00, 00, 00 } - // In the command the 2nd byte indicates which Skylander to query data from. Index starts at - // 0x10 for the 1st Skylander (as reported in the Status command.) The 16th Skylander - // indexed would be 0x20. - - // 3rd byte is the block to write to. - - // Bytes 4 - 19 ({ 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f }) are the - // data to write. - - // The response does not appear to return the id of the Skylander being written, the 2nd - // byte is 0x00; however, the 3rd byte echos the block that was written (0x00 in example - // above.) - - case 'W': - { - if (cmd->length == 19) - { - const u8 sky_num = buf[1] & 0xF; - const u8 block = buf[2]; - system.GetSkylanderPortal().WriteBlock(sky_num, block, &buf[3], result.data()); - m_queries.push(result); - data = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], - buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], - buf[14], buf[15], buf[16], buf[17], buf[18]}; - expected_count = 27; - } - break; - } - default: - ERROR_LOG_FMT(IOS_USB, "Unhandled Skylander Portal Query: {}", buf[0]); - break; - } - break; + // Get Interface case 0x0A: expected_count = 8; break; + // Set Interface case 0x0B: expected_count = 8; break; @@ -397,9 +142,282 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) break; } } + else + { + // Skylander Portal Requests + auto& system = Core::System::GetInstance(); + auto& memory = system.GetMemory(); + u8* buf = memory.GetPointerForRange(cmd->data_address, cmd->length); + if (cmd->length == 0 || buf == nullptr) + { + ERROR_LOG_FMT(IOS_USB, "Skylander command invalid"); + return IPC_EINVAL; + } + // Data to be queued to be sent back via the Interrupt Transfer (if needed) + std::array interrupt_response = {}; + + // The first byte of the Control Request is always a char for Skylanders + switch (buf[0]) + { + case 'A': + { + // Activation + // Command { 'A', (00 | 01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'A', (00 | 01), + // ff, 77, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00 } + // The 2nd byte of the command is whether to activate (0x01) or deactivate (0x00) the + // portal. The response echos back the activation byte as the 2nd byte of the response. The + // 3rd and 4th bytes of the response appear to vary from wired to wireless. On wired + // portals, the bytes appear to always be ff 77. On wireless portals, during activation the + // 3rd byte appears to count down from ff (possibly a battery power indication) and during + // deactivation ed and eb responses have been observed. The 4th byte appears to always be 00 + // for wireless portals. + + // Wii U Wireless: 41 01 f4 00 41 00 ed 00 41 01 f4 00 41 00 eb 00 41 01 f3 00 41 00 ed 00 + if (cmd->length == 2) + { + control_response = {buf[0], buf[1]}; + interrupt_response = {0x41, buf[1], 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + m_queries.push(interrupt_response); + expected_count = 10; + system.GetSkylanderPortal().Activate(); + } + break; + } + case 'C': + { + // Color + // Command { 'C', 12, 34, 56, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'C', 12, 34, 56, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // The 3 bytes {12, 34, 56} are RGB values. + + // This command should set the color of the LED in the portal, however this appears + // deprecated in most of the recent portals. On portals that do not have LEDs, this command + // is silently ignored and do not require a response. + if (cmd->length == 4) + { + system.GetSkylanderPortal().SetLEDs(0x01, buf[1], buf[2], buf[3]); + control_response = {0x43, buf[1], buf[2], buf[3]}; + expected_count = 12; + } + break; + } + case 'J': + { + // Sided color + // The 2nd byte is the side + // 0x00: right + // 0x01: left and right + // 0x02: left + + // The 3rd, 4th and 5th bytes are red, green and blue + + // The 6th byte is unknown. Observed values are 0x00, 0x0D and 0xF4 + + // The 7th byte is the fade duration. Exact value-time corrolation unknown. Observed values + // are 0x00, 0x01 and 0x07. Custom commands show that the higher this value the longer the + // duration. + + // Empty J response is sent after the fade is completed. + if (cmd->length == 7) + { + control_response = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]}; + expected_count = 15; + interrupt_response = {buf[0]}; + m_queries.push(interrupt_response); + system.GetSkylanderPortal().SetLEDs(buf[1], buf[2], buf[3], buf[4]); + } + break; + } + case 'L': + { + // Light + // This command is used while playing audio through the portal + + // The 2nd bytes is the position + // 0x00: right + // 0x01: trap led + // 0x02: left + + // The 3rd, 4th and 5th bytes are red, green and blue + // the trap led is white-only + // increasing or decreasing the values results in a brighter or dimmer light + if (cmd->length == 5) + { + control_response = {buf[0], buf[1], buf[2], buf[3], buf[4]}; + expected_count = 13; + + u8 side = buf[1]; + if (side == 0x02) + { + side = 0x04; + } + system.GetSkylanderPortal().SetLEDs(side, buf[2], buf[3], buf[4]); + } + break; + } + case 'M': + { + // Audio Firmware version + // Respond with version obtained from Trap Team wired portal + if (cmd->length == 2) + { + control_response = {buf[0], buf[1]}; + expected_count = 10; + interrupt_response = {buf[0], buf[1], 0x00, 0x19}; + m_queries.push(interrupt_response); + } + break; + } + // Query + // Command { 'Q', 10, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'Q', 10, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // In the command the 2nd byte indicates which Skylander to query data + // from. Index starts at 0x10 for the 1st Skylander (as reported in the Status command.) The + // 16th Skylander indexed would be 0x20. The 3rd byte indicate which block to read from. + + // A response with the 2nd byte of 0x01 indicates an error in the read. Otherwise, the + // response indicates the Skylander's index in the 2nd byte, the block read in the 3rd byte, + // data (16 bytes) is contained in bytes 4-19. + + // A Skylander has 64 blocks of data indexed from 0x00 to 0x3f. SwapForce characters have 2 + // character indexes, these may not be sequential. + case 'Q': + { + if (cmd->length == 3) + { + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + system.GetSkylanderPortal().QueryBlock(sky_num, block, interrupt_response.data()); + m_queries.push(interrupt_response); + control_response = {buf[0], buf[1], buf[2]}; + expected_count = 11; + } + break; + } + case 'R': + { + // Ready + // Command { 'R', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'R', 02, 0a, 03, 02, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // The 4 byte sequence after the R (0x52) is unknown, but appears consistent based on device + // type. + if (cmd->length == 2) + { + control_response = {0x52, 0x00}; + interrupt_response = {0x52, 0x02, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + m_queries.push(interrupt_response); + expected_count = 10; + } + break; + } + // Status + // Command { 'S', 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'S', 55, 00, 00, 55, 3e, + // (00|01), 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00 } + // Status is the default command. If you open the HID device and + // activate the portal, you will get status outputs. + + // The 4 bytes {55, 00, 00, 55} are the status of characters on the portal. The 4 bytes are + // treated as a 32-bit binary array. Each unique Skylander placed on a board is represented + // by 2 bits starting with the first Skylander in the least significant bit. This bit is + // present whenever the Skylandar is added or present on the portal. When the Skylander is + // added to the board, both bits are set in the next status message as a one-time signal. + // When a Skylander is removed from the board, only the most significant bit of the 2 bits + // is set. + + // Different portals can track a different number of RFID tags. The Wii Wireless portal + // tracks 4, the Wired portal can track 8. The maximum number of unique Skylanders tracked + // at any time is 16, after which new Skylanders appear to cycle unused bits. + + // Certain Skylanders, e.g. SwapForce Skylanders, are represented as 2 ID in the bit array. + // This may be due to the presence of 2 RFIDs, one for each half of the Skylander. + + // The 6th byte {3e} is a counter and increments by one. It will roll over when reaching + // {ff}. + + // The purpose of the (00\|01) byte at the 7th position appear to indicate if the portal has + // been activated: {01} when active and {00} when deactivated. + case 'S': + { + if (cmd->length == 1) + { + // The Status interrupt responses are automatically handled via the GetStatus method + control_response = {buf[0]}; + expected_count = 9; + } + break; + } + case 'V': + { + if (cmd->length == 4) + { + control_response = {buf[0], buf[1], buf[2], buf[3]}; + expected_count = 12; + } + break; + } + // Write + // Command { 'W', 10, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00 } + // Response { 'W', 00, 00, 00, 00, 00, + // 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, + // 00, 00, 00, 00 } + // In the command the 2nd byte indicates which Skylander to query data from. Index starts at + // 0x10 for the 1st Skylander (as reported in the Status command.) The 16th Skylander + // indexed would be 0x20. + + // 3rd byte is the block to write to. + + // Bytes 4 - 19 ({ 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f }) are the + // data to write. + + // The response does not appear to return the id of the Skylander being written, the 2nd + // byte is 0x00; however, the 3rd byte echos the block that was written (0x00 in example + // above.) + + case 'W': + { + if (cmd->length == 19) + { + const u8 sky_num = buf[1] & 0xF; + const u8 block = buf[2]; + system.GetSkylanderPortal().WriteBlock(sky_num, block, &buf[3], interrupt_response.data()); + m_queries.push(interrupt_response); + control_response = {buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], + buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], + buf[14], buf[15], buf[16], buf[17], buf[18]}; + expected_count = 27; + } + break; + } + default: + ERROR_LOG_FMT(IOS_USB, "Unhandled Skylander Portal Query: {}", buf[0]); + break; + } + } + if (expected_count == 0) return IPC_EINVAL; - ScheduleTransfer(std::move(cmd), data, expected_count, expected_time_us); + + ScheduleTransfer(std::move(cmd), control_response, expected_count, expected_time_us); return 0; } @@ -427,26 +445,26 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) ERROR_LOG_FMT(IOS_USB, "Skylander command invalid"); return IPC_EINVAL; } - std::array result = {}; + std::array interrupt_response = {}; s32 expected_count; u64 expected_time_us; // Audio requests are 64 bytes long, are the only Interrupt requests longer than 32 bytes, // echo the request as the response and respond after 1ms if (cmd->length > 32 && cmd->length <= 64) { - std::array audio_result = {}; - u8* audio_buf = audio_result.data(); + std::array audio_interrupt_response = {}; + u8* audio_buf = audio_interrupt_response.data(); memcpy(audio_buf, buf, cmd->length); expected_time_us = 1000; expected_count = cmd->length; - ScheduleTransfer(std::move(cmd), audio_result, expected_count, expected_time_us); + ScheduleTransfer(std::move(cmd), audio_interrupt_response, expected_count, expected_time_us); return 0; } // If some data was requested from the Control Message, then the Interrupt message needs to // respond with that data. Check if the queries queue is empty if (!m_queries.empty()) { - result = m_queries.front(); + interrupt_response = m_queries.front(); m_queries.pop(); // This needs to happen after ~22 milliseconds expected_time_us = 22000; @@ -454,11 +472,11 @@ int SkylanderUSB::SubmitTransfer(std::unique_ptr cmd) // If there is no relevant data to respond with, respond with the currentstatus of the Portal else { - result = system.GetSkylanderPortal().GetStatus(); + interrupt_response = system.GetSkylanderPortal().GetStatus(); expected_time_us = 2000; } expected_count = 32; - ScheduleTransfer(std::move(cmd), result, expected_count, expected_time_us); + ScheduleTransfer(std::move(cmd), interrupt_response, expected_count, expected_time_us); return 0; } @@ -616,18 +634,21 @@ std::array SkylanderPortal::GetStatus() status |= s.status; } - std::array result = {0x53, 0x00, 0x00, 0x00, 0x00, m_interrupt_counter++, - active, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00}; - memcpy(&result[1], &status, sizeof(status)); - return result; + std::array interrupt_response = {0x53, 0x00, 0x00, 0x00, 0x00, m_interrupt_counter++, + active, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + memcpy(&interrupt_response[1], &status, sizeof(status)); + return interrupt_response; } void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) { + if (!IsSkylanderNumberValid(sky_num) || !IsBlockNumberValid(block)) + return; + std::lock_guard lock(sky_mutex); const auto& skylander = skylanders[sky_num]; @@ -647,6 +668,9 @@ void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf) { + if (!IsSkylanderNumberValid(sky_num) || !IsBlockNumberValid(block)) + return; + std::lock_guard lock(sky_mutex); auto& skylander = skylanders[sky_num]; @@ -738,6 +762,9 @@ bool SkylanderPortal::CreateSkylander(const std::string& file_path, u16 sky_id, bool SkylanderPortal::RemoveSkylander(u8 sky_num) { + if (!IsSkylanderNumberValid(sky_num)) + return false; + DEBUG_LOG_FMT(IOS_USB, "Cleared Skylander from slot {}", sky_num); std::lock_guard lock(sky_mutex); auto& skylander = skylanders[sky_num]; @@ -801,4 +828,14 @@ u8 SkylanderPortal::LoadSkylander(u8* buf, File::IOFile in_file) return found_slot; } +bool SkylanderPortal::IsSkylanderNumberValid(u8 sky_num) +{ + return sky_num < MAX_SKYLANDERS; +} + +bool SkylanderPortal::IsBlockNumberValid(u8 block) +{ + return block < 64; +} + } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.h b/Source/Core/Core/IOS/USB/Emulated/Skylander.h index cfd862f881..5c7b308a42 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylander.h +++ b/Source/Core/Core/IOS/USB/Emulated/Skylander.h @@ -107,6 +107,10 @@ protected: SkylanderLEDColor m_color_trap = {}; std::array skylanders; + +private: + static bool IsSkylanderNumberValid(u8 sky_num); + static bool IsBlockNumberValid(u8 block); }; } // namespace IOS::HLE::USB diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp index 346612fa3b..7f4f9994fd 100644 --- a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // DolphinQt code copied and modified for Dolphin from the RPCS3 Qt utility for Creating, Loading -// and Clearing skylanders +// and Clearing Skylanders #include "DolphinQt/SkylanderPortal/SkylanderPortalWindow.h" @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -522,7 +521,7 @@ const std::map, const char*> list_skylande SkylanderPortalWindow::SkylanderPortalWindow(QWidget* parent) : QWidget(parent) { setWindowTitle(tr("Skylanders Manager")); - setObjectName(tr("skylanders_manager")); + setObjectName(QString::fromStdString("skylanders_manager")); setMinimumSize(QSize(700, 200)); CreateMainWindow(); @@ -611,7 +610,7 @@ void SkylanderPortalWindow::OnEmulationStateChanged(Core::State state) CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Skylander Creator")); - setObjectName(tr("skylanders_creator")); + setObjectName(QString::fromStdString("skylanders_creator")); setMinimumSize(QSize(500, 150)); auto* layout = new QVBoxLayout; @@ -620,8 +619,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) for (const auto& entry : list_skylanders) { const uint qvar = (entry.first.first << 16) | entry.first.second; - combo_skylist->addItem(tr(entry.second), QVariant(qvar)); - filterlist << tr(entry.second); + combo_skylist->addItem(QString::fromStdString(entry.second), QVariant(qvar)); + filterlist << QString::fromStdString(entry.second); } combo_skylist->addItem(tr("--Unknown--"), QVariant(0xFFFFFFFF)); combo_skylist->setEditable(true); @@ -645,7 +644,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) auto* label_var = new QLabel(tr("Variant:")); auto* edit_id = new QLineEdit(tr("0")); auto* edit_var = new QLineEdit(tr("0")); - auto* rxv = new QRegularExpressionValidator(QRegularExpression(tr("\\d*")), this); + auto* rxv = + new QRegularExpressionValidator(QRegularExpression(QString::fromStdString("\\d*")), this); edit_id->setValidator(rxv); edit_var->setValidator(rxv); hbox_idvar->addWidget(label_id); @@ -693,8 +693,7 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) const auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); if (found_sky != list_skylanders.end()) { - std::string name = std::string(found_sky->second) + ".sky"; - predef_name += tr(name.c_str()); + predef_name += QString::fromStdString(std::string(found_sky->second) + ".sky"); } else { @@ -702,8 +701,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) predef_name += str.arg(sky_id, sky_var); } - m_file_path = QFileDialog::getSaveFileName(this, tr("Create Skylander File"), predef_name, - tr("Skylander Object (*.sky);;")); + m_file_path = DolphinFileDialog::getSaveFileName(this, tr("Create Skylander File"), predef_name, + tr("Skylander Object (*.sky);;")); if (m_file_path.isEmpty()) { return; @@ -713,12 +712,12 @@ CreateSkylanderDialog::CreateSkylanderDialog(QWidget* parent) : QDialog(parent) if (!system.GetSkylanderPortal().CreateSkylander(m_file_path.toStdString(), sky_id, sky_var)) { - QMessageBox::warning(this, tr("Failed to create skylander file!"), - tr("Failed to create skylander file:\n%1").arg(m_file_path), + QMessageBox::warning(this, tr("Failed to create Skylander file!"), + tr("Failed to create Skylander file:\n%1").arg(m_file_path), QMessageBox::Ok); return; } - s_last_skylander_path = QFileInfo(m_file_path).absolutePath() + tr("/"); + s_last_skylander_path = QFileInfo(m_file_path).absolutePath() + QString::fromStdString("/"); accept(); }); @@ -753,13 +752,14 @@ void SkylanderPortalWindow::CreateSkylander(u8 slot) void SkylanderPortalWindow::LoadSkylander(u8 slot) { - const QString file_path = DolphinFileDialog::getOpenFileName( - this, tr("Select Skylander File"), s_last_skylander_path, tr("Skylander (*.sky);;")); + const QString file_path = + DolphinFileDialog::getOpenFileName(this, tr("Select Skylander File"), s_last_skylander_path, + QString::fromStdString("Skylander (*.sky);;")); if (file_path.isEmpty()) { return; } - s_last_skylander_path = QFileInfo(file_path).absolutePath() + tr("/"); + s_last_skylander_path = QFileInfo(file_path).absolutePath() + QString::fromStdString("/"); LoadSkylanderPath(slot, file_path); } @@ -770,8 +770,8 @@ void SkylanderPortalWindow::LoadSkylanderPath(u8 slot, const QString& path) if (!sky_file) { QMessageBox::warning( - this, tr("Failed to open the skylander file!"), - tr("Failed to open the skylander file(%1)!\nFile may already be in use on the portal.") + this, tr("Failed to open the Skylander file!"), + tr("Failed to open the Skylander file(%1)!\nFile may already be in use on the portal.") .arg(path), QMessageBox::Ok); return; @@ -780,8 +780,8 @@ void SkylanderPortalWindow::LoadSkylanderPath(u8 slot, const QString& path) if (!sky_file.ReadBytes(file_data.data(), file_data.size())) { QMessageBox::warning( - this, tr("Failed to read the skylander file!"), - tr("Failed to read the skylander file(%1)!\nFile was too small.").arg(path), + this, tr("Failed to read the Skylander file!"), + tr("Failed to read the Skylander file(%1)!\nFile was too small.").arg(path), QMessageBox::Ok); return; } @@ -804,8 +804,8 @@ void SkylanderPortalWindow::LoadSkylanderPath(u8 slot, const QString& path) u8 portal_slot = system.GetSkylanderPortal().LoadSkylander(file_data.data(), std::move(sky_file)); if (portal_slot == 0xFF) { - QMessageBox::warning(this, tr("Failed to load the skylander file!"), - tr("Failed to load the skylander file(%1)!\n").arg(path), QMessageBox::Ok); + QMessageBox::warning(this, tr("Failed to load the Skylander file!"), + tr("Failed to load the Skylander file(%1)!\n").arg(path), QMessageBox::Ok); return; } m_sky_slots[slot] = {portal_slot, sky_id, sky_var}; @@ -817,6 +817,13 @@ void SkylanderPortalWindow::ClearSkylander(u8 slot) auto& system = Core::System::GetInstance(); if (auto slot_infos = m_sky_slots[slot]) { + if (!system.GetSkylanderPortal().RemoveSkylander(slot_infos->portal_slot)) + { + QMessageBox::warning(this, tr("Failed to clear Skylander!"), + tr("Failed to clear the Skylander from slot(%1)!\n").arg(slot), + QMessageBox::Ok); + return; + } system.GetSkylanderPortal().RemoveSkylander(slot_infos->portal_slot); m_sky_slots[slot].reset(); UpdateEdits(); @@ -833,7 +840,7 @@ void SkylanderPortalWindow::UpdateEdits() auto found_sky = list_skylanders.find(std::make_pair(sd->sky_id, sd->sky_var)); if (found_sky != list_skylanders.end()) { - display_string = tr(found_sky->second); + display_string = QString::fromStdString(found_sky->second); } else {