From afdf6de0413e6098f3f0c2318fe03a04dfaacc6c Mon Sep 17 00:00:00 2001 From: Mandar1jn Date: Thu, 24 Aug 2023 17:24:30 +0200 Subject: [PATCH] Skylanders: Improve figure data view and generation Co-authored-by: deReeperJosh --- Source/Android/jni/SkylanderConfig.cpp | 30 +- Source/Core/Core/CMakeLists.txt | 8 +- .../Emulated/{ => Skylanders}/Skylander.cpp | 154 ++----- .../USB/Emulated/{ => Skylanders}/Skylander.h | 12 +- .../Emulated/Skylanders/SkylanderCrypto.cpp | 154 +++++++ .../USB/Emulated/Skylanders/SkylanderCrypto.h | 22 + .../Emulated/Skylanders/SkylanderFigure.cpp | 423 ++++++++++++++++++ .../USB/Emulated/Skylanders/SkylanderFigure.h | 85 ++++ Source/Core/Core/IOS/USB/Host.cpp | 2 +- Source/Core/Core/System.cpp | 2 +- Source/Core/DolphinLib.props | 8 +- Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + .../SkylanderPortal/SkylanderModifyDialog.cpp | 344 ++++++++++++++ .../SkylanderPortal/SkylanderModifyDialog.h | 29 ++ .../SkylanderPortal/SkylanderPortalWindow.cpp | 45 +- .../SkylanderPortal/SkylanderPortalWindow.h | 4 +- Source/UnitTests/Core/CMakeLists.txt | 2 + .../UnitTests/Core/IOS/USB/SkylandersTest.cpp | 186 ++++++++ Source/UnitTests/UnitTests.vcxproj | 1 + 20 files changed, 1369 insertions(+), 146 deletions(-) rename Source/Core/Core/IOS/USB/Emulated/{ => Skylanders}/Skylander.cpp (93%) rename Source/Core/Core/IOS/USB/Emulated/{ => Skylanders}/Skylander.h (93%) create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h create mode 100644 Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.cpp create mode 100644 Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.h create mode 100644 Source/UnitTests/Core/IOS/USB/SkylandersTest.cpp diff --git a/Source/Android/jni/SkylanderConfig.cpp b/Source/Android/jni/SkylanderConfig.cpp index 76a0718624..51c621c919 100644 --- a/Source/Android/jni/SkylanderConfig.cpp +++ b/Source/Android/jni/SkylanderConfig.cpp @@ -7,7 +7,7 @@ #include "AndroidCommon/AndroidCommon.h" #include "AndroidCommon/IDCache.h" -#include "Core/IOS/USB/Emulated/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/System.h" extern "C" { @@ -107,11 +107,12 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_loadSkylander name = it->second.name; } - return env->NewObject(pair_class, pair_init, - env->NewObject(integer_class, int_init, - system.GetSkylanderPortal().LoadSkylander( - file_data.data(), std::move(sky_file))), - ToJString(env, name)); + return env->NewObject( + pair_class, pair_init, + env->NewObject(integer_class, int_init, + system.GetSkylanderPortal().LoadSkylander( + std::make_unique(std::move(sky_file)))), + ToJString(env, name)); } JNIEXPORT jobject JNICALL @@ -124,7 +125,11 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_createSkyland std::string file_name = GetJString(env, fileName); auto& system = Core::System::GetInstance(); - system.GetSkylanderPortal().CreateSkylander(file_name, sky_id, sky_var); + { + IOS::HLE::USB::SkylanderFigure figure(file_name); + figure.Create(sky_id, sky_var); + figure.Close(); + } system.GetSkylanderPortal().RemoveSkylander(slot); jclass pair_class = env->FindClass("android/util/Pair"); @@ -154,10 +159,11 @@ Java_org_dolphinemu_dolphinemu_features_skylanders_SkylanderConfig_createSkyland name = it->second.name; } - return env->NewObject(pair_class, pair_init, - env->NewObject(integer_class, integer_init, - system.GetSkylanderPortal().LoadSkylander( - file_data.data(), std::move(sky_file))), - ToJString(env, name)); + return env->NewObject( + pair_class, pair_init, + env->NewObject(integer_class, integer_init, + system.GetSkylanderPortal().LoadSkylander( + std::make_unique(std::move(sky_file)))), + ToJString(env, name)); } } diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 4a40eb7ec8..448653847f 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -413,8 +413,12 @@ add_library(core IOS/USB/Common.h IOS/USB/Emulated/Infinity.cpp IOS/USB/Emulated/Infinity.h - IOS/USB/Emulated/Skylander.cpp - IOS/USB/Emulated/Skylander.h + IOS/USB/Emulated/Skylanders/Skylander.cpp + IOS/USB/Emulated/Skylanders/Skylander.h + IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp + IOS/USB/Emulated/Skylanders/SkylanderCrypto.h + IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp + IOS/USB/Emulated/Skylanders/SkylanderFigure.h IOS/USB/Host.cpp IOS/USB/Host.h IOS/USB/OH0/OH0.cpp diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp similarity index 93% rename from Source/Core/Core/IOS/USB/Emulated/Skylander.cpp rename to Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp index d42eb70eba..643c33d4b8 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Skylander.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/Skylander.cpp @@ -1,7 +1,7 @@ // Copyright 2022 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "Core/IOS/USB/Emulated/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include #include @@ -14,6 +14,7 @@ #include "Common/Timer.h" #include "Core/Core.h" #include "Core/HW/Memmap.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h" #include "Core/System.h" namespace IOS::HLE::USB @@ -1030,15 +1031,6 @@ void SkylanderUSB::ScheduleTransfer(std::unique_ptr command, 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); -} - void SkylanderPortal::Activate() { std::lock_guard lock(sky_mutex); @@ -1190,14 +1182,14 @@ void SkylanderPortal::QueryBlock(u8 sky_num, u8 block, u8* reply_buf) reply_buf[0] = 'Q'; reply_buf[2] = block; - if (skylander.status & 1) + if (skylander.status & Skylander::READY) { reply_buf[1] = (0x10 | sky_num); - memcpy(reply_buf + 3, skylander.data.data() + (16 * block), 16); + skylander.figure->GetBlock(block, reply_buf + 3); } else { - reply_buf[1] = sky_num; + reply_buf[1] = 0x01; } } @@ -1216,98 +1208,15 @@ void SkylanderPortal::WriteBlock(u8 sky_num, u8 block, const u8* to_write_buf, u if (skylander.status & 1) { reply_buf[1] = (0x10 | sky_num); - memcpy(skylander.data.data() + (block * 16), to_write_buf, 16); - skylander.Save(); + skylander.figure->SetBlock(block, to_write_buf); + skylander.figure->Save(); } else { - reply_buf[1] = sky_num; + reply_buf[1] = 0x01; } } -static u16 SkylanderCRC16(u16 init_value, const u8* buffer, u32 size) -{ - static constexpr std::array CRC_CCITT_TABLE{ - 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 m_sky_id, u16 m_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 NUID of the figure - Common::Random::Generate(&file_data[0], 4); - - // The BCC (Block Check Character) - file_data[4] = file_data[0] ^ file_data[1] ^ file_data[2] ^ file_data[3]; - - // ATQA - file_data[5] = 0x81; - file_data[6] = 0x01; - - // SAK - file_data[7] = 0x0F; - - // Set the skylander info - memcpy(&file_data[0x10], &m_sky_id, sizeof(m_sky_id)); - memcpy(&file_data[0x1C], &m_sky_var, sizeof(m_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) { if (!IsSkylanderNumberValid(sky_num)) @@ -1317,13 +1226,13 @@ bool SkylanderPortal::RemoveSkylander(u8 sky_num) std::lock_guard lock(sky_mutex); auto& skylander = skylanders[sky_num]; - if (skylander.sky_file.IsOpen()) + if (skylander.figure->FileIsOpen()) { - skylander.Save(); - skylander.sky_file.Close(); + skylander.figure->Save(); + skylander.figure->Close(); } - if (skylander.status & 1) + if (skylander.status & Skylander::READY) { skylander.status = Skylander::REMOVING; skylander.queued_status.push(Skylander::REMOVING); @@ -1334,15 +1243,17 @@ bool SkylanderPortal::RemoveSkylander(u8 sky_num) return false; } -u8 SkylanderPortal::LoadSkylander(u8* buf, File::IOFile in_file) +u8 SkylanderPortal::LoadSkylander(std::unique_ptr figure) { std::lock_guard lock(sky_mutex); u32 sky_serial = 0; + std::array block = {}; + figure->GetBlock(0, block.data()); for (int i = 3; i > -1; i--) { sky_serial <<= 8; - sky_serial |= buf[i]; + sky_serial |= block[i]; } u8 found_slot = 0xFF; @@ -1370,10 +1281,7 @@ u8 SkylanderPortal::LoadSkylander(u8* buf, File::IOFile in_file) 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.figure = std::move(figure); skylander.status = Skylander::ADDED; skylander.queued_status.push(Skylander::ADDED); skylander.queued_status.push(Skylander::READY); @@ -1403,4 +1311,32 @@ std::pair SkylanderPortal::CalculateIDs(const std::array figure; u8 status = 0; std::queue queued_status; - std::array data{}; u32 last_id = 0; - void Save(); enum : u8 { @@ -140,9 +140,9 @@ 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 m_sky_id, u16 m_sky_var); bool RemoveSkylander(u8 sky_num); - u8 LoadSkylander(u8* buf, File::IOFile in_file); + u8 LoadSkylander(std::unique_ptr figure); + Skylander* GetSkylander(u8 slot); std::pair CalculateIDs(const std::array& file_data); private: diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp new file mode 100644 index 0000000000..3361498b80 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp @@ -0,0 +1,154 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h" + +#include +#include +#include + +#include "Common/BitUtils.h" +#include "Common/CommonTypes.h" +#include "Common/Swap.h" + +namespace IOS::HLE::USB::SkylanderCrypto +{ +u16 ComputeCRC16(std::span data) +{ + const u16 polynomial = 0x1021; + + u16 crc = 0xFFFF; + + for (size_t i = 0; i < data.size(); ++i) + { + crc ^= data[i] << 8; + + for (size_t j = 0; j < 8; j++) + { + if (Common::ExtractBit(crc, 15)) + { + crc = (crc << 1) ^ polynomial; + } + else + { + crc <<= 1; + } + } + } + + return crc; +} +// CRC-64 algorithm that is limited to 48 bits every iteration +u64 ComputeCRC48(std::span data) +{ + const u64 polynomial = 0x42f0e1eba9ea3693; + const u64 initial_register_value = 2ULL * 2ULL * 3ULL * 1103ULL * 12868356821ULL; + + u64 crc = initial_register_value; + for (size_t i = 0; i < data.size(); ++i) + { + crc ^= (static_cast(data[i]) << 40); + for (size_t j = 0; j < 8; ++j) + { + if (Common::ExtractBit(crc, 47)) + { + crc = (crc << 1) ^ polynomial; + } + else + { + crc <<= 1; + } + } + } + return crc & 0x0000FFFFFFFFFFFF; +} +u64 CalculateKeyA(u8 sector, std::span nuid) +{ + if (sector == 0) + { + return 73ULL * 2017ULL * 560381651ULL; + } + + std::array data = {nuid[0], nuid[1], nuid[2], nuid[3], sector}; + + u64 big_endian_crc = ComputeCRC48(data); + u64 little_endian_crc = Common::swap64(big_endian_crc) >> 16; + + return little_endian_crc; +} +void ComputeChecksumType0(const u8* data_start, u8* output) +{ + std::array input = {}; + memcpy(input.data(), data_start, 0x1E); + u16 crc = ComputeCRC16(input); + memcpy(output, &crc, 2); +} +void ComputeChecksumType1(const u8* data_start, u8* output) +{ + std::array input = {}; + memcpy(input.data(), data_start, 0x10); + input[0xE] = 0x05; + input[0xF] = 0x00; + u16 crc = ComputeCRC16(input); + memcpy(output, &crc, 2); +} +void ComputeChecksumType2(const u8* data_start, u8* output) +{ + std::array input = {}; + memcpy(input.data(), data_start, 0x20); + memcpy(input.data() + 0x20, data_start + 0x30, 0x10); + u16 crc = ComputeCRC16(input); + memcpy(output, &crc, 2); +} +void ComputeChecksumType3(const u8* data_start, u8* output) +{ + std::array input = {}; + memcpy(input.data(), data_start, 0x20); + memcpy(input.data() + 0x20, data_start + 0x30, 0x10); + u16 crc = ComputeCRC16(input); + memcpy(output, &crc, 2); +} + +void ComputeChecksumType6(const u8* data_start, u8* output) +{ + std::array input = {}; + memcpy(input.data(), data_start, 0x20); + memcpy(input.data() + 0x20, data_start + 0x30, 0x20); + + input[0x0] = 0x06; + input[0x1] = 0x01; + + u16 crc = ComputeCRC16(input); + memcpy(output, &crc, 2); +} +std::array ComputeToyCode(u64 code) +{ + if (code == 0) + { + static constexpr std::array invalid_code_result{ + static_cast('N'), static_cast('/'), static_cast('A')}; + return invalid_code_result; + } + + std::array code_bytes; + for (size_t i = 0; i < code_bytes.size(); ++i) + { + code_bytes[i] = static_cast(code % 29); + code /= 29; + } + + static constexpr char lookup_table[] = "23456789BCDFGHJKLMNPQRSTVWXYZ"; + std::array code_chars; + for (size_t i = 0; i < code_bytes.size(); ++i) + { + code_chars[i] = static_cast(lookup_table[code_bytes[9 - i]]); + } + + std::array result; + std::memcpy(&result[0], &code_chars[0], 5); + result[5] = static_cast('-'); + std::memcpy(&result[6], &code_chars[5], 5); + + return result; +} +} // namespace IOS::HLE::USB::SkylanderCrypto diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h new file mode 100644 index 0000000000..fb522d9c4d --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h @@ -0,0 +1,22 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +namespace IOS::HLE::USB::SkylanderCrypto +{ +u16 ComputeCRC16(std::span data); +u64 ComputeCRC48(std::span data); +u64 CalculateKeyA(u8 sector, std::span nuid); +void ComputeChecksumType0(const u8* data_start, u8* output); +void ComputeChecksumType1(const u8* data_start, u8* output); +void ComputeChecksumType2(const u8* data_start, u8* output); +void ComputeChecksumType3(const u8* data_start, u8* output); +void ComputeChecksumType6(const u8* data_start, u8* output); +std::array ComputeToyCode(u64 code); +} // namespace IOS::HLE::USB::SkylanderCrypto diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp new file mode 100644 index 0000000000..56535f9c6b --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp @@ -0,0 +1,423 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "Common/BitUtils.h" +#include "Common/CommonTypes.h" +#include "Common/IOFile.h" +#include "Common/Logging/Log.h" +#include "Common/Random.h" +#include "Common/StringUtil.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h" + +using namespace IOS::HLE::USB::SkylanderCrypto; + +namespace IOS::HLE::USB +{ +void SkylanderFigure::PopulateSectorTrailers() +{ + // Set the sector permissions + u32 first_block = 0x690F0F0F; + u32 other_blocks = 0x69080F7F; + memcpy(&m_data[0x36], &first_block, sizeof(first_block)); + for (size_t index = 1; index < 0x10; index++) + { + memcpy(&m_data[(index * 0x40) + 0x36], &other_blocks, sizeof(other_blocks)); + } +} + +void SkylanderFigure::PopulateKeys() +{ + for (u8 sector = 0; sector < 0x10; sector++) + { + u16 key_offset = (sector * 64) + (3 * 16); + u64 key = CalculateKeyA(sector, std::span(m_data.begin(), 4)); + + for (u32 j = 0; j < 6; j++) + { + u16 index = key_offset + (5 - j); + u8 byte = (key >> (j * 8)) & 0xFF; + m_data[index] = byte; + } + } +} + +SkylanderFigure::SkylanderFigure(const std::string& file_path) +{ + m_sky_file = File::IOFile(file_path, "w+b"); + m_data = {}; +} +// Generate a AES key without the block filled in +void SkylanderFigure::GenerateIncompleteHashIn(u8* dest) const +{ + std::array hash_in = {}; + + // copy first 2 blocks into hash + GetBlock(0, hash_in.data()); + GetBlock(1, hash_in.data() + 0x10); + + // Skip 1 byte. Is a block index that needs to be set per block. + + // Byte array of ascii string " Copyright (C) 2010 Activision. All Rights Reserved.". The space at + // the start of the string is intentional + static constexpr std::array HASH_CONST = { + 0x20, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x28, 0x43, 0x29, + 0x20, 0x32, 0x30, 0x31, 0x30, 0x20, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x73, 0x69, + 0x6F, 0x6E, 0x2E, 0x20, 0x41, 0x6C, 0x6C, 0x20, 0x52, 0x69, 0x67, 0x68, 0x74, 0x73, + 0x20, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x2E, 0x20}; + + memcpy(hash_in.data() + 0x21, HASH_CONST.data(), HASH_CONST.size()); + + memcpy(dest, hash_in.data(), 0x56); +} +void SkylanderFigure::Encrypt(std::span input) +{ + std::array hash_in = {}; + + GenerateIncompleteHashIn(hash_in.data()); + + std::array encrypted = {}; + + std::array current_block = {}; + + // Run for every block + for (u8 i = 0; i < 64; ++i) + { + memcpy(current_block.data(), input.data() + (i * BLOCK_SIZE), BLOCK_SIZE); + + // Skip sector trailer and the first 8 blocks + if (((i + 1) % 4 == 0) || i < 8) + { + memcpy(encrypted.data() + (i * BLOCK_SIZE), current_block.data(), BLOCK_SIZE); + continue; + } + + // Block index + hash_in[0x20] = i; + + std::array hash_out = {}; + + mbedtls_md5_ret(hash_in.data(), 0x56, hash_out.data()); + + mbedtls_aes_context aes_context = {}; + + mbedtls_aes_setkey_enc(&aes_context, hash_out.data(), 128); + + mbedtls_aes_crypt_ecb(&aes_context, MBEDTLS_AES_ENCRYPT, current_block.data(), + encrypted.data() + (i * BLOCK_SIZE)); + } + + memcpy(m_data.data(), encrypted.data(), FIGURE_SIZE); + + DEBUG_LOG_FMT(IOS_USB, "Encrypted skylander data: \n{}", HexDump(encrypted.data(), FIGURE_SIZE)); +} +SkylanderFigure::SkylanderFigure(File::IOFile file) +{ + m_sky_file = std::move(file); + m_sky_file.Seek(0, File::SeekOrigin::Begin); + m_sky_file.ReadBytes(m_data.data(), m_data.size()); +} +bool SkylanderFigure::Create(u16 sky_id, u16 sky_var, + std::optional> requested_nuid) +{ + if (!m_sky_file) + { + return false; + } + + memset(m_data.data(), 0, m_data.size()); + + PopulateSectorTrailers(); + + // Set the NUID of the figure + if (requested_nuid) + std::memcpy(&m_data[0], requested_nuid->data(), 4); + else + Common::Random::Generate(&m_data[0], 4); + + // The BCC (Block Check Character) + m_data[4] = m_data[0] ^ m_data[1] ^ m_data[2] ^ m_data[3]; + + // ATQA + m_data[5] = 0x81; + m_data[6] = 0x01; + + // SAK + m_data[7] = 0x0F; + + // Set the skylander info + memcpy(&m_data[0x10], &sky_id, sizeof(sky_id)); + memcpy(&m_data[0x1C], &sky_var, sizeof(sky_var)); + + // Set checksum + ComputeChecksumType0(m_data.data(), m_data.data() + 0x1E); + + PopulateKeys(); + + Save(); + return true; +} +void SkylanderFigure::Save() +{ + m_sky_file.Seek(0, File::SeekOrigin::Begin); + m_sky_file.WriteBytes(m_data.data(), FIGURE_SIZE); +} + +void SkylanderFigure::GetBlock(u8 index, u8* dest) const +{ + memcpy(dest, m_data.data() + (index * BLOCK_SIZE), BLOCK_SIZE); +} + +FigureData SkylanderFigure::GetData() const +{ + FigureData figure_data = {.figure_id = Common::BitCastPtr(m_data.data() + 0x10), + .variant_id = Common::BitCastPtr(m_data.data() + 0x1C)}; + + auto filter = std::make_pair(figure_data.figure_id, figure_data.variant_id); + Type type = Type::Item; + if (IOS::HLE::USB::list_skylanders.count(filter) != 0) + { + auto found = IOS::HLE::USB::list_skylanders.at(filter); + type = found.type; + } + + figure_data.normalized_type = NormalizeSkylanderType(type); + if (figure_data.normalized_type == Type::Skylander) + { + std::array decrypted = {}; + + DecryptFigure(&decrypted); + + // Area with highest area counter is the newest + u16 area_offset = ((decrypted[0x89] + 1U) != decrypted[0x249]) ? 0x80 : 0x240; + + figure_data.skylander_data = { + .money = Common::BitCastPtr(decrypted.data() + area_offset + 0x3), + .hero_level = Common::BitCastPtr(decrypted.data() + area_offset + 0x5A), + .playtime = Common::BitCastPtr(decrypted.data() + area_offset + 0x5), + .last_reset = {.minute = Common::BitCastPtr(decrypted.data() + area_offset + 0x60), + .hour = Common::BitCastPtr(decrypted.data() + area_offset + 0x61), + .day = Common::BitCastPtr(decrypted.data() + area_offset + 0x62), + .month = Common::BitCastPtr(decrypted.data() + area_offset + 0x63), + .year = Common::BitCastPtr(decrypted.data() + area_offset + 0x64)}, + .last_placed = {.minute = Common::BitCastPtr(decrypted.data() + area_offset + 0x50), + .hour = Common::BitCastPtr(decrypted.data() + area_offset + 0x51), + .day = Common::BitCastPtr(decrypted.data() + area_offset + 0x52), + .month = Common::BitCastPtr(decrypted.data() + area_offset + 0x53), + .year = Common::BitCastPtr(decrypted.data() + area_offset + 0x54)}}; + + figure_data.skylander_data.toy_code = + ComputeToyCode(Common::BitCastPtr(decrypted.data() + 0x14)); + + std::array nickname{}; + + // First nickname half + for (size_t i = 0; i < 8; ++i) + { + nickname[i] = Common::BitCastPtr(decrypted.data() + area_offset + 0x20 + (i * 2)); + } + + // Second nickname half + for (size_t i = 0; i < 8; ++i) + { + nickname[i + 8] = Common::BitCastPtr(decrypted.data() + area_offset + 0x40 + (i * 2)); + } + + figure_data.skylander_data.nickname = nickname; + } + else if (figure_data.normalized_type == Type::Trophy) + { + std::array decrypted = {}; + + DecryptFigure(&decrypted); + + // Area with highest area counter is the newest + u16 area_offset = ((decrypted[0x89] + 1U) != decrypted[0x249]) ? 0x80 : 0x240; + + figure_data.trophy_data.unlocked_villains = *(decrypted.data() + area_offset + 0x14); + } + + return figure_data; +} +void SkylanderFigure::SetData(FigureData* figure_data) +{ + std::array decrypted = {}; + + DecryptFigure(&decrypted); + + if (figure_data->normalized_type == Type::Skylander) + { + // Only update area with lowest counter + u16 area_offset = (decrypted[0x89] != (decrypted[0x249] + 1U)) ? 0x80 : 0x240; + u16 other_area_offset = (area_offset == 0x80) ? 0x240 : 0x80; + + memcpy(decrypted.data() + area_offset + 0x3, &figure_data->skylander_data.money, 2); + memcpy(decrypted.data() + area_offset + 0x5A, &figure_data->skylander_data.hero_level, 2); + memcpy(decrypted.data() + area_offset + 0x5, &figure_data->skylander_data.playtime, 4); + + { + memcpy(decrypted.data() + area_offset + 0x60, &figure_data->skylander_data.last_reset.minute, + 1); + memcpy(decrypted.data() + area_offset + 0x61, &figure_data->skylander_data.last_reset.hour, + 1); + memcpy(decrypted.data() + area_offset + 0x62, &figure_data->skylander_data.last_reset.day, 1); + memcpy(decrypted.data() + area_offset + 0x63, &figure_data->skylander_data.last_reset.month, + 1); + memcpy(decrypted.data() + area_offset + 0x64, &figure_data->skylander_data.last_reset.year, + 2); + } + + { + memcpy(decrypted.data() + area_offset + 0x50, &figure_data->skylander_data.last_placed.minute, + 1); + memcpy(decrypted.data() + area_offset + 0x51, &figure_data->skylander_data.last_placed.hour, + 1); + memcpy(decrypted.data() + area_offset + 0x52, &figure_data->skylander_data.last_placed.day, + 1); + memcpy(decrypted.data() + area_offset + 0x53, &figure_data->skylander_data.last_placed.month, + 1); + memcpy(decrypted.data() + area_offset + 0x54, &figure_data->skylander_data.last_placed.year, + 2); + } + + { + for (size_t i = 0; i < 8; ++i) + { + memcpy(decrypted.data() + area_offset + 0x20 + (i * 2), + &figure_data->skylander_data.nickname[i], 0x2); + } + + for (size_t i = 0; i < 8; ++i) + { + memcpy(decrypted.data() + area_offset + 0x40 + (i * 2), + &figure_data->skylander_data.nickname[8 + i], 0x2); + } + } + + { + ComputeChecksumType3(decrypted.data() + area_offset + 0x50, + decrypted.data() + area_offset + 0xA); + ComputeChecksumType2(decrypted.data() + area_offset + 0x10, + decrypted.data() + area_offset + 0xC); + decrypted[area_offset + 9] = decrypted[other_area_offset + 9] + 1; + ComputeChecksumType1(decrypted.data() + area_offset, decrypted.data() + area_offset + 0xE); + } + + { + area_offset = (decrypted[0x112] != (decrypted[0x2D2] + 1U)) ? 0x110 : 0x2D0; + other_area_offset = (area_offset == 0x110) ? 0x2D0 : 0x110; + + decrypted[area_offset + 2] = decrypted[other_area_offset + 2] + 1; + + ComputeChecksumType6(decrypted.data() + area_offset, decrypted.data() + area_offset); + } + } + else if (figure_data->normalized_type == Type::Trophy) + { + u16 area_offset = (decrypted[0x89] != (decrypted[0x249] + 1U)) ? 0x80 : 0x240; + u16 other_area_offset = (area_offset == 0x80) ? 0x240 : 0x80; + + { + memcpy(decrypted.data() + area_offset + 0x14, &figure_data->trophy_data.unlocked_villains, 1); + + { + ComputeChecksumType3(decrypted.data() + area_offset + 0x50, + decrypted.data() + area_offset + 0xA); + ComputeChecksumType2(decrypted.data() + area_offset + 0x10, + decrypted.data() + area_offset + 0xC); + decrypted[area_offset + 9] = decrypted[other_area_offset + 9] + 1; + ComputeChecksumType1(decrypted.data() + area_offset, decrypted.data() + area_offset + 0xE); + } + } + + { + area_offset = (decrypted[0x112] != (decrypted[0x2D2] + 1U)) ? 0x110 : 0x2D0; + other_area_offset = (area_offset == 0x110) ? 0x2D0 : 0x110; + + decrypted[area_offset + 2] = decrypted[other_area_offset + 2] + 1; + + ComputeChecksumType6(decrypted.data() + area_offset, decrypted.data() + area_offset); + } + } + + // Encrypt again + Encrypt(decrypted); + + Save(); +} +void SkylanderFigure::DecryptFigure(std::array* dest) const +{ + std::array hash_in = {}; + + GenerateIncompleteHashIn(hash_in.data()); + + std::array decrypted = {}; + + std::array current_block = {}; + + // Run for every block + for (u8 i = 0; i < BLOCK_COUNT; ++i) + { + GetBlock(i, current_block.data()); + + // Skip sector trailer and the first 8 blocks + if (((i + 1) % 4 == 0) || i < 8) + { + memcpy(decrypted.data() + (i * BLOCK_SIZE), current_block.data(), BLOCK_SIZE); + continue; + } + + // Check if block is all 0 + u16 total = 0; + for (size_t j = 0; j < BLOCK_SIZE; j++) + { + total += current_block[j]; + } + if (total == 0) + { + continue; + } + + // Block index + hash_in[0x20] = i; + + std::array hash_out = {}; + + mbedtls_md5_ret(hash_in.data(), 0x56, hash_out.data()); + + mbedtls_aes_context aes_context = {}; + + mbedtls_aes_setkey_dec(&aes_context, hash_out.data(), 128); + + mbedtls_aes_crypt_ecb(&aes_context, MBEDTLS_AES_DECRYPT, current_block.data(), + decrypted.data() + (i * BLOCK_SIZE)); + } + + memcpy(dest->data(), decrypted.data(), FIGURE_SIZE); + + DEBUG_LOG_FMT(IOS_USB, "Decrypted skylander data: \n{}", HexDump(decrypted.data(), FIGURE_SIZE)); +} +void SkylanderFigure::Close() +{ + m_sky_file.Close(); +} +void SkylanderFigure::SetBlock(u8 block, const u8* buf) +{ + memcpy(m_data.data() + (block * BLOCK_SIZE), buf, BLOCK_SIZE); +} +bool SkylanderFigure::FileIsOpen() const +{ + return m_sky_file.IsOpen(); +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h new file mode 100644 index 0000000000..0e652d66ca --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h @@ -0,0 +1,85 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/IOFile.h" + +namespace IOS::HLE::USB +{ +enum class Type : u8; + +struct SkylanderDateTime final +{ + u8 minute; + u8 hour; + u8 day; + u8 month; + u16 year; +}; + +struct SkylanderData final +{ + std::array toy_code; + u16 money; + u16 hero_level; + u32 playtime; + // Null-terminated UTF-16 string + std::array nickname; + SkylanderDateTime last_reset; + SkylanderDateTime last_placed; +}; + +struct Trophydata final +{ + u8 unlocked_villains; +}; + +struct FigureData final +{ + Type normalized_type; + u16 figure_id; + u16 variant_id; + union + { + SkylanderData skylander_data; + Trophydata trophy_data; + }; +}; + +constexpr u32 BLOCK_COUNT = 0x40; +constexpr u32 BLOCK_SIZE = 0x10; +constexpr u32 FIGURE_SIZE = BLOCK_COUNT * BLOCK_SIZE; + +class SkylanderFigure +{ +public: + SkylanderFigure(const std::string& file_path); + SkylanderFigure(File::IOFile file); + bool Create(u16 sky_id, u16 sky_var, + std::optional> requested_nuid = std::nullopt); + void Save(); + void Close(); + bool FileIsOpen() const; + void GetBlock(u8 index, u8* dest) const; + void SetBlock(u8 block, const u8* buf); + void DecryptFigure(std::array* dest) const; + FigureData GetData() const; + void SetData(FigureData* data); + +private: + void PopulateSectorTrailers(); + void PopulateKeys(); + void GenerateIncompleteHashIn(u8* dest) const; + void Encrypt(std::span); + + File::IOFile m_sky_file; + std::array m_data; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index 42deee2c62..c2641932ac 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -23,7 +23,7 @@ #include "Core/Core.h" #include "Core/IOS/USB/Common.h" #include "Core/IOS/USB/Emulated/Infinity.h" -#include "Core/IOS/USB/Emulated/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/IOS/USB/LibusbDevice.h" #include "Core/NetPlayProto.h" #include "Core/System.h" diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp index aeb81e5122..9ab1021f38 100644 --- a/Source/Core/Core/System.cpp +++ b/Source/Core/Core/System.cpp @@ -26,7 +26,7 @@ #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/PowerPC.h" #include "IOS/USB/Emulated/Infinity.h" -#include "IOS/USB/Emulated/Skylander.h" +#include "IOS/USB/Emulated/Skylanders/Skylander.h" #include "VideoCommon/Assets/CustomAssetLoader.h" #include "VideoCommon/CommandProcessor.h" #include "VideoCommon/Fifo.h" diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 8c86b02051..2c75fc9d1d 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -384,7 +384,9 @@ - + + + @@ -1026,7 +1028,9 @@ - + + + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index b271fab7fb..391b1f1afb 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -339,6 +339,8 @@ add_executable(dolphin-emu Settings/USBDeviceAddToWhitelistDialog.h Settings/WiiPane.cpp Settings/WiiPane.h + SkylanderPortal/SkylanderModifyDialog.cpp + SkylanderPortal/SkylanderModifyDialog.h SkylanderPortal/SkylanderPortalWindow.cpp SkylanderPortal/SkylanderPortalWindow.h TAS/GCTASInputWindow.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 35f1110e4c..b16a5396d0 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -210,6 +210,7 @@ + @@ -253,6 +254,7 @@ + diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.cpp b/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.cpp new file mode 100644 index 0000000000..f450ec50ce --- /dev/null +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.cpp @@ -0,0 +1,344 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "SkylanderModifyDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" +#include "Core/System.h" + +#include "DolphinQt/QtUtils/SetWindowDecorations.h" + +SkylanderModifyDialog::SkylanderModifyDialog(QWidget* parent, u8 slot) + : QDialog(parent), m_slot(slot) +{ + bool should_show = true; + + QVBoxLayout* layout = new QVBoxLayout; + + IOS::HLE::USB::Skylander* skylander = + Core::System::GetInstance().GetSkylanderPortal().GetSkylander(slot); + + m_figure = skylander->figure.get(); + m_figure_data = m_figure->GetData(); + + auto* hbox_name = new QHBoxLayout; + QString name = QString(); + + if ((m_figure_data.skylander_data.nickname[0] != 0x00 && + m_figure_data.normalized_type == IOS::HLE::USB::Type::Skylander)) + { + name = QStringLiteral("\"%1\"").arg(QString::fromUtf16( + reinterpret_cast(m_figure_data.skylander_data.nickname.data()))); + } + else + { + auto found = IOS::HLE::USB::list_skylanders.find( + std::make_pair(m_figure_data.figure_id, m_figure_data.variant_id)); + if (found != IOS::HLE::USB::list_skylanders.end()) + { + name = QString::fromStdString(found->second.name); + } + else + { + // Should never be able to happen. Still good to have + name = + tr("Unknown (Id:%1 Var:%2)").arg(m_figure_data.figure_id).arg(m_figure_data.variant_id); + } + } + + auto* label_name = new QLabel(QString::fromStdString("Modifying Skylander %1").arg(name)); + + hbox_name->addWidget(label_name); + + layout->addLayout(hbox_name); + + m_buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel); + + connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + if (m_figure_data.normalized_type == IOS::HLE::USB::Type::Skylander) + { + PopulateSkylanderOptions(layout); + } + else if (m_figure_data.normalized_type == IOS::HLE::USB::Type::Trophy) + { + should_show &= PopulateTrophyOptions(layout); + } + else if (m_figure_data.normalized_type == IOS::HLE::USB::Type::Item) + { + should_show = false; + QMessageBox::warning( + this, tr("No data to modify!"), + tr("The type of this Skylander does not have any data that can be modified!"), + QMessageBox::Ok); + } + else if (m_figure_data.normalized_type == IOS::HLE::USB::Type::Unknown) + { + should_show = false; + QMessageBox::warning(this, tr("Unknow Skylander type!"), + tr("The type of this Skylander is unknown!"), QMessageBox::Ok); + } + else + { + should_show = false; + QMessageBox::warning( + this, tr("Unable to modify Skylander!"), + tr("The type of this Skylander is unknown, or can't be modified at this time!"), + QMessageBox::Ok); + QMessageBox::warning(this, tr("Can't be modified yet!"), + tr("This Skylander type can't be modified yet!"), QMessageBox::Ok); + } + + layout->addWidget(m_buttons); + + this->setLayout(layout); + + SetQWidgetWindowDecorations(this); + + if (should_show) + { + this->show(); + this->raise(); + } +} + +void SkylanderModifyDialog::PopulateSkylanderOptions(QVBoxLayout* layout) +{ + auto* hbox_toy_code = new QHBoxLayout(); + auto* label_toy_code = new QLabel(tr("Toy code:")); + auto* edit_toy_code = new QLineEdit(QString::fromUtf8(m_figure_data.skylander_data.toy_code)); + edit_toy_code->setDisabled(true); + + auto* hbox_money = new QHBoxLayout(); + auto* label_money = new QLabel(tr("Money:")); + auto* edit_money = new QLineEdit(QStringLiteral("%1").arg(m_figure_data.skylander_data.money)); + + auto* hbox_hero = new QHBoxLayout(); + auto* label_hero = new QLabel(tr("Hero level:")); + auto* edit_hero = + new QLineEdit(QStringLiteral("%1").arg(m_figure_data.skylander_data.hero_level)); + + auto toUtf16 = QStringDecoder(QStringDecoder::Utf16); + auto* hbox_nick = new QHBoxLayout(); + auto* label_nick = new QLabel(tr("Nickname:")); + auto* edit_nick = new QLineEdit(QString::fromUtf16( + reinterpret_cast(m_figure_data.skylander_data.nickname.data()))); + + auto* hbox_playtime = new QHBoxLayout(); + auto* label_playtime = new QLabel(tr("Playtime:")); + auto* edit_playtime = + new QLineEdit(QStringLiteral("%1").arg(m_figure_data.skylander_data.playtime)); + + auto* hbox_last_reset = new QHBoxLayout(); + auto* label_last_reset = new QLabel(tr("Last reset:")); + auto* edit_last_reset = + new QDateTimeEdit(QDateTime(QDate(m_figure_data.skylander_data.last_reset.year, + m_figure_data.skylander_data.last_reset.month, + m_figure_data.skylander_data.last_reset.day), + QTime(m_figure_data.skylander_data.last_reset.hour, + m_figure_data.skylander_data.last_reset.minute))); + + auto* hbox_last_placed = new QHBoxLayout(); + auto* label_last_placed = new QLabel(tr("Last placed:")); + auto* edit_last_placed = + new QDateTimeEdit(QDateTime(QDate(m_figure_data.skylander_data.last_placed.year, + m_figure_data.skylander_data.last_placed.month, + m_figure_data.skylander_data.last_placed.day), + QTime(m_figure_data.skylander_data.last_placed.hour, + m_figure_data.skylander_data.last_placed.minute))); + + edit_money->setValidator(new QIntValidator(0, 65000, this)); + edit_hero->setValidator(new QIntValidator(0, 100, this)); + edit_nick->setValidator(new QRegularExpressionValidator( + QRegularExpression(QString::fromStdString("^\\p{L}{0,15}$")), this)); + edit_playtime->setValidator(new QIntValidator(0, INT_MAX, this)); + edit_last_reset->setDisplayFormat(QString::fromStdString("dd/MM/yyyy hh:mm")); + edit_last_placed->setDisplayFormat(QString::fromStdString("dd/MM/yyyy hh:mm")); + + edit_toy_code->setToolTip(tr("The toy code for this figure. Only available for real figures.")); + edit_money->setToolTip(tr("The amount of money this skylander should have. Between 0 and 65000")); + edit_hero->setToolTip(tr("The hero level of this skylander. Only seen in Skylanders: Spyro's " + "Adventures. Between 0 and 100")); + edit_nick->setToolTip(tr("The nickname for this skylander. Limited to 15 characters")); + edit_playtime->setToolTip( + tr("The total time this figure has been used inside a game in seconds")); + edit_last_reset->setToolTip(tr("The last time the figure has been reset. If the figure has never " + "been reset, the first time the figure was placed on a portal")); + edit_last_placed->setToolTip(tr("The last time the figure has been placed on a portal")); + + hbox_toy_code->addWidget(label_toy_code); + hbox_toy_code->addWidget(edit_toy_code); + + hbox_money->addWidget(label_money); + hbox_money->addWidget(edit_money); + + hbox_hero->addWidget(label_hero); + hbox_hero->addWidget(edit_hero); + + hbox_nick->addWidget(label_nick); + hbox_nick->addWidget(edit_nick); + + hbox_playtime->addWidget(label_playtime); + hbox_playtime->addWidget(edit_playtime); + + hbox_last_reset->addWidget(label_last_reset); + hbox_last_reset->addWidget(edit_last_reset); + + hbox_last_placed->addWidget(label_last_placed); + hbox_last_placed->addWidget(edit_last_placed); + + layout->addLayout(hbox_toy_code); + layout->addLayout(hbox_money); + layout->addLayout(hbox_hero); + layout->addLayout(hbox_nick); + layout->addLayout(hbox_playtime); + layout->addLayout(hbox_last_reset); + layout->addLayout(hbox_last_placed); + + connect(m_buttons, &QDialogButtonBox::accepted, this, [=, this]() { + if (!edit_money->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect money value!"), + tr("Make sure that the money value is between 0 and 65000!"), + QMessageBox::Ok); + } + else if (!edit_hero->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect hero level value!"), + tr("Make sure that the hero level value is between 0 and 100!"), + QMessageBox::Ok); + } + else if (!edit_nick->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect nickname!"), + tr("Make sure that the nickname is between 0 and 15 characters long!"), + QMessageBox::Ok); + } + else if (!edit_playtime->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect playtime value!"), + tr("Make sure that the playtime value is valid!"), QMessageBox::Ok); + } + else if (!edit_last_reset->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect last reset time!"), + tr("Make sure that the last reset datetime value is valid!"), + QMessageBox::Ok); + } + else if (!edit_last_placed->hasAcceptableInput()) + { + QMessageBox::warning(this, tr("Incorrect last placed time!"), + tr("Make sure that the last placed datetime value is valid!"), + QMessageBox::Ok); + } + else + { + m_allow_close = true; + m_figure_data.skylander_data = { + .money = edit_money->text().toUShort(), + .hero_level = edit_hero->text().toUShort(), + .playtime = edit_playtime->text().toUInt(), + .last_reset = {.minute = static_cast(edit_last_reset->time().minute()), + .hour = static_cast(edit_last_reset->time().hour()), + .day = static_cast(edit_last_reset->date().day()), + .month = static_cast(edit_last_reset->date().month()), + .year = static_cast(edit_last_reset->date().year())}, + .last_placed = {.minute = static_cast(edit_last_placed->time().minute()), + .hour = static_cast(edit_last_placed->time().hour()), + .day = static_cast(edit_last_placed->date().day()), + .month = static_cast(edit_last_placed->date().month()), + .year = static_cast(edit_last_placed->date().year())}}; + + std::u16string nickname = edit_nick->text().toStdU16String(); + nickname.copy(reinterpret_cast(m_figure_data.skylander_data.nickname.data()), + nickname.length()); + + if (m_figure->FileIsOpen()) + { + m_figure->SetData(&m_figure_data); + } + else + { + QMessageBox::warning(this, tr("Could not save your changes!"), + tr("The file associated to this file was closed! Did you clear the " + "slot before saving?"), + QMessageBox::Ok); + } + + this->accept(); + } + }); +} + +bool SkylanderModifyDialog::PopulateTrophyOptions(QVBoxLayout* layout) +{ + static constexpr u16 KAOS_TROPHY_ID = 3503; + static constexpr u16 SEA_TROPHY_ID = 3502; + + if (m_figure_data.figure_id == KAOS_TROPHY_ID) + { + QMessageBox::warning(this, tr("Can't edit villains for this trophy!"), + tr("Kaos is the only villain for this trophy and is always unlocked. No " + "need to edit anything!"), + QMessageBox::Ok); + return false; + } + + constexpr size_t MAX_VILLAINS = 4; + std::array shift_distances; + + if (m_figure_data.figure_id == SEA_TROPHY_ID) + shift_distances = {0, 1, 2, 4}; + else + shift_distances = {0, 2, 3, 4}; + + std::array edit_villains; + for (size_t i = 0; i < MAX_VILLAINS; ++i) + { + edit_villains[i] = new QCheckBox(); + edit_villains[i]->setChecked(static_cast(m_figure_data.trophy_data.unlocked_villains & + (0b1 << shift_distances[i]))); + auto* const label = new QLabel(tr("Captured villain %1:").arg(i + 1)); + auto* const hbox = new QHBoxLayout(); + hbox->addWidget(label); + hbox->addWidget(edit_villains[i]); + + layout->addLayout(hbox); + } + + connect(m_buttons, &QDialogButtonBox::accepted, this, [=, this]() { + m_figure_data.trophy_data.unlocked_villains = 0x0; + for (size_t i = 0; i < MAX_VILLAINS; ++i) + m_figure_data.trophy_data.unlocked_villains |= + edit_villains[i]->isChecked() ? (0b1 << shift_distances[i]) : 0b0; + + m_figure->SetData(&m_figure_data); + m_allow_close = true; + this->accept(); + }); + + return true; +} + +void SkylanderModifyDialog::accept() +{ + if (m_allow_close) + { + auto* skylander = Core::System::GetInstance().GetSkylanderPortal().GetSkylander(m_slot); + skylander->queued_status.push(IOS::HLE::USB::Skylander::REMOVED); + skylander->queued_status.push(IOS::HLE::USB::Skylander::ADDED); + skylander->queued_status.push(IOS::HLE::USB::Skylander::READY); + QDialog::accept(); + } +} diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.h b/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.h new file mode 100644 index 0000000000..5df66e8e2a --- /dev/null +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderModifyDialog.h @@ -0,0 +1,29 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Common/CommonTypes.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h" + +class QVBoxLayout; +class QDialogButtonBox; + +class SkylanderModifyDialog : public QDialog +{ +public: + explicit SkylanderModifyDialog(QWidget* parent = nullptr, u8 slot = 0); + +private: + void PopulateSkylanderOptions(QVBoxLayout* layout); + bool PopulateTrophyOptions(QVBoxLayout* layout); + void accept() override; + + bool m_allow_close = false; + u8 m_slot; + IOS::HLE::USB::FigureData m_figure_data; + IOS::HLE::USB::SkylanderFigure* m_figure; + QDialogButtonBox* m_buttons; +}; diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp index 654be376d4..734770108a 100644 --- a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,13 +31,14 @@ #include "Common/FileUtil.h" #include "Core/Config/MainSettings.h" -#include "Core/IOS/USB/Emulated/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/System.h" #include "DolphinQt/QtUtils/DolphinFileDialog.h" #include "DolphinQt/QtUtils/SetWindowDecorations.h" #include "DolphinQt/Resources.h" #include "DolphinQt/Settings.h" +#include "SkylanderModifyDialog.h" SkylanderPortalWindow::SkylanderPortalWindow(QWidget* parent) : QWidget(parent) { @@ -124,15 +126,18 @@ void SkylanderPortalWindow::CreateMainWindow() auto* load_file_btn = new QPushButton(tr("Load File")); auto* clear_btn = new QPushButton(tr("Clear Slot")); auto* load_btn = new QPushButton(tr("Load Slot")); + auto* modify_btn = new QPushButton(tr("Modify Slot")); connect(create_btn, &QAbstractButton::clicked, this, &SkylanderPortalWindow::CreateSkylanderAdvanced); connect(clear_btn, &QAbstractButton::clicked, this, [this]() { ClearSlot(GetCurrentSlot()); }); connect(load_btn, &QAbstractButton::clicked, this, &SkylanderPortalWindow::LoadSelected); connect(load_file_btn, &QAbstractButton::clicked, this, &SkylanderPortalWindow::LoadFromFile); + connect(modify_btn, &QAbstractButton::clicked, this, &SkylanderPortalWindow::ModifySkylander); command_layout->addWidget(create_btn); command_layout->addWidget(load_file_btn); command_layout->addWidget(clear_btn); command_layout->addWidget(load_btn); + command_layout->addWidget(modify_btn); m_command_buttons->setLayout(command_layout); main_layout->addWidget(m_command_buttons); @@ -647,6 +652,20 @@ void SkylanderPortalWindow::CreateSkylanderAdvanced() create_window->raise(); } +void SkylanderPortalWindow::ModifySkylander() +{ + if (auto sky_slot = m_sky_slots[GetCurrentSlot()]) + { + new SkylanderModifyDialog(this, sky_slot.value().portal_slot); + } + else + { + QMessageBox::warning(this, tr("Failed to modify Skylander!"), + tr("Make sure there is a Skylander in slot %1!").arg(GetCurrentSlot()), + QMessageBox::Ok); + } +} + void SkylanderPortalWindow::ClearSlot(u8 slot) { auto& system = Core::System::GetInstance(); @@ -771,16 +790,18 @@ void SkylanderPortalWindow::RefreshList() void SkylanderPortalWindow::CreateSkyfile(const QString& path, bool load_after) { - auto& system = Core::System::GetInstance(); - - if (!system.GetSkylanderPortal().CreateSkylander(path.toStdString(), m_sky_id, m_sky_var)) { - QMessageBox::warning( - this, tr("Failed to create Skylander file!"), - tr("Failed to create Skylander file:\n%1\n(Skylander may already be on the portal)") - .arg(path), - QMessageBox::Ok); - return; + IOS::HLE::USB::SkylanderFigure figure(path.toStdString()); + if (!figure.Create(m_sky_id, m_sky_var)) + { + QMessageBox::warning( + this, tr("Failed to create Skylander file!"), + tr("Failed to create Skylander file:\n%1\n(Skylander may already be on the portal)") + .arg(path), + QMessageBox::Ok); + return; + } + figure.Close(); } m_last_skylander_path = QFileInfo(path).absolutePath() + QString::fromStdString("/"); @@ -814,8 +835,8 @@ void SkylanderPortalWindow::LoadSkyfilePath(u8 slot, const QString& path) auto& system = Core::System::GetInstance(); const std::pair id_var = system.GetSkylanderPortal().CalculateIDs(file_data); - const u8 portal_slot = - system.GetSkylanderPortal().LoadSkylander(file_data.data(), std::move(sky_file)); + const u8 portal_slot = system.GetSkylanderPortal().LoadSkylander( + std::make_unique(std::move(sky_file))); if (portal_slot == 0xFF) { QMessageBox::warning(this, tr("Failed to load the Skylander file!"), diff --git a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h index ce67dbff7e..a59dedcecf 100644 --- a/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h +++ b/Source/Core/DolphinQt/SkylanderPortal/SkylanderPortalWindow.h @@ -14,7 +14,8 @@ #include #include "Core/Core.h" -#include "Core/IOS/USB/Emulated/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h" class QCheckBox; class QGroupBox; @@ -64,6 +65,7 @@ private: void LoadFromFile(); void ClearSlot(u8 slot); void CreateSkylanderAdvanced(); + void ModifySkylander(); // Behind the scenes void OnEmulationStateChanged(Core::State state); diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 1e24489239..57ad971140 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -15,6 +15,8 @@ add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp) add_dolphin_test(FileSystemTest IOS/FS/FileSystemTest.cpp) +add_dolphin_test(SkylandersTest IOS/USB/SkylandersTest.cpp) + if(_M_X86) add_dolphin_test(PowerPCTest PowerPC/DivUtilsTest.cpp diff --git a/Source/UnitTests/Core/IOS/USB/SkylandersTest.cpp b/Source/UnitTests/Core/IOS/USB/SkylandersTest.cpp new file mode 100644 index 0000000000..e0b8eb7b98 --- /dev/null +++ b/Source/UnitTests/Core/IOS/USB/SkylandersTest.cpp @@ -0,0 +1,186 @@ +#include + +#include +#include + +#include "Common/BitUtils.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderCrypto.h" +#include "Core/IOS/USB/Emulated/Skylanders/SkylanderFigure.h" + +using namespace IOS::HLE::USB::SkylanderCrypto; + +// Figure data generated by: +// +// const std::string temp_dir = File::CreateTempDir(); +// IOS::HLE::USB::SkylanderFigure figure(temp_dir + "/test.sky"); +// figure.Create(0x1D6, 0x3000, std::array({0x01, 0x23, 0x45, 0x67})); +// +// IOS::HLE::USB::FigureData data = figure.GetData(); +// data.skylander_data.money = 5000; +// data.skylander_data.hero_level = 50; +// data.skylander_data.playtime = 1564894; +// const std::u16string nickname = UTF8ToUTF16("Test"); +// std::memset(data.skylander_data.nickname.data(), 0, data.skylander_data.nickname.size()); +// std::memcpy(data.skylander_data.nickname.data(), nickname.data(), nickname.size() * 2); +// data.skylander_data.last_reset.minute = 5; +// data.skylander_data.last_reset.hour = 7; +// data.skylander_data.last_reset.day = 11; +// data.skylander_data.last_reset.month = 3; +// data.skylander_data.last_reset.year = 2020; +// data.skylander_data.last_placed.minute = 44; +// data.skylander_data.last_placed.hour = 8; +// data.skylander_data.last_placed.day = 14; +// data.skylander_data.last_placed.month = 4; +// data.skylander_data.last_placed.year = 2021; +// figure.SetData(&data); +// +// data.skylander_data.money = 5600; +// data.skylander_data.hero_level = 51; +// data.skylander_data.playtime = 1764894; +// std::memset(data.skylander_data.nickname.data(), 0, data.skylander_data.nickname.size()); +// std::memcpy(data.skylander_data.nickname.data(), nickname.data(), nickname.size() * 2); +// data.skylander_data.last_reset.minute = 5; +// data.skylander_data.last_reset.hour = 7; +// data.skylander_data.last_reset.day = 11; +// data.skylander_data.last_reset.month = 3; +// data.skylander_data.last_reset.year = 2020; +// data.skylander_data.last_placed.minute = 59; +// data.skylander_data.last_placed.hour = 9; +// data.skylander_data.last_placed.day = 14; +// data.skylander_data.last_placed.month = 4; +// data.skylander_data.last_placed.year = 2021; +// figure.SetData(&data); +// +// std::array decrypted = {}; +// figure.DecryptFigure(&decrypted); +// File::IOFile f(temp_dir + "/decrypted.sky", "wb"); +// f.WriteBytes(decrypted.data(), decrypted.size()); +// +static constexpr std::array decrypted_figure = { + 0x01, 0x23, 0x45, 0x67, 0x00, 0x81, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xD6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xCB, 0x7D, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x4B, 0x0B, 0x20, 0x10, 0x7C, 0xCB, 0x0F, 0x0F, 0x0F, 0x69, 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, 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, + 0x3C, 0x98, 0xF9, 0x25, 0xA1, 0x7F, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x88, 0x13, 0xDE, 0xE0, 0x17, 0x00, 0x01, 0x88, 0x3C, 0xC4, 0xE3, 0x76, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1A, 0xF5, 0x2D, 0x76, 0x76, 0xBC, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2C, 0x08, 0x0E, 0x04, 0xE5, 0x07, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x07, 0x0B, 0x03, 0xE4, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x89, 0xC3, 0xC7, 0xDF, 0x9D, 0x5D, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x54, 0x5B, 0x01, 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, + 0xC5, 0x19, 0x6F, 0x78, 0x33, 0xDA, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x56, 0x2F, 0x85, 0xD1, 0xD8, 0x3B, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x70, 0x42, 0x51, 0x82, 0x0F, 0xF8, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0xE3, 0x74, 0xBB, 0x2B, 0xE4, 0x19, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x7B, 0xC0, 0xEA, 0x64, 0xB9, 0x16, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xE0, 0x15, 0x1E, 0xEE, 0x1A, 0x00, 0x02, 0x03, 0xE7, 0xC4, 0xE3, 0x0F, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE8, 0xF6, 0x00, 0xCD, 0x52, 0xF7, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3B, 0x09, 0x0E, 0x04, 0xE5, 0x07, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x07, 0x0B, 0x03, 0xE4, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xCE, 0x9B, 0xD4, 0x9E, 0x85, 0x34, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0xD7, 0x02, 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, + 0x5D, 0xAD, 0x3E, 0x37, 0x6E, 0xD5, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x11, 0x77, 0x96, 0x90, 0xC0, 0x52, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x82, 0x41, 0x7C, 0x39, 0x2B, 0xB3, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0xA4, 0x2C, 0xA8, 0x6A, 0xFC, 0x70, 0x7F, 0x0F, 0x08, 0x69, 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, 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, + 0x37, 0x1A, 0x42, 0xC3, 0x17, 0x91, 0x7F, 0x0F, 0x08, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// Can be assumed to also mean ComputeCRC48 is correct +TEST(Skylanders, Keygen) +{ + struct + { + std::array nuid; + u8 sector; + u64 expected; + } const inputs[]{{{0x00, 0x00, 0x00, 0x00}, 0, 0x4B0B20107CCB}, + {{0x94, 0xB0, 0xEE, 0x2D}, 0, 0x4B0B20107CCB}, + {{0x00, 0x00, 0x00, 0x00}, 11, 0xEA168579FF28}, + {{0x94, 0xB0, 0xEE, 0x2D}, 1, 0x278e4DA896B5}, + {{0xF7, 0xDB, 0xFD, 0x5F}, 2, 0x75B9B1F4B9EB}}; + + for (auto& test : inputs) + { + auto actual = CalculateKeyA(test.sector, test.nuid); + EXPECT_EQ(test.expected, actual); + } +} + +// Can be assumed to also mean ComputeCRC16 is correct +TEST(Skylanders, Checksums) +{ + std::array actual = {}; + ComputeChecksumType0(decrypted_figure.data(), actual.data()); + EXPECT_EQ(Common::BitCastPtr(decrypted_figure.data() + 0x1E), + Common::BitCastPtr(actual.data())); + + u16 area_offset = 0x80; + + for (u8 i = 0; i < 2; i++) + { + ComputeChecksumType3(decrypted_figure.data() + area_offset + 0x50, actual.data()); + EXPECT_EQ(Common::BitCastPtr(decrypted_figure.data() + area_offset + 0xA), + Common::BitCastPtr(actual.data())); + + ComputeChecksumType2(decrypted_figure.data() + area_offset + 0x10, actual.data()); + EXPECT_EQ(Common::BitCastPtr(decrypted_figure.data() + area_offset + 0xC), + Common::BitCastPtr(actual.data())); + + ComputeChecksumType1(decrypted_figure.data() + area_offset, actual.data()); + EXPECT_EQ(Common::BitCastPtr(decrypted_figure.data() + area_offset + 0xE), + Common::BitCastPtr(actual.data())); + + area_offset += 0x90; + + ComputeChecksumType6(decrypted_figure.data() + area_offset, actual.data()); + EXPECT_EQ(Common::BitCastPtr(decrypted_figure.data() + area_offset), + Common::BitCastPtr(actual.data())); + + area_offset += 0x130; + } +} + +TEST(Skylanders, ToyCode) +{ + const std::array code_chars = ComputeToyCode(0x14E2CE497CB0B); + const std::string_view code_string(reinterpret_cast(code_chars.data()), + code_chars.size()); + EXPECT_EQ(code_string, "WCJGC-HHR5Q"); +} diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj index d48b0cc4ff..cb630585f1 100644 --- a/Source/UnitTests/UnitTests.vcxproj +++ b/Source/UnitTests/UnitTests.vcxproj @@ -66,6 +66,7 @@ +