Compare commits

...

9 Commits

Author SHA1 Message Date
Joshua de Reeper 2784129ada
Merge cf225c0d48 into 13b90874f9 2024-05-14 19:33:02 +00:00
Joshua de Reeper cf225c0d48 nsyshid: Emulated Backend, and Emulated Skylander Portal 2024-05-14 20:32:37 +01:00
splatoon1enjoyer 13b90874f9
Fix commas edge case in strings when parsing an assembly line (#1201) 2024-05-13 16:52:25 +02:00
Exzap cf41c3b136
CI: Use submodule commit of vcpkg 2024-05-10 09:33:32 +02:00
Xphalnos 97d8cf4ba3
vcpkg: Update libraries (#1198) 2024-05-10 09:32:06 +02:00
GaryOderNichts b2a6cccc89
nn_act: Implement GetTransferableId (#1197) 2024-05-09 12:12:34 +02:00
GaryOderNichts 10d553e1c9
zlib125: Implement `deflateInit_` (#1194) 2024-05-07 11:56:28 +02:00
Exzap 3f8722f0a6 Track online-enable and network-service settings per-account instead of globally 2024-05-06 18:18:42 +02:00
Exzap 065fb7eb58 coreinit: Add reschedule special case to avoid a deadlock
Fixes Just Dance 2019 locking up on boot
2024-05-06 09:15:36 +02:00
31 changed files with 1620 additions and 166 deletions

View File

@ -28,7 +28,6 @@ jobs:
run: |
cd dependencies/vcpkg
git fetch --unshallow
git checkout 431eb6bda0950874c8d4ed929cc66e15d8aae46f
- name: Setup release mode parameters (for deploy)
if: ${{ inputs.deploymode == 'release' }}
@ -138,7 +137,6 @@ jobs:
run: |
cd dependencies/vcpkg
git fetch --unshallow
git checkout 431eb6bda0950874c8d4ed929cc66e15d8aae46f
- name: Setup release mode parameters (for deploy)
if: ${{ inputs.deploymode == 'release' }}
@ -218,7 +216,6 @@ jobs:
run: |
cd dependencies/vcpkg
git fetch --unshallow
git pull --all
- name: Setup release mode parameters (for deploy)
if: ${{ inputs.deploymode == 'release' }}

2
dependencies/vcpkg vendored

@ -1 +1 @@
Subproject commit 53bef8994c541b6561884a8395ea35715ece75db
Subproject commit cbf4a6641528cee6f172328984576f51698de726

View File

@ -445,10 +445,14 @@ add_library(CemuCafe
OS/libs/nsyshid/AttachDefaultBackends.cpp
OS/libs/nsyshid/Whitelist.cpp
OS/libs/nsyshid/Whitelist.h
OS/libs/nsyshid/BackendEmulated.cpp
OS/libs/nsyshid/BackendEmulated.h
OS/libs/nsyshid/BackendLibusb.cpp
OS/libs/nsyshid/BackendLibusb.h
OS/libs/nsyshid/BackendWindowsHID.cpp
OS/libs/nsyshid/BackendWindowsHID.h
OS/libs/nsyshid/Skylander.cpp
OS/libs/nsyshid/Skylander.h
OS/libs/nsyskbd/nsyskbd.cpp
OS/libs/nsyskbd/nsyskbd.h
OS/libs/nsysnet/nsysnet.cpp

View File

@ -763,6 +763,11 @@ namespace coreinit
uint32 coreIndex = OSGetCoreId();
if (!newThread->context.hasCoreAffinitySet(coreIndex))
return false;
// special case: if current and new thread are running only on the same core then reschedule even if priority is equal
// this resolves a deadlock in Just Dance 2019 where one thread would always reacquire the same mutex within it's timeslice, blocking another thread on the same core from acquiring it
if ((1<<coreIndex) == newThread->context.affinity && currentThread->context.affinity == newThread->context.affinity && currentThread->effectivePriority == newThread->effectivePriority)
return true;
// otherwise reschedule if new thread has higher priority
return newThread->effectivePriority < currentThread->effectivePriority;
}

View File

@ -308,6 +308,22 @@ void nnActExport_GetPrincipalIdEx(PPCInterpreter_t* hCPU)
osLib_returnFromFunction(hCPU, 0); // ResultSuccess
}
void nnActExport_GetTransferableId(PPCInterpreter_t* hCPU)
{
ppcDefineParamU32(unique, 0);
cemuLog_logDebug(LogType::Force, "nn_act.GetTransferableId(0x{:08x})", hCPU->gpr[3]);
uint64 transferableId;
uint32 r = nn::act::GetTransferableIdEx(&transferableId, unique, iosu::act::ACT_SLOT_CURRENT);
if (NN_RESULT_IS_FAILURE(r))
{
transferableId = 0;
}
osLib_returnFromFunction64(hCPU, _swapEndianU64(transferableId));
}
void nnActExport_GetTransferableIdEx(PPCInterpreter_t* hCPU)
{
ppcDefineParamStructPtr(transferableId, uint64, 0);
@ -691,6 +707,7 @@ void nnAct_load()
osLib_addFunction("nn_act", "GetPrincipalId__Q2_2nn3actFv", nnActExport_GetPrincipalId);
osLib_addFunction("nn_act", "GetPrincipalIdEx__Q2_2nn3actFPUiUc", nnActExport_GetPrincipalIdEx);
// transferable id
osLib_addFunction("nn_act", "GetTransferableId__Q2_2nn3actFUi", nnActExport_GetTransferableId);
osLib_addFunction("nn_act", "GetTransferableIdEx__Q2_2nn3actFPULUiUc", nnActExport_GetTransferableIdEx);
// persistent id
osLib_addFunction("nn_act", "GetPersistentId__Q2_2nn3actFv", nnActExport_GetPersistentId);

View File

@ -1,5 +1,6 @@
#include "nsyshid.h"
#include "Backend.h"
#include "BackendEmulated.h"
#if NSYSHID_ENABLE_BACKEND_LIBUSB
@ -37,5 +38,13 @@ namespace nsyshid::backend
}
}
#endif // NSYSHID_ENABLE_BACKEND_WINDOWS_HID
// add emulated backend
{
auto backendEmulated = std::make_shared<backend::emulated::BackendEmulated>();
if (backendEmulated->IsInitialisedOk())
{
AttachBackend(backendEmulated);
}
}
}
} // namespace nsyshid::backend

View File

@ -23,6 +23,55 @@ namespace nsyshid
/* +0x12 */ uint16be maxPacketSizeTX;
} HID_t;
struct TransferCommand
{
uint8* data;
sint32 length;
TransferCommand(uint8* data, sint32 length)
: data(data), length(length)
{
}
virtual ~TransferCommand() = default;
};
struct ReadMessage final : TransferCommand
{
sint32 bytesRead;
ReadMessage(uint8* data, sint32 length, sint32 bytesRead)
: bytesRead(bytesRead), TransferCommand(data, length)
{
}
using TransferCommand::TransferCommand;
};
struct WriteMessage final : TransferCommand
{
sint32 bytesWritten;
WriteMessage(uint8* data, sint32 length, sint32 bytesWritten)
: bytesWritten(bytesWritten), TransferCommand(data, length)
{
}
using TransferCommand::TransferCommand;
};
struct ReportMessage final : TransferCommand
{
uint8* reportData;
sint32 length;
uint8* originalData;
sint32 originalLength;
ReportMessage(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength)
: reportData(reportData), length(length), originalData(originalData),
originalLength(originalLength), TransferCommand(reportData, length)
{
}
using TransferCommand::TransferCommand;
};
static_assert(offsetof(HID_t, vendorId) == 0x8, "");
static_assert(offsetof(HID_t, productId) == 0xA, "");
static_assert(offsetof(HID_t, ifIndex) == 0xC, "");
@ -69,7 +118,7 @@ namespace nsyshid
ErrorTimeout,
};
virtual ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) = 0;
virtual ReadResult Read(ReadMessage* message) = 0;
enum class WriteResult
{
@ -78,7 +127,7 @@ namespace nsyshid
ErrorTimeout,
};
virtual WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) = 0;
virtual WriteResult Write(WriteMessage* message) = 0;
virtual bool GetDescriptor(uint8 descType,
uint8 descIndex,
@ -88,7 +137,7 @@ namespace nsyshid
virtual bool SetProtocol(uint32 ifIndef, uint32 protocol) = 0;
virtual bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) = 0;
virtual bool SetReport(ReportMessage* message) = 0;
};
class Backend {
@ -121,6 +170,8 @@ namespace nsyshid
std::shared_ptr<Device> FindDevice(std::function<bool(const std::shared_ptr<Device>&)> isWantedDevice);
bool FindDeviceById(uint16 vendorId, uint16 productId);
bool IsDeviceWhitelisted(uint16 vendorId, uint16 productId);
// called from OnAttach() - attach devices that your backend can see here

View File

@ -0,0 +1,29 @@
#include "BackendEmulated.h"
#include "Skylander.h"
#include "config/CemuConfig.h"
namespace nsyshid::backend::emulated
{
BackendEmulated::BackendEmulated()
{
cemuLog_logDebug(LogType::Force, "nsyshid::BackendEmulated: emulated backend initialised");
}
BackendEmulated::~BackendEmulated() = default;
bool BackendEmulated::IsInitialisedOk()
{
return true;
}
void BackendEmulated::AttachVisibleDevices()
{
if (GetConfig().emulated_usb_devices.emulate_skylander_portal && !FindDeviceById(0x1430, 0x0150))
{
cemuLog_logDebug(LogType::Force, "Attaching Emulated Portal");
// Add Skylander Portal
auto device = std::make_shared<SkylanderPortalDevice>();
AttachDevice(device);
}
}
} // namespace nsyshid::backend::emulated

View File

@ -0,0 +1,16 @@
#include "nsyshid.h"
#include "Backend.h"
namespace nsyshid::backend::emulated
{
class BackendEmulated : public nsyshid::Backend {
public:
BackendEmulated();
~BackendEmulated();
bool IsInitialisedOk() override;
protected:
void AttachVisibleDevices() override;
};
} // namespace nsyshid::backend::emulated

View File

@ -241,11 +241,6 @@ namespace nsyshid::backend::libusb
ret);
return nullptr;
}
if (desc.idVendor == 0x0e6f && desc.idProduct == 0x0241)
{
cemuLog_logDebug(LogType::Force,
"nsyshid::BackendLibusb::CheckAndCreateDevice(): lego dimensions portal detected");
}
auto device = std::make_shared<DeviceLibusb>(m_ctx,
desc.idVendor,
desc.idProduct,
@ -471,7 +466,7 @@ namespace nsyshid::backend::libusb
return m_libusbHandle != nullptr && m_handleInUseCounter >= 0;
}
Device::ReadResult DeviceLibusb::Read(uint8* data, sint32 length, sint32& bytesRead)
Device::ReadResult DeviceLibusb::Read(ReadMessage* message)
{
auto handleLock = AquireHandleLock();
if (!handleLock->IsValid())
@ -488,8 +483,8 @@ namespace nsyshid::backend::libusb
{
ret = libusb_bulk_transfer(handleLock->GetHandle(),
this->m_libusbEndpointIn,
data,
length,
message->data,
message->length,
&actualLength,
timeout);
}
@ -500,8 +495,8 @@ namespace nsyshid::backend::libusb
// success
cemuLog_logDebug(LogType::Force, "nsyshid::DeviceLibusb::read(): read {} of {} bytes",
actualLength,
length);
bytesRead = actualLength;
message->length);
message->bytesRead = actualLength;
return ReadResult::Success;
}
cemuLog_logDebug(LogType::Force,
@ -510,7 +505,7 @@ namespace nsyshid::backend::libusb
return ReadResult::Error;
}
Device::WriteResult DeviceLibusb::Write(uint8* data, sint32 length, sint32& bytesWritten)
Device::WriteResult DeviceLibusb::Write(WriteMessage* message)
{
auto handleLock = AquireHandleLock();
if (!handleLock->IsValid())
@ -520,23 +515,23 @@ namespace nsyshid::backend::libusb
return WriteResult::Error;
}
bytesWritten = 0;
message->bytesWritten = 0;
int actualLength = 0;
int ret = libusb_bulk_transfer(handleLock->GetHandle(),
this->m_libusbEndpointOut,
data,
length,
message->data,
message->length,
&actualLength,
0);
if (ret == 0)
{
// success
bytesWritten = actualLength;
message->bytesWritten = actualLength;
cemuLog_logDebug(LogType::Force,
"nsyshid::DeviceLibusb::write(): wrote {} of {} bytes",
bytesWritten,
length);
message->bytesWritten,
message->length);
return WriteResult::Success;
}
cemuLog_logDebug(LogType::Force,
@ -713,8 +708,7 @@ namespace nsyshid::backend::libusb
return true;
}
bool DeviceLibusb::SetReport(uint8* reportData, sint32 length, uint8* originalData,
sint32 originalLength)
bool DeviceLibusb::SetReport(ReportMessage* message)
{
auto handleLock = AquireHandleLock();
if (!handleLock->IsValid())
@ -731,8 +725,8 @@ namespace nsyshid::backend::libusb
bRequest,
wValue,
wIndex,
reportData,
length,
message->reportData,
message->length,
timeout);
#endif

