UNPKG

miijs

Version:

Read, Edit, Write, and make Special Miis from a Wiimote binary file or 3DS QR Code to a binary file or QR code

1,437 lines (1,411 loc) 106 kB
const fs = require('fs'); const nodeCanvas = require('canvas'); const { createCanvas, loadImage, ImageData } = nodeCanvas; const jsQR = require('jsqr'); const Jimp = require('jimp'); const THREE = require('three'); const QRCodeStyling = require("qr-code-styling"); const { JSDOM } = require("jsdom"); const httpsLib = require('https'); const asmCrypto=require("./asmCrypto.js"); const path=require("path"); const createGL = require('gl'); const {FFLCharModelDescDefault,createCharModel,initCharModelTextures,initializeFFL,parseHexOrB64ToUint8Array}=require("./ffl.js"); const ModuleFFL=require("./ffl-emscripten-single-file.js"); const FFLShaderMaterial=require("./FFLShaderMaterial.js"); function getKeyByValue(object, value) { for (var key in object) { if (object[key] === value) { return key; } } } //If FFLResHigh.dat is in the same directory as Node.js is calling the library from, use it by default var _fflRes=null; if(fs.existsSync("./FFLResHigh.dat")){ _fflRes=new Uint8Array(fs.readFileSync("./FFLResHigh.dat","")); } function getBinaryFromAddress(addr, bin){ let byte = bin.readUInt8(addr); let binaryString = ''; for (let i = 7; i >= 0; i--) { binaryString += ((byte >> i) & 1) ? '1' : '0'; } return binaryString; } var NONCE_OFFSET = 0xC; var NONCE_LENGTH = 8; var TAG_LENGTH = 0x10; var aes_key = new Uint8Array([0x59, 0xFC, 0x81, 0x7E, 0x64, 0x46, 0xEA, 0x61, 0x90, 0x34, 0x7B, 0x20, 0xE9, 0xBD, 0xCE, 0x52]); var pad = new Uint8Array([0,0,0,0]); function Uint8Cat(){ var destLength = 0 for(var i = 0;i < arguments.length;i++)destLength += arguments[i].length; var dest = new Uint8Array(destLength); var index = 0; for(i = 0;i < arguments.length;i++){ dest.set(arguments[i],index); index += arguments[i].length; } return dest; } function decodeAesCcm(data){ var nonce = Uint8Cat(data.subarray(0,NONCE_LENGTH),pad); var ciphertext = data.subarray(NONCE_LENGTH,0x70); var plaintext = asmCrypto.AES_CCM.decrypt(ciphertext,aes_key,nonce,undefined,TAG_LENGTH); return Uint8Cat(plaintext.subarray(0,NONCE_OFFSET),data.subarray(0,NONCE_LENGTH),plaintext.subarray(NONCE_OFFSET,plaintext.length - 4)); } function crcCalc(data){ var crc = 0; for (var byteIndex = 0;byteIndex < data.length; byteIndex++){ for (var bitIndex = 7; bitIndex >= 0; bitIndex--){ crc = (((crc << 1) | ((data[byteIndex] >> bitIndex) & 0x1)) ^ (((crc & 0x8000) != 0) ? 0x1021 : 0)); } } for(var counter = 16; counter > 0; counter--){ crc = ((crc << 1) ^ (((crc & 0x8000) != 0) ? 0x1021 : 0)); } return(crc & 0xFFFF); } function encodeAesCcm(data){ var nonce = Uint8Cat(data.subarray(NONCE_OFFSET,NONCE_OFFSET + NONCE_LENGTH),pad); var crcSrc = Uint8Cat(data,new Uint8Array([0,0])); var crc = crcCalc(crcSrc); var cfsd = Uint8Cat(crcSrc,new Uint8Array([crc >>> 8,crc & 0xff])); var plaintext = Uint8Cat(cfsd.subarray(0,NONCE_OFFSET),cfsd.subarray(NONCE_OFFSET + NONCE_LENGTH,cfsd.length),pad,pad); var ciphertext = asmCrypto.AES_CCM.encrypt(plaintext,aes_key,nonce,undefined,TAG_LENGTH); return Uint8Cat(cfsd.subarray(NONCE_OFFSET,NONCE_OFFSET + NONCE_LENGTH),ciphertext.subarray(0,ciphertext.length - 24),ciphertext.subarray(ciphertext.length - TAG_LENGTH,ciphertext.length)) } async function downloadImage(url) { return new Promise((resolve, reject) => { httpsLib.get(url, (res) => { if (res.statusCode === 200) { const data = []; res.on('data', chunk => data.push(chunk)); res.on('end', () => resolve(Buffer.concat(data))); res.on('error', reject); } else { res.resume(); reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`)); } }); }); } var favCols=["Red","Orange","Yellow","Lime","Green","Blue","Cyan","Pink","Purple","Brown","White","Black"]; var skinCols=["White","Tanned White","Darker White","Tanned Darker","Mostly Black","Black"]; var hairCols=["Black","Brown","Red","Reddish Brown","Grey","Light Brown","Dark Blonde","Blonde"]; var eyeCols=["Black","Grey","Brown","Lime","Blue","Green"]; var wiiFaceFeatures=["None","Blush","Makeup and Blush","Freckles","Bags","Wrinkles on Cheeks","Wrinkles near Eyes","Chin Wrinkle","Makeup","Stubble","Wrinkles near Mouth","Wrinkles"]; var wiiMouthColors=["Peach","Red","Pink"]; var wiiGlassesCols=["Grey","Brown","Red","Blue","Yellow","White"]; var wiiNoses={ '0': 1, '1': 10, '2': 2, '3': 3, '4': 6, '5': 0, '6': 5, '7': 4, '8': 8, '9': 9, '10': 7, '11': 11 }; var mouthTable={ '0': '113', '1': '121', '2': '231', '3': '222', '4': '232', '5': '132', '6': '124', '7': '211', '8': '123', '9': '221', '10': '133', '11': '223', '12': '234', '13': '134', '14': '224', '15': '213', '16': '114', '17': '212', '18': '214', '19': '131', '20': '233', '21': '112', '22': '122', '23': '111' }; var eyebrowTable={ '0': '121', '1': '112', '2': '231', '3': '212', '4': '134', '5': '124', '6': '111', '7': '113', '8': '133', '9': '122', '10': '221', '11': '211', '12': '131', '13': '223', '14': '222', '15': '213', '16': '224', '17': '114', '18': '214', '19': '132', '20': '232', '21': '123', '22': '233', '23': '234' }; var eyeTable={ '0': '131', '1': '113', '2': '111', '3': '413', '4': '121', '5': '311', '6': '332', '7': '411', '8': '112', '9': '222', '10': '414', '11': '221', '12': '232', '13': '331', '14': '424', '15': '114', '16': '133', '17': '132', '18': '314', '19': '231', '20': '134', '21': '233', '22': '433', '23': '213', '24': '313', '25': '214', '26': '123', '27': '124', '28': '324', '29': '432', '30': '323', '31': '333', '32': '212', '33': '211', '34': '223', '35': '234', '36': '312', '37': '322', '38': '431', '39': '122', '40': '224', '41': '321', '42': '412', '43': '423', '44': '421', '45': '422', '46': '334', '47': '434' }; var hairTable={ '0': '534', '1': '413', '2': '632', '3': '521', '4': '422', '5': '433', '6': '522', '7': '434', '8': '414', '9': '612', '10': '512', '11': '513', '12': '411', '13': '421', '14': '511', '15': '624', '16': '621', '17': '533', '18': '622', '19': '423', '20': '532', '21': '524', '22': '531', '23': '312', '24': '614', '25': '432', '26': '412', '27': '424', '28': '613', '29': '634', '30': '314', '31': '134', '32': '211', '33': '111', '34': '334', '35': '514', '36': '313', '37': '231', '38': '321', '39': '122', '40': '121', '41': '323', '42': '331', '43': '311', '44': '112', '45': '113', '46': '631', '47': '221', '48': '212', '49': '123', '50': '223', '51': '131', '52': '232', '53': '623', '54': '332', '55': '233', '56': '114', '57': '324', '58': '213', '59': '133', '60': '224', '61': '611', '62': '234', '63': '523', '64': '214', '65': '333', '66': '222', '67': '322', '68': '124', '69': '431', '70': '132', '71': '633' }; var faceFeatures3DS=["None","Near Eye Creases","Cheek Creases","Far Eye Creases","Near Nose Creases","Giant Bags","Cleft Chin","Chin Crease","Sunken Eyes","Far Cheek Creases","Lines Near Eyes","Wrinkles"]; var makeups3DS=["None","Blush","Orange Blush","Blue Eyes","Blush 2","Orange Blush 2","Blue Eyes and Blush","Orange Eyes and Blush","Purple Eyes and Blush 2","Freckles","Beard Stubble","Beard and Mustache Stubble"]; var mouthCols3DS=["Orange","Red","Pink","Peach","Black"]; var glassesCols3DS=["Black","Brown","Red","Blue","Yellow","Grey"]; var tables={ faces: [ 0x00,0x01,0x08, 0x02,0x03,0x09, 0x04,0x05,0x0a, 0x06,0x07,0x0b ], hairs: [ [0x21,0x2f,0x28, 0x25,0x20,0x6b, 0x30,0x33,0x37, 0x46,0x2c,0x42], [0x34,0x32,0x26, 0x31,0x2b,0x1f, 0x38,0x44,0x3e, 0x73,0x4c,0x77], [0x40,0x51,0x74, 0x79,0x16,0x3a, 0x3c,0x57,0x7d, 0x75,0x49,0x4b], [0x2a,0x59,0x39, 0x36,0x50,0x22, 0x17,0x56,0x58, 0x76,0x27,0x24], [0x2d,0x43,0x3b, 0x41,0x29,0x1e, 0x0c,0x10,0x0a, 0x52,0x80,0x81], [0x0e,0x5f,0x69, 0x64,0x06,0x14, 0x5d,0x66,0x1b, 0x04,0x11,0x6e], [0x7b,0x08,0x6a, 0x48,0x03,0x15, 0x00,0x62,0x3f, 0x5a,0x0b,0x78], [0x05,0x4a,0x6c, 0x5e,0x7c,0x19, 0x63,0x45,0x23, 0x0d,0x7a,0x71], [0x35,0x18,0x55, 0x53,0x47,0x83, 0x60,0x65,0x1d, 0x07,0x0f,0x70], [0x4f,0x01,0x6d, 0x7f,0x5b,0x1a, 0x3d,0x67,0x02, 0x4d,0x12,0x5c], [0x54,0x09,0x13, 0x82,0x61,0x68, 0x2e,0x4e,0x1c, 0x72,0x7e,0x6f] ], eyebrows: [ [0x06,0x00,0x0c, 0x01,0x09,0x13, 0x07,0x15,0x08, 0x11,0x05,0x04], [0x0b,0x0a,0x02, 0x03,0x0e,0x14, 0x0f,0x0d,0x16, 0x12,0x10,0x17] ], eyes: [ [0x02,0x04,0x00, 0x08,0x27,0x11, 0x01,0x1a,0x10, 0x0f,0x1b,0x14], [0x21,0x0b,0x13, 0x20,0x09,0x0c, 0x17,0x22,0x15, 0x19,0x28,0x23], [0x05,0x29,0x0d, 0x24,0x25,0x06, 0x18,0x1e,0x1f, 0x12,0x1c,0x2e], [0x07,0x2c,0x26, 0x2a,0x2d,0x1d, 0x03,0x2b,0x16, 0x0a,0x0e,0x2f], [0x30,0x31,0x32, 0x35,0x3b,0x38, 0x36,0x3a,0x39, 0x37,0x33,0x34] ], noses: [ [0x01,0x0a,0x02, 0x03,0x06,0x00, 0x05,0x04,0x08, 0x09,0x07,0x0B], [0x0d,0x0e,0x0c, 0x11,0x10,0x0f] ], mouths: [ [0x17,0x01,0x13, 0x15,0x16,0x05, 0x00,0x08,0x0a, 0x10,0x06,0x0d], [0x07,0x09,0x02, 0x11,0x03,0x04, 0x0f,0x0b,0x14, 0x12,0x0e,0x0c], [0x1b,0x1e,0x18, 0x19,0x1d,0x1c, 0x1a,0x23,0x1f, 0x22,0x21,0x20] ] }; var convTables={ face3DSToWii:[0,1,2,2,3,1,4,5,4,6,7,6], features3DSToWii:["0","6",5,6,"6",4,7,7,8,10,"6",11],//If typeof===String, choose a makeup in that field's place - there is no suitable replacement. Read the discrepancies in the README for more information. makeup3DSToWii:[0,1,1,2,1,1,2,2,2,3,9,9], nose3DSToWii:[ [0,1,2,3,4,5,6,7,8,9,10,11], [0,3,4,6,9,2] ], mouth3DSToWii:[ ["111","121","131","112","122","132","113","123","133","114","124","134"], ["211","221","231","212","222","232","213","223","233","214","224","234"], ["121","214","134","123","121","112","124","133","221","224","121","232"] ], hair3DSToWii:[ [ "111","221","121", "231","211","121", "212","131","233", "132","112","222" ], [ "232","223","321", "123","311","134", "114","124","234", "114","134","234" ], [ "214","523","433", "214","531","512", "523","433","134", "414","523","134" ], [ "331","333","324", "332","333","334", "312","322","322", "113","122","313" ], [ "113","322","133", "333","323","314", "411","621","521", "424","424","424" ], [ "511","411","411", "422","522","523", "534","523","434", "422","533","424" ], [ "511","531","534", "623","521","524", "534","523","523", "424","513","523" ], [ "411","523","512", "513","432","432", "621","431","514", "421","432","514" ], [ "623","614","633", "633","633","624", "434","633","634", "624","624","634" ], [ "634","413","412", "413","413","412", "611","622","632", "611","622","632" ], [ "423","632","423", "612","612","613", "631","631","613", "631","631","613" ] ], eyebrows3DSToWii:[ [ "111","121","131", "112","122","132", "113","123","133", "114","124","134" ], [ "211","221","231", "212","222","232", "213","223","233", "214","224","234" ] ], eyes3DSToWii:[ [ "111","121","131", "112","122","132", "113","123","133", "114","124","134" ], [ "211","221","231", "212","222","232", "213","223","233", "214","224","234" ], [ "311","321","331", "312","322","332", "313","323","333", "314","324","334" ], [ "411","421","431", "412","422","432", "413","423","433", "414","424","434" ], [ "322","322","312", "224","224","431", "224","224","111", "121","411","431" ] ], hairWiiTo3DS:[ [ [0,0],[0,2],[0,7], [0,10],[3,10],[0,9], [4,0],[1,3],[3,8], [1,6],[1,7],[1,5] ], [ [0,4],[0,1],[0,3], [0,6],[0,11],[1,0], [2,5],[1,1],[0,8], [2,0],[2,6],[1,8] ], [ [1,4],[1,2],[3,0], [4,0],[3,6],[3,3], [3,11],[4,4],[4,3], [4,5],[3,2],[3,5] ], [ [4,6],[7,9],[7,7], [9,5],[5,9],[7,5], [9,1],[10,2],[7,0], [6,1],[5,8],[8,9] ], [ [5,0],[6,4],[2,4], [4,8],[5,4],[5,5], [6,10],[6,8],[5,10], [7,8],[6,5],[6,6] ], [ [9,6],[4,7],[10,6], [10,1],[9,10],[9,8], [10,8],[8,1],[2,0], [9,1],[8,9],[8,8] ] ], faceWiiTo3DS:[ 0,1, 3,4, 6,7, 9,10 ], featureWiiTo3DS:[ 0,"1","6", "9",5,2, 3,7,8, "10",9,11 ] }; function lookupTable(table,value,paginated){ if(paginated){ for(var i=0;i<tables[table].length;i++){ for(var j=0;j<tables[table][i].length;j++){ if(tables[table][i][j]===value){ return [i,j]; } } } } else{ for(var i=0;i<tables[table].length;i++){ if(tables[table][i]===value){ return i; } } } return undefined; } var kidNames={ "Male":[ "Aaron", "Adam", "Adrian", "Aiden", "Ayden", "Alex", "Alexander", "Alfie", "Andrew", "Anthony", "Archie", "Austin", "Ben", "Benjamin", "Bentley", "Bill", "Billy", "Blake", "Bradley", "Brandon", "Brayden", "Brody", "Bryson", "Caleb", "Callum", "Cameron", "Carlos", "Charlie", "Charles", "Carson", "Carter", "Chase", "Chris", "Christian", "Cody", "Colton", "Connor", "Cooper", "Damian", "Daniel", "David", "Dexter", "Dominic", "Dylan", "Easton", "Edward", "Eli", "Elijah", "Elliot", "Ethan", "Evan", "Finlay", "Frankie", "Freddie", "Gabriel", "Gavin", "George", "Grayson", "Harrison", "Harvey", "Henry", "Hudson", "Hugo", "Hunter", "Ian", "Isaac", "Isaiah", "Jace", "Jack", "Jackson", "Jaxon", "Jacob", "Jake", "James", "Jason", "Jayden", "Jenson", "Jeremiah", "John", "Juan", "Jonathan", "Jordan", "Jose", "Joseph", "Josiah", "Joshua", "Jude", "Julian", "Justin", "Kai", "Kayden", "Kevin", "Kian", "Landon", "Levi", "Leo", "Logan", "Lucas", "Luke", "Luis", "Lachlan", "Mason", "Matthew", "Max", "Michael", "Miguel", "Nathan", "Nathaniel", "Nicholas", "Noah", "Nolan", "Olly", "Oliver", "Owen", "Parker", "Philip", "Rhys", "Reece", "Rob", "Robert", "Ryan", "Ryder", "Samuel", "Sebastian", "Seth", "Thomas", "Tommy", "Trent", "Tristan", "Tyler", "William", "Liam", "Wyatt", "Xavier", "Zac", "Zachary", "Alex", "Alexis", "Angel", "Bailey", "Darcy", "Darcey", "Genesis", "Kennedy", "Mackenzie", "Morgan", "Peyton", "Sam", "Taylor" ], "Female":[ "Aaliyah", "Abigail", "Addison", "Madison", "Maddison", "Alexa", "Alexandra", "Alison", "Allison", "Alyssa", "Amelia", "Amy", "Andrea", "Anna", "Annabelle", "Aria", "Ariana", "Arianna", "Ashley", "Aubree", "Aubrey", "Audrey", "Autumn", "Ava", "Avery", "Bella", "Bethany", "Brianna", "Brooklyn", "Camila", "Caroline", "Charlotte", "Chloe", "Khloe", "Claire", "Ella", "Ellie", "Elenor", "Elizabeth", "Lizabeth", "Liza", "Emily", "Emma", "Eva", "Evie", "Evelyn", "Faith", "Gabriella", "Gianna", "Grace", "Hailey", "Hannah", "Harper", "Heidi", "Hollie", "Holly", "Isabella", "Isobel", "Jasmine", "Jessica", "Jocelyn", "Julia", "Katherine", "Kayla", "Kaylee", "Kimberly", "Kylie", "Lacey", "Lauren", "Layla", "Leah", "Lexie", "Lilian", "Lily", "Lola", "London", "Lucy", "Lydia", "Madeline", "Madelyn", "Maisie", "Makayla", "Maya", "Mya", "Megan", "Melanie", "Mia", "Molly", "Naomi", "Natalie", "Nevaeh", "Olivia", "Paige", "Poppy", "Piper", "Reagan", "Rebecca", "Riley", "Rosie", "Samantha", "Sarah", "Savannah", "Scarlett", "Serenity", "Skye", "Skylar", "Sofia", "Sophia", "Sophie", "Spring", "Stella", "Summer", "Sydney", "Trinity", "Vanessa", "Victoria", "Violet", "Winter", "Zara", "Zoe", "Zoey", "Alex", "Alexis", "Angel", "Bailey", "Darcy", "Darcey", "Genesis", "Kennedy", "Mackenzie", "Morgan", "Peyton", "Sam", "Taylor" ] }; var defaultInstrs={ wii:{ male:{ "col": "On the info page (first tab), set the Favorite Color to Red (1 from the left, top row).", "heightWeight": "On the build page (second tab), set the height to 50%, and the weight to 50%.", "faceShape": "On the face page (third tab), set the shape to the one 1 from the top, in the left column.", "skinCol": "On the face page (third tab), set the color to the one 1 from the left, on the top row.", "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", "hairStyle": "On the hair page (fourth tab), set the hair style to the one 1 from the left, 1 from the top, on page 1.", "hairFlipped": "", "hairColor": "On the hair page (fourth tab), set the hair color to the one 2 from the left, on the top row.", "eyebrowStyle": "On the eyebrow page (fifth tab), set the eyebrow style to the one 1 from the left, 1 from the top, on page 1.", "eyebrowColor": "On the eyebrow page (fifth tab), set the eyebrow color to the one 2 from the left, on the top row.", "eyebrowY": "", "eyebrowSize": "", "eyebrowRot": "", "eyebrowDist": "", "eyeType": "On the eye page (sixth tab), set the eye type to the one 1 from the left, 1 from the top, on page 1.", "eyeColor": "On the eye page (sixth tab), set the color to the one 1 from the left, on the top row.", "eyeY": "", "eyeSize": "", "eyeRot": "", "eyeDist": "", "noseType": "On the nose page (seventh tab), set the nose to the one 1 from the top, and 1 from the left.", "noseY": "", "noseSize": "", "mouthType": "On the mouth page (eighth tab), set the mouth type to the one 1 from the left, 1 from the top, on page 1.", "mouthCol": "On the mouth page (eighth tab), set the color to the one 1 from the left.", "mouthY": "", "mouthSize": "", "glasses": "On the glasses page (within the ninth tab), set the glasses to the one 1 from the top, and 1 from the left.", "glassesCol": "On the glasses page (within the ninth tab), set the color to the one 1 from the left, on the top row.", "glassesY": "", "glassesSize": "", "stache": "On the mustache page (within the ninth tab), set the mustache to the one on the top-left.", "stacheY": "", "stacheSize": "", "mole": "", "moleX": "", "moleY": "", "moleSize": "", "beard": "On the beard page (within the ninth tab), set the beard to the one on the top-left.", "beardCol": "On the mustache OR beard pages (within the ninth tab), set the color to the one 1 from the left, on the top row." }, female:{ "col": "On the info page (first tab), set the Favorite Color to Red (1 from the left, top row).", "heightWeight": "On the build page (second tab), set the height to 50%, and the weight to 50%.", "faceShape": "On the face page (third tab), set the shape to the one 1 from the top, in the left column.", "skinCol": "On the face page (third tab), set the color to the one 1 from the left, on the top row.", "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", "hairStyle": "On the hair page (fourth tab), set the hair style to the one 1 from the left, 1 from the top, on page 4.", "hairFlipped": "", "hairColor": "On the hair page (fourth tab), set the hair color to the one 2 from the left, on the top row.", "eyebrowStyle": "On the eyebrow page (fifth tab), set the eyebrow style to the one 2 from the left, 1 from the top, on page 1.", "eyebrowColor": "On the eyebrow page (fifth tab), set the eyebrow color to the one 2 from the left, on the top row.", "eyebrowY": "", "eyebrowSize": "", "eyebrowRot": "", "eyebrowDist": "", "eyeType": "On the eye page (sixth tab), set the eye type to the one 2 from the left, 1 from the top, on page 1.", "eyeColor": "On the eye page (sixth tab), set the color to the one 1 from the left, on the top row.", "eyeY": "", "eyeSize": "", "eyeRot": "On the eye page (sixth tab), press the rotate clockwise button 1 times.", "eyeDist": "", "noseType": "On the nose page (seventh tab), set the nose to the one 0 from the top, and 1 from the left.", "noseY": "", "noseSize": "", "mouthType": "On the mouth page (eighth tab), set the mouth type to the one 1 from the left, 1 from the top, on page 1.", "mouthCol": "On the mouth page (eighth tab), set the color to the one 1 from the left.", "mouthY": "", "mouthSize": "", "glasses": "On the glasses page (within the ninth tab), set the glasses to the one 1 from the top, and 1 from the left.", "glassesCol": "On the glasses page (within the ninth tab), set the color to the one 1 from the left, on the top row.", "glassesY": "", "glassesSize": "", "stache": "On the mustache page (within the ninth tab), set the mustache to the one on the top-left.", "stacheY": "", "stacheSize": "", "mole": "", "moleX": "", "moleY": "", "moleSize": "", "beard": "On the beard page (within the ninth tab), set the beard to the one on the top-left.", "beardCol": "On the mustache OR beard pages (within the ninth tab), set the color to the one 1 from the left, on the top row." } }, "3ds":{ "male":{ "faceShape": "On the face page (first tab), set the face shape to the one 1 from the top, and 1 from the left.", "skinCol": "On the face page (first tab), set the color to the one 1 from the top.", "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", "feature": "On the face page's wrinkles tab, set the facial feature to \"None\" (the one 2 from the top, and 1 from the left).", "hairStyle": "On the hair page (second tab), set the hair style to the one 1 from the top, and 1 from the left, on page 1.", "hairFlipped": "", "hairColor": "On the hair page (second tab), set the hair color to the one 2 from the top.", "eyebrowStyle": "On the eyebrow page (third tab), set the eyebrow style to the one 1 from the left, 1 from the top, on page 1.", "eyebrowColor": "On the eyebrow page (third tab), set the eyebrow color to the one 2 from the top.", "eyebrowY": "", "eyebrowSize": "", "eyebrowRot": "", "eyebrowDist": "", "eyebrowSquash": "", "eyeType": "On the eye page (fourth tab), set the eye type to the one 1 from the left, 1 from the top, on page 1.", "eyeColor": "On the eye page (fourth tab), set the color to the one 1 from the top.", "eyeY": "", "eyeSize": "", "eyeRot": "", "eyeDist": "", "eyeSquash": "", "noseType": "On the nose page (fifth tab), set the nose to the one 1 from the top, and 1 from the left, on page 0.", "noseY": "", "noseSize": "", "mouthType": "On the mouth page (sixth tab), set the mouth type to the one 1 from the left, 1 from the top, on page 1.", "mouthCol": "On the mouth page (sixth tab), set the color to the one 1 from the top.", "mouthY": "", "mouthSize": "", "mouthSquash": "", "glasses": "On the glasses page (within the seventh tab), set the glasses to the one 1 from the top, and 1 from the left.", "glassesCol": "On the glasses page (within the seventh tab), set the color to the one 1 from the top.", "glassesY": "", "glassesSize": "", "stache": "On the mustache page (within the seventh tab), set the mustache to the one on the top-left.", "stacheY": "", "stacheSize": "", "mole": "", "moleX": "", "moleY": "", "moleSize": "", "beard": "On the beard page (within the seventh tab), set the beard to the one on the top-left.", "beardCol": "On the mustache OR beard pages (within the seventh tab), set the color to the one 1 from the top.", "heightWeight": "On the build page (eighth tab), set the height to 50%, and the weight to 50%.", "col": "On the info page (after pressing \"Next\"), set the Favorite Color to Red (1 from the left, top row)." }, "female":{ "faceShape": "On the face page (first tab), set the face shape to the one 1 from the top, and 1 from the left.", "skinCol": "On the face page (first tab), set the color to the one 1 from the top.", "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", "feature": "On the face page's wrinkles tab, set the facial feature to \"None\" (the one 2 from the top, and 1 from the left).", "hairStyle": "On the hair page (second tab), set the hair style to the one 3 from the top, and 1 from the left, on page 5.", "hairFlipped": "", "hairColor": "On the hair page (second tab), set the hair color to the one 2 from the top.", "eyebrowStyle": "On the eyebrow page (third tab), set the eyebrow style to the one 2 from the left, 1 from the top, on page 1.", "eyebrowColor": "On the eyebrow page (third tab), set the eyebrow color to the one 2 from the top.", "eyebrowY": "", "eyebrowSize": "", "eyebrowRot": "", "eyebrowDist": "", "eyebrowSquash": "", "eyeType": "On the eye page (fourth tab), set the eye type to the one 2 from the left, 1 from the top, on page 1.", "eyeColor": "On the eye page (fourth tab), set the color to the one 1 from the top.", "eyeY": "", "eyeSize": "", "eyeRot": "", "eyeDist": "", "eyeSquash": "", "noseType": "On the nose page (fifth tab), set the nose to the one 1 from the top, and 1 from the left, on page 0.", "noseY": "", "noseSize": "", "mouthType": "On the mouth page (sixth tab), set the mouth type to the one 1 from the left, 1 from the top, on page 1.", "mouthCol": "On the mouth page (sixth tab), set the color to the one 1 from the top.", "mouthY": "", "mouthSize": "", "mouthSquash": "", "glasses": "On the glasses page (within the seventh tab), set the glasses to the one 1 from the top, and 1 from the left.", "glassesCol": "On the glasses page (within the seventh tab), set the color to the one 1 from the top.", "glassesY": "", "glassesSize": "", "stache": "On the mustache page (within the seventh tab), set the mustache to the one on the top-left.", "stacheY": "", "stacheSize": "", "mole": "", "moleX": "", "moleY": "", "moleSize": "", "beard": "On the beard page (within the seventh tab), set the beard to the one on the top-left.", "beardCol": "On the mustache OR beard pages (within the seventh tab), set the color to the one 1 from the top.", "heightWeight": "On the build page (eighth tab), set the height to 50%, and the weight to 50%.", "col": "On the info page (after pressing \"Next\"), set the Favorite Color to Red (1 from the left, top row)." } } }; async function renderMii(studioMii,fflRes=_fflRes) { var width=600,height=600; /* ---------- WebGL 1 context ---------- */ const gl = createGL(width, height, { preserveDrawingBuffer: true }); /* ---------- dummy canvas to keep Three.js happy ---------- */ const dummyCanvas = { width, height, getContext: () => gl, addEventListener () {}, removeEventListener () {}, style: {}, }; /* ---------- Three.js renderer ---------- */ const renderer = new THREE.WebGLRenderer({ canvas: dummyCanvas, context: gl, }); renderer.setSize(width, height); renderer.setClearColor(0xffffff); let moduleFFL, currentCharModel; /* ---------- simple scene ---------- */ const scene = new THREE.Scene(); scene.background = new THREE.Color().setHex(0xffffff, THREE.ColorManagement ? THREE.ColorManagement.workingColorSpace : ''); let camera = new THREE.PerspectiveCamera(15, width / height, 1, 5000); camera.position.set(0, 30, 500); function updateCharModelInScene(data, modelDesc) { // Decode data. if (typeof data === 'string') { data = parseHexOrB64ToUint8Array(data); } // Continue assuming it is Uint8Array. // If an existing CharModel exists, update it. if (currentCharModel) { // Remove current CharModel from the scene, then dispose it. currentCharModel.meshes && scene.remove(currentCharModel.meshes); currentCharModel.dispose(); } // Create a new CharModel. currentCharModel = createCharModel(data, modelDesc, FFLShaderMaterial, moduleFFL); // Initialize textures for the new CharModel. initCharModelTextures(currentCharModel, renderer); // Add CharModel meshes to scene. scene.add(currentCharModel.meshes); } const box = new THREE.Mesh( new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({ color: 0x00ffff }) ); scene.add(box); const initResult = await initializeFFL(fflRes, ModuleFFL); moduleFFL = initResult.module; updateCharModelInScene(studioMii, FFLCharModelDescDefault); // Use default expression. renderer.render(scene, camera); /* ---------- read pixels ---------- */ const pixels = new Uint8Array(width * height * 4); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); /* ---------- flip rows (Uint8Array → Buffer) ---------- */ const src = Buffer.from(pixels); // <-- Convert here const flipped = Buffer.alloc(src.length); const rowBytes = width * 4; for (let y = 0; y < height; y++) { const srcStart = y * rowBytes; const dstStart = (height - y - 1) * rowBytes; src.copy(flipped, dstStart, srcStart, srcStart + rowBytes); } /* ---------- draw into Node-canvas ---------- */ const canvas = createCanvas(width, height); const ctx = canvas.getContext('2d'); const img = new ImageData(new Uint8ClampedArray(flipped.buffer), width, height); ctx.putImageData(img, 0, 0); return canvas.toBuffer('image/png'); } var exports={ readWiiBin:function(binOrPath){ var thisMii={ info:{}, face:{}, nose:{}, mouth:{}, mole:{}, hair:{}, eyebrows:{}, eyes:{}, glasses:{}, facialHair:{} }; let data; if(/[^01]/ig.test(binOrPath)){ data = fs.readFileSync(binOrPath); } else{ data=Buffer.from(binOrPath); } const get = address => getBinaryFromAddress(address, data); var name=""; for(var i=0;i<10;i++){ name+=data.slice(3+i*2, 4+i*2)+""; } thisMii.name=name.replaceAll("\x00",""); var cname=""; for(var i=0;i<10;i++){ cname+=data.slice(55+i*2, 56+i*2)+""; } thisMii.creatorName=cname.replaceAll("\x00",""); thisMii.info.creatorName=thisMii.creatorName; thisMii.info.name=thisMii.name;//Up to ten characters thisMii.info.gender=get(0x00)[1]==="1"?"Female":"Male";//0 for Male, 1 for Female thisMii.info.miiId=parseInt(get(0x18),2).toString(16)+parseInt(get(0x19),2).toString(16)+parseInt(get(0x1A),2).toString(16)+parseInt(get(0x1B),2).toString(16); thisMii.info.systemId=parseInt(get(0x1C),2).toString(16)+parseInt(get(0x1D),2).toString(16)+parseInt(get(0x1E),2).toString(16)+parseInt(get(0x1F),2).toString(16); var temp=get(0x20); thisMii.face.shape=parseInt(temp.slice(0,3),2);//0-7 thisMii.face.col=skinCols[parseInt(temp.slice(3,6),2)];//0-5 temp=get(0x21); thisMii.face.feature=wiiFaceFeatures[parseInt(get(0x20).slice(6,8)+temp.slice(0,2),2)];//0-11 thisMii.info.mingle=temp[5]==="0";//0 for Mingle, 1 for Don't Mingle temp=get(0x2C); for(var i=0;i<12;i++){ if(wiiNoses[i]===parseInt(temp.slice(0,4),2)){ thisMii.nose.type=i; } } thisMii.nose.size=parseInt(temp.slice(4,8),2); thisMii.nose.yPos=parseInt(get(0x2D).slice(0,5),2);//From top to bottom, 0-18, default 9 temp=get(0x2E); thisMii.mouth.type=mouthTable[""+parseInt(temp.slice(0,5),2)];//0-23, Needs lookup table thisMii.mouth.col=wiiMouthColors[parseInt(temp.slice(5,7),2)];//0-2, refer to mouthColors array temp2=get(0x2F); thisMii.mouth.size=parseInt(temp[7]+temp2.slice(0,3),2);//0-8, default 4 thisMii.mouth.yPos=parseInt(temp2.slice(3,8),2);//0-18, default 9, from top to bottom temp=get(0x00); var temp2=get(0x01); thisMii.info.birthMonth=parseInt(temp.slice(2,6),2); thisMii.info.birthday=parseInt(temp.slice(6,8)+temp2.slice(0,3),2); thisMii.info.favColor=favCols[parseInt(temp2.slice(3,7),2)];//0-11, refer to cols array thisMii.info.favorited=temp2[7]==="0"?false:true; thisMii.info.height=parseInt(get(0x16),2);//0-127 thisMii.info.weight=parseInt(get(0x17),2);//0-127 thisMii.info.downloadedFromCheckMiiOut=get(0x21)[7]==="0"?false:true; temp=get(0x34); temp2=get(0x35); thisMii.mole.on=temp[0]==="0"?false:true;//0 for Off, 1 for On thisMii.mole.size=parseInt(temp.slice(1,5),2);//0-8, default 4 thisMii.mole.xPos=parseInt(temp2.slice(2,7),2);//0-16, Default 2 thisMii.mole.yPos=parseInt(temp.slice(5,8)+temp2.slice(0,2),2);//Top to bottom temp=get(0x22); temp2=get(0x23); thisMii.hair.type=hairTable[""+parseInt(temp.slice(0,7),2)];//0-71, Needs lookup table thisMii.hair.col=hairCols[parseInt(temp[7]+temp2.slice(0,2),2)];//0-7, refer to hairCols array thisMii.hair.flipped=temp2[2]==="0"?false:true; temp=get(0x24); temp2=get(0x25); thisMii.eyebrows.type=eyebrowTable[""+parseInt(temp.slice(0,5),2)];//0-23, Needs lookup table thisMii.eyebrows.rotation=parseInt(temp.slice(6,8)+temp2.slice(0,2),2);//0-11, default varies based on eyebrow type temp=get(0x26); temp2=get(0x27); thisMii.eyebrows.col=hairCols[parseInt(temp.slice(0,3),2)]; thisMii.eyebrows.size=parseInt(temp.slice(3,7),2);//0-8, default 4 thisMii.eyebrows.yPos=(parseInt(temp[7]+temp2.slice(0,4),2))-3;//0-15, default 10 thisMii.eyebrows.distApart=parseInt(temp2.slice(4,8),2);//0-12, default 2 thisMii.eyes.type=eyeTable[parseInt(get(0x28).slice(0,6),2)];//0-47, needs lookup table temp=get(0x29); thisMii.eyes.rotation=parseInt(temp.slice(0,3),2);//0-7, default varies based on eye type thisMii.eyes.yPos=parseInt(temp.slice(3,8),2);//0-18, default 12, top to bottom temp=get(0x2A); thisMii.eyes.col=eyeCols[parseInt(temp.slice(0,3),2)];//0-5 thisMii.eyes.size=parseInt(temp.slice(4,7),2);//0-7, default 4 temp2=get(0x2B); thisMii.eyes.distApart=parseInt(temp[7]+temp2.slice(0,3),2);//0-12, default 2 temp=get(0x30); thisMii.glasses.type=parseInt(temp.slice(0,4),2);//0-8 thisMii.glasses.col=wiiGlassesCols[parseInt(temp.slice(4,7),2)];//0-5 temp=get(0x31); thisMii.glasses.size=parseInt(temp.slice(0,3),2);//0-7, default 4 thisMii.glasses.yPos=parseInt(temp.slice(3,8),2);//0-20, default 10 temp=get(0x32); temp2=get(0x33); thisMii.facialHair.mustacheType=parseInt(temp.slice(0,2),2);//0-3 thisMii.facialHair.beardType=parseInt(temp.slice(2,4),2);//0-3 thisMii.facialHair.col=hairCols[parseInt(temp.slice(4,7),2)];//0-7 thisMii.facialHair.mustacheSize=parseInt(temp[7]+temp2.slice(0,3),2);//0-30, default 20 thisMii.facialHair.mustacheYPos=parseInt(temp2.slice(3,8),2);//0-16, default 2 thisMii.console="wii"; return thisMii; }, read3DSQR:async function(binOrPath){ function readMii(data){ var miiJson={ info:{}, perms:{}, hair:{}, face:{}, eyes:{}, eyebrows:{}, nose:{}, mouth:{}, facialHair:{}, glasses:{}, mole:{} }; const get = address => getBinaryFromAddress(address, data); var temp=get(0x18); var temp2=get(0x19); miiJson.info.birthday=parseInt(temp2.slice(6,8)+temp.slice(0,3),2); miiJson.info.birthMonth=parseInt(temp.slice(3,7),2); var name=""; for(var i=0x1A;i<0x2E;i+=2){ if(get(i)==="00000000"){ break; } name+=data.slice(i,i+1); } miiJson.name=name.replaceAll("\x00",""); var cname=""; for(var i=0x48;i<0x5C;i+=2){ if(get(i)==="00000000"){ break; } cname+=data.slice(i,i+1); } miiJson.creatorName=cname.replaceAll("\x00",""); miiJson.info.name=miiJson.name; miiJson.info.creatorName=miiJson.creatorName; miiJson.info.height=parseInt(get(0x2E),2); miiJson.info.weight=parseInt(get(0x2F),2); miiJson.info.gender=temp[7]==="1"?"Female":"Male"; temp=get(0x30); miiJson.perms.sharing=temp[7]==="1"?false:true; miiJson.info.favColor=favCols[parseInt(temp2.slice(2,6),2)]; miiJson.perms.copying=get(0x01)[7]==="1"?true:false; miiJson.hair.style=lookupTable("hairs",parseInt(get(0x32),2),true); miiJson.face.shape=lookupTable("faces",parseInt(temp.slice(3,7),2),false); miiJson.face.col=skinCols[parseInt(temp.slice(0,3),2)]; temp=get(0x31); miiJson.face.feature=faceFeatures3DS[parseInt(temp.slice(4,8),2)]; miiJson.face.makeup=makeups3DS[parseInt(temp.slice(0,4),2)]; temp=get(0x34); miiJson.eyes.type=lookupTable("eyes",parseInt(temp.slice(2,8),2),true); temp2=get(0x33); miiJson.hair.col=hairCols[parseInt(temp2.slice(5,8),2)]; miiJson.hair.flipped=temp2[4]==="0"?false:true; miiJson.eyes.col=eyeCols[parseInt(get(0x35)[7]+temp.slice(0,2),2)]; temp=get(0x35); miiJson.eyes.size=parseInt(temp.slice(3,7),2); miiJson.eyes.squash=parseInt(temp.slice(0,3),2); temp=get(0x36); temp2=get(0x37); miiJson.eyes.rot=parseInt(temp.slice(3,8),2); miiJson.eyes.distApart=parseInt(temp2[7]+temp.slice(0,3),2); miiJson.eyes.yPos=parseInt(temp2.slice(2,7),2); temp=get(0x38); miiJson.eyebrows.style=lookupTable("eyebrows",parseInt(temp.slice(3,8),2),true); miiJson.eyebrows.col=hairCols[parseInt(temp.slice(0,3),2)]; temp=get(0x39); miiJson.eyebrows.size=parseInt(temp.slice(4,8),2); miiJson.eyebrows.squash=parseInt(temp.slice(1,4),2); temp=get(0x3A); miiJson.eyebrows.rot=parseInt(temp.slice(4,8),2); temp2=get(0x3B); miiJson.eyebrows.distApart=parseInt(temp2[7]+temp.slice(0,3),2); miiJson.eyebrows.yPos=parseInt(temp2.slice(2,7),2)-3; temp=get(0x3C); miiJson.nose.type=lookupTable("noses",parseInt(temp.slice(3,8),2),true); temp2=get(0x3D); miiJson.nose.size=parseInt(temp2[7]+temp.slice(0,3),2); miiJson.nose.yPos=parseInt(temp2.slice(2,7),2); temp=get(0x3E); miiJson.mouth.type=lookupTable("mouths",parseInt(temp.slice(2,8),2),true); temp2=get(0x3F); miiJson.mouth.col=mouthCols3DS[parseInt(temp2[7]+temp.slice(0,2),2)]; miiJson.mouth.size=parseInt(temp2.slice(3,7),2); miiJson.mouth.squash=parseInt(temp2.slice(0,3),2); temp=get(0x40); miiJson.mouth.yPos=parseInt(temp.slice(3,8),2); miiJson.facialHair.mustacheType=parseInt(temp.slice(0,3),2); temp=get(0x42); miiJson.facialHair.beardType=parseInt(temp.slice(5,8),2); miiJson.facialHair.col=hairCols[parseInt(temp.slice(2,5),2)]; temp2=get(0x43); miiJson.facialHair.mustacheSize=parseInt(temp2.slice(6,8)+temp.slice(0,2),2); miiJson.facialHair.mustacheYPos=parseInt(temp2.slice(1,6),2); temp=get(0x44); miiJson.glasses.type=parseInt(temp.slice(4,8),2); miiJson.glasses.col=glassesCols3DS[parseInt(temp.slice(1,4),2)]; temp2=get(0x45); miiJson.glasses.size=parseInt(temp2.slice(5,8)+temp[0],2); miiJson.glasses.yPos=parseInt(temp2.slice(0,5),2); temp=get(0x46); miiJson.mole.on=temp[7]==="0"?false:true; miiJson.mole.size=parseInt(temp.slice(3,7),2); temp2=get(0x47); miiJson.mole.xPos=parseInt(temp2.slice(6,8)+temp.slice(0,3),2); miiJson.mole.yPos=parseInt(temp2.slice(1,6),2); miiJson.console="3ds"; return miiJson; } let qrCode; if(/[^01]/ig.test(binOrPath)){ var data=fs.readFileSync(binOrPath); var img=await loadImage(data); const canvas = createCanvas(img.width, img.height); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); qrCode = jsQR(imageData.data, imageData.width, imageData.height).binaryData; } else{ var d=binOrPath.match(/(0|1){1,8}/g); qrCode=[]; d.forEach(byte=>{ qrCode.push(parseInt(byte,2)); }); } if (qrCode) { var data = decodeAesCcm(new Uint8Array(qrCode)); return Promise.resolve(readMii(Buffer.from(data))); } else { console.error('Failed to decode QR code'); } }, writeWiiBin:function(jsonIn, outPath){ if(jsonIn.console?.toLowerCase() !== "wii"){ this.convertMii(jsonIn); } var mii=jsonIn; var miiBin="0"; miiBin+=mii.info.gender==="Male"?"0":"1"; miiBin+=mii.info.birthMonth.toString(2).padStart(4,"0"); miiBin+=mii.info.birthday.toString(2).padStart(5,"0"); miiBin+=favCols.indexOf(mii.info.favColor).toString(2).padStart(4,"0"); miiBin+=mii.info.favorited?1:0; for(var i=0;i<10;i++){ miiBin+="00000000"; if(i<mii.name.length){ miiBin+=mii.name.charCodeAt(i).toString(2).padStart(8,"0"); } else{ miiBin+="00000000"; } }