From dfc486fc6b436edd6693cb38fd449ff18df8bd4d Mon Sep 17 00:00:00 2001 From: Der_Floh Date: Sat, 10 Oct 2026 21:58:42 +0200 Subject: [PATCH] Skylanders Portal IPC Server --- 3rdparty/pine/pine_server.h | 118 +++---- rpcs3/Emu/CMakeLists.txt | 2 + rpcs3/Emu/Io/Skylander.cpp | 77 ++++- rpcs3/Emu/Io/Skylander.h | 5 +- rpcs3/Emu/Io/SkylanderPortalIPC.cpp | 354 +++++++++++++++++++++ rpcs3/Emu/Io/SkylanderPortalIPC.h | 20 ++ rpcs3/Emu/Io/SkylanderPortalIPC_config.cpp | 66 ++++ rpcs3/Emu/Io/SkylanderPortalIPC_config.h | 23 ++ rpcs3/Emu/System.cpp | 11 + rpcs3/emucore.vcxproj | 4 + rpcs3/emucore.vcxproj.filters | 12 + rpcs3/rpcs3.vcxproj | 17 + rpcs3/rpcs3.vcxproj.filters | 12 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 9 + rpcs3/rpcs3qt/sky_ipc_settings_dialog.cpp | 78 +++++ rpcs3/rpcs3qt/sky_ipc_settings_dialog.h | 10 + rpcs3/rpcs3qt/skylander_dialog.cpp | 60 +--- rpcs3/rpcs3qt/skylander_dialog.h | 4 +- 20 files changed, 778 insertions(+), 112 deletions(-) create mode 100644 rpcs3/Emu/Io/SkylanderPortalIPC.cpp create mode 100644 rpcs3/Emu/Io/SkylanderPortalIPC.h create mode 100644 rpcs3/Emu/Io/SkylanderPortalIPC_config.cpp create mode 100644 rpcs3/Emu/Io/SkylanderPortalIPC_config.h create mode 100644 rpcs3/rpcs3qt/sky_ipc_settings_dialog.cpp create mode 100644 rpcs3/rpcs3qt/sky_ipc_settings_dialog.h diff --git a/3rdparty/pine/pine_server.h b/3rdparty/pine/pine_server.h index 5c59e87da3..e9f51855d3 100644 --- a/3rdparty/pine/pine_server.h +++ b/3rdparty/pine/pine_server.h @@ -56,6 +56,63 @@ namespace pine typedef unsigned long long SOCKET; +#ifndef _WIN32 + inline std::string get_socket_path(std::string_view file_name, int slot, int default_slot) + { +#ifdef __APPLE__ + const char* runtime_dir = std::getenv("TMPDIR"); +#else + const char* runtime_dir = std::getenv("XDG_RUNTIME_DIR"); +#endif + // fallback in case macOS or other OSes don't implement the XDG base + // spec + std::string path = runtime_dir ? std::string(runtime_dir) + '/' : std::string("/tmp/"); + path += file_name; + + if (slot != default_slot) + { + fmt::append(path, ".%d", slot); + } + + return path; + } +#endif + +#ifdef _WIN32 + inline const char* bind_and_listen(SOCKET sock, u16 port) + { + const int exclusive = 1; + if (setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&exclusive), sizeof(exclusive)) != 0) + return "setsockopt(SO_EXCLUSIVEADDRUSE) failed"; + + sockaddr_in server{}; + server.sin_family = AF_INET; + server.sin_port = htons(port); + + if (inet_pton(AF_INET, "127.0.0.1", &server.sin_addr.s_addr) != 1) + return "inet_pton failed"; +#else + inline const char* bind_and_listen(int sock, const std::string& socket_path) + { + sockaddr_un server{}; + + if (socket_path.size() >= sizeof(server.sun_path)) + return "socket path is too long"; + + server.sun_family = AF_UNIX; + std::memcpy(server.sun_path, socket_path.c_str(), socket_path.size() + 1); + + unlink(socket_path.c_str()); +#endif + if (bind(sock, reinterpret_cast(&server), sizeof(server)) != 0) + return "bind failed"; + + if (listen(sock, 4096) != 0) + return "listen failed"; + + return nullptr; + } + template class pine_server : public Impl { @@ -528,7 +585,6 @@ namespace pine { #ifdef _WIN32 WSADATA wsa {}; - struct sockaddr_in server {}; m_sock = INVALID_SOCKET; m_msgsock = INVALID_SOCKET; @@ -544,45 +600,9 @@ namespace pine return; } - // yes very good windows s/sun/sin/g sure is fine - server.sin_family = AF_INET; - // localhost only - if (!inet_pton(server.sin_family, "127.0.0.1", &server.sin_addr.s_addr)) - { - fmt::throw_exception("IPC: Failed to convert localhost"); - } - server.sin_port = htons(Impl::get_port()); - - if (bind(m_sock, reinterpret_cast(&server), sizeof(server)) == SOCKET_ERROR) - { - Impl::error("IPC: Error while binding to socket! Shutting down..."); - return; - } - + const char* failure = bind_and_listen(m_sock, static_cast(Impl::get_port())); #else - char* runtime_dir = nullptr; -#ifdef __APPLE__ - runtime_dir = std::getenv("TMPDIR"); -#else - runtime_dir = std::getenv("XDG_RUNTIME_DIR"); -#endif - // fallback in case macOS or other OSes don't implement the XDG base - // spec - if (runtime_dir == nullptr) - m_socket_name = "/tmp/rpcs3.sock"; - else - { - m_socket_name = runtime_dir; - m_socket_name += "/rpcs3.sock"; - } - - const int slot = Impl::get_port(); - if (slot != IPC_DEFAULT_SLOT) - { - fmt::append(m_socket_name, ".%d", slot); - } - - struct sockaddr_un server {}; + m_socket_name = get_socket_path("rpcs3.sock", Impl::get_port(), IPC_DEFAULT_SLOT); m_sock = socket(AF_UNIX, SOCK_STREAM, 0); if (m_sock < 0) @@ -590,23 +610,13 @@ namespace pine Impl::error("IPC: Cannot open socket! Shutting down..."); return; } - server.sun_family = AF_UNIX; - strcpy(server.sun_path, m_socket_name.c_str()); - // we unlink the socket so that when releasing this thread the socket gets - // freed even if we didn't close correctly the loop - unlink(m_socket_name.c_str()); - if (bind(m_sock, std::bit_cast(&server), sizeof(struct sockaddr_un))) - { - Impl::error("IPC: Error while binding to socket! Shutting down..."); - return; - } + const char* failure = bind_and_listen(m_sock, m_socket_name); #endif - - // maximum queue of 4096 commands before refusing, approximated to the - // nearest legal value. We do not use SOMAXCONN as windows have this idea - // that a "reasonable" value is 5, which is not. - listen(m_sock, 4096); + if (failure) + { + Impl::error("IPC: Error while setting up socket (%s)! Shutting down...", failure); + } } pine_server(const pine_server&) = delete; diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 018e9d11bb..3bc8024019 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(rpcs3_emu STATIC perf_monitor.cpp IPC_config.cpp IPC_socket.cpp + Io/SkylanderPortalIPC_config.cpp ) if(USE_LTO) @@ -445,6 +446,7 @@ target_sources(rpcs3_emu PRIVATE Io/RB3MidiKeyboard.cpp Io/recording_config.cpp Io/Skylander.cpp + Io/SkylanderPortalIPC.cpp Io/TopShotElite.cpp Io/TopShotFearmaster.cpp Io/Turntable.cpp diff --git a/rpcs3/Emu/Io/Skylander.cpp b/rpcs3/Emu/Io/Skylander.cpp index 9cb909edf1..d5e54c2665 100644 --- a/rpcs3/Emu/Io/Skylander.cpp +++ b/rpcs3/Emu/Io/Skylander.cpp @@ -6,6 +6,32 @@ LOG_CHANNEL(skylander_log, "skylander"); sky_portal g_skyportal; +u16 skylander_crc16(u16 init_value, const u8* buffer, u32 size) +{ + constexpr 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; +} + void skylander::save() { if (!sky_file) @@ -153,32 +179,41 @@ bool sky_portal::remove_skylander(u8 sky_num) return false; } -u8 sky_portal::load_skylander(const std::array& data, fs::file in_file) +u8 sky_portal::load_skylander(const std::array& data, fs::file in_file, int requested_slot) { std::lock_guard lock(sky_mutex); const u32 sky_serial = read_from_ptr>(data); u8 found_slot = 0xFF; - // mimics spot retaining on the portal - for (u8 i = 0; i < 8; i++) + if (requested_slot >= 0 && requested_slot <= 7) { - if ((skylanders[i].status & 1) == 0) + if ((skylanders[requested_slot].status & 1) == 0) + found_slot = static_cast(requested_slot); + } + else + { + // mimics spot retaining on the portal + for (u8 i = 0; i < 8; i++) { - if (skylanders[i].last_id == sky_serial) + if ((skylanders[i].status & 1) == 0) { - found_slot = i; - break; - } + if (skylanders[i].last_id == sky_serial) + { + found_slot = i; + break; + } - if (i < found_slot) - { - found_slot = i; + if (i < found_slot) + { + found_slot = i; + } } } } - ensure(found_slot != 0xFF); + if (found_slot == 0xFF) + return 0xFF; skylander& thesky = skylanders[found_slot]; memcpy(thesky.data.data(), data.data(), thesky.data.size()); @@ -191,6 +226,24 @@ u8 sky_portal::load_skylander(const std::array& data, fs::file return found_slot; } +void sky_portal::get_figure_info(u8 sky_num, u8& out_status, u16& out_id, u16& out_variant) +{ + ensure(sky_num < 8); + std::lock_guard lock(sky_mutex); + const auto& s = skylanders[sky_num]; + out_status = s.status; + if (s.status & 1) + { + out_id = read_from_ptr>(s.data, 0x10); + out_variant = read_from_ptr>(s.data, 0x1C); + } + else + { + out_id = 0; + out_variant = 0; + } +} + usb_device_skylander::usb_device_skylander(const std::array& location) : usb_device_emulated(location) { diff --git a/rpcs3/Emu/Io/Skylander.h b/rpcs3/Emu/Io/Skylander.h index 0522f18064..a85c12de13 100644 --- a/rpcs3/Emu/Io/Skylander.h +++ b/rpcs3/Emu/Io/Skylander.h @@ -27,7 +27,8 @@ public: void write_block(u8 sky_num, u8 block, const u8* to_write_buf, u8* reply_buf); bool remove_skylander(u8 sky_num); - u8 load_skylander(const std::array& data, fs::file in_file); + u8 load_skylander(const std::array& data, fs::file in_file, int requested_slot = -1); + void get_figure_info(u8 sky_num, u8& out_status, u16& out_id, u16& out_variant); protected: shared_mutex sky_mutex; @@ -41,6 +42,8 @@ protected: extern sky_portal g_skyportal; +u16 skylander_crc16(u16 init_value, const u8* buffer, u32 size); + class usb_device_skylander : public usb_device_emulated { public: diff --git a/rpcs3/Emu/Io/SkylanderPortalIPC.cpp b/rpcs3/Emu/Io/SkylanderPortalIPC.cpp new file mode 100644 index 0000000000..cda51e2f10 --- /dev/null +++ b/rpcs3/Emu/Io/SkylanderPortalIPC.cpp @@ -0,0 +1,354 @@ +#include "stdafx.h" +#include "SkylanderPortalIPC.h" +#include "SkylanderPortalIPC_config.h" + +#ifdef _WIN32 +#include +#include +using sky_socket_t = SOCKET; +static constexpr sky_socket_t sky_invalid_socket = INVALID_SOCKET; +static int sky_recv(sky_socket_t s, char* buf, int len) { return recv(s, buf, len, 0); } +static int sky_send_some(sky_socket_t s, const char* buf, int len) { return send(s, buf, len, 0); } +static void sky_close(sky_socket_t s) { closesocket(s); } +static bool sky_interrupted() { return WSAGetLastError() == WSAEINTR; } + +static bool sky_set_recv_timeout(sky_socket_t s, u32 timeout_ms) +{ + const DWORD timeout = timeout_ms; + return setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)) == 0; +} +#else +#include +#include +#include +#include +#include +using sky_socket_t = int; +static constexpr sky_socket_t sky_invalid_socket = -1; +static int sky_recv(sky_socket_t s, char* buf, int len) { return static_cast(read(s, buf, len)); } +static int sky_send_some(sky_socket_t s, const char* buf, int len) { return static_cast(write(s, buf, len)); } +static void sky_close(sky_socket_t s) { close(s); } +static bool sky_interrupted() { return errno == EINTR; } + +static bool sky_set_recv_timeout(sky_socket_t s, u32 timeout_ms) +{ + timeval timeout{}; + timeout.tv_sec = static_cast(timeout_ms / 1000); + timeout.tv_usec = static_cast(timeout_ms % 1000 * 1000); + return setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == 0; +} +#endif + +#include "Skylander.h" +#include "3rdparty/pine/pine_server.h" + +#include + +LOG_CHANNEL(skylander_ipc_log, "SkylanderIPC"); + +static constexpr usz max_command_size = 4096; +static constexpr u32 client_timeout_ms = 1000; + +static bool is_absolute_local_path(std::string_view path) +{ +#ifdef _WIN32 + // Requiring a drive letter also rules out network (\\server\share) and device (\\.\) paths + const bool has_drive_letter = path.size() >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) && path[1] == ':'; + return has_drive_letter && (path[2] == '\\' || path[2] == '/'); +#else + return path.starts_with('/'); +#endif +} + +static std::string handle_load(int slot, const std::string& path) +{ + if (slot < -1 || slot > 7) + return "error invalid slot\n"; + + if (!is_absolute_local_path(path)) + return "error path must be absolute\n"; + + std::array buf{}; + + // Checked before opening, so that devices and pipes are rejected without ever being opened + fs::stat_t info{}; + if (!fs::get_stat(path, info) || info.is_directory || info.size != buf.size()) + return "error not a skylander file\n"; + + fs::file sky_file(path, fs::read + fs::write + fs::lock); + if (!sky_file) + return "error cannot open file\n"; + + if (sky_file.read(buf.data(), buf.size()) != buf.size()) + return "error file too small\n"; + + if (read_from_ptr>(buf, 0x1E) != skylander_crc16(0xFFFF, buf.data(), 0x1E)) + return "error not a skylander file\n"; + + const u8 result = g_skyportal.load_skylander(buf, std::move(sky_file), slot); + if (result == 0xFF) + return "error no free slot\n"; + + return "ok " + std::to_string(result) + "\n"; +} + +static std::string handle_remove(int slot) +{ + if (slot < 0 || slot > 7) + return "error invalid slot\n"; + + if (!g_skyportal.remove_skylander(static_cast(slot))) + return "error slot empty\n"; + + return "ok\n"; +} + +static std::string handle_status() +{ + std::string result; + for (u8 i = 0; i < 8; i++) + { + u8 status; + u16 id, variant; + g_skyportal.get_figure_info(i, status, id, variant); + if (status & 1) + result += "slot" + std::to_string(i) + " loaded " + std::to_string(id) + " " + std::to_string(variant) + "\n"; + else + result += "slot" + std::to_string(i) + " empty\n"; + } + result += "ok\n"; + return result; +} + +static std::string handle_clear() +{ + for (u8 i = 0; i < 8; i++) + g_skyportal.remove_skylander(i); + return "ok\n"; +} + +static bool parse_int(const std::string& s, int& out) +{ + if (s.empty()) + return false; + char* end; + errno = 0; + const long val = std::strtol(s.c_str(), &end, 10); + if (end == s.c_str() || *end != '\0' || errno != 0) + return false; + out = static_cast(val); + return true; +} + +static std::string process_command(const std::string& line) +{ + if (line.empty()) + return "error empty command\n"; + + const usz first_space = line.find(' '); + const std::string cmd = line.substr(0, first_space); + + if (cmd == "status") + return handle_status(); + + if (cmd == "clear") + return handle_clear(); + + if (cmd == "remove") + { + if (first_space == std::string::npos) + return "error missing slot\n"; + int slot; + if (!parse_int(line.substr(first_space + 1), slot)) + return "error invalid slot\n"; + return handle_remove(slot); + } + + if (cmd == "load") + { + if (first_space == std::string::npos) + return "error missing arguments\n"; + const std::string rest = line.substr(first_space + 1); + const usz second_space = rest.find(' '); + if (second_space == std::string::npos) + return "error missing path\n"; + int slot; + if (!parse_int(rest.substr(0, second_space), slot)) + return "error invalid arguments\n"; + const std::string path = rest.substr(second_space + 1); + return handle_load(slot, path); + } + + return "error unknown command\n"; +} + +static bool sky_send(sky_socket_t s, std::string_view data) +{ + while (!data.empty()) + { + const int sent = sky_send_some(s, data.data(), ::narrow(data.size())); + + if (sent > 0) + data.remove_prefix(sent); + else if (sent < 0 && sky_interrupted()) + continue; + else + return false; + } + + return true; +} + +static bool read_command(sky_socket_t client_sock, std::string& line) +{ + if (!sky_set_recv_timeout(client_sock, client_timeout_ms)) + return false; + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(client_timeout_ms); + + while (line.size() < max_command_size && std::chrono::steady_clock::now() < deadline) + { + char ch; + const int received = sky_recv(client_sock, &ch, 1); + + if (received == 0) + return true; + + if (received < 0) + { + if (sky_interrupted()) + continue; + + return false; + } + + if (ch == '\n' || ch == '\r') + return true; + + line += ch; + } + + return false; +} + +void SkylanderPortalIPCServer::operator()() +{ + const int port = g_cfg_sky_ipc.get_port(); + +#ifdef _WIN32 + WSADATA wsa{}; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) + { + skylander_ipc_log.error("Cannot initialize winsock"); + return; + } + + const sky_socket_t listen_sock = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sock == sky_invalid_socket) + { + skylander_ipc_log.error("Cannot create socket"); + WSACleanup(); + return; + } + + if (const char* failure = pine::bind_and_listen(listen_sock, static_cast(port))) + { + skylander_ipc_log.error("Cannot start server on port %d: %s", port, failure); + sky_close(listen_sock); + WSACleanup(); + return; + } + + skylander_ipc_log.notice("Server started on port %d", port); +#else + const std::string socket_path = pine::get_socket_path("rpcs3.skylanders.sock", port, g_cfg_sky_ipc.sky_ipc_port.def); + + const sky_socket_t listen_sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (listen_sock == sky_invalid_socket) + { + skylander_ipc_log.error("Cannot create socket"); + return; + } + + if (const char* failure = pine::bind_and_listen(listen_sock, socket_path)) + { + skylander_ipc_log.error("Cannot start server at %s: %s", socket_path, failure); + sky_close(listen_sock); + return; + } + + skylander_ipc_log.notice("Server started at %s", socket_path); +#endif + + while (thread_ctrl::state() != thread_state::aborting) + { + pollfd pfd{}; + pfd.fd = listen_sock; + pfd.events = POLLIN; + +#ifdef _WIN32 + const int poll_result = WSAPoll(&pfd, 1, 10); +#else + const int poll_result = poll(&pfd, 1, 10); +#endif + if (poll_result < 0 && !sky_interrupted()) + { + skylander_ipc_log.error("Waiting for connections failed, stopping server"); + break; + } + + if (poll_result <= 0) + continue; + + const sky_socket_t client_sock = accept(listen_sock, nullptr, nullptr); + if (client_sock == sky_invalid_socket) + continue; + + std::string line; + if (read_command(client_sock, line) && !line.empty()) + { + if (!sky_send(client_sock, process_command(line))) + skylander_ipc_log.warning("Failed to send the response"); + } + + sky_close(client_sock); + } + + sky_close(listen_sock); +#ifdef _WIN32 + WSACleanup(); +#else + unlink(socket_path.c_str()); +#endif + + skylander_ipc_log.notice("Server stopped"); +} + +SkylanderPortalIPCServer& SkylanderPortalIPCServer::operator=(thread_state) +{ + return *this; +} + +SkylanderPortalIPCServerManager::SkylanderPortalIPCServerManager(bool enabled) +{ + set_server_enabled(enabled); +} + +void SkylanderPortalIPCServerManager::set_server_enabled(bool enabled) +{ + if (enabled) + { + const int port = g_cfg_sky_ipc.get_port(); + if (!m_server || port != m_old_port) + { + skylander_ipc_log.notice("Starting server with port %d", port); + m_server = std::make_unique(); + m_old_port = port; + } + } + else if (m_server) + { + skylander_ipc_log.notice("Stopping server"); + m_server.reset(); + } +} diff --git a/rpcs3/Emu/Io/SkylanderPortalIPC.h b/rpcs3/Emu/Io/SkylanderPortalIPC.h new file mode 100644 index 0000000000..ca88b16b1e --- /dev/null +++ b/rpcs3/Emu/Io/SkylanderPortalIPC.h @@ -0,0 +1,20 @@ +#pragma once +#include "Utilities/Thread.h" + +struct SkylanderPortalIPCServer +{ + static constexpr auto thread_name = "Skylanders Portal IPC"sv; + void operator()(); + SkylanderPortalIPCServer& operator=(thread_state); +}; + +class SkylanderPortalIPCServerManager +{ + using server_thread = named_thread; + std::unique_ptr m_server; + int m_old_port = 0; + +public: + explicit SkylanderPortalIPCServerManager(bool enabled); + void set_server_enabled(bool enabled); +}; diff --git a/rpcs3/Emu/Io/SkylanderPortalIPC_config.cpp b/rpcs3/Emu/Io/SkylanderPortalIPC_config.cpp new file mode 100644 index 0000000000..14c0013ff8 --- /dev/null +++ b/rpcs3/Emu/Io/SkylanderPortalIPC_config.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" +#include "SkylanderPortalIPC_config.h" + +cfg_sky_ipc g_cfg_sky_ipc; + +LOG_CHANNEL(skylander_ipc_log, "SkylanderIPC"); + +void cfg_sky_ipc::load() +{ + const std::string path = cfg_sky_ipc::get_path(); + + fs::file cfg_file(path, fs::read); + if (cfg_file) + { + skylander_ipc_log.notice("Loading Skylanders IPC config. Path: %s", path); + from_string(cfg_file.to_string()); + } + else + { + skylander_ipc_log.notice("Skylanders IPC config missing. Using default settings. Path: %s", path); + from_default(); + } +} + +void cfg_sky_ipc::save() const +{ +#ifdef _WIN32 + const std::string path_to_cfg = fs::get_config_dir(true); + if (!fs::create_path(path_to_cfg)) + { + skylander_ipc_log.error("Could not create path: %s", path_to_cfg); + } +#endif + + const std::string path = cfg_sky_ipc::get_path(); + + if (!cfg::node::save(path)) + { + skylander_ipc_log.error("Could not save config: %s (error=%s)", path, fs::g_tls_error); + } +} + +std::string cfg_sky_ipc::get_path() +{ + return fs::get_config_dir(true) + "sky_ipc.yml"; +} + +bool cfg_sky_ipc::get_server_enabled() const +{ + return sky_ipc_server_enabled.get(); +} + +int cfg_sky_ipc::get_port() const +{ + return sky_ipc_port; +} + +void cfg_sky_ipc::set_server_enabled(const bool enabled) +{ + this->sky_ipc_server_enabled.set(enabled); +} + +void cfg_sky_ipc::set_port(const int port) +{ + this->sky_ipc_port.set(port); +} diff --git a/rpcs3/Emu/Io/SkylanderPortalIPC_config.h b/rpcs3/Emu/Io/SkylanderPortalIPC_config.h new file mode 100644 index 0000000000..84091a9266 --- /dev/null +++ b/rpcs3/Emu/Io/SkylanderPortalIPC_config.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Utilities/Config.h" + +struct cfg_sky_ipc : cfg::node +{ + cfg::_bool sky_ipc_server_enabled{ this, "Skylanders IPC Server enabled", false }; + cfg::_int<1025, 65535> sky_ipc_port{ this, "Skylanders IPC Port", 28013 }; + + void load(); + void save() const; + + bool get_server_enabled() const; + int get_port() const; + + void set_server_enabled(const bool enabled); + void set_port(const int port); + +private: + static std::string get_path(); +}; + +extern cfg_sky_ipc g_cfg_sky_ipc; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index c918fd5a5f..cb0b142d6e 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -56,6 +56,8 @@ #include "Utilities/JIT.h" #include "Emu/IPC_socket.h" +#include "Emu/Io/SkylanderPortalIPC.h" +#include "Emu/Io/SkylanderPortalIPC_config.h" #if defined(HAVE_VULKAN) #include "Emu/RSX/VK/VulkanAPI.h" @@ -822,6 +824,15 @@ void Emulator::Init() { g_fxo->init(true); } + + // Load Skylanders IPC config and start server if enabled + g_cfg_sky_ipc.load(); + sys_log.notice("Using Skylanders IPC config:\n%s", g_cfg_sky_ipc.to_string()); + + if (g_cfg_sky_ipc.get_server_enabled()) + { + g_fxo->init(true); + } } void Emulator::SetUsr(const std::string& user) diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index c4994f98f7..1bf0276e49 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -115,6 +115,7 @@ + @@ -486,6 +487,7 @@ + @@ -663,6 +665,7 @@ + @@ -883,6 +886,7 @@ + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 36bc3dacef..a348720145 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -891,6 +891,12 @@ Emu\Io + + Emu\Io + + + Emu\Io + Emu\Io @@ -2220,6 +2226,12 @@ Emu\Io + + Emu\Io + + + Emu\Io + Emu\Io diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 214e90fcf5..9e64183fc9 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -342,6 +342,9 @@ true + + true + true @@ -657,6 +660,9 @@ true + + true + true @@ -880,6 +886,7 @@ + @@ -1618,6 +1625,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\protobuf\protobuf\src" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 3c4fcc2ba2..2929544456 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1035,6 +1035,15 @@ Generated Files\Release + + Gui\ipc + + + Generated Files\Debug + + + Generated Files\Release + Gui\utils @@ -1861,6 +1870,9 @@ Gui\ipc + + Gui\ipc + Gui\settings diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 40ce350e47..9aaaa7ca79 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -54,6 +54,7 @@ add_library(rpcs3_ui STATIC input_dialog.cpp instruction_editor_dialog.cpp ipc_settings_dialog.cpp + sky_ipc_settings_dialog.cpp kamen_rider_dialog.cpp kernel_explorer.cpp localized.cpp diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 4b24210680..7e1c47ed72 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -35,6 +35,7 @@ #include "camera_settings_dialog.h" #include "ps_move_tracker_dialog.h" #include "ipc_settings_dialog.h" +#include "sky_ipc_settings_dialog.h" #include "config_checker.h" #include "shortcut_dialog.h" #include "steam_utils.h" @@ -3160,6 +3161,12 @@ void main_window::CreateConnects() dlg.exec(); }); + connect(ui->confSkyIPCAct, &QAction::triggered, this, [this]() + { + sky_ipc_settings_dialog dlg(this); + dlg.exec(); + }); + connect(ui->confAutopauseManagerAct, &QAction::triggered, this, [this]() { auto_pause_settings_dialog dlg(this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index f86a253eb5..21f8e1e2fa 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -324,6 +324,7 @@ + @@ -1423,6 +1424,14 @@ Configure IPC + + + Skylanders Portal IPC + + + Configure Skylanders Portal IPC + + Log Viewer diff --git a/rpcs3/rpcs3qt/sky_ipc_settings_dialog.cpp b/rpcs3/rpcs3qt/sky_ipc_settings_dialog.cpp new file mode 100644 index 0000000000..f5a70840f1 --- /dev/null +++ b/rpcs3/rpcs3qt/sky_ipc_settings_dialog.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sky_ipc_settings_dialog.h" +#include "Emu/Io/SkylanderPortalIPC_config.h" +#include "Emu/Io/SkylanderPortalIPC.h" +#include "Emu/IdManager.h" +#include "Emu/System.h" + +sky_ipc_settings_dialog::sky_ipc_settings_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Skylanders Portal IPC Settings")); + setObjectName("sky_ipc_settings_dialog"); + setMinimumSize(QSize(200, 100)); + + QVBoxLayout* vbox_global = new QVBoxLayout(); + + QCheckBox* checkbox_server_enabled = new QCheckBox(tr("Enable Skylanders Portal IPC Server")); + + QGroupBox* group_server_port = new QGroupBox(tr("IPC Server Port")); + QHBoxLayout* hbox_group_port = new QHBoxLayout(); + QLineEdit* line_edit_server_port = new QLineEdit(); + line_edit_server_port->setValidator(new QIntValidator(1025, 65535, this)); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Save); + buttons->button(QDialogButtonBox::Save)->setDefault(true); + + hbox_group_port->addWidget(line_edit_server_port); + group_server_port->setLayout(hbox_group_port); + + vbox_global->addWidget(checkbox_server_enabled); + vbox_global->addWidget(group_server_port); + vbox_global->addWidget(buttons); + + setLayout(vbox_global); + + connect(buttons, &QDialogButtonBox::accepted, this, [this, checkbox_server_enabled, line_edit_server_port]() + { + bool ok = true; + const bool server_enabled = checkbox_server_enabled->isChecked(); + const int server_port = line_edit_server_port->text().toInt(&ok); + + if (!ok || server_port < 1025 || server_port > 65535) + { + QMessageBox::critical(this, tr("Invalid port"), tr("The server port must be an integer in the range 1025 - 65535!"), QMessageBox::Ok); + return; + } + + g_cfg_sky_ipc.set_server_enabled(server_enabled); + g_cfg_sky_ipc.set_port(server_port); + g_cfg_sky_ipc.save(); + + if (auto manager = g_fxo->try_get()) + { + manager->set_server_enabled(server_enabled); + } + else if (server_enabled && Emu.IsRunning()) + { + g_fxo->init(true); + } + + accept(); + }); + + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + g_cfg_sky_ipc.load(); + + checkbox_server_enabled->setChecked(g_cfg_sky_ipc.get_server_enabled()); + line_edit_server_port->setText(QString::number(g_cfg_sky_ipc.get_port())); +} diff --git a/rpcs3/rpcs3qt/sky_ipc_settings_dialog.h b/rpcs3/rpcs3qt/sky_ipc_settings_dialog.h new file mode 100644 index 0000000000..9241c3d5ce --- /dev/null +++ b/rpcs3/rpcs3qt/sky_ipc_settings_dialog.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class sky_ipc_settings_dialog : public QDialog +{ + Q_OBJECT +public: + sky_ipc_settings_dialog(QWidget* parent = nullptr); +}; diff --git a/rpcs3/rpcs3qt/skylander_dialog.cpp b/rpcs3/rpcs3qt/skylander_dialog.cpp index 9c459ea837..d39a44a2ca 100644 --- a/rpcs3/rpcs3qt/skylander_dialog.cpp +++ b/rpcs3/rpcs3qt/skylander_dialog.cpp @@ -14,7 +14,6 @@ #include skylander_dialog* skylander_dialog::inst = nullptr; -std::optional> skylander_dialog::sky_slots[UI_SKY_NUM]; QString last_skylander_path; static const std::map, const std::string> list_skylanders = { @@ -500,32 +499,6 @@ static const std::map, const std::string> {{3503, 0x0000}, "Kaos Trophy"}, }; -u16 skylander_crc16(u16 init_value, const u8* buffer, u32 size) -{ - constexpr 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; -} - skylander_creator_dialog::skylander_creator_dialog(QWidget* parent) : QDialog(parent) { @@ -729,6 +702,11 @@ skylander_dialog::skylander_dialog(QWidget* parent) setLayout(vbox_panel); update_edits(); + + m_update_timer = new QTimer(this); + m_update_timer->setInterval(500); + connect(m_update_timer, &QTimer::timeout, this, &skylander_dialog::update_edits); + m_update_timer->start(); } skylander_dialog::~skylander_dialog() @@ -746,13 +724,8 @@ skylander_dialog* skylander_dialog::get_dlg(QWidget* parent) void skylander_dialog::clear_skylander(u8 slot) { - if (const auto& slot_infos = sky_slots[slot]) - { - const auto& [cur_slot, id, var] = slot_infos.value(); - g_skyportal.remove_skylander(cur_slot); - sky_slots[slot] = {}; - update_edits(); - } + g_skyportal.remove_skylander(slot); + update_edits(); } void skylander_dialog::create_skylander(u8 slot) @@ -794,14 +767,13 @@ void skylander_dialog::load_skylander_path(u8 slot, const QString& path) } clear_skylander(slot); - - const u16 sky_id = reinterpret_cast&>(data[0x10]); - const u16 sky_var = reinterpret_cast&>(data[0x1C]); - - const u8 portal_slot = g_skyportal.load_skylander(data, std::move(sky_file)); - sky_slots[slot] = std::tuple(portal_slot, sky_id, sky_var); - + const u8 portal_slot = g_skyportal.load_skylander(data, std::move(sky_file), slot); update_edits(); + + if (portal_slot == 0xFF) + { + QMessageBox::warning(this, tr("Failed to load the skylander!"), tr("Failed to load the skylander(%1)!\nSkylander %2 is already in use on the portal.").arg(path).arg(slot + 1), QMessageBox::Ok); + } } void skylander_dialog::update_edits() @@ -809,9 +781,11 @@ void skylander_dialog::update_edits() for (auto i = 0; i < UI_SKY_NUM; i++) { QString display_string; - if (const auto& sd = sky_slots[i]) + u8 status; + u16 sky_id, sky_var; + g_skyportal.get_figure_info(static_cast(i), status, sky_id, sky_var); + if (status & 1) { - const auto& [portal_slot, sky_id, sky_var] = sd.value(); const auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var)); if (found_sky != list_skylanders.cend()) { diff --git a/rpcs3/rpcs3qt/skylander_dialog.h b/rpcs3/rpcs3qt/skylander_dialog.h index bd4224d91b..784ad201be 100644 --- a/rpcs3/rpcs3qt/skylander_dialog.h +++ b/rpcs3/rpcs3qt/skylander_dialog.h @@ -1,10 +1,10 @@ #pragma once -#include #include "util/types.hpp" #include #include +#include constexpr auto UI_SKY_NUM = 8; @@ -42,8 +42,8 @@ protected: protected: QLineEdit* edit_skylanders[UI_SKY_NUM]{}; - static std::optional> sky_slots[UI_SKY_NUM]; private: + QTimer* m_update_timer; static skylander_dialog* inst; };