View File

@ -63,9 +63,9 @@ namespace nsyshid::backend::libusb
bool IsOpened() override;
ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) override;
ReadResult Read(ReadMessage* message) override;
WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) override;
WriteResult Write(WriteMessage* message) override;
bool GetDescriptor(uint8 descType,
uint8 descIndex,
@ -75,7 +75,7 @@ namespace nsyshid::backend::libusb
bool SetProtocol(uint32 ifIndex, uint32 protocol) override;
bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) override;
bool SetReport(ReportMessage* message) override;
uint8 m_libusbBusNumber;
uint8 m_libusbDeviceAddress;

View File

@ -196,20 +196,20 @@ namespace nsyshid::backend::windows
return m_hFile != INVALID_HANDLE_VALUE;
}
Device::ReadResult DeviceWindowsHID::Read(uint8* data, sint32 length, sint32& bytesRead)
Device::ReadResult DeviceWindowsHID::Read(ReadMessage* message)
{
bytesRead = 0;
message->bytesRead = 0;
DWORD bt;
OVERLAPPED ovlp = {0};
ovlp.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
uint8* tempBuffer = (uint8*)malloc(length + 1);
uint8* tempBuffer = (uint8*)malloc(message->length + 1);
sint32 transferLength = 0; // minus report byte
_debugPrintHex("HID_READ_BEFORE", data, length);
_debugPrintHex("HID_READ_BEFORE", message->data, message->length);
cemuLog_logDebug(LogType::Force, "HidRead Begin (Length 0x{:08x})", length);
BOOL readResult = ReadFile(this->m_hFile, tempBuffer, length + 1, &bt, &ovlp);
cemuLog_logDebug(LogType::Force, "HidRead Begin (Length 0x{:08x})", message->length);
BOOL readResult = ReadFile(this->m_hFile, tempBuffer, message->length + 1, &bt, &ovlp);
if (readResult != FALSE)
{
// sometimes we get the result immediately
@ -247,7 +247,7 @@ namespace nsyshid::backend::windows
ReadResult result = ReadResult::Success;
if (bt != 0)
{
memcpy(data, tempBuffer + 1, transferLength);
memcpy(message->data, tempBuffer + 1, transferLength);
sint32 hidReadLength = transferLength;
char debugOutput[1024] = {0};
@ -257,7 +257,7 @@ namespace nsyshid::backend::windows
}
cemuLog_logDebug(LogType::Force, "HIDRead data: {}", debugOutput);
bytesRead = transferLength;
message->bytesRead = transferLength;
result = ReadResult::Success;
}
else
@ -270,19 +270,19 @@ namespace nsyshid::backend::windows
return result;
}
Device::WriteResult DeviceWindowsHID::Write(uint8* data, sint32 length, sint32& bytesWritten)
Device::WriteResult DeviceWindowsHID::Write(WriteMessage* message)
{
bytesWritten = 0;
message->bytesWritten = 0;
DWORD bt;
OVERLAPPED ovlp = {0};
ovlp.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
uint8* tempBuffer = (uint8*)malloc(length + 1);
memcpy(tempBuffer + 1, data, length);
uint8* tempBuffer = (uint8*)malloc(message->length + 1);
memcpy(tempBuffer + 1, message->data, message->length);
tempBuffer[0] = 0; // report byte?
cemuLog_logDebug(LogType::Force, "HidWrite Begin (Length 0x{:08x})", length);
BOOL writeResult = WriteFile(this->m_hFile, tempBuffer, length + 1, &bt, &ovlp);
cemuLog_logDebug(LogType::Force, "HidWrite Begin (Length 0x{:08x})", message->length);
BOOL writeResult = WriteFile(this->m_hFile, tempBuffer, message->length + 1, &bt, &ovlp);
if (writeResult != FALSE)
{
// sometimes we get the result immediately
@ -314,7 +314,7 @@ namespace nsyshid::backend::windows
if (bt != 0)
{
bytesWritten = length;
message->bytesWritten = message->length;
return WriteResult::Success;
}
return WriteResult::Error;
@ -407,12 +407,12 @@ namespace nsyshid::backend::windows
return true;
}
bool DeviceWindowsHID::SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength)
bool DeviceWindowsHID::SetReport(ReportMessage* message)
{
sint32 retryCount = 0;
while (true)
{
BOOL r = HidD_SetOutputReport(this->m_hFile, reportData, length);
BOOL r = HidD_SetOutputReport(this->m_hFile, message->reportData, message->length);
if (r != FALSE)
break;
Sleep(20); // retry

View File

@ -41,15 +41,15 @@ namespace nsyshid::backend::windows
bool IsOpened() override;
ReadResult Read(uint8* data, sint32 length, sint32& bytesRead) override;
ReadResult Read(ReadMessage* message) override;
WriteResult Write(uint8* data, sint32 length, sint32& bytesWritten) override;
WriteResult Write(WriteMessage* message) override;
bool GetDescriptor(uint8 descType, uint8 descIndex, uint8 lang, uint8* output, uint32 outputMaxLength) override;
bool SetProtocol(uint32 ifIndef, uint32 protocol) override;
bool SetReport(uint8* reportData, sint32 length, uint8* originalData, sint32 originalLength) override;
bool SetReport(ReportMessage* message) override;
private:
wchar_t* m_devicePath;

View File

@ -0,0 +1,401 @@
#include "Skylander.h"
#include "nsyshid.h"
#include "Backend.h"
namespace nsyshid
{
SkylanderUSB g_skyportal;
SkylanderPortalDevice::SkylanderPortalDevice()
: Device(0x1430, 0x0150, 1, 2, 0)
{
m_IsOpened = false;
}
bool SkylanderPortalDevice::Open()
{
if (!IsOpened())
{
m_IsOpened = true;
}
return true;
}
void SkylanderPortalDevice::Close()
{
if (IsOpened())
{
m_IsOpened = false;
}
}
bool SkylanderPortalDevice::IsOpened()
{
return m_IsOpened;
}
Device::ReadResult SkylanderPortalDevice::Read(ReadMessage* message)
{
memcpy(message->data, g_skyportal.get_status().data(), message->length);
message->bytesRead = message->length;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return Device::ReadResult::Success;
}
Device::WriteResult SkylanderPortalDevice::Write(WriteMessage* message)
{
message->bytesWritten = message->length;
return Device::WriteResult::Success;
}
bool SkylanderPortalDevice::GetDescriptor(uint8 descType,
uint8 descIndex,
uint8 lang,
uint8* output,
uint32 outputMaxLength)
{
uint8 configurationDescriptor[0x29];
uint8* currentWritePtr;
// configuration descriptor
currentWritePtr = configurationDescriptor + 0;
*(uint8*)(currentWritePtr + 0) = 9; // bLength
*(uint8*)(currentWritePtr + 1) = 2; // bDescriptorType
*(uint16be*)(currentWritePtr + 2) = 0x0029; // wTotalLength
*(uint8*)(currentWritePtr + 4) = 1; // bNumInterfaces
*(uint8*)(currentWritePtr + 5) = 1; // bConfigurationValue
*(uint8*)(currentWritePtr + 6) = 0; // iConfiguration
*(uint8*)(currentWritePtr + 7) = 0x80; // bmAttributes
*(uint8*)(currentWritePtr + 8) = 0xFA; // MaxPower
currentWritePtr = currentWritePtr + 9;
// configuration descriptor
*(uint8*)(currentWritePtr + 0) = 9; // bLength
*(uint8*)(currentWritePtr + 1) = 0x04; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0; // bInterfaceNumber
*(uint8*)(currentWritePtr + 3) = 0; // bAlternateSetting
*(uint8*)(currentWritePtr + 4) = 2; // bNumEndpoints
*(uint8*)(currentWritePtr + 5) = 3; // bInterfaceClass
*(uint8*)(currentWritePtr + 6) = 0; // bInterfaceSubClass
*(uint8*)(currentWritePtr + 7) = 0; // bInterfaceProtocol
*(uint8*)(currentWritePtr + 8) = 0; // iInterface
currentWritePtr = currentWritePtr + 9;
// configuration descriptor
*(uint8*)(currentWritePtr + 0) = 9; // bLength
*(uint8*)(currentWritePtr + 1) = 0x21; // bDescriptorType
*(uint16be*)(currentWritePtr + 2) = 0x0111; // bcdHID
*(uint8*)(currentWritePtr + 4) = 0x00; // bCountryCode
*(uint8*)(currentWritePtr + 5) = 0x01; // bNumDescriptors
*(uint8*)(currentWritePtr + 6) = 0x22; // bDescriptorType
*(uint16be*)(currentWritePtr + 7) = 0x001D; // wDescriptorLength
currentWritePtr = currentWritePtr + 9;
// endpoint descriptor 1
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
currentWritePtr = currentWritePtr + 7;
// endpoint descriptor 2
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x02; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
currentWritePtr = currentWritePtr + 7;
cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29);
memcpy(output, configurationDescriptor,
std::min<uint32>(outputMaxLength, sizeof(configurationDescriptor)));
return true;
}
bool SkylanderPortalDevice::SetProtocol(uint32 ifIndex, uint32 protocol)
{
return true;
}
bool SkylanderPortalDevice::SetReport(ReportMessage* message)
{
g_skyportal.control_transfer(message->originalData, message->originalLength);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
return true;
}
void SkylanderUSB::control_transfer(uint8* buf, sint32 originalLength)
{
std::array<uint8, 64> interrupt_response = {};
switch (buf[0])
{
case 'A':
{
interrupt_response = {buf[0], buf[1], 0xFF, 0x77};
g_skyportal.activate();
break;
}
case 'C':
{
// Colours
break;
}
case 'J':
{
interrupt_response = {buf[0]};
break;
}
case 'L':
{
// Trap Team Portal side lights
break;
}
case 'M':
{
interrupt_response = {buf[0], buf[1], 0x00, 0x19};
break;
}
case 'Q':
{
const uint8 sky_num = buf[1] & 0xF;
const uint8 block = buf[2];
g_skyportal.query_block(sky_num, block, interrupt_response.data());
break;
}
case 'R':
{
interrupt_response = {buf[0], 0x02, 0x1b};
// g_skyportal.deactivate();
break;
}
case 'S':
{
// Status
break;
}
case 'V':
{
// Unsure
break;
}
case 'W':
{
const uint8 sky_num = buf[1] & 0xF;
const uint8 block = buf[2];
g_skyportal.write_block(sky_num, block, &buf[3], interrupt_response.data());
break;
}
default:
cemu_assert_error();
break;
}
if (interrupt_response[0] != 0)
{
std::lock_guard lock(m_queryMutex);
m_queries.push(interrupt_response);
}
}
void SkylanderUSB::activate()
{
std::lock_guard lock(sky_mutex);
if (m_activated)
{
// If the portal was already active no change is needed
return;
}
// If not we need to advertise change to all the figures present on the portal
for (auto& s : skylanders)
{
if (s.status & 1)
{
s.queued_status.push(3);
s.queued_status.push(1);
}
}
m_activated = true;
}
void SkylanderUSB::deactivate()
{
std::lock_guard lock(sky_mutex);
for (auto& s : skylanders)
{
// check if at the end of the updates there would be a figure on the portal
if (!s.queued_status.empty())
{
s.status = s.queued_status.back();
s.queued_status = std::queue<uint8>();
}
s.status &= 1;
}
m_activated = false;
}
uint8 SkylanderUSB::load_skylander(uint8* buf, std::FILE* file)
{
std::lock_guard lock(sky_mutex);
uint32 sky_serial = 0;
for (int i = 3; i > -1; i--)
{
sky_serial <<= 8;
sky_serial |= buf[i];
}
uint8 found_slot = 0xFF;
// mimics spot retaining on the portal
for (auto i = 0; i < 16; i++)
{
if ((skylanders[i].status & 1) == 0)
{
if (skylanders[i].last_id == sky_serial)
{
found_slot = i;
break;
}
if (i < found_slot)
{
found_slot = i;
}
}
}
if (found_slot != 0xFF)
{
auto& skylander = skylanders[found_slot];
memcpy(skylander.data.data(), buf, skylander.data.size());
skylander.sky_file = std::move(file);
skylander.status = Skylander::ADDED;
skylander.queued_status.push(Skylander::ADDED);
skylander.queued_status.push(Skylander::READY);
skylander.last_id = sky_serial;
}
return found_slot;
}
bool SkylanderUSB::remove_skylander(uint8 sky_num)
{
std::lock_guard lock(sky_mutex);
auto& thesky = skylanders[sky_num];
if (thesky.status & 1)
{
thesky.status = 2;
thesky.queued_status.push(2);
thesky.queued_status.push(0);
thesky.Save();
std::fclose(thesky.sky_file);
return true;
}
return false;
}
void SkylanderUSB::query_block(uint8 sky_num, uint8 block, uint8* reply_buf)
{
std::lock_guard lock(sky_mutex);
const auto& skylander = skylanders[sky_num];
reply_buf[0] = 'Q';
reply_buf[2] = block;
if (skylander.status & 1)
{
reply_buf[1] = (0x10 | sky_num);
memcpy(reply_buf + 3, skylander.data.data() + (16 * block), 16);
}
else
{
reply_buf[1] = sky_num;
}
}
void SkylanderUSB::write_block(uint8 sky_num, uint8 block,
const uint8* to_write_buf, uint8* reply_buf)
{
std::lock_guard lock(sky_mutex);
auto& skylander = skylanders[sky_num];
reply_buf[0] = 'W';
reply_buf[2] = block;
if (skylander.status & 1)
{
reply_buf[1] = (0x10 | sky_num);
memcpy(skylander.data.data() + (block * 16), to_write_buf, 16);
skylander.Save();
}
else
{
reply_buf[1] = sky_num;
}
}
std::array<uint8, 64> SkylanderUSB::get_status()
{
std::array<uint8, 64> interrupt_response = {};
if (!m_queries.empty())
{
std::lock_guard lock(m_queryMutex);
interrupt_response = m_queries.front();
m_queries.pop();
// This needs to happen after ~22 milliseconds
}
else
{
std::lock_guard lock(sky_mutex);
uint32 status = 0;
uint8 active = 0x00;
if (m_activated)
{
active = 0x01;
}
for (int i = 16 - 1; i >= 0; i--)
{
auto& s = skylanders[i];
if (!s.queued_status.empty())
{
s.status = s.queued_status.front();
s.queued_status.pop();
}
status <<= 2;
status |= s.status;
}
interrupt_response = {0x53, 0x00, 0x00, 0x00, 0x00, m_interrupt_counter++,
active, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00};
memcpy(&interrupt_response[1], &status, sizeof(status));
}
return interrupt_response;
}
void SkylanderUSB::Skylander::Save()
{
if (!sky_file)
return;
#if BOOST_OS_WINDOWS
_fseeki64(sky_file, 0, 0);
#else
fseeko(sky_file, 0, 0);
#endif
std::fwrite(&data[0], sizeof(data[0]), data.size(), sky_file);
}
} // namespace nsyshid

View File

@ -0,0 +1,81 @@
#include <mutex>
#include "nsyshid.h"
#include "Backend.h"
namespace nsyshid
{
class SkylanderPortalDevice final : public Device {
public:
SkylanderPortalDevice();
~SkylanderPortalDevice() = default;
bool Open() override;
void Close() override;
bool IsOpened() override;
ReadResult Read(ReadMessage* message) override;
WriteResult Write(WriteMessage* message) override;
bool GetDescriptor(uint8 descType,
uint8 descIndex,
uint8 lang,
uint8* output,
uint32 outputMaxLength) override;
bool SetProtocol(uint32 ifIndex, uint32 protocol) override;
bool SetReport(ReportMessage* message) override;
private:
bool m_IsOpened;
};
class SkylanderUSB {
public:
struct Skylander final
{
std::FILE* sky_file;
uint8 status = 0;
std::queue<uint8> queued_status;
std::array<uint8, 0x40 * 0x10> data{};
uint32 last_id = 0;
void Save();
enum : uint8
{
REMOVED = 0,
READY = 1,
REMOVING = 2,
ADDED = 3
};
};
void control_transfer(uint8* buf, sint32 originalLength);
void activate();
void deactivate();
void set_leds(uint8 r, uint8 g, uint8 b);
std::array<uint8, 64> get_status();
void query_block(uint8 sky_num, uint8 block, uint8* reply_buf);
void write_block(uint8 sky_num, uint8 block, const uint8* to_write_buf,
uint8* reply_buf);
uint8 load_skylander(uint8* buf, std::FILE* file);
bool remove_skylander(uint8 sky_num);
protected:
std::mutex sky_mutex;
std::array<Skylander, 16> skylanders;
private:
std::queue<std::array<uint8, 64>> m_queries;
bool m_activated = true;
uint8 m_interrupt_counter = 0;
std::mutex m_queryMutex;
};
extern SkylanderUSB g_skyportal;
} // namespace nsyshid

View File

@ -256,6 +256,19 @@ namespace nsyshid
device->m_productId);
}
bool FindDeviceById(uint16 vendorId, uint16 productId)
{
std::lock_guard<std::recursive_mutex> lock(hidMutex);
for (const auto& device : deviceList)
{
if (device->m_vendorId == vendorId && device->m_productId == productId)
{
return true;
}
}
return false;
}
void export_HIDAddClient(PPCInterpreter_t* hCPU)
{
ppcDefineParamTypePtr(hidClient, HIDClient_t, 0);
@ -406,7 +419,8 @@ namespace nsyshid
sint32 originalLength, MPTR callbackFuncMPTR, MPTR callbackParamMPTR)
{
cemuLog_logDebug(LogType::Force, "_hidSetReportAsync begin");
if (device->SetReport(reportData, length, originalData, originalLength))
ReportMessage message(reportData, length, originalData, originalLength);
if (device->SetReport(&message))
{
DoHIDTransferCallback(callbackFuncMPTR,
callbackParamMPTR,
@ -434,7 +448,8 @@ namespace nsyshid
{
_debugPrintHex("_hidSetReportSync Begin", reportData, length);
sint32 returnCode = 0;
if (device->SetReport(reportData, length, originalData, originalLength))
ReportMessage message(reportData, length, originalData, originalLength);
if (device->SetReport(&message))
{
returnCode = originalLength;
}
@ -511,17 +526,16 @@ namespace nsyshid
return -1;
}
memset(data, 0, maxLength);
sint32 bytesRead = 0;
Device::ReadResult readResult = device->Read(data, maxLength, bytesRead);
ReadMessage message(data, maxLength, 0);
Device::ReadResult readResult = device->Read(&message);
switch (readResult)
{
case Device::ReadResult::Success:
{
cemuLog_logDebug(LogType::Force, "nsyshid.hidReadInternalSync(): read {} of {} bytes",
bytesRead,
message.bytesRead,
maxLength);
return bytesRead;
return message.bytesRead;
}
break;
case Device::ReadResult::Error:
@ -609,15 +623,15 @@ namespace nsyshid
cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): cannot write to a non-opened device");
return -1;
}
sint32 bytesWritten = 0;
Device::WriteResult writeResult = device->Write(data, maxLength, bytesWritten);
WriteMessage message(data, maxLength, 0);
Device::WriteResult writeResult = device->Write(&message);
switch (writeResult)
{
case Device::WriteResult::Success:
{
cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): wrote {} of {} bytes", bytesWritten,
cemuLog_logDebug(LogType::Force, "nsyshid.hidWriteInternalSync(): wrote {} of {} bytes", message.bytesWritten,
maxLength);
return bytesWritten;
return message.bytesWritten;
}
break;
case Device::WriteResult::Error:
@ -758,6 +772,11 @@ namespace nsyshid
return nullptr;
}
bool Backend::FindDeviceById(uint16 vendorId, uint16 productId)
{
return nsyshid::FindDeviceById(vendorId, productId);
}
bool Backend::IsDeviceWhitelisted(uint16 vendorId, uint16 productId)
{
return Whitelist::GetInstance().IsDeviceWhitelisted(vendorId, productId);

View File

@ -213,6 +213,32 @@ void zlib125Export_inflateReset2(PPCInterpreter_t* hCPU)
osLib_returnFromFunction(hCPU, r);
}
void zlib125Export_deflateInit_(PPCInterpreter_t* hCPU)
{
ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
ppcDefineParamS32(level, 1);
ppcDefineParamStr(version, 2);
ppcDefineParamS32(streamsize, 3);
z_stream hzs;
zlib125_setupHostZStream(zstream, &hzs, false);
// setup internal memory allocator if requested
if (zstream->zalloc == nullptr)
zstream->zalloc = PPCInterpreter_makeCallableExportDepr(zlib125_zcalloc);
if (zstream->zfree == nullptr)
zstream->zfree = PPCInterpreter_makeCallableExportDepr(zlib125_zcfree);
if (streamsize != sizeof(z_stream_ppc2))
assert_dbg();
sint32 r = deflateInit_(&hzs, level, version, sizeof(z_stream));
zlib125_setupUpdateZStream(&hzs, zstream);
osLib_returnFromFunction(hCPU, r);
}
void zlib125Export_deflateInit2_(PPCInterpreter_t* hCPU)
{
ppcDefineParamStructPtr(zstream, z_stream_ppc2, 0);
@ -345,6 +371,7 @@ namespace zlib
osLib_addFunction("zlib125", "inflateReset", zlib125Export_inflateReset);
osLib_addFunction("zlib125", "inflateReset2", zlib125Export_inflateReset2);
osLib_addFunction("zlib125", "deflateInit_", zlib125Export_deflateInit_);
osLib_addFunction("zlib125", "deflateInit2_", zlib125Export_deflateInit2_);
osLib_addFunction("zlib125", "deflateBound", zlib125Export_deflateBound);
osLib_addFunction("zlib125", "deflate", zlib125Export_deflate);

View File

@ -2418,6 +2418,9 @@ bool ppcAssembler_assembleSingleInstruction(char const* text, PPCAssemblerInOut*
_ppcAssembler_translateAlias(instructionName);
// parse operands
internalInfo.listOperandStr.clear();
bool isInString = false;
while (currentPtr < endPtr)
{
currentPtr++;
@ -2425,7 +2428,10 @@ bool ppcAssembler_assembleSingleInstruction(char const* text, PPCAssemblerInOut*
// find end of operand
while (currentPtr < endPtr)
{
if (*currentPtr == ',')
if (*currentPtr == '"')
isInString=!isInString;
if (*currentPtr == ',' && !isInString)
break;
currentPtr++;
}

View File

@ -131,7 +131,12 @@ uint32 ActiveSettings::GetPersistentId()
bool ActiveSettings::IsOnlineEnabled()
{
return GetConfig().account.online_enabled && Account::GetAccount(GetPersistentId()).IsValidOnlineAccount() && HasRequiredOnlineFiles();
if(!Account::GetAccount(GetPersistentId()).IsValidOnlineAccount())
return false;
if(!HasRequiredOnlineFiles())
return false;
NetworkService networkService = static_cast<NetworkService>(GetConfig().GetAccountNetworkService(GetPersistentId()));
return networkService == NetworkService::Nintendo || networkService == NetworkService::Pretendo || networkService == NetworkService::Custom;
}
bool ActiveSettings::HasRequiredOnlineFiles()
@ -139,8 +144,9 @@ bool ActiveSettings::HasRequiredOnlineFiles()
return s_has_required_online_files;
}
NetworkService ActiveSettings::GetNetworkService() {
return static_cast<NetworkService>(GetConfig().account.active_service.GetValue());
NetworkService ActiveSettings::GetNetworkService()
{
return GetConfig().GetAccountNetworkService(GetPersistentId());
}
bool ActiveSettings::DumpShadersEnabled()

View File

@ -328,8 +328,22 @@ void CemuConfig::Load(XMLConfigParser& parser)
// account
auto acc = parser.get("Account");
account.m_persistent_id = acc.get("PersistentId", account.m_persistent_id);
account.online_enabled = acc.get("OnlineEnabled", account.online_enabled);
account.active_service = acc.get("ActiveService",account.active_service);
// legacy online settings, we only parse these for upgrading purposes
account.legacy_online_enabled = acc.get("OnlineEnabled", account.legacy_online_enabled);
account.legacy_active_service = acc.get("ActiveService",account.legacy_active_service);
// per-account online setting
auto accService = parser.get("AccountService");
account.service_select.clear();
for (auto element = accService.get("SelectedService"); element.valid(); element = accService.get("SelectedService", element))
{
uint32 persistentId = element.get_attribute<uint32>("PersistentId", 0);
sint32 serviceIndex = element.get_attribute<sint32>("Service", 0);
NetworkService networkService = static_cast<NetworkService>(serviceIndex);
if (persistentId < Account::kMinPersistendId)
continue;
if(networkService == NetworkService::Offline || networkService == NetworkService::Nintendo || networkService == NetworkService::Pretendo || networkService == NetworkService::Custom)
account.service_select.emplace(persistentId, networkService);
}
// debug
auto debug = parser.get("Debug");
#if BOOST_OS_WINDOWS
@ -344,6 +358,10 @@ void CemuConfig::Load(XMLConfigParser& parser)
auto dsuc = input.get("DSUC");
dsu_client.host = dsuc.get_attribute("host", dsu_client.host);
dsu_client.port = dsuc.get_attribute("port", dsu_client.port);
// emulatedusbdevices
auto usbdevices = parser.get("EmulatedUsbDevices");
emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal);
}
void CemuConfig::Save(XMLConfigParser& parser)
@ -512,8 +530,17 @@ void CemuConfig::Save(XMLConfigParser& parser)
// account
auto acc = config.set("Account");
acc.set("PersistentId", account.m_persistent_id.GetValue());
acc.set("OnlineEnabled", account.online_enabled.GetValue());
acc.set("ActiveService",account.active_service.GetValue());
// legacy online mode setting
acc.set("OnlineEnabled", account.legacy_online_enabled.GetValue());
acc.set("ActiveService",account.legacy_active_service.GetValue());
// per-account online setting
auto accService = config.set("AccountService");
for(auto& it : account.service_select)
{
auto entry = accService.set("SelectedService");
entry.set_attribute("PersistentId", it.first);
entry.set_attribute("Service", static_cast<sint32>(it.second));
}
// debug
auto debug = config.set("Debug");
#if BOOST_OS_WINDOWS
@ -528,6 +555,10 @@ void CemuConfig::Save(XMLConfigParser& parser)
auto dsuc = input.set("DSUC");
dsuc.set_attribute("host", dsu_client.host);
dsuc.set_attribute("port", dsu_client.port);
// emulated usb devices
auto usbdevices = config.set("EmulatedUsbDevices");
usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue());
}
GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId)
@ -609,3 +640,30 @@ void CemuConfig::AddRecentNfcFile(std::string_view file)
while (recent_nfc_files.size() > kMaxRecentEntries)
recent_nfc_files.pop_back();
}
NetworkService CemuConfig::GetAccountNetworkService(uint32 persistentId)
{
auto it = account.service_select.find(persistentId);
if (it != account.service_select.end())
{
NetworkService serviceIndex = it->second;
// make sure the returned service is valid
if (serviceIndex != NetworkService::Offline &&
serviceIndex != NetworkService::Nintendo &&
serviceIndex != NetworkService::Pretendo &&
serviceIndex != NetworkService::Custom)
return NetworkService::Offline;
if( static_cast<NetworkService>(serviceIndex) == NetworkService::Custom && !NetworkConfig::XMLExists() )
return NetworkService::Offline; // custom is selected but no custom config exists
return serviceIndex;
}
// if not found, return the legacy value
if(!account.legacy_online_enabled)
return NetworkService::Offline;
return static_cast<NetworkService>(account.legacy_active_service.GetValue() + 1); // +1 because "Offline" now takes index 0
}
void CemuConfig::SetAccountSelectedService(uint32 persistentId, NetworkService serviceIndex)
{
account.service_select[persistentId] = serviceIndex;
}

