#include "Dimensions.h" #include "nsyshid.h" #include "Backend.h" #include "Common/FileStream.h" #include #include namespace nsyshid { static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, 0x3C, 0xA8, 0xD8, 0x75, 0x47, 0x68, 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; static constexpr std::array PWD_CONSTANT = {0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4C, 0x45, 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; DimensionsUSB g_dimensionstoypad; const std::map s_listMinis = { {0, "Blank Tag"}, {1, "Batman"}, {2, "Gandalf"}, {3, "Wyldstyle"}, {4, "Aquaman"}, {5, "Bad Cop"}, {6, "Bane"}, {7, "Bart Simpson"}, {8, "Benny"}, {9, "Chell"}, {10, "Cole"}, {11, "Cragger"}, {12, "Cyborg"}, {13, "Cyberman"}, {14, "Doc Brown"}, {15, "The Doctor"}, {16, "Emmet"}, {17, "Eris"}, {18, "Gimli"}, {19, "Gollum"}, {20, "Harley Quinn"}, {21, "Homer Simpson"}, {22, "Jay"}, {23, "Joker"}, {24, "Kai"}, {25, "ACU Trooper"}, {26, "Gamer Kid"}, {27, "Krusty the Clown"}, {28, "Laval"}, {29, "Legolas"}, {30, "Lloyd"}, {31, "Marty McFly"}, {32, "Nya"}, {33, "Owen Grady"}, {34, "Peter Venkman"}, {35, "Slimer"}, {36, "Scooby-Doo"}, {37, "Sensei Wu"}, {38, "Shaggy"}, {39, "Stay Puft"}, {40, "Superman"}, {41, "Unikitty"}, {42, "Wicked Witch of the West"}, {43, "Wonder Woman"}, {44, "Zane"}, {45, "Green Arrow"}, {46, "Supergirl"}, {47, "Abby Yates"}, {48, "Finn the Human"}, {49, "Ethan Hunt"}, {50, "Lumpy Space Princess"}, {51, "Jake the Dog"}, {52, "Harry Potter"}, {53, "Lord Voldemort"}, {54, "Michael Knight"}, {55, "B.A. Baracus"}, {56, "Newt Scamander"}, {57, "Sonic the Hedgehog"}, {58, "Future Update (unreleased)"}, {59, "Gizmo"}, {60, "Stripe"}, {61, "E.T."}, {62, "Tina Goldstein"}, {63, "Marceline the Vampire Queen"}, {64, "Batgirl"}, {65, "Robin"}, {66, "Sloth"}, {67, "Hermione Granger"}, {68, "Chase McCain"}, {69, "Excalibur Batman"}, {70, "Raven"}, {71, "Beast Boy"}, {72, "Betelgeuse"}, {73, "Lord Vortech (unreleased)"}, {74, "Blossom"}, {75, "Bubbles"}, {76, "Buttercup"}, {77, "Starfire"}, {78, "World 15 (unreleased)"}, {79, "World 16 (unreleased)"}, {80, "World 17 (unreleased)"}, {81, "World 18 (unreleased)"}, {82, "World 19 (unreleased)"}, {83, "World 20 (unreleased)"}, {768, "Unknown 768"}, {769, "Supergirl Red Lantern"}, {770, "Unknown 770"}}; const std::map s_listTokens = { {1000, "Police Car"}, {1001, "Aerial Squad Car"}, {1002, "Missile Striker"}, {1003, "Gravity Sprinter"}, {1004, "Street Shredder"}, {1005, "Sky Clobberer"}, {1006, "Batmobile"}, {1007, "Batblaster"}, {1008, "Sonic Batray"}, {1009, "Benny's Spaceship"}, {1010, "Lasercraft"}, {1011, "The Annihilator"}, {1012, "DeLorean Time Machine"}, {1013, "Electric Time Machine"}, {1014, "Ultra Time Machine"}, {1015, "Hoverboard"}, {1016, "Cyclone Board"}, {1017, "Ultimate Hoverjet"}, {1018, "Eagle Interceptor"}, {1019, "Eagle Sky Blazer"}, {1020, "Eagle Swoop Diver"}, {1021, "Swamp Skimmer"}, {1022, "Cragger's Fireship"}, {1023, "Croc Command Sub"}, {1024, "Cyber-Guard"}, {1025, "Cyber-Wrecker"}, {1026, "Laser Robot Walker"}, {1027, "K-9"}, {1028, "K-9 Ruff Rover"}, {1029, "K-9 Laser Cutter"}, {1030, "TARDIS"}, {1031, "Laser-Pulse TARDIS"}, {1032, "Energy-Burst TARDIS"}, {1033, "Emmet's Excavator"}, {1034, "Destroy Dozer"}, {1035, "Destruct-o-Mech"}, {1036, "Winged Monkey"}, {1037, "Battle Monkey"}, {1038, "Commander Monkey"}, {1039, "Axe Chariot"}, {1040, "Axe Hurler"}, {1041, "Soaring Chariot"}, {1042, "Shelob the Great"}, {1043, "8-Legged Stalker"}, {1044, "Poison Slinger"}, {1045, "Homer's Car"}, {1047, "The SubmaHomer"}, {1046, "The Homercraft"}, {1048, "Taunt-o-Vision"}, {1050, "The MechaHomer"}, {1049, "Blast Cam"}, {1051, "Velociraptor"}, {1053, "Venom Raptor"}, {1052, "Spike Attack Raptor"}, {1054, "Gyrosphere"}, {1055, "Sonic Beam Gyrosphere"}, {1056, " Gyrosphere"}, {1057, "Clown Bike"}, {1058, "Cannon Bike"}, {1059, "Anti-Gravity Rocket Bike"}, {1060, "Mighty Lion Rider"}, {1061, "Lion Blazer"}, {1062, "Fire Lion"}, {1063, "Arrow Launcher"}, {1064, "Seeking Shooter"}, {1065, "Triple Ballista"}, {1066, "Mystery Machine"}, {1067, "Mystery Tow & Go"}, {1068, "Mystery Monster"}, {1069, "Boulder Bomber"}, {1070, "Boulder Blaster"}, {1071, "Cyclone Jet"}, {1072, "Storm Fighter"}, {1073, "Lightning Jet"}, {1074, "Electro-Shooter"}, {1075, "Blade Bike"}, {1076, "Flight Fire Bike"}, {1077, "Blades of Fire"}, {1078, "Samurai Mech"}, {1079, "Samurai Shooter"}, {1080, "Soaring Samurai Mech"}, {1081, "Companion Cube"}, {1082, "Laser Deflector"}, {1083, "Gold Heart Emitter"}, {1084, "Sentry Turret"}, {1085, "Turret Striker"}, {1086, "Flight Turret Carrier"}, {1087, "Scooby Snack"}, {1088, "Scooby Fire Snack"}, {1089, "Scooby Ghost Snack"}, {1090, "Cloud Cuckoo Car"}, {1091, "X-Stream Soaker"}, {1092, "Rainbow Cannon"}, {1093, "Invisible Jet"}, {1094, "Laser Shooter"}, {1095, "Torpedo Bomber"}, {1096, "NinjaCopter"}, {1097, "Glaciator"}, {1098, "Freeze Fighter"}, {1099, "Travelling Time Train"}, {1100, "Flight Time Train"}, {1101, "Missile Blast Time Train"}, {1102, "Aqua Watercraft"}, {1103, "Seven Seas Speeder"}, {1104, "Trident of Fire"}, {1105, "Drill Driver"}, {1106, "Bane Dig 'n' Drill"}, {1107, "Bane Drill 'n' Blast"}, {1108, "Quinn Mobile"}, {1109, "Quinn Ultra Racer"}, {1110, "Missile Launcher"}, {1111, "The Joker's Chopper"}, {1112, "Mischievous Missile Blaster"}, {1113, "Lock 'n' Laser Jet"}, {1114, "Hover Pod"}, {1115, "Krypton Striker"}, {1116, "Super Stealth Pod"}, {1117, "Dalek"}, {1118, "Fire 'n' Ride Dalek"}, {1119, "Silver Shooter Dalek"}, {1120, "Ecto-1"}, {1121, "Ecto-1 Blaster"}, {1122, "Ecto-1 Water Diver"}, {1123, "Ghost Trap"}, {1124, "Ghost Stun 'n' Trap"}, {1125, "Proton Zapper"}, {1126, "Unknown"}, {1127, "Unknown"}, {1128, "Unknown"}, {1129, "Unknown"}, {1130, "Unknown"}, {1131, "Unknown"}, {1132, "Lloyd's Golden Dragon"}, {1133, "Sword Projector Dragon"}, {1134, "Unknown"}, {1135, "Unknown"}, {1136, "Unknown"}, {1137, "Unknown"}, {1138, "Unknown"}, {1139, "Unknown"}, {1140, "Unknown"}, {1141, "Unknown"}, {1142, "Unknown"}, {1143, "Unknown"}, {1144, "Mega Flight Dragon"}, {1145, "Unknown"}, {1146, "Unknown"}, {1147, "Unknown"}, {1148, "Unknown"}, {1149, "Unknown"}, {1150, "Unknown"}, {1151, "Unknown"}, {1152, "Unknown"}, {1153, "Unknown"}, {1154, "Unknown"}, {1155, "Flying White Dragon"}, {1156, "Golden Fire Dragon"}, {1157, "Ultra Destruction Dragon"}, {1158, "Arcade Machine"}, {1159, "8-Bit Shooter"}, {1160, "The Pixelator"}, {1161, "G-6155 Spy Hunter"}, {1162, "Interdiver"}, {1163, "Aerial Spyhunter"}, {1164, "Slime Shooter"}, {1165, "Slime Exploder"}, {1166, "Slime Streamer"}, {1167, "Terror Dog"}, {1168, "Terror Dog Destroyer"}, {1169, "Soaring Terror Dog"}, {1170, "Ancient Psychic Tandem War Elephant"}, {1171, "Cosmic Squid"}, {1172, "Psychic Submarine"}, {1173, "BMO"}, {1174, "DOGMO"}, {1175, "SNAKEMO"}, {1176, "Jakemobile"}, {1177, "Snail Dude Jake"}, {1178, "Hover Jake"}, {1179, "Lumpy Car"}, {1181, "Lumpy Land Whale"}, {1180, "Lumpy Truck"}, {1182, "Lunatic Amp"}, {1183, "Shadow Scorpion"}, {1184, "Heavy Metal Monster"}, {1185, "B.A.'s Van"}, {1186, "Fool Smasher"}, {1187, "Pain Plane"}, {1188, "Phone Home"}, {1189, "Mobile Uplink"}, {1190, "Super-Charged Satellite"}, {1191, "Niffler"}, {1192, "Sinister Scorpion"}, {1193, "Vicious Vulture"}, {1194, "Swooping Evil"}, {1195, "Brutal Bloom"}, {1196, "Crawling Creeper"}, {1197, "Ecto-1 (2016)"}, {1198, "Ectozer"}, {1199, "PerfEcto"}, {1200, "Flash 'n' Finish"}, {1201, "Rampage Record Player"}, {1202, "Stripe's Throne"}, {1203, "R.C. Racer"}, {1204, "Gadget-O-Matic"}, {1205, "Scarlet Scorpion"}, {1206, "Hogwarts Express"}, {1208, "Steam Warrior"}, {1207, "Soaring Steam Plane"}, {1209, "Enchanted Car"}, {1210, "Shark Sub"}, {1211, "Monstrous Mouth"}, {1212, "IMF Scrambler"}, {1213, "Shock Cycle"}, {1214, "IMF Covert Jet"}, {1215, "IMF Sports Car"}, {1216, "IMF Tank"}, {1217, "IMF Splorer"}, {1218, "Sonic Speedster"}, {1219, "Blue Typhoon"}, {1220, "Moto Bug"}, {1221, "The Tornado"}, {1222, "Crabmeat"}, {1223, "Eggcatcher"}, {1224, "K.I.T.T."}, {1225, "Goliath Armored Semi"}, {1226, "K.I.T.T. Jet"}, {1227, "Police Helicopter"}, {1228, "Police Hovercraft"}, {1229, "Police Plane"}, {1230, "Bionic Steed"}, {1231, "Bat-Raptor"}, {1232, "Ultrabat"}, {1233, "Batwing"}, {1234, "The Black Thunder"}, {1235, "Bat-Tank"}, {1236, "Skeleton Organ"}, {1237, "Skeleton Jukebox"}, {1238, "Skele-Turkey"}, {1239, "One-Eyed Willy's Pirate Ship"}, {1240, "Fanged Fortune"}, {1241, "Inferno Cannon"}, {1242, "Buckbeak"}, {1243, "Giant Owl"}, {1244, "Fierce Falcon"}, {1245, "Saturn's Sandworm"}, {1247, "Haunted Vacuum"}, {1246, "Spooky Spider"}, {1248, "PPG Smartphone"}, {1249, "PPG Hotline"}, {1250, "Powerpuff Mag-Net"}, {1253, "Mega Blast Bot"}, {1251, "Ka-Pow Cannon"}, {1252, "Slammin' Guitar"}, {1254, "Octi"}, {1255, "Super Skunk"}, {1256, "Sonic Squid"}, {1257, "T-Car"}, {1258, "T-Forklift"}, {1259, "T-Plane"}, {1260, "Spellbook of Azarath"}, {1261, "Raven Wings"}, {1262, "Giant Hand"}, {1263, "Titan Robot"}, {1264, "T-Rocket"}, {1265, "Robot Retriever"}}; DimensionsToypadDevice::DimensionsToypadDevice() : Device(0x0E6F, 0x0241, 1, 2, 0) { m_IsOpened = false; } bool DimensionsToypadDevice::Open() { if (!IsOpened()) { m_IsOpened = true; } return true; } void DimensionsToypadDevice::Close() { if (IsOpened()) { m_IsOpened = false; } } bool DimensionsToypadDevice::IsOpened() { return m_IsOpened; } Device::ReadResult DimensionsToypadDevice::Read(ReadMessage* message) { memcpy(message->data, g_dimensionstoypad.GetStatus().data(), message->length); message->bytesRead = message->length; return Device::ReadResult::Success; } Device::WriteResult DimensionsToypadDevice::Write(WriteMessage* message) { if (message->length != 32) return Device::WriteResult::Error; g_dimensionstoypad.SendCommand(std::span{message->data, 32}); message->bytesWritten = message->length; return Device::WriteResult::Success; } bool DimensionsToypadDevice::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 + 1) = 0x02; // bEndpointAddress *(uint8*)(currentWritePtr + 2) = 0x03; // bmAttributes *(uint16be*)(currentWritePtr + 3) = 0x40; // wMaxPacketSize *(uint8*)(currentWritePtr + 5) = 0x01; // bInterval currentWritePtr = currentWritePtr + 7; cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29); memcpy(output, configurationDescriptor, std::min(outputMaxLength, sizeof(configurationDescriptor))); return true; } bool DimensionsToypadDevice::SetProtocol(uint8 ifIndex, uint8 protocol) { cemuLog_log(LogType::Force, "Toypad Protocol"); return true; } bool DimensionsToypadDevice::SetReport(ReportMessage* message) { cemuLog_log(LogType::Force, "Toypad Report"); return true; } std::array DimensionsUSB::GetStatus() { std::array response = {}; bool responded = false; do { if (!m_queries.empty()) { response = m_queries.front(); m_queries.pop(); responded = true; } else if (!m_figureAddedRemovedResponses.empty() && m_isAwake) { std::lock_guard lock(m_dimensionsMutex); response = m_figureAddedRemovedResponses.front(); m_figureAddedRemovedResponses.pop(); responded = true; } else { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } while (responded == false); return response; } void DimensionsUSB::SendCommand(std::span buf) { const uint8 command = buf[2]; const uint8 sequence = buf[3]; std::array q_result{}; switch (command) { case 0xB0: // Wake { // Consistent device response to the wake command q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, 0x32, 0x30, 0x31, 0x34, 0x46}; break; } case 0xB1: // Seed { // Initialise a random number generator using the seed provided g_dimensionstoypad.GenerateRandomNumber(std::span{buf.begin() + 4, 8}, sequence, q_result); break; } case 0xB3: // Challenge { // Get the next number in the sequence based on the RNG from 0xB1 command g_dimensionstoypad.GetChallengeResponse(std::span{buf.begin() + 4, 8}, sequence, q_result); break; } case 0xC0: // Color case 0xC1: // Get Pad Color case 0xC2: // Fade case 0xC3: // Flash case 0xC4: // Fade Random case 0xC6: // Fade All case 0xC7: // Flash All case 0xC8: // Color All { // Send a blank response to acknowledge color has been sent to toypad q_result = {0x55, 0x01, sequence}; q_result[3] = GenerateChecksum(q_result, 3); break; } case 0xD2: // Read { // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] g_dimensionstoypad.QueryBlock(buf[4], buf[5], q_result, sequence); break; } case 0xD3: // Write { // Write 4 bytes to page buf[5] to the figure at index buf[4] g_dimensionstoypad.WriteBlock(buf[4], buf[5], std::span{buf.begin() + 6, 4}, q_result, sequence); break; } case 0xD4: // Model { // Get the model id of the figure at index buf[4] g_dimensionstoypad.GetModel(std::span{buf.begin() + 4, 8}, sequence, q_result); break; } case 0xD0: // Tag List case 0xE1: // PWD case 0xE5: // Active case 0xFF: // LEDS Query { // Further investigation required cemuLog_log(LogType::Force, "Unimplemented LD Function: {:x}", command); break; } default: { cemuLog_log(LogType::Force, "Unknown LD Function: {:x}", command); break; } } m_queries.push(q_result); } uint32 DimensionsUSB::LoadFigure(const std::array& buf, std::unique_ptr file, uint8 pad, uint8 index) { std::lock_guard lock(m_dimensionsMutex); const uint32 id = GetFigureId(buf); DimensionsMini& figure = GetFigureByIndex(index); figure.dimFile = std::move(file); figure.id = id; figure.pad = pad; figure.index = index + 1; figure.data = buf; // When a figure is added to the toypad, respond to the game with the pad they were added to, their index, // the direction (0x00 in byte 6 for added) and their UID std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); m_figureAddedRemovedResponses.push(figureChangeResponse); return id; } bool DimensionsUSB::RemoveFigure(uint8 pad, uint8 index, bool fullRemove) { std::lock_guard lock(m_dimensionsMutex); DimensionsMini& figure = GetFigureByIndex(index); if (figure.index == 255) return false; // When a figure is removed from the toypad, respond to the game with the pad they were removed from, their index, // the direction (0x01 in byte 6 for removed) and their UID if (fullRemove) { std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); m_figureAddedRemovedResponses.push(figureChangeResponse); figure.Save(); figure.dimFile.reset(); } figure.index = 255; figure.pad = 255; figure.id = 0; return true; } bool DimensionsUSB::TempRemove(uint8 index) { std::lock_guard lock(m_dimensionsMutex); DimensionsMini& figure = GetFigureByIndex(index); if (figure.index == 255) return false; // Send a response to the game that the figure has been "Picked up" from existing slot, // until either the movement is cancelled, or user chooses a space to move to std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); m_figureAddedRemovedResponses.push(figureChangeResponse); return true; } bool DimensionsUSB::CancelRemove(uint8 index) { std::lock_guard lock(m_dimensionsMutex); DimensionsMini& figure = GetFigureByIndex(index); if (figure.index == 255) return false; // Cancel the previous movement of the figure std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); m_figureAddedRemovedResponses.push(figureChangeResponse); return true; } bool DimensionsUSB::CreateFigure(fs::path pathName, uint32 id) { FileStream* dimFile(FileStream::createFile2(pathName)); if (!dimFile) return false; std::array fileData{}; RandomUID(fileData); fileData[3] = id & 0xFF; std::array uid = {fileData[0], fileData[1], fileData[2], fileData[4], fileData[5], fileData[6], fileData[7]}; // Only characters are created with their ID encrypted and stored in pages 36 and 37, // as well as a password stored in page 43. Blank tags have their information populated // by the game when it calls the write_block command. if (id != 0) { const std::array figureKey = GenerateFigureKey(fileData); std::array valueToEncrypt = {uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF), uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF)}; std::array encrypted = Encrypt(valueToEncrypt, figureKey); std::memcpy(&fileData[36 * 4], &encrypted[0], 4); std::memcpy(&fileData[37 * 4], &encrypted[4], 4); std::memcpy(&fileData[43 * 4], PWDGenerate(fileData).data(), 4); } else { // Page 38 is used as verification for blank tags fileData[(38 * 4) + 1] = 1; } if (fileData.size() != dimFile->writeData(fileData.data(), fileData.size())) { delete dimFile; return false; } delete dimFile; return true; } bool DimensionsUSB::MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex) { if (oldIndex == index) { // Don't bother removing and loading again, just send response to the game CancelRemove(index); return true; } // When moving figures between spaces on the toypad, remove any figure from the space they are moving to, // then remove them from their current space, then load them to the space they are moving to RemoveFigure(pad, index, true); DimensionsMini& figure = GetFigureByIndex(oldIndex); const std::array data = figure.data; std::unique_ptr inFile = std::move(figure.dimFile); RemoveFigure(oldPad, oldIndex, false); LoadFigure(data, std::move(inFile), pad, index); return true; } void DimensionsUSB::GenerateRandomNumber(std::span buf, uint8 sequence, std::array& replyBuf) { // Decrypt payload into an 8 byte array std::array value = Decrypt(buf, std::nullopt); // Seed is the first 4 bytes (little endian) of the decrypted payload uint32 seed = (uint32&)value[0]; // Confirmation is the second 4 bytes (big endian) of the decrypted payload uint32 conf = (uint32be&)value[4]; // Initialize rng using the seed from decrypted payload InitializeRNG(seed); // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are blank std::array valueToEncrypt = {value[4], value[5], value[6], value[7], 0, 0, 0, 0}; std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); replyBuf[0] = 0x55; replyBuf[1] = 0x09; replyBuf[2] = sequence; // Copy encrypted value to response data memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); replyBuf[11] = GenerateChecksum(replyBuf, 11); } void DimensionsUSB::GetChallengeResponse(std::span buf, uint8 sequence, std::array& replyBuf) { // Decrypt payload into an 8 byte array std::array value = Decrypt(buf, std::nullopt); // Confirmation is the first 4 bytes of the decrypted payload uint32 conf = (uint32be&)value[0]; // Generate next random number based on RNG uint32 nextRandom = GetNext(); // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) // followed by the confirmation from the decrypted payload std::array valueToEncrypt = {uint8(nextRandom & 0xFF), uint8((nextRandom >> 8) & 0xFF), uint8((nextRandom >> 16) & 0xFF), uint8((nextRandom >> 24) & 0xFF), value[0], value[1], value[2], value[3]}; std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); replyBuf[0] = 0x55; replyBuf[1] = 0x09; replyBuf[2] = sequence; // Copy encrypted value to response data memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); replyBuf[11] = GenerateChecksum(replyBuf, 11); if (!m_isAwake) m_isAwake = true; } void DimensionsUSB::InitializeRNG(uint32 seed) { m_randomA = 0xF1EA5EED; m_randomB = seed; m_randomC = seed; m_randomD = seed; for (int i = 0; i < 42; i++) { GetNext(); } } uint32 DimensionsUSB::GetNext() { uint32 e = m_randomA - std::rotl(m_randomB, 21); m_randomA = m_randomB ^ std::rotl(m_randomC, 19); m_randomB = m_randomC + std::rotl(m_randomD, 6); m_randomC = m_randomD + e; m_randomD = e + m_randomA; return m_randomD; } std::array DimensionsUSB::Decrypt(std::span buf, std::optional> key) { // Value to decrypt is separated in to two little endian 32 bit unsigned integers uint32 dataOne = (uint32&)buf[0]; uint32 dataTwo = (uint32&)buf[4]; // Use the key as 4 32 bit little endian unsigned integers uint32 keyOne; uint32 keyTwo; uint32 keyThree; uint32 keyFour; if (key) { keyOne = (uint32&)key.value()[0]; keyTwo = (uint32&)key.value()[4]; keyThree = (uint32&)key.value()[8]; keyFour = (uint32&)key.value()[12]; } else { keyOne = (uint32&)COMMAND_KEY[0]; keyTwo = (uint32&)COMMAND_KEY[4]; keyThree = (uint32&)COMMAND_KEY[8]; keyFour = (uint32&)COMMAND_KEY[12]; } uint32 sum = 0xC6EF3720; uint32 delta = 0x9E3779B9; for (int i = 0; i < 32; i++) { dataTwo -= (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); dataOne -= (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); sum -= delta; } cemu_assert(sum == 0); std::array decrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; return decrypted; } std::array DimensionsUSB::Encrypt(std::span buf, std::optional> key) { // Value to encrypt is separated in to two little endian 32 bit unsigned integers uint32 dataOne = (uint32&)buf[0]; uint32 dataTwo = (uint32&)buf[4]; // Use the key as 4 32 bit little endian unsigned integers uint32 keyOne; uint32 keyTwo; uint32 keyThree; uint32 keyFour; if (key) { keyOne = (uint32&)key.value()[0]; keyTwo = (uint32&)key.value()[4]; keyThree = (uint32&)key.value()[8]; keyFour = (uint32&)key.value()[12]; } else { keyOne = (uint32&)COMMAND_KEY[0]; keyTwo = (uint32&)COMMAND_KEY[4]; keyThree = (uint32&)COMMAND_KEY[8]; keyFour = (uint32&)COMMAND_KEY[12]; } uint32 sum = 0; uint32 delta = 0x9E3779B9; for (int i = 0; i < 32; i++) { sum += delta; dataOne += (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); dataTwo += (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); } cemu_assert(sum == 0xC6EF3720); std::array encrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; return encrypted; } std::array DimensionsUSB::GenerateFigureKey(const std::array& buf) { std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; uint32 scrambleA = Scramble(uid, 3); uint32 scrambleB = Scramble(uid, 4); uint32 scrambleC = Scramble(uid, 5); uint32 scrambleD = Scramble(uid, 6); return {uint8((scrambleA >> 24) & 0xFF), uint8((scrambleA >> 16) & 0xFF), uint8((scrambleA >> 8) & 0xFF), uint8(scrambleA & 0xFF), uint8((scrambleB >> 24) & 0xFF), uint8((scrambleB >> 16) & 0xFF), uint8((scrambleB >> 8) & 0xFF), uint8(scrambleB & 0xFF), uint8((scrambleC >> 24) & 0xFF), uint8((scrambleC >> 16) & 0xFF), uint8((scrambleC >> 8) & 0xFF), uint8(scrambleC & 0xFF), uint8((scrambleD >> 24) & 0xFF), uint8((scrambleD >> 16) & 0xFF), uint8((scrambleD >> 8) & 0xFF), uint8(scrambleD & 0xFF)}; } uint32 DimensionsUSB::Scramble(const std::array& uid, uint8 count) { std::vector toScramble; toScramble.reserve(uid.size() + CHAR_CONSTANT.size()); for (uint8 x : uid) { toScramble.push_back(x); } for (uint8 c : CHAR_CONSTANT) { toScramble.push_back(c); } toScramble[(count * 4) - 1] = 0xaa; std::array randomized = DimensionsRandomize(toScramble, count); return (uint32be&)randomized[0]; } std::array DimensionsUSB::PWDGenerate(const std::array& buf) { std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; std::vector pwdCalc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; for (uint8 i = 0; i < uid.size(); i++) { pwdCalc.insert(pwdCalc.begin() + i, uid[i]); } return DimensionsRandomize(pwdCalc, 8); } std::array DimensionsUSB::DimensionsRandomize(const std::vector key, uint8 count) { uint32 scrambled = 0; for (uint8 i = 0; i < count; i++) { const uint32 v4 = std::rotr(scrambled, 25); const uint32 v5 = std::rotr(scrambled, 10); const uint32 b = (uint32&)key[i * 4]; scrambled = b + v4 + v5 - scrambled; } return {uint8(scrambled & 0xFF), uint8(scrambled >> 8 & 0xFF), uint8(scrambled >> 16 & 0xFF), uint8(scrambled >> 24 & 0xFF)}; } uint32 DimensionsUSB::GetFigureId(const std::array& buf) { const std::array figureKey = GenerateFigureKey(buf); const std::span modelNumber = std::span{buf.begin() + (36 * 4), 8}; const std::array decrypted = Decrypt(modelNumber, figureKey); const uint32 figNum = (uint32&)decrypted[0]; // Characters have their model number encrypted in page 36 if (figNum < 1000) { return figNum; } // Vehicles/Gadgets have their model number written as little endian in page 36 return (uint32&)modelNumber[0]; } DimensionsUSB::DimensionsMini& DimensionsUSB::GetFigureByIndex(uint8 index) { return m_figures[index]; } void DimensionsUSB::QueryBlock(uint8 index, uint8 page, std::array& replyBuf, uint8 sequence) { std::lock_guard lock(m_dimensionsMutex); replyBuf[0] = 0x55; replyBuf[1] = 0x12; replyBuf[2] = sequence; replyBuf[3] = 0x00; // Index from game begins at 1 rather than 0, so minus 1 here if (const uint8 figureIndex = index - 1; figureIndex < 7) { const DimensionsMini& figure = GetFigureByIndex(figureIndex); // Query 4 pages of 4 bytes from the figure, copy this to the response if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) { std::memcpy(&replyBuf[4], figure.data.data() + (4 * page), 16); } } replyBuf[20] = GenerateChecksum(replyBuf, 20); } void DimensionsUSB::WriteBlock(uint8 index, uint8 page, std::span toWriteBuf, std::array& replyBuf, uint8 sequence) { std::lock_guard lock(m_dimensionsMutex); replyBuf[0] = 0x55; replyBuf[1] = 0x02; replyBuf[2] = sequence; replyBuf[3] = 0x00; // Index from game begins at 1 rather than 0, so minus 1 here if (const uint8 figureIndex = index - 1; figureIndex < 7) { DimensionsMini& figure = GetFigureByIndex(figureIndex); // Copy 4 bytes to the page on the figure requested by the game if (figure.index != 255 && page < 0x2D) { // Id is written to page 36 if (page == 36) { figure.id = (uint32&)toWriteBuf[0]; } std::memcpy(figure.data.data() + (page * 4), toWriteBuf.data(), 4); figure.Save(); } } replyBuf[4] = GenerateChecksum(replyBuf, 4); } void DimensionsUSB::GetModel(std::span buf, uint8 sequence, std::array& replyBuf) { // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation std::array value = Decrypt(buf, std::nullopt); uint8 index = value[0]; uint32 conf = (uint32be&)value[4]; // Response is the figure's id (little endian) followed by the confirmation from payload // Index from game begins at 1 rather than 0, so minus 1 here std::array valueToEncrypt = {}; if (const uint8 figureIndex = index - 1; figureIndex < 7) { const DimensionsMini& figure = GetFigureByIndex(figureIndex); valueToEncrypt = {uint8(figure.id & 0xFF), uint8((figure.id >> 8) & 0xFF), uint8((figure.id >> 16) & 0xFF), uint8((figure.id >> 24) & 0xFF), value[4], value[5], value[6], value[7]}; } std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); replyBuf[0] = 0x55; replyBuf[1] = 0x0a; replyBuf[2] = sequence; replyBuf[3] = 0x00; memcpy(&replyBuf[4], encrypted.data(), encrypted.size()); replyBuf[12] = GenerateChecksum(replyBuf, 12); } void DimensionsUSB::RandomUID(std::array& uid_buffer) { uid_buffer[0] = 0x04; uid_buffer[7] = 0x80; std::random_device rd; std::mt19937 mt(rd()); std::uniform_int_distribution dist(0, 255); uid_buffer[1] = dist(mt); uid_buffer[2] = dist(mt); uid_buffer[4] = dist(mt); uid_buffer[5] = dist(mt); uid_buffer[6] = dist(mt); } uint8 DimensionsUSB::GenerateChecksum(const std::array& data, int num_of_bytes) const { int checksum = 0; for (int i = 0; i < num_of_bytes; i++) { checksum += data[i]; } return (checksum & 0xFF); } void DimensionsUSB::DimensionsMini::Save() { if (!dimFile) return; dimFile->SetPosition(0); dimFile->writeData(data.data(), data.size()); } std::map DimensionsUSB::GetListMinifigs() { return s_listMinis; } std::map DimensionsUSB::GetListTokens() { return s_listTokens; } std::string DimensionsUSB::FindFigure(uint32 figNum) { for (const auto& it : GetListMinifigs()) { if (it.first == figNum) { return it.second; } } for (const auto& it : GetListTokens()) { if (it.first == figNum) { return it.second; } } return fmt::format("Unknown ({})", figNum); } } // namespace nsyshid