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
JavaScript
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";
}
}