View File

@ -8,6 +8,8 @@
#include <wx/language.h>
#include <wx/intl.h>
enum class NetworkService;
struct GameEntry
{
GameEntry() = default;
@ -483,8 +485,9 @@ struct CemuConfig
struct
{
ConfigValueBounds<uint32> m_persistent_id{ Account::kMinPersistendId, Account::kMinPersistendId, 0xFFFFFFFF };
ConfigValue<bool> online_enabled{false};
ConfigValue<int> active_service{0};
ConfigValue<bool> legacy_online_enabled{false};
ConfigValue<int> legacy_active_service{0};
std::unordered_map<uint32, NetworkService> service_select; // per-account service index. Key is persistentId
}account{};
// input
@ -509,6 +512,16 @@ struct CemuConfig
bool GetGameListCustomName(uint64 titleId, std::string& customName);
void SetGameListCustomName(uint64 titleId, std::string customName);
NetworkService GetAccountNetworkService(uint32 persistentId);
void SetAccountSelectedService(uint32 persistentId, NetworkService serviceIndex);
// emulated usb devices
struct
{
ConfigValue<bool> emulate_skylander_portal{false};
ConfigValue<bool> emulate_infinity_base{true};
}emulated_usb_devices{};
private:
GameEntry* GetGameEntryByTitleId(uint64 titleId);
GameEntry* CreateGameEntry(uint64 titleId);

View File

@ -34,14 +34,15 @@ void NetworkConfig::Load(XMLConfigParser& parser)
bool NetworkConfig::XMLExists()
{
static std::optional<bool> s_exists; // caches result of fs::exists
if(s_exists.has_value())
return *s_exists;
std::error_code ec;
if (!fs::exists(ActiveSettings::GetConfigPath("network_services.xml"), ec))
{
if (static_cast<NetworkService>(GetConfig().account.active_service.GetValue()) == NetworkService::Custom)
{
GetConfig().account.active_service = 0;
}
s_exists = false;
return false;
}
s_exists = true;
return true;
}

View File

@ -5,9 +5,11 @@
enum class NetworkService
{
Offline,
Nintendo,
Pretendo,
Custom
Custom,
COUNT = Custom
};
struct NetworkConfig

View File

@ -101,6 +101,8 @@ add_library(CemuGui
PairingDialog.h
TitleManager.cpp
TitleManager.h
EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp
EmulatedUSBDevices/EmulatedUSBDeviceFrame.h
windows/PPCThreadsViewer
windows/PPCThreadsViewer/DebugPPCThreadsWindow.cpp
windows/PPCThreadsViewer/DebugPPCThreadsWindow.h

View File

@ -0,0 +1,659 @@
#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h"
#include "config/CemuConfig.h"
#include "gui/helpers/wxHelpers.h"
#include "gui/wxHelper.h"
#include "util/helpers/helpers.h"
#include "Cafe/OS/libs/nsyshid/nsyshid.h"
#include "Cafe/OS/libs/nsyshid/Skylander.h"
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/filedlg.h>
#include <wx/notebook.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include "resource/embedded/resources.h"
const std::map<const std::pair<const uint16, const uint16>, const std::string>
list_skylanders = {
{{0, 0x0000}, "Whirlwind"},
{{0, 0x1801}, "Series 2 Whirlwind"},
{{0, 0x1C02}, "Polar Whirlwind"},
{{0, 0x2805}, "Horn Blast Whirlwind"},
{{0, 0x3810}, "Eon's Elite Whirlwind"},
{{1, 0x0000}, "Sonic Boom"},
{{1, 0x1801}, "Series 2 Sonic Boom"},
{{2, 0x0000}, "Warnado"},
{{2, 0x2206}, "LightCore Warnado"},
{{3, 0x0000}, "Lightning Rod"},
{{3, 0x1801}, "Series 2 Lightning Rod"},
{{4, 0x0000}, "Bash"},
{{4, 0x1801}, "Series 2 Bash"},
{{5, 0x0000}, "Terrafin"},
{{5, 0x1801}, "Series 2 Terrafin"},
{{5, 0x2805}, "Knockout Terrafin"},
{{5, 0x3810}, "Eon's Elite Terrafin"},
{{6, 0x0000}, "Dino Rang"},
{{6, 0x4810}, "Eon's Elite Dino Rang"},
{{7, 0x0000}, "Prism Break"},
{{7, 0x1801}, "Series 2 Prism Break"},
{{7, 0x2805}, "Hyper Beam Prism Break"},
{{7, 0x1206}, "LightCore Prism Break"},
{{8, 0x0000}, "Sunburn"},
{{9, 0x0000}, "Eruptor"},
{{9, 0x1801}, "Series 2 Eruptor"},
{{9, 0x2C02}, "Volcanic Eruptor"},
{{9, 0x2805}, "Lava Barf Eruptor"},
{{9, 0x1206}, "LightCore Eruptor"},
{{9, 0x3810}, "Eon's Elite Eruptor"},
{{10, 0x0000}, "Ignitor"},
{{10, 0x1801}, "Series 2 Ignitor"},
{{10, 0x1C03}, "Legendary Ignitor"},
{{11, 0x0000}, "Flameslinger"},
{{11, 0x1801}, "Series 2 Flameslinger"},
{{12, 0x0000}, "Zap"},
{{12, 0x1801}, "Series 2 Zap"},
{{13, 0x0000}, "Wham Shell"},
{{13, 0x2206}, "LightCore Wham Shell"},
{{14, 0x0000}, "Gill Grunt"},
{{14, 0x1801}, "Series 2 Gill Grunt"},
{{14, 0x2805}, "Anchors Away Gill Grunt"},
{{14, 0x3805}, "Tidal Wave Gill Grunt"},
{{14, 0x3810}, "Eon's Elite Gill Grunt"},
{{15, 0x0000}, "Slam Bam"},
{{15, 0x1801}, "Series 2 Slam Bam"},
{{15, 0x1C03}, "Legendary Slam Bam"},
{{15, 0x4810}, "Eon's Elite Slam Bam"},
{{16, 0x0000}, "Spyro"},
{{16, 0x1801}, "Series 2 Spyro"},
{{16, 0x2C02}, "Dark Mega Ram Spyro"},
{{16, 0x2805}, "Mega Ram Spyro"},
{{16, 0x3810}, "Eon's Elite Spyro"},
{{17, 0x0000}, "Voodood"},
{{17, 0x4810}, "Eon's Elite Voodood"},
{{18, 0x0000}, "Double Trouble"},
{{18, 0x1801}, "Series 2 Double Trouble"},
{{18, 0x1C02}, "Royal Double Trouble"},
{{19, 0x0000}, "Trigger Happy"},
{{19, 0x1801}, "Series 2 Trigger Happy"},
{{19, 0x2C02}, "Springtime Trigger Happy"},
{{19, 0x2805}, "Big Bang Trigger Happy"},
{{19, 0x3810}, "Eon's Elite Trigger Happy"},
{{20, 0x0000}, "Drobot"},
{{20, 0x1801}, "Series 2 Drobot"},
{{20, 0x1206}, "LightCore Drobot"},
{{21, 0x0000}, "Drill Seargeant"},
{{21, 0x1801}, "Series 2 Drill Seargeant"},
{{22, 0x0000}, "Boomer"},
{{22, 0x4810}, "Eon's Elite Boomer"},
{{23, 0x0000}, "Wrecking Ball"},
{{23, 0x1801}, "Series 2 Wrecking Ball"},
{{24, 0x0000}, "Camo"},
{{24, 0x2805}, "Thorn Horn Camo"},
{{25, 0x0000}, "Zook"},
{{25, 0x1801}, "Series 2 Zook"},
{{25, 0x4810}, "Eon's Elite Zook"},
{{26, 0x0000}, "Stealth Elf"},
{{26, 0x1801}, "Series 2 Stealth Elf"},
{{26, 0x2C02}, "Dark Stealth Elf"},
{{26, 0x1C03}, "Legendary Stealth Elf"},
{{26, 0x2805}, "Ninja Stealth Elf"},
{{26, 0x3810}, "Eon's Elite Stealth Elf"},
{{27, 0x0000}, "Stump Smash"},
{{27, 0x1801}, "Series 2 Stump Smash"},
{{28, 0x0000}, "Dark Spyro"},
{{29, 0x0000}, "Hex"},
{{29, 0x1801}, "Series 2 Hex"},
{{29, 0x1206}, "LightCore Hex"},
{{30, 0x0000}, "Chop Chop"},
{{30, 0x1801}, "Series 2 Chop Chop"},
{{30, 0x2805}, "Twin Blade Chop Chop"},
{{30, 0x3810}, "Eon's Elite Chop Chop"},
{{31, 0x0000}, "Ghost Roaster"},
{{31, 0x4810}, "Eon's Elite Ghost Roaster"},
{{32, 0x0000}, "Cynder"},
{{32, 0x1801}, "Series 2 Cynder"},
{{32, 0x2805}, "Phantom Cynder"},
{{100, 0x0000}, "Jet Vac"},
{{100, 0x1403}, "Legendary Jet Vac"},
{{100, 0x2805}, "Turbo Jet Vac"},
{{100, 0x3805}, "Full Blast Jet Vac"},
{{100, 0x1206}, "LightCore Jet Vac"},
{{101, 0x0000}, "Swarm"},
{{102, 0x0000}, "Crusher"},
{{102, 0x1602}, "Granite Crusher"},
{{103, 0x0000}, "Flashwing"},
{{103, 0x1402}, "Jade Flash Wing"},
{{103, 0x2206}, "LightCore Flashwing"},
{{104, 0x0000}, "Hot Head"},
{{105, 0x0000}, "Hot Dog"},
{{105, 0x1402}, "Molten Hot Dog"},
{{105, 0x2805}, "Fire Bone Hot Dog"},
{{106, 0x0000}, "Chill"},
{{106, 0x1603}, "Legendary Chill"},
{{106, 0x2805}, "Blizzard Chill"},
{{106, 0x1206}, "LightCore Chill"},
{{107, 0x0000}, "Thumpback"},
{{108, 0x0000}, "Pop Fizz"},
{{108, 0x1402}, "Punch Pop Fizz"},
{{108, 0x3C02}, "Love Potion Pop Fizz"},
{{108, 0x2805}, "Super Gulp Pop Fizz"},
{{108, 0x3805}, "Fizzy Frenzy Pop Fizz"},
{{108, 0x1206}, "LightCore Pop Fizz"},
{{109, 0x0000}, "Ninjini"},
{{109, 0x1602}, "Scarlet Ninjini"},
{{110, 0x0000}, "Bouncer"},
{{110, 0x1603}, "Legendary Bouncer"},
{{111, 0x0000}, "Sprocket"},
{{111, 0x2805}, "Heavy Duty Sprocket"},
{{112, 0x0000}, "Tree Rex"},
{{112, 0x1602}, "Gnarly Tree Rex"},
{{113, 0x0000}, "Shroomboom"},
{{113, 0x3805}, "Sure Shot Shroomboom"},
{{113, 0x1206}, "LightCore Shroomboom"},
{{114, 0x0000}, "Eye Brawl"},
{{115, 0x0000}, "Fright Rider"},
{{200, 0x0000}, "Anvil Rain"},
{{201, 0x0000}, "Hidden Treasure"},
{{201, 0x2000}, "Platinum Hidden Treasure"},
{{202, 0x0000}, "Healing Elixir"},
{{203, 0x0000}, "Ghost Pirate Swords"},
{{204, 0x0000}, "Time Twist Hourglass"},
{{205, 0x0000}, "Sky Iron Shield"},
{{206, 0x0000}, "Winged Boots"},
{{207, 0x0000}, "Sparx the Dragonfly"},
{{208, 0x0000}, "Dragonfire Cannon"},
{{208, 0x1602}, "Golden Dragonfire Cannon"},
{{209, 0x0000}, "Scorpion Striker"},
{{210, 0x3002}, "Biter's Bane"},
{{210, 0x3008}, "Sorcerous Skull"},
{{210, 0x300B}, "Axe of Illusion"},
{{210, 0x300E}, "Arcane Hourglass"},
{{210, 0x3012}, "Spell Slapper"},
{{210, 0x3014}, "Rune Rocket"},
{{211, 0x3001}, "Tidal Tiki"},
{{211, 0x3002}, "Wet Walter"},
{{211, 0x3006}, "Flood Flask"},
{{211, 0x3406}, "Legendary Flood Flask"},
{{211, 0x3007}, "Soaking Staff"},
{{211, 0x300B}, "Aqua Axe"},
{{211, 0x3016}, "Frost Helm"},
{{212, 0x3003}, "Breezy Bird"},
{{212, 0x3006}, "Drafty Decanter"},
{{212, 0x300D}, "Tempest Timer"},
{{212, 0x3010}, "Cloudy Cobra"},
{{212, 0x3011}, "Storm Warning"},
{{212, 0x3018}, "Cyclone Saber"},
{{213, 0x3004}, "Spirit Sphere"},
{{213, 0x3404}, "Legendary Spirit Sphere"},
{{213, 0x3008}, "Spectral Skull"},
{{213, 0x3408}, "Legendary Spectral Skull"},
{{213, 0x300B}, "Haunted Hatchet"},
{{213, 0x300C}, "Grim Gripper"},
{{213, 0x3010}, "Spooky Snake"},
{{213, 0x3017}, "Dream Piercer"},
{{214, 0x3000}, "Tech Totem"},
{{214, 0x3007}, "Automatic Angel"},
{{214, 0x3009}, "Factory Flower"},
{{214, 0x300C}, "Grabbing Gadget"},
{{214, 0x3016}, "Makers Mana"},
{{214, 0x301A}, "Topsy Techy"},
{{215, 0x3005}, "Eternal Flame"},
{{215, 0x3009}, "Fire Flower"},
{{215, 0x3011}, "Scorching Stopper"},
{{215, 0x3012}, "Searing Spinner"},
{{215, 0x3017}, "Spark Spear"},
{{215, 0x301B}, "Blazing Belch"},
{{216, 0x3000}, "Banded Boulder"},
{{216, 0x3003}, "Rock Hawk"},
{{216, 0x300A}, "Slag Hammer"},
{{216, 0x300E}, "Dust Of Time"},
{{216, 0x3013}, "Spinning Sandstorm"},
{{216, 0x301A}, "Rubble Trouble"},
{{217, 0x3003}, "Oak Eagle"},
{{217, 0x3005}, "Emerald Energy"},
{{217, 0x300A}, "Weed Whacker"},
{{217, 0x3010}, "Seed Serpent"},
{{217, 0x3018}, "Jade Blade"},
{{217, 0x301B}, "Shrub Shrieker"},
{{218, 0x3000}, "Dark Dagger"},
{{218, 0x3014}, "Shadow Spider"},
{{218, 0x301A}, "Ghastly Grimace"},
{{219, 0x3000}, "Shining Ship"},
{{219, 0x300F}, "Heavenly Hawk"},
{{219, 0x301B}, "Beam Scream"},
{{220, 0x301E}, "Kaos Trap"},
{{220, 0x351F}, "Ultimate Kaos Trap"},
{{230, 0x0000}, "Hand of Fate"},
{{230, 0x3403}, "Legendary Hand of Fate"},
{{231, 0x0000}, "Piggy Bank"},
{{232, 0x0000}, "Rocket Ram"},
{{233, 0x0000}, "Tiki Speaky"},
{{300, 0x0000}, "Dragons Peak"},
{{301, 0x0000}, "Empire of Ice"},
{{302, 0x0000}, "Pirate Seas"},
{{303, 0x0000}, "Darklight Crypt"},
{{304, 0x0000}, "Volcanic Vault"},
{{305, 0x0000}, "Mirror of Mystery"},
{{306, 0x0000}, "Nightmare Express"},
{{307, 0x0000}, "Sunscraper Spire"},
{{308, 0x0000}, "Midnight Museum"},
{{404, 0x0000}, "Legendary Bash"},
{{416, 0x0000}, "Legendary Spyro"},
{{419, 0x0000}, "Legendary Trigger Happy"},
{{430, 0x0000}, "Legendary Chop Chop"},
{{450, 0x0000}, "Gusto"},
{{451, 0x0000}, "Thunderbolt"},
{{452, 0x0000}, "Fling Kong"},
{{453, 0x0000}, "Blades"},
{{453, 0x3403}, "Legendary Blades"},
{{454, 0x0000}, "Wallop"},
{{455, 0x0000}, "Head Rush"},
{{455, 0x3402}, "Nitro Head Rush"},
{{456, 0x0000}, "Fist Bump"},
{{457, 0x0000}, "Rocky Roll"},
{{458, 0x0000}, "Wildfire"},
{{458, 0x3402}, "Dark Wildfire"},
{{459, 0x0000}, "Ka Boom"},
{{460, 0x0000}, "Trail Blazer"},
{{461, 0x0000}, "Torch"},
{{462, 0x3000}, "Snap Shot"},
{{462, 0x3402}, "Dark Snap Shot"},
{{463, 0x0000}, "Lob Star"},
{{463, 0x3402}, "Winterfest Lob-Star"},
{{464, 0x0000}, "Flip Wreck"},
{{465, 0x0000}, "Echo"},
{{466, 0x0000}, "Blastermind"},
{{467, 0x0000}, "Enigma"},
{{468, 0x0000}, "Deja Vu"},
{{468, 0x3403}, "Legendary Deja Vu"},
{{469, 0x0000}, "Cobra Candabra"},
{{469, 0x3402}, "King Cobra Cadabra"},
{{470, 0x0000}, "Jawbreaker"},
{{470, 0x3403}, "Legendary Jawbreaker"},
{{471, 0x0000}, "Gearshift"},
{{472, 0x0000}, "Chopper"},
{{473, 0x0000}, "Tread Head"},
{{474, 0x0000}, "Bushwack"},
{{474, 0x3403}, "Legendary Bushwack"},
{{475, 0x0000}, "Tuff Luck"},
{{476, 0x0000}, "Food Fight"},
{{476, 0x3402}, "Dark Food Fight"},
{{477, 0x0000}, "High Five"},
{{478, 0x0000}, "Krypt King"},
{{478, 0x3402}, "Nitro Krypt King"},
{{479, 0x0000}, "Short Cut"},
{{480, 0x0000}, "Bat Spin"},
{{481, 0x0000}, "Funny Bone"},
{{482, 0x0000}, "Knight Light"},
{{483, 0x0000}, "Spotlight"},
{{484, 0x0000}, "Knight Mare"},
{{485, 0x0000}, "Blackout"},
{{502, 0x0000}, "Bop"},
{{505, 0x0000}, "Terrabite"},
{{506, 0x0000}, "Breeze"},
{{508, 0x0000}, "Pet Vac"},
{{508, 0x3402}, "Power Punch Pet Vac"},
{{507, 0x0000}, "Weeruptor"},
{{507, 0x3402}, "Eggcellent Weeruptor"},
{{509, 0x0000}, "Small Fry"},
{{510, 0x0000}, "Drobit"},
{{519, 0x0000}, "Trigger Snappy"},
{{526, 0x0000}, "Whisper Elf"},
{{540, 0x0000}, "Barkley"},
{{540, 0x3402}, "Gnarly Barkley"},
{{541, 0x0000}, "Thumpling"},
{{514, 0x0000}, "Gill Runt"},
{{542, 0x0000}, "Mini-Jini"},
{{503, 0x0000}, "Spry"},
{{504, 0x0000}, "Hijinx"},
{{543, 0x0000}, "Eye Small"},
{{601, 0x0000}, "King Pen"},
{{602, 0x0000}, "Tri-Tip"},
{{603, 0x0000}, "Chopscotch"},
{{604, 0x0000}, "Boom Bloom"},
{{605, 0x0000}, "Pit Boss"},
{{606, 0x0000}, "Barbella"},
{{607, 0x0000}, "Air Strike"},
{{608, 0x0000}, "Ember"},
{{609, 0x0000}, "Ambush"},
{{610, 0x0000}, "Dr. Krankcase"},
{{611, 0x0000}, "Hood Sickle"},
{{612, 0x0000}, "Tae Kwon Crow"},
{{613, 0x0000}, "Golden Queen"},
{{614, 0x0000}, "Wolfgang"},
{{615, 0x0000}, "Pain-Yatta"},
{{616, 0x0000}, "Mysticat"},
{{617, 0x0000}, "Starcast"},
{{618, 0x0000}, "Buckshot"},
{{619, 0x0000}, "Aurora"},
{{620, 0x0000}, "Flare Wolf"},
{{621, 0x0000}, "Chompy Mage"},
{{622, 0x0000}, "Bad Juju"},
{{623, 0x0000}, "Grave Clobber"},
{{624, 0x0000}, "Blaster-Tron"},
{{625, 0x0000}, "Ro-Bow"},
{{626, 0x0000}, "Chain Reaction"},
{{627, 0x0000}, "Kaos"},
{{628, 0x0000}, "Wild Storm"},
{{629, 0x0000}, "Tidepool"},
{{630, 0x0000}, "Crash Bandicoot"},
{{631, 0x0000}, "Dr. Neo Cortex"},
{{1000, 0x0000}, "Boom Jet (Bottom)"},
{{1001, 0x0000}, "Free Ranger (Bottom)"},
{{1001, 0x2403}, "Legendary Free Ranger (Bottom)"},
{{1002, 0x0000}, "Rubble Rouser (Bottom)"},
{{1003, 0x0000}, "Doom Stone (Bottom)"},
{{1004, 0x0000}, "Blast Zone (Bottom)"},
{{1004, 0x2402}, "Dark Blast Zone (Bottom)"},
{{1005, 0x0000}, "Fire Kraken (Bottom)"},
{{1005, 0x2402}, "Jade Fire Kraken (Bottom)"},
{{1006, 0x0000}, "Stink Bomb (Bottom)"},
{{1007, 0x0000}, "Grilla Drilla (Bottom)"},
{{1008, 0x0000}, "Hoot Loop (Bottom)"},
{{1008, 0x2402}, "Enchanted Hoot Loop (Bottom)"},
{{1009, 0x0000}, "Trap Shadow (Bottom)"},
{{1010, 0x0000}, "Magna Charge (Bottom)"},
{{1010, 0x2402}, "Nitro Magna Charge (Bottom)"},
{{1011, 0x0000}, "Spy Rise (Bottom)"},
{{1012, 0x0000}, "Night Shift (Bottom)"},
{{1012, 0x2403}, "Legendary Night Shift (Bottom)"},
{{1013, 0x0000}, "Rattle Shake (Bottom)"},
{{1013, 0x2402}, "Quick Draw Rattle Shake (Bottom)"},
{{1014, 0x0000}, "Freeze Blade (Bottom)"},
{{1014, 0x2402}, "Nitro Freeze Blade (Bottom)"},
{{1015, 0x0000}, "Wash Buckler (Bottom)"},
{{1015, 0x2402}, "Dark Wash Buckler (Bottom)"},
{{2000, 0x0000}, "Boom Jet (Top)"},
{{2001, 0x0000}, "Free Ranger (Top)"},
{{2001, 0x2403}, "Legendary Free Ranger (Top)"},
{{2002, 0x0000}, "Rubble Rouser (Top)"},
{{2003, 0x0000}, "Doom Stone (Top)"},
{{2004, 0x0000}, "Blast Zone (Top)"},
{{2004, 0x2402}, "Dark Blast Zone (Top)"},
{{2005, 0x0000}, "Fire Kraken (Top)"},
{{2005, 0x2402}, "Jade Fire Kraken (Top)"},
{{2006, 0x0000}, "Stink Bomb (Top)"},
{{2007, 0x0000}, "Grilla Drilla (Top)"},
{{2008, 0x0000}, "Hoot Loop (Top)"},
{{2008, 0x2402}, "Enchanted Hoot Loop (Top)"},
{{2009, 0x0000}, "Trap Shadow (Top)"},
{{2010, 0x0000}, "Magna Charge (Top)"},
{{2010, 0x2402}, "Nitro Magna Charge (Top)"},
{{2011, 0x0000}, "Spy Rise (Top)"},
{{2012, 0x0000}, "Night Shift (Top)"},
{{2012, 0x2403}, "Legendary Night Shift (Top)"},
{{2013, 0x0000}, "Rattle Shake (Top)"},
{{2013, 0x2402}, "Quick Draw Rattle Shake (Top)"},
{{2014, 0x0000}, "Freeze Blade (Top)"},
{{2014, 0x2402}, "Nitro Freeze Blade (Top)"},
{{2015, 0x0000}, "Wash Buckler (Top)"},
{{2015, 0x2402}, "Dark Wash Buckler (Top)"},
{{3000, 0x0000}, "Scratch"},
{{3001, 0x0000}, "Pop Thorn"},
{{3002, 0x0000}, "Slobber Tooth"},
{{3002, 0x2402}, "Dark Slobber Tooth"},
{{3003, 0x0000}, "Scorp"},
{{3004, 0x0000}, "Fryno"},
{{3004, 0x3805}, "Hog Wild Fryno"},
{{3005, 0x0000}, "Smolderdash"},
{{3005, 0x2206}, "LightCore Smolderdash"},
{{3006, 0x0000}, "Bumble Blast"},
{{3006, 0x2402}, "Jolly Bumble Blast"},
{{3006, 0x2206}, "LightCore Bumble Blast"},
{{3007, 0x0000}, "Zoo Lou"},
{{3007, 0x2403}, "Legendary Zoo Lou"},
{{3008, 0x0000}, "Dune Bug"},
{{3009, 0x0000}, "Star Strike"},
{{3009, 0x2602}, "Enchanted Star Strike"},
{{3009, 0x2206}, "LightCore Star Strike"},
{{3010, 0x0000}, "Countdown"},
{{3010, 0x2402}, "Kickoff Countdown"},
{{3010, 0x2206}, "LightCore Countdown"},
{{3011, 0x0000}, "Wind Up"},
{{3011, 0x2404}, "Gear Head VVind Up"},
{{3012, 0x0000}, "Roller Brawl"},
{{3013, 0x0000}, "Grim Creeper"},
{{3013, 0x2603}, "Legendary Grim Creeper"},
{{3013, 0x2206}, "LightCore Grim Creeper"},
{{3014, 0x0000}, "Rip Tide"},
{{3015, 0x0000}, "Punk Shock"},
{{3200, 0x0000}, "Battle Hammer"},
{{3201, 0x0000}, "Sky Diamond"},
{{3202, 0x0000}, "Platinum Sheep"},
{{3203, 0x0000}, "Groove Machine"},
{{3204, 0x0000}, "UFO Hat"},
{{3300, 0x0000}, "Sheep Wreck Island"},
{{3301, 0x0000}, "Tower of Time"},
{{3302, 0x0000}, "Fiery Forge"},
{{3303, 0x0000}, "Arkeyan Crossbow"},
{{3220, 0x0000}, "Jet Stream"},
{{3221, 0x0000}, "Tomb Buggy"},
{{3222, 0x0000}, "Reef Ripper"},
{{3223, 0x0000}, "Burn Cycle"},
{{3224, 0x0000}, "Hot Streak"},
{{3224, 0x4402}, "Dark Hot Streak"},
{{3224, 0x4004}, "E3 Hot Streak"},
{{3224, 0x441E}, "Golden Hot Streak"},
{{3225, 0x0000}, "Shark Tank"},
{{3226, 0x0000}, "Thump Truck"},
{{3227, 0x0000}, "Crypt Crusher"},
{{3228, 0x0000}, "Stealth Stinger"},
{{3228, 0x4402}, "Nitro Stealth Stinger"},
{{3231, 0x0000}, "Dive Bomber"},
{{3231, 0x4402}, "Spring Ahead Dive Bomber"},
{{3232, 0x0000}, "Sky Slicer"},
{{3233, 0x0000}, "Clown Cruiser (Nintendo Only)"},
{{3233, 0x4402}, "Dark Clown Cruiser (Nintendo Only)"},
{{3234, 0x0000}, "Gold Rusher"},
{{3234, 0x4402}, "Power Blue Gold Rusher"},
{{3235, 0x0000}, "Shield Striker"},
{{3236, 0x0000}, "Sun Runner"},
{{3236, 0x4403}, "Legendary Sun Runner"},
{{3237, 0x0000}, "Sea Shadow"},
{{3237, 0x4402}, "Dark Sea Shadow"},
{{3238, 0x0000}, "Splatter Splasher"},
{{3238, 0x4402}, "Power Blue Splatter Splasher"},
{{3239, 0x0000}, "Soda Skimmer"},
{{3239, 0x4402}, "Nitro Soda Skimmer"},
{{3240, 0x0000}, "Barrel Blaster (Nintendo Only)"},
{{3240, 0x4402}, "Dark Barrel Blaster (Nintendo Only)"},
{{3241, 0x0000}, "Buzz Wing"},
{{3400, 0x0000}, "Fiesta"},
{{3400, 0x4515}, "Frightful Fiesta"},
{{3401, 0x0000}, "High Volt"},
{{3402, 0x0000}, "Splat"},
{{3402, 0x4502}, "Power Blue Splat"},
{{3406, 0x0000}, "Stormblade"},
{{3411, 0x0000}, "Smash Hit"},
{{3411, 0x4502}, "Steel Plated Smash Hit"},
{{3412, 0x0000}, "Spitfire"},
{{3412, 0x4502}, "Dark Spitfire"},
{{3413, 0x0000}, "Hurricane Jet Vac"},
{{3413, 0x4503}, "Legendary Hurricane Jet Vac"},
{{3414, 0x0000}, "Double Dare Trigger Happy"},
{{3414, 0x4502}, "Power Blue Double Dare Trigger Happy"},
{{3415, 0x0000}, "Super Shot Stealth Elf"},
{{3415, 0x4502}, "Dark Super Shot Stealth Elf"},
{{3416, 0x0000}, "Shark Shooter Terrafin"},
{{3417, 0x0000}, "Bone Bash Roller Brawl"},
{{3417, 0x4503}, "Legendary Bone Bash Roller Brawl"},
{{3420, 0x0000}, "Big Bubble Pop Fizz"},
{{3420, 0x450E}, "Birthday Bash Big Bubble Pop Fizz"},
{{3421, 0x0000}, "Lava Lance Eruptor"},
{{3422, 0x0000}, "Deep Dive Gill Grunt"},
{{3423, 0x0000}, "Turbo Charge Donkey Kong (Nintendo Only)"},
{{3423, 0x4502}, "Dark Turbo Charge Donkey Kong (Nintendo Only)"},
{{3424, 0x0000}, "Hammer Slam Bowser (Nintendo Only)"},
{{3424, 0x4502}, "Dark Hammer Slam Bowser (Nintendo Only)"},
{{3425, 0x0000}, "Dive-Clops"},
{{3425, 0x450E}, "Missile-Tow Dive-Clops"},
{{3426, 0x0000}, "Astroblast"},
{{3426, 0x4503}, "Legendary Astroblast"},
{{3427, 0x0000}, "Nightfall"},
{{3428, 0x0000}, "Thrillipede"},
{{3428, 0x450D}, "Eggcited Thrillipede"},
{{3500, 0x0000}, "Sky Trophy"},
{{3501, 0x0000}, "Land Trophy"},
{{3502, 0x0000}, "Sea Trophy"},
{{3503, 0x0000}, "Kaos Trophy"},
};
EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent)
: wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition,
wxDefaultSize, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL)
{
SetIcon(wxICON(X_BOX));
auto& config = GetConfig();
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* notebook = new wxNotebook(this, wxID_ANY);
notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal"));
sizer->Add(notebook, 1, wxEXPAND | wxALL, 2);
SetSizerAndFit(sizer);
Layout();
Centre(wxBOTH);
}
EmulatedUSBDeviceFrame::~EmulatedUSBDeviceFrame() {}
wxPanel* EmulatedUSBDeviceFrame::AddSkylanderPage(wxNotebook* notebook)
{
auto* panel = new wxPanel(notebook);
auto* panel_sizer = new wxBoxSizer(wxVERTICAL);
auto* box = new wxStaticBox(panel, wxID_ANY, _("Skylanders Manager"));
auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL);
auto* row = new wxBoxSizer(wxHORIZONTAL);
m_emulate_portal =
new wxCheckBox(box, wxID_ANY, _("Emulate Skylander Portal"));
m_emulate_portal->SetValue(
GetConfig().emulated_usb_devices.emulate_skylander_portal);
m_emulate_portal->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) {
GetConfig().emulated_usb_devices.emulate_skylander_portal =
m_emulate_portal->IsChecked();
g_config.Save();
});
row->Add(m_emulate_portal, 1, wxEXPAND | wxALL, 2);
box_sizer->Add(row, 1, wxEXPAND | wxALL, 2);
for (int i = 0; i < 16; i++)
{
box_sizer->Add(AddSkylanderRow(i, box), 1, wxEXPAND | wxALL, 2);
}
panel_sizer->Add(box_sizer, 1, wxEXPAND | wxALL, 2);
panel->SetSizerAndFit(panel_sizer);
return panel;
}
wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 row_number,
wxStaticBox* box)
{
auto* row = new wxBoxSizer(wxHORIZONTAL);
row->Add(new wxStaticText(box, wxID_ANY,
fmt::format("{} {}", _("Skylander").ToStdString(),
(row_number + 1))), 1, wxEXPAND | wxALL, 2);
m_skylander_slots[row_number] =
new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize,
wxTE_READONLY);
m_skylander_slots[row_number]->SetMinSize(wxSize(150, -1));
m_skylander_slots[row_number]->Disable();
row->Add(m_skylander_slots[row_number], 1, wxEXPAND | wxALL, 2);
auto* load_button = new wxButton(box, wxID_ANY, _("Load"));
load_button->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) {
LoadSkylander(row_number);
});
auto* clear_button = new wxButton(box, wxID_ANY, _("Clear"));
clear_button->Bind(wxEVT_BUTTON, [row_number, this](wxCommandEvent&) {
ClearSkylander(row_number);
});
row->Add(load_button, 1, wxEXPAND | wxALL, 2);
row->Add(clear_button, 1, wxEXPAND | wxALL, 2);
return row;
}
void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot)
{
wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "",
"SKY files (*.sky)|*.sky",
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty())
return;
FILE* sky_file = std::fopen(openFileDialog.GetPath().c_str(), "r+b");
if (!sky_file)
{
return;
}
std::array<uint8, 0x40 * 0x10> file_data;
size_t read_count = std::fread(&file_data[0], sizeof(file_data[0]),
file_data.size(), sky_file);
if (read_count != file_data.size())
{
return;
}
ClearSkylander(slot);
uint16 sky_id = uint16(file_data[0x11]) << 8 | uint16(file_data[0x10]);
uint16 sky_var = uint16(file_data[0x1D]) << 8 | uint16(file_data[0x1C]);
uint8 portal_slot = nsyshid::g_skyportal.load_skylander(file_data.data(),
std::move(sky_file));
sky_slots[slot] = std::tuple(portal_slot, sky_id, sky_var);
UpdateSkylanderEdits();
}
void EmulatedUSBDeviceFrame::CreateSkylander(uint8 slot)
{
cemuLog_log(LogType::Force, "Create Skylander: {}", slot);
}
void EmulatedUSBDeviceFrame::ClearSkylander(uint8 slot)
{
if (auto slot_infos = sky_slots[slot])
{
auto [cur_slot, id, var] = slot_infos.value();
nsyshid::g_skyportal.remove_skylander(cur_slot);
sky_slots[slot] = {};
UpdateSkylanderEdits();
}
}
void EmulatedUSBDeviceFrame::UpdateSkylanderEdits()
{
for (auto i = 0; i < 16; i++)
{
std::string display_string;
if (auto sd = sky_slots[i])
{
auto [portal_slot, sky_id, sky_var] = sd.value();
auto found_sky = list_skylanders.find(std::make_pair(sky_id, sky_var));
if (found_sky != list_skylanders.end())
{
display_string = found_sky->second;
}
else
{
display_string = fmt::format("Unknown (Id:{} Var:{})", sky_id, sky_var);
}
}
else
{
display_string = "None";
}
m_skylander_slots[i]->ChangeValue(display_string);
}
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <array>
#include <wx/frame.h>
class wxBoxSizer;
class wxCheckBox;
class wxFlexGridSizer;
class wxNotebook;
class wxPanel;
class wxStaticBox;
class wxTextCtrl;
class EmulatedUSBDeviceFrame : public wxFrame {
public:
EmulatedUSBDeviceFrame(wxWindow* parent);
~EmulatedUSBDeviceFrame();
private:
wxCheckBox* m_emulate_portal;
std::array<wxTextCtrl*, 16> m_skylander_slots;
std::array<std::optional<std::tuple<uint8, uint16, uint16>>, 16> sky_slots;
wxPanel* AddSkylanderPage(wxNotebook* notebook);
wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box);
void LoadSkylander(uint8 slot);
void CreateSkylander(uint8 slot);
void ClearSkylander(uint8 slot);
void UpdateSkylanderEdits();
};

View File

@ -683,18 +683,6 @@ wxPanel* GeneralSettings2::AddAccountPage(wxNotebook* notebook)
content->Add(m_delete_account, 0, wxEXPAND | wxALL | wxALIGN_RIGHT, 5);
m_delete_account->Bind(wxEVT_BUTTON, &GeneralSettings2::OnAccountDelete, this);
wxString choices[] = { _("Nintendo"), _("Pretendo"), _("Custom") };
m_active_service = new wxRadioBox(online_panel, wxID_ANY, _("Network Service"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 3, wxRA_SPECIFY_COLS);
if (!NetworkConfig::XMLExists())
m_active_service->Enable(2, false);
m_active_service->SetItemToolTip(0, _("Connect to the official Nintendo Network Service"));
m_active_service->SetItemToolTip(1, _("Connect to the Pretendo Network Service"));
m_active_service->SetItemToolTip(2, _("Connect to a custom Network Service (configured via network_services.xml)"));
m_active_service->Bind(wxEVT_RADIOBOX, &GeneralSettings2::OnAccountServiceChanged,this);
content->Add(m_active_service, 0, wxEXPAND | wxALL, 5);
box_sizer->Add(content, 1, wxEXPAND, 5);
online_panel_sizer->Add(box_sizer, 0, wxEXPAND | wxALL, 5);
@ -704,17 +692,33 @@ wxPanel* GeneralSettings2::AddAccountPage(wxNotebook* notebook)
m_active_account->Enable(false);
m_create_account->Enable(false);
m_delete_account->Enable(false);
}
}
{
wxString choices[] = { _("Offline"), _("Nintendo"), _("Pretendo"), _("Custom") };
m_active_service = new wxRadioBox(online_panel, wxID_ANY, _("Network Service"), wxDefaultPosition, wxDefaultSize, std::size(choices), choices, 4, wxRA_SPECIFY_COLS);
if (!NetworkConfig::XMLExists())
m_active_service->Enable(3, false);
m_active_service->SetItemToolTip(0, _("Online functionality disabled for this account"));
m_active_service->SetItemToolTip(1, _("Connect to the official Nintendo Network Service"));
m_active_service->SetItemToolTip(2, _("Connect to the Pretendo Network Service"));
m_active_service->SetItemToolTip(3, _("Connect to a custom Network Service (configured via network_services.xml)"));
m_active_service->Bind(wxEVT_RADIOBOX, &GeneralSettings2::OnAccountServiceChanged,this);
online_panel_sizer->Add(m_active_service, 0, wxEXPAND | wxALL, 5);
if (CafeSystem::IsTitleRunning())
{
m_active_service->Enable(false);
}
}
{
auto* box = new wxStaticBox(online_panel, wxID_ANY, _("Online settings"));
auto* box = new wxStaticBox(online_panel, wxID_ANY, _("Online play requirements"));
auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL);
m_online_enabled = new wxCheckBox(box, wxID_ANY, _("Enable online mode"));
m_online_enabled->Bind(wxEVT_CHECKBOX, &GeneralSettings2::OnOnlineEnable, this);
box_sizer->Add(m_online_enabled, 0, wxEXPAND | wxALL, 5);
auto* row = new wxFlexGridSizer(0, 2, 0, 0);
row->SetFlexibleDirection(wxBOTH);
@ -873,6 +877,14 @@ GeneralSettings2::GeneralSettings2(wxWindow* parent, bool game_launched)
DisableSettings(game_launched);
}
uint32 GeneralSettings2::GetSelectedAccountPersistentId()
{
const auto active_account = m_active_account->GetSelection();
if (active_account == wxNOT_FOUND)
return GetConfig().account.m_persistent_id.GetInitValue();
return dynamic_cast<wxAccountData*>(m_active_account->GetClientObject(active_account))->GetAccount().GetPersistentId();
}
void GeneralSettings2::StoreConfig()
{
auto* app = (CemuApp*)wxTheApp;
@ -1038,14 +1050,7 @@ void GeneralSettings2::StoreConfig()
config.notification.friends = m_friends_data->GetValue();
// account
const auto active_account = m_active_account->GetSelection();
if (active_account == wxNOT_FOUND)
config.account.m_persistent_id = config.account.m_persistent_id.GetInitValue();
else
config.account.m_persistent_id = dynamic_cast<wxAccountData*>(m_active_account->GetClientObject(active_account))->GetAccount().GetPersistentId();
config.account.online_enabled = m_online_enabled->GetValue();
config.account.active_service = m_active_service->GetSelection();
config.account.m_persistent_id = GetSelectedAccountPersistentId();
// debug
config.crash_dump = (CrashDump)m_crash_dump->GetSelection();
@ -1371,14 +1376,13 @@ void GeneralSettings2::UpdateAccountInformation()
{
m_account_grid->SetSplitterPosition(100);
m_online_status->SetLabel(_("At least one issue has been found"));
const auto selection = m_active_account->GetSelection();
if(selection == wxNOT_FOUND)
{
m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_ERROR).ConvertToImage().Scale(16, 16));
m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() & ~wxBORDER_NONE);
ResetAccountInformation();
m_online_status->SetLabel(_("No account selected"));
return;
}
@ -1404,11 +1408,26 @@ void GeneralSettings2::UpdateAccountInformation()
index = 0;
country_property->SetChoiceSelection(index);
const bool online_valid = account.IsValidOnlineAccount() && ActiveSettings::HasRequiredOnlineFiles();
if (online_valid)
const bool online_fully_valid = account.IsValidOnlineAccount() && ActiveSettings::HasRequiredOnlineFiles();
if (ActiveSettings::HasRequiredOnlineFiles())
{
if(account.IsValidOnlineAccount())
m_online_status->SetLabel(_("Selected account is a valid online account"));
else
m_online_status->SetLabel(_("Selected account is not linked to a NNID or PNID"));
}
else
{
if(NCrypto::OTP_IsPresent() != NCrypto::SEEPROM_IsPresent())
m_online_status->SetLabel(_("OTP.bin or SEEPROM.bin is missing"));
else if(NCrypto::OTP_IsPresent() && NCrypto::SEEPROM_IsPresent())
m_online_status->SetLabel(_("OTP and SEEPROM present but no certificate files were found"));
else
m_online_status->SetLabel(_("Online play is not set up. Follow the guide below to get started"));
}
if(online_fully_valid)
{
m_online_status->SetLabel(_("Your account is a valid online account"));
m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_CHECK_YES).ConvertToImage().Scale(16, 16));
m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() | wxBORDER_NONE);
}
@ -1417,7 +1436,28 @@ void GeneralSettings2::UpdateAccountInformation()
m_validate_online->SetBitmap(wxBITMAP_PNG_FROM_DATA(PNG_ERROR).ConvertToImage().Scale(16, 16));
m_validate_online->SetWindowStyleFlag(m_validate_online->GetWindowStyleFlag() & ~wxBORDER_NONE);
}
// enable/disable network service field depending on online requirements
m_active_service->Enable(online_fully_valid && !CafeSystem::IsTitleRunning());
if(online_fully_valid)
{
NetworkService service = GetConfig().GetAccountNetworkService(account.GetPersistentId());
m_active_service->SetSelection(static_cast<int>(service));
// set the config option here for the selected service
// this will guarantee that it's actually written to settings.xml
// allowing us to eventually get rid of the legacy option in the (far) future
GetConfig().SetAccountSelectedService(account.GetPersistentId(), service);
}
else
{
m_active_service->SetSelection(0); // force offline
}
wxString tmp = _("Network service");
tmp.append(" (");
tmp.append(wxString::FromUTF8(boost::nowide::narrow(account.GetMiiName())));
tmp.append(")");
m_active_service->SetLabel(tmp);
// refresh pane size
m_account_grid->InvalidateBestSize();
//m_account_grid->GetParent()->FitInside();
@ -1663,9 +1703,8 @@ void GeneralSettings2::ApplyConfig()
break;
}
}
m_online_enabled->SetValue(config.account.online_enabled);
m_active_service->SetSelection(config.account.active_service);
m_active_service->SetSelection((int)config.GetAccountNetworkService(ActiveSettings::GetPersistentId()));
UpdateAccountInformation();
// debug
@ -1673,20 +1712,6 @@ void GeneralSettings2::ApplyConfig()
m_gdb_port->SetValue(config.gdb_port.GetValue());
}
void GeneralSettings2::OnOnlineEnable(wxCommandEvent& event)
{
event.Skip();
if (!m_online_enabled->GetValue())
return;
// show warning if player enables online mode
const auto result = wxMessageBox(_("Please be aware that online mode lets you connect to OFFICIAL servers and therefore there is a risk of getting banned.\nOnly proceed if you are willing to risk losing online access with your Wii U and/or NNID."),
_("Warning"), wxYES_NO | wxCENTRE | wxICON_EXCLAMATION, this);
if (result == wxNO)
m_online_enabled->SetValue(false);
}
void GeneralSettings2::OnAudioAPISelected(wxCommandEvent& event)
{
IAudioAPI::AudioAPI api;
@ -1952,6 +1977,9 @@ void GeneralSettings2::OnActiveAccountChanged(wxCommandEvent& event)
void GeneralSettings2::OnAccountServiceChanged(wxCommandEvent& event)
{
auto& config = GetConfig();
uint32 peristentId = GetSelectedAccountPersistentId();
config.SetAccountSelectedService(peristentId, static_cast<NetworkService>(m_active_service->GetSelection()));
UpdateAccountInformation();
}
@ -2005,12 +2033,12 @@ void GeneralSettings2::OnShowOnlineValidator(wxCommandEvent& event)
err << _("The following error(s) have been found:") << '\n';
if (validator.otp == OnlineValidator::FileState::Missing)
err << _("otp.bin missing in Cemu root directory") << '\n';
err << _("otp.bin missing in Cemu directory") << '\n';
else if(validator.otp == OnlineValidator::FileState::Corrupted)
err << _("otp.bin is invalid") << '\n';
if (validator.seeprom == OnlineValidator::FileState::Missing)
err << _("seeprom.bin missing in Cemu root directory") << '\n';
err << _("seeprom.bin missing in Cemu directory") << '\n';
else if(validator.seeprom == OnlineValidator::FileState::Corrupted)
err << _("seeprom.bin is invalid") << '\n';
@ -2045,9 +2073,10 @@ void GeneralSettings2::OnShowOnlineValidator(wxCommandEvent& event)
wxString GeneralSettings2::GetOnlineAccountErrorMessage(OnlineAccountError error)
{
switch (error) {
switch (error)
{
case OnlineAccountError::kNoAccountId:
return _("AccountId missing (The account is not connected to a NNID)");
return _("AccountId missing (The account is not connected to a NNID/PNID)");
case OnlineAccountError::kNoPasswordCached:
return _("IsPasswordCacheEnabled is set to false (The remember password option on your Wii U must be enabled for this account before dumping it)");
case OnlineAccountError::kPasswordCacheEmpty:

View File

@ -71,7 +71,6 @@ private:
wxButton* m_create_account, * m_delete_account;
wxChoice* m_active_account;
wxRadioBox* m_active_service;
wxCheckBox* m_online_enabled;
wxCollapsiblePane* m_account_information;
wxPropertyGrid* m_account_grid;
wxBitmapButton* m_validate_online;
@ -99,10 +98,11 @@ private:
void OnMLCPathSelect(wxCommandEvent& event);
void OnMLCPathChar(wxKeyEvent& event);
void OnShowOnlineValidator(wxCommandEvent& event);
void OnOnlineEnable(wxCommandEvent& event);
void OnAccountServiceChanged(wxCommandEvent& event);
static wxString GetOnlineAccountErrorMessage(OnlineAccountError error);
uint32 GetSelectedAccountPersistentId();
// updates cemu audio devices
void UpdateAudioDevice();
// refreshes audio device list for dropdown

View File

@ -30,6 +30,7 @@
#include "Cafe/Filesystem/FST/FST.h"
#include "gui/TitleManager.h"
#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h"
#include "Cafe/CafeSystem.h"
@ -110,6 +111,7 @@ enum
MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER = 20600,
MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER,
MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER,
MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES,
// cpu
// cpu->timer speed
MAINFRAME_MENU_ID_TIMER_SPEED_1X = 20700,
@ -188,6 +190,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_INPUT, MainWindow::OnOptionsInput)
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput)
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput)
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, MainWindow::OnToolsInput)
EVT_MENU(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, MainWindow::OnToolsInput)
// cpu menu
EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_8X, MainWindow::OnDebugSetting)
EVT_MENU(MAINFRAME_MENU_ID_TIMER_SPEED_4X, MainWindow::OnDebugSetting)
@ -948,38 +951,6 @@ void MainWindow::OnAccountSelect(wxCommandEvent& event)
g_config.Save();
}
//void MainWindow::OnConsoleRegion(wxCommandEvent& event)
//{
// switch (event.GetId())
// {
// case MAINFRAME_MENU_ID_OPTIONS_REGION_AUTO:
// GetConfig().console_region = ConsoleRegion::Auto;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_JPN:
// GetConfig().console_region = ConsoleRegion::JPN;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_USA:
// GetConfig().console_region = ConsoleRegion::USA;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_EUR:
// GetConfig().console_region = ConsoleRegion::EUR;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_CHN:
// GetConfig().console_region = ConsoleRegion::CHN;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_KOR:
// GetConfig().console_region = ConsoleRegion::KOR;
// break;
// case MAINFRAME_MENU_ID_OPTIONS_REGION_TWN:
// GetConfig().console_region = ConsoleRegion::TWN;
// break;
// default:
// cemu_assert_debug(false);
// }
//
// g_config.Save();
//}
void MainWindow::OnConsoleLanguage(wxCommandEvent& event)
{
switch (event.GetId())
@ -1547,6 +1518,29 @@ void MainWindow::OnToolsInput(wxCommandEvent& event)
});
m_title_manager->Show();
}
break;
}
case MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES:
{
if (m_usb_devices)
{
m_usb_devices->Show(true);
m_usb_devices->Raise();
m_usb_devices->SetFocus();
}
else
{
m_usb_devices = new EmulatedUSBDeviceFrame(this);
m_usb_devices->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event)
{
if (event.CanVeto()) {
m_usb_devices->Show(false);
event.Veto();
}
});
m_usb_devices->Show(true);
}
break;
}
break;
}
@ -2198,6 +2192,7 @@ void MainWindow::RecreateMenu()
m_memorySearcherMenuItem->Enable(false);
toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, _("&Title Manager"));
toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_DOWNLOAD_MANAGER, _("&Download Manager"));
toolsMenu->Append(MAINFRAME_MENU_ID_TOOLS_EMULATED_USB_DEVICES, _("&Emulated USB Devices"));
m_menuBar->Append(toolsMenu, _("&Tools"));

View File

@ -22,6 +22,7 @@ struct GameEntry;
class DiscordPresence;
class TitleManager;
class GraphicPacksWindow2;
class EmulatedUSBDeviceFrame;
class wxLaunchGameEvent;
wxDECLARE_EVENT(wxEVT_LAUNCH_GAME, wxLaunchGameEvent);
@ -164,6 +165,7 @@ private:
MemorySearcherTool* m_toolWindow = nullptr;
TitleManager* m_title_manager = nullptr;
EmulatedUSBDeviceFrame* m_usb_devices = nullptr;
PadViewFrame* m_padView = nullptr;
GraphicPacksWindow2* m_graphic_pack_window = nullptr;

View File

@ -1,7 +1,7 @@
{
"name": "cemu",
"version-string": "1.0",
"builtin-baseline": "53bef8994c541b6561884a8395ea35715ece75db",
"builtin-baseline": "cbf4a6641528cee6f172328984576f51698de726",
"dependencies": [
"pugixml",
"zlib",