| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <array> | ||
| #include <span> | ||
| #include <string> | ||
|
|
||
| #include "Common/BitUtils.h" | ||
| #include "Common/CommonTypes.h" | ||
| #include "Common/Swap.h" | ||
|
|
||
| namespace IOS::HLE::USB::SkylanderCrypto | ||
| { | ||
| u16 ComputeCRC16(std::span<const u8> 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<const u8> 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<u64>(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<const u8, 0x4> nuid) | ||
| { | ||
| if (sector == 0) | ||
| { | ||
| return 73ULL * 2017ULL * 560381651ULL; | ||
| } | ||
|
|
||
| std::array<u8, 5> 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<u8, 0x1E> 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<u8, 0x10> 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<u8, 0x30> 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<u8, 0x110> 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<u8, 0x40> 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<u8, 11> ComputeToyCode(u64 code) | ||
| { | ||
| if (code == 0) | ||
| { | ||
| static constexpr std::array<u8, 11> invalid_code_result{ | ||
| static_cast<u8>('N'), static_cast<u8>('/'), static_cast<u8>('A')}; | ||
| return invalid_code_result; | ||
| } | ||
|
|
||
| std::array<u8, 10> code_bytes; | ||
| for (size_t i = 0; i < code_bytes.size(); ++i) | ||
| { | ||
| code_bytes[i] = static_cast<u8>(code % 29); | ||
| code /= 29; | ||
| } | ||
|
|
||
| static constexpr char lookup_table[] = "23456789BCDFGHJKLMNPQRSTVWXYZ"; | ||
| std::array<u8, 10> code_chars; | ||
| for (size_t i = 0; i < code_bytes.size(); ++i) | ||
| { | ||
| code_chars[i] = static_cast<u8>(lookup_table[code_bytes[9 - i]]); | ||
| } | ||
|
|
||
| std::array<u8, 11> result; | ||
| std::memcpy(&result[0], &code_chars[0], 5); | ||
| result[5] = static_cast<u8>('-'); | ||
| std::memcpy(&result[6], &code_chars[5], 5); | ||
|
|
||
| return result; | ||
| } | ||
| } // namespace IOS::HLE::USB::SkylanderCrypto |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <array> | ||
| #include <span> | ||
|
|
||
| #include "Common/CommonTypes.h" | ||
|
|
||
| namespace IOS::HLE::USB::SkylanderCrypto | ||
| { | ||
| u16 ComputeCRC16(std::span<const u8> data); | ||
| u64 ComputeCRC48(std::span<const u8> data); | ||
| u64 CalculateKeyA(u8 sector, std::span<const u8, 0x4> 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<u8, 11> ComputeToyCode(u64 code); | ||
| } // namespace IOS::HLE::USB::SkylanderCrypto |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <array> | ||
| #include <optional> | ||
| #include <span> | ||
| #include <string> | ||
|
|
||
| #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<u8, 11> toy_code; | ||
| u16 money; | ||
| u16 hero_level; | ||
| u32 playtime; | ||
| // Null-terminated UTF-16 string | ||
| std::array<u16, 0x10> 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<std::array<u8, 4>> 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<u8, FIGURE_SIZE>* dest) const; | ||
| FigureData GetData() const; | ||
| void SetData(FigureData* data); | ||
|
|
||
| private: | ||
| void PopulateSectorTrailers(); | ||
| void PopulateKeys(); | ||
| void GenerateIncompleteHashIn(u8* dest) const; | ||
| void Encrypt(std::span<const u8, FIGURE_SIZE>); | ||
|
|
||
| File::IOFile m_sky_file; | ||
| std::array<u8, FIGURE_SIZE> m_data; | ||
| }; | ||
| } // namespace IOS::HLE::USB |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,344 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include "SkylanderModifyDialog.h" | ||
|
|
||
| #include <QBoxLayout> | ||
| #include <QCheckBox> | ||
| #include <QDateTimeEdit> | ||
| #include <QDialog> | ||
| #include <QDialogButtonBox> | ||
| #include <QLabel> | ||
| #include <QLineEdit> | ||
| #include <QMessageBox> | ||
| #include <QStringConverter> | ||
| #include <QValidator> | ||
|
|
||
| #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<char16_t*>(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<char16_t*>(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<u8>(edit_last_reset->time().minute()), | ||
| .hour = static_cast<u8>(edit_last_reset->time().hour()), | ||
| .day = static_cast<u8>(edit_last_reset->date().day()), | ||
| .month = static_cast<u8>(edit_last_reset->date().month()), | ||
| .year = static_cast<u16>(edit_last_reset->date().year())}, | ||
| .last_placed = {.minute = static_cast<u8>(edit_last_placed->time().minute()), | ||
| .hour = static_cast<u8>(edit_last_placed->time().hour()), | ||
| .day = static_cast<u8>(edit_last_placed->date().day()), | ||
| .month = static_cast<u8>(edit_last_placed->date().month()), | ||
| .year = static_cast<u16>(edit_last_placed->date().year())}}; | ||
|
|
||
| std::u16string nickname = edit_nick->text().toStdU16String(); | ||
| nickname.copy(reinterpret_cast<char16_t*>(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<int, MAX_VILLAINS> 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<QCheckBox*, MAX_VILLAINS> edit_villains; | ||
| for (size_t i = 0; i < MAX_VILLAINS; ++i) | ||
| { | ||
| edit_villains[i] = new QCheckBox(); | ||
| edit_villains[i]->setChecked(static_cast<bool>(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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // Copyright 2023 Dolphin Emulator Project | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <QDialog> | ||
|
|
||
| #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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <array> | ||
| #include <string_view> | ||
|
|
||
| #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<u8, 4>({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<u8, IOS::HLE::USB::FIGURE_SIZE> decrypted = {}; | ||
| // figure.DecryptFigure(&decrypted); | ||
| // File::IOFile f(temp_dir + "/decrypted.sky", "wb"); | ||
| // f.WriteBytes(decrypted.data(), decrypted.size()); | ||
| // | ||
| static constexpr std::array<u8, IOS::HLE::USB::FIGURE_SIZE> 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<u8, 4> 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<u8, 2> actual = {}; | ||
| ComputeChecksumType0(decrypted_figure.data(), actual.data()); | ||
| EXPECT_EQ(Common::BitCastPtr<u16>(decrypted_figure.data() + 0x1E), | ||
| Common::BitCastPtr<u16>(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<u16>(decrypted_figure.data() + area_offset + 0xA), | ||
| Common::BitCastPtr<u16>(actual.data())); | ||
|
|
||
| ComputeChecksumType2(decrypted_figure.data() + area_offset + 0x10, actual.data()); | ||
| EXPECT_EQ(Common::BitCastPtr<u16>(decrypted_figure.data() + area_offset + 0xC), | ||
| Common::BitCastPtr<u16>(actual.data())); | ||
|
|
||
| ComputeChecksumType1(decrypted_figure.data() + area_offset, actual.data()); | ||
| EXPECT_EQ(Common::BitCastPtr<u16>(decrypted_figure.data() + area_offset + 0xE), | ||
| Common::BitCastPtr<u16>(actual.data())); | ||
|
|
||
| area_offset += 0x90; | ||
|
|
||
| ComputeChecksumType6(decrypted_figure.data() + area_offset, actual.data()); | ||
| EXPECT_EQ(Common::BitCastPtr<u16>(decrypted_figure.data() + area_offset), | ||
| Common::BitCastPtr<u16>(actual.data())); | ||
|
|
||
| area_offset += 0x130; | ||
| } | ||
| } | ||
|
|
||
| TEST(Skylanders, ToyCode) | ||
| { | ||
| const std::array<u8, 11> code_chars = ComputeToyCode(0x14E2CE497CB0B); | ||
| const std::string_view code_string(reinterpret_cast<const char*>(code_chars.data()), | ||
| code_chars.size()); | ||
| EXPECT_EQ(code_string, "WCJGC-HHR5Q"); | ||
| } |