UNPKG

miijs

Version:

The most complete and easy to use Mii library available.

1,977 lines (1,924 loc) 96.3 kB
import { backTables, lookupTables, islandAddresses } from "./data.js"; import { decryptMii, decryptStudio2, encryptMii, encryptStudio2 } from "./miiCrypto.js"; import lodash from "lodash"; let amiiboHandlerPromise; async function loadAmiiboHandler() { amiiboHandlerPromise ??= import("./amiiboHandler.js"); return amiiboHandlerPromise; } async function extractMiiFromAmiibo(data) { return (await loadAmiiboHandler()).extractMiiFromAmiibo(data); } function encodeMiitomoMii(data) { return Buffer.concat([ encoders.appendCrc(data.subarray(0, 0x60)), data.subarray(0x60) ]); } function encodeMiitopiaMii(data) { return Buffer.concat([ encoders.appendCrc(data.subarray(0, 0x60)), data.subarray(0x60) ]); } function binaryToHex(binaryString) { const decimal = BigInt('0b' + binaryString); return decimal.toString(16).toUpperCase().padStart(Math.ceil(binaryString.length / 4), '0'); } function getMiiIDTimestamp(id, epoch, intervals, bits) { const idBigInt = BigInt('0x' + id); const mask = (1n << BigInt(bits)) - 1n; // Create mask for specified bits const seconds = (idBigInt & mask) * intervals; return new Date(Number(BigInt(epoch) + seconds * 1000n)); } function getIDFromTimestamp(timestamp, epochMs, intervalSeconds, bits) { const t = timestamp ? new Date(timestamp) : new Date(); //Accepts Date or ISO string/number const timestampMs = BigInt(t.getTime()); const epoch = BigInt(epochMs); const intervalMs = BigInt(intervalSeconds) * 1000n; const ticks = (timestampMs - epoch) / intervalMs; const mask = (1n << BigInt(bits)) - 1n; const id = ticks & mask; return id.toString(2).padStart(bits, "0").slice(0,bits); } function getKeyByValue(object, value) { for (var key in object) { if (object[key] === value) { return key; } } return null; } function crc16(data, current = 0x0000) { let crc = current; for (let i = 0; i < data.length; i++) { const byte = data[i]; for (let bit = 7; bit >= 0; bit--) { crc = ((crc << 1) | ((byte >> bit) & 0x1)) & 0x1FFFF; if (crc & 0x10000) { crc ^= 0x1021; } } } for (let i = 0; i < 16; i++) { crc = (crc << 1) & 0x1FFFF; if (crc & 0x10000) { crc ^= 0x1021; } } return crc & 0xFFFF; } const crc32CksumTable = new Uint32Array(256); function generateCrc32Table(table, poly = 0x04C11DB7) { for (let i = 0; i < 256; i++) { let crc = i << 24; for (let j = 0; j < 8; j++) { crc = (crc & 0x80000000) ? (crc << 1) ^ poly : crc << 1; } table[i] = crc >>> 0; } } generateCrc32Table(crc32CksumTable); function crc32(input, table = crc32CksumTable) { let crc = 0x00000000; for (let i = 0; i < input.length; i++) { const byte = (input[i] ^ (crc >>> 24)) & 0xFF; crc = (table[byte] ^ (crc << 8)) >>> 0; } return (crc ^ 0xFFFFFFFF) >>> 0; } function miiCrcCalc(dat, mode = 16) { return mode === 32 ? crc32(dat) : crc16(dat); } const decoders = { 'eyebrowYPositions': (y) => y - 3 }; const encoders = { 'eyebrowYPositions': (y) => y + 3, 'appendCrc': (buf, mode = 16) => { buf = buf.slice(0, -(mode / 8));//Since our structs for validation also indicate to the encoder to add the "checksums" as blank padding, we strip that const crc = miiCrcCalc(buf, mode); // turn number into 2 bytes const crcBytes = Buffer.alloc(mode / 8); if (mode === 32) { crcBytes.writeUInt32BE(crc >>> 0, 0); } else { crcBytes.writeUInt16BE(crc, 0); } // append return Buffer.concat([buf, crcBytes]); }, 'switchId': (id) => { id = Buffer.from(id.padStart(32,"0"), "hex"); id[6] = (id[6] & 0x0f) | 0x40; // 0100xxxx id[8] = (id[8] & 0x3f) | 0x80; // 10xxxxxx return id.toString("hex"); } }; const processors = { rcdPreProcess: (obj) => { if(!obj.hasOwnProperty("meta")) obj.meta={type:"Default"}; switch (obj.meta.type) { case 'Special': obj.meta.miiId = '010'; break; case 'Foreign': obj.meta.miiId = '110'; break; default: obj.meta.miiId = '100'; break; } obj.meta.miiId += getIDFromTimestamp(obj.meta.creationTimestamp, Date.UTC(2006, 0, 1), 4, 29); obj.meta.miiId = binaryToHex(obj.meta.miiId); if (obj.meta?.type === "Special" && obj.perms.mingle) { console.warn(`Cannot have Mingle enabled for Special Miis. Disabled Mingle in the buffer output, the original object is left intact.`); obj.perms.mingle = false; } return obj; }, cffcdPreProcess: (obj,ogDevice) => { if (!obj.hasOwnProperty("meta")) obj.meta = { type: "Default" }; if (!obj.meta.hasOwnProperty("type")) obj.meta.type = "Default"; if (obj.meta.type === "Special" && obj.perms?.sharing) { console.warn(`Cannot have Sharing enabled for Special Miis. Disabled sharing in the buffer output, the original object is left intact.`); obj.perms.sharing = false; } obj.meta.miiId = binaryToHex(`${obj.meta.type === "Special" ? 0 : 1}001${getIDFromTimestamp(obj.meta.creationTimestamp, Date.UTC(2010, 0, 1), 2, 28)}`); if(ogDevice!==undefined){ obj.meta.originalDevice=ogDevice; } return obj; }, rcdPostProcess: (obj, ogD) => { switch ((parseInt(obj.meta.miiId[0], 16) >> 1).toString(2).padStart(3, '0')) { case '010': obj.meta.type = "Special"; break; case '110': obj.meta.type = "Foreign"; break; default://100 obj.meta.type = "Default"; break; } obj.meta.creationTimestamp = getMiiIDTimestamp(obj.meta.miiId, Date.UTC(2006, 0, 1), 4n, 29); obj.meta.originalDevice = ogD; return obj; }, cffcdPostProcess: (obj) => { obj.meta.type = (parseInt(obj.meta.miiId, 16) & 0x80000000) === 0 ? "Special" : "Default"; obj.meta.creationTimestamp = getMiiIDTimestamp(obj.meta.miiId, Date.UTC(2010, 0, 1), 2n, 28); return obj; } }; function forwardPort(data, from, to = "SWITCH") { from = from.toUpperCase().replaceAll(" ",""); to = to.toUpperCase().replaceAll(" ", ""); if(from==="SWITCH") return data; if (!getKeyByValue(ConsoleFormats,from)) { throw new Error(`${from} is not a valid type, expected one of ${Object.keys(ConsoleFormats).join(", ")}.`); } if (!getKeyByValue(ConsoleFormats,to)) { throw new Error(`${to} is not a valid type, expected one of ${Object.keys(ConsoleFormats).join(", ")}.`); } if (from === "WII" || from === "DS") { const modernFeatureOrMakeup = lookupTables.makeupsFeatures[data.face.feature]; if (typeof modernFeatureOrMakeup === 'string') { data.face.feature = 0; data.face.makeup = Number(modernFeatureOrMakeup); } else if (typeof modernFeatureOrMakeup === 'number') { data.face.feature = modernFeatureOrMakeup; data.face.makeup = 0; } else { data.face.feature = 0; data.face.makeup = 0; } } if (to === "SWITCH" || from === "3DS") { if (!data.beard.color) data.beard.color = 8; if (!data.eyebrows.color) data.eyebrows.color = 8; if (!data.hair.color) data.hair.color = 8; data.eyes.color += 8; if (data.mouth.color < 5) { data.mouth.color += 19; } if (!data.glasses.color) { data.glasses.color = 8; } else if (data.glasses.color < 6) { data.glasses.color += 13; } else { data.glasses.color = 0; } } return data; } function backPort(data, to, from = "SWITCH") { from = from.toUpperCase().replaceAll(" ",""); to = to.toUpperCase().replaceAll(" ", ""); if(from==="WII" || from === "DS") return data; if (!getKeyByValue(ConsoleFormats,from)) { throw new Error(`${from} is not a valid type, expected one of ${Object.keys(ConsoleFormats).join(", ")}.`); } if (!getKeyByValue(ConsoleFormats,to)) { throw new Error(`${to} is not a valid type, expected one of ${Object.keys(ConsoleFormats).join(", ")}.`); } if (from === "SWITCH" || to === "3DS") { data.hair.color = backTables.switch.hairsColors[data.hair.color]; data.eyes.color = backTables.switch.eyesColors[data.eyes.color]; if(data.glasses.type>8) data.glasses.type = backTables.switch.glassesTypes[data.glasses.type-9]; data.glasses.color = backTables.switch.glassesColors[data.glasses.color]; data.mouth.color = backTables.switch.mouthsColors[data.mouth.color]; data.face.color = backTables.switch.faceColors[data.face.color]; data.eyebrows.color = backTables.switch.hairsColors[data.eyebrows.color]; data.beard.color = backTables.switch.hairsColors[data.beard.color]; } if (from === "3DS" || to === "WII" || to === "DS") { if (data.mouth.color > 2) data.mouth.color = 0; if (data.beard.type > 3) data.beard.type = 3; if (data.beard.mustache.type === 4) { data.beard.mustache.type = 2; } else if (data.beard.mustache.type === 5) { data.beard.mustache.type = 0; data.beard.type = 1; } //Later Miis have two separate fields, so you can have makeup and facial features (such as wrinkles) applied at the same time. The Wii/NDS only has one that covers both. //We prioritize facial features here because the Wii/NDS supports more of those than they do makeup types, and is more likely to apply. //Additionally, facial features are more likely to be inherent to the face itself. const legacyFeature = backTables["3ds"].features[data.face.feature]; const legacyMakeup = backTables["3ds"].makeups[data.face.makeup]; const numericLegacyFeature = Number(legacyFeature); const numericLegacyMakeup = Number(legacyMakeup); if (typeof legacyFeature !== 'string') { data.face.feature = Number.isFinite(numericLegacyFeature) ? numericLegacyFeature : 0; } else if (numericLegacyMakeup > 0) { data.face.feature = numericLegacyMakeup; } else { data.face.feature = Number.isFinite(numericLegacyFeature) ? numericLegacyFeature : 0; } data.face.makeup = 0; if (data.hair.type > 71) data.hair.type = backTables["3ds"].hairs[data.hair.type - 72]; if (data.face.type > 7) data.face.type = backTables["3ds"].faces[data.face.type - 8]; if (data.eyes.type > 47) data.eyes.type = backTables["3ds"].eyes[data.eyes.type - 48]; if (data.nose.type > 11) data.nose.type = backTables["3ds"].noses[data.nose.type - 12]; if (data.mouth.type > 23) data.mouth.type = backTables["3ds"].mouths[data.mouth.type - 24]; } return data; } /** * SwitchSDK -> NSDB? * AMii -> Translation/canonical layer between all formats by HEYimHeroic? * MiiNG -> PNG with Mii data embedded by HEYimHeroic, should look into. */ /** Enum of Mii Formats - see comments on individual items for a description of the format. */ const MiiFormats = /** @type {const} */ ({ /** * Revolution Character Data * Wii Miis */ RCD: 'rcd', /** * Revolution Store Data * RCD with a Checksum appended */ RSD: 'rsd', /** * Alias for RSD format * @deprecated This format is commonly used to represent RSD, but has been used in an official capacity as an alias of CHARINFO. * @returns {'rcd'} */ get MII() { //console.warn('MII is deprecated. Use RCD for Wii formats, or CHARINFO for Switch formats instead.'); return 'rcd'; }, /** * Alias for RSD format * @deprecated This format is named after a homebrew importer/exporter for Miis on the Wii, and is the same as RSD. * @returns {'rcd'} */ get MIIGX() { //console.warn('MIIGX is deprecated. Use RCD instead, which is the same in all but name.'); return 'rcd'; }, /** * Alias for RSD format * @deprecated This extension is named after an unofficial Mii creator 'My Avatar Editor', and is the same as RSD. * @returns {'rcd'} */ get MAE() { //console.warn('MAE is deprecated. Use RCD instead which is the same in all but name.'); return 'rcd'; }, /** * NDS Character Data * NDS Miis */ NCD: 'ncd', /** * NDS Store Data * NCD with a Checksum appended */ NSD: 'nsd', /** * Version 3 Miis * @deprecated This extension is used to refer to CFCD/CFSD/FFCD/FFSD, which are functionally the same but have enough difference for encoding to need to differentiate. * @returns {'ffcd'} */ get ver3() { //console.warn('This extension is used to refer to CFCD/CFSD/FFCD/FFSD, which are functionally the same but have enough difference for encoding to need to differentiate. Use FFCD/FFSD.'); return 'ffcd'; }, /** * CTR Face Character Data * 3DS Miis, identical to FFCD. */ CFCD: 'cfcd', /** * Cafe Face Character Data (Unofficial Name) * Wii U Miis, identical to CFCD. */ FFCD: 'ffcd', /** * CTR Face Store Data (Unofficial Name) * CFCD with a buffer and padding at the end. Identical to FFSD. */ CFSD: 'cfsd', /** * Cafe Face Store Data * FFCD with a buffer and padding at the end. Identical to CFSD. */ FFSD: 'ffsd', /** * CTR Face Encrypted Data (Unofficial Name) * Holds encrypted data as seen in QR codes. Identical to FFED. */ CFED: 'cfed', /** * Cafe Face Encrypted Data (Unofficial Name) * Holds encrypted data as seen in QR codes. Identical to CFED. */ FFED: 'ffed', /** * Miitomo * The first 0x60 Bytes are FFSD, then Miitomo QR data follows. */ Miitomo: 'miitomo', MIITOMO: 'miitomo', /** * Miitomo Encrypted */ MIITOMOE: 'miitomoe', /** * Miitopia * The first 0x60 Bytes are FFSD, then Miitopia QR data follows. */ Miitopia: 'mt', MT: 'mt', MIITOPIA: 'mt', /** * Miitopia Encrypted */ MTE: 'mte', /** * Tomodachi Life * The first 0x5C Bytes are FFCD, then extra data follows. */ TomodachiLife: 'tlc', /** * Tomodachi Life * The first 0x5C Bytes are FFCD, then extra data follows. */ TLS: 'tls', TLC: 'tlc', /** * Tomodachi Life Encrypted */ TLE: 'tle', TLEC: 'tlec', /** * NX Face Store Data (Unofficial Name) * Miis in the Switch NAND, alias of NFDB. */ NFSD: 'nfsd', /** * NX Face Store Data (Unofficial Name) * Miis in the Switch NAND, alias of NFDB. */ storedata: 'nfsd', /** * NX Face Database (Unofficial Name) * Miis in the Switch NAND, alias of NFSD. */ NFDB: 'nfsd', /** * Alias for NFSD format * There are official usecases of the sampledb extension, however this is not recommended as a filename. * @depricated * @returns {'nfsd'} */ get SAMPLEDB() { //console.warn('SAMPLEDB is deprecated. Use NFSD instead which is the same in all but name.'); return 'nfsd'; }, /** * NX Face Character Data (Unofficial Name) * NFSD without the Mii ID or checksums. */ NFCD: 'nfcd', /** * NX Face Character Data (Unofficial Name) * NFSD without the Mii ID or checksums. */ coredata: 'nfcd', /** * Character Info * Switch Miis */ CHARINFO: 'charinfo', /** * Ultimate Face Store Data * @deprecated This filetype was thought to be used exclusively in Smash Bros Ultimate, however its application is now known to be much broader than that. Use CHARINFO instead. * @returns {'nfsd'} */ get UFSD() { //console.warn('UFSD is deprecated. Use NFSD instead which is the same in all but name.'); return 'nfsd'; }, /** * My Nintendo Mii Studio (Unofficial Name) * Browser Localstorage format after editing Miis via Mii Studio */ MNMS: 'mnms', /** * Encrypted My Nintendo Mii Studio (Unofficial Name) * Used for requesting image data from My Nintendo's Mii Studio */ EMNMS: 'emnms', /** * Named after Mii Studio * Alias of MNMS */ STUDIO: 'mnms', /** * Localstorage object in Mii Studio (Unofficial Name) * Alias of MNMS */ localstorage: 'mnms', /** * Amiibo Files (Unofficial Name) * @deprecated Amiibos are generally .bins but here we use Amiibo for better identification purposes as all formats could be .bin. Same as NTAG. * @returns {'ntag'} */ get AMIIBO() { //console.warn('AMIIBO is deprecated. Use NTAG or NTAG_ALT instead which are the same in all but name.'); return 'ntag'; }, /** * NTAG215 Data (Unofficial Name) * NTAG215s are generally .bins but here we use NTAG for better identification purposes as all formats could be .bin. * NTAG215 chips are what are used for Amiibo chips. * Same as NTAG_ALT but 540 Bytes instead of 532. */ NTAG: 'ntag', /** * NTAG215 Data Alternate Size (Unofficial Name) * Same as NTAG but 532 Bytes instead of 540. */ NTAG_ALT: 'ntag_alt', /** * Decrypted Amiibo Bin (Unofficial Name) * Contains an unencrypted Amiibo tag */ NTAG_INTERNAL: 'ntag_internal', /** * NDS Miis * @deprecated NDS Miis come in two formats, please be more specific about NCD or NSD. * @returns {'ncd'} */ get NDS() { //console.warn('NDS Miis come in two formats, please be more specific about NCD or NSD.'); return 'ncd'; }, /** * Wii Miis * @deprecated Wii Miis come in two formats, please be more specific about RCD or RSD. * @returns {'rcd'} */ get WII() { //console.warn('Wii Miis come in two formats, please be more specific about RCD or RSD.'); return 'rcd'; }, /** * 3DS Miis * @deprecated 3DS Miis come in multiple formats. Please be more specific about which one you need. * @returns {'ffcd'} */ get ["3DS"]() { //console.warn('3DS Miis come in multiple formats. Please be more specific about which one you need.'); return 'ffcd'; }, /** * Wii U Miis * @deprecated Wii U Miis come in multiple formats. Please be more specific about which one you need. * @returns {'ffcd'} */ get WIIU() { //console.warn('Wii U Miis come in multiple formats. Please be more specific about which one you need.'); return 'ffcd'; }, /** * Switch Miis * @deprecated Switch Miis come in multiple formats, please be more specific about which one you need. * @returns {'charinfo'} */ get SWITCH() { //console.warn('Switch Miis come in multiple formats, please be more specific about which one you need.'); return 'charinfo'; }, /** * Mii Creator Miis * @deprecated This is not an official file type, we are supporting it due to its widespread usage. Please use .CHARINFO instead. */ MIIC:'miic' }); /** Enum of consoles */ const ConsoleFormats = /** @type {const} */ ({ DS: "DS", WII: "WII", "3DS": "3DS", WIIU: "WIIU", SWITCH: "SWITCH", SWITCH2: "SWITCH2" }); /** * Layout of where to extract each field of each format. * Unless otherwise specified, fields are numbers. */ let commonStructs = { [MiiFormats.NCD]: [ //0x0 { word: true, len: 16 }, { name: "favorited", bool: true, len: 1 }, { name: "favoriteColor", len: 4, max: 11 }, { name: "birthday", len: 5 }, { name: "birthMonth", len: 4, max: 12//0=Not Set }, { name: "gender",//0 Male, 1 Female len: 1 }, { name: "unknown", len: 1 }, //0x2 { name: "name", text: 'le', len: 160 }, //0x16 { name: "height", len: 8, max: 127 }, //0x17 { name: "weight", len: 8, max: 127 }, //0x18 { name: "miiId", len: 32, hex: true }, //x01C { name: "systemId", len: 32, hex: true, decoder: (id) => `${id}`.padEnd(16, "0"), encoder: (id) => id.slice(0, 8) }, //0x20 { word: true, len: 16 }, { name: "fromCheckMiiOut", bool: true, len: 1 }, { name: "unknown", len: 1 }, { name: "mingle", len: 1, decoder: (mingle) => mingle == 0 ? true : false,//Mingle is 0 for true, 1 for false encoder: (mingle) => mingle ? 0 : 1 }, { name: "unknown", len: 3, }, { name: "faceFeature", len: 4, max: 11 }, { name: "faceColor", len: 3, max: 5 }, { name: "faceType", len: 3, max: 7 }, //0x22 { word: true, len: 16 }, { name: "unknown", len: 5 }, { name: "hairFlipped", bool: true, len: 1 }, { name: "hairColor", len: 3, max: 7 }, { name: "hairType", len: 7, max: 71 //qk, Add decoder to make canonical value }, //0x24 { word: true, len: 16 }, { name: "unknown", len: 6 }, { name: "eyebrowRotation", len: 4, max: 11 }, { name: "unknown", len: 1 }, { name: "eyebrowType", len: 5, max: 23 //qk, Add decoder to make canonical value }, //0x26 { word: true, len: 16 }, { name: "eyebrowDistanceApart", len: 4, max: 12 }, { name: "eyebrowYPosition", len: 5, min: 3, max: 18, decoder: decoders.eyebrowYPositions, encoder: encoders.eyebrowYPositions }, { name: "eyebrowSize", len: 4, max: 8 }, { name: "eyebrowColor", len: 3, max: 7 }, //0x28 { word: true, len: 16 }, { name: "eyeYPosition", len: 5, max: 18 }, { name: "eyeRotation", len: 3, max: 7 }, { name: "unknown", len: 2 }, { name: "eyeType", len: 6, max: 47 //qk, Add decoder to make canonical value }, //0x2A { word: true, len: 16 }, { name: "unknown", len: 5 }, { name: "eyeDistanceApart", len: 4, max: 12 }, { name: "eyeSize", len: 3, max: 7 }, { name: "unknown", len: 1 }, { name: "eyeColor", len: 3, max: 5 }, //0x2C { word: true, len: 16 }, { name: "unknown", len: 3 }, { name: "noseYPosition", len: 5, max: 18 }, { name: "noseSize", len: 4, max: 8 }, { name: "noseType", len: 4, max: 11 }, //0x2E { word: true, len: 16 }, { name: "mouthYPosition", len: 5, max: 18 }, { name: "mouthSize", len: 4, max: 8 }, { name: "mouthColor", len: 2, max: 2 }, { name: "mouthType", len: 5, max: 23 //qk, Turn this into a canonical value }, //0x30 { word: true, len: 16 }, { name: "glassesYPosition", len: 5, max: 20 }, { name: "glassesSize", len: 3, max: 7 }, { name: "disablesMii", len: 1, max: 0 }, { name: "glassesColor", len: 3, max: 5 }, { name: "glassesType", len: 4, max: 8 }, //0x32 { word: true, len: 16 }, { name: "mustacheYPosition", len: 5, max: 16 }, { name: "mustacheSize", len: 4, max: 8 }, { name: "beardColor", len: 3, max: 7 }, { name: "beardType", len: 2, max: 3 }, { name: "mustacheType", len: 2, max: 3 }, //0x34 { word: true, len: 16 }, { name: "unknown", len: 1 }, { name: "moleXPosition", len: 5, max: 16 }, { name: "moleYPosition", len: 5, max: 30 }, { name: "moleSize", len: 4, max: 8 }, { name: "moleActive", bool: true, len: 1 }, //0x36 { name: "creatorName", text: 'le', len: 160 }//0x49 ], [MiiFormats.RCD]: [ //0x0 { name: "unknown", len: 1 }, { name: "gender",//0 Male, 1 Female len: 1 }, { name: "birthMonth", len: 4, max: 12//0=Not Set }, //Last two bits of 0x0 - 0x1 { name: "birthday", len: 5 }, { name: "favoriteColor", len: 4, max: 11 }, { name: "favorited", bool: true, len: 1 }, //0x2 { name: "name", text: true, len: 160 }, //0x16 { name: "height", len: 8, max: 127 }, //0x17 { name: "weight", len: 8, max: 127 }, //0x18 { name: "miiId", len: 32, hex: true }, //x01C { name: "systemId", len: 32, hex: true, decoder: (id) => `${id}`.padEnd(16, "0"), encoder: (id) => (id ? id : '00000000').slice(0, 8) }, //0x20 { name: "faceType", len: 3, max: 7 }, { name: "faceColor", len: 3, max: 5 }, //Last two bits of 0x2 - 0x21 { name: "faceFeature", len: 4, max: 11 }, { name: "unknown", len: 3, }, { name: "mingle", len: 1, decoder: (mingle) => mingle == 0 ? true : false,//Mingle is 0 for true, 1 for false encoder: (mingle) => mingle ? 0 : 1 }, { name: "unknown", len: 1 }, { name: "fromCheckMiiOut", bool: true, len: 1 }, //0x22 { name: "hairType", len: 7, max: 71 //qk, Add decoder to make canonical value }, //Last bit of 0x22 - 0x23 { name: "hairColor", len: 3, max: 7 }, { name: "hairFlipped", bool: true, len: 1 }, { name: "unknown", len: 5 }, //0x24 { name: "eyebrowType", len: 5, max: 23 //qk, Add decoder to make canonical value }, { name: "unknown", len: 1 }, //Last two bits of 0x24 - 0x25 { name: "eyebrowRotation", len: 4, max: 11 }, { name: "unknown", len: 6 }, //0x26 { name: "eyebrowColor", len: 3, max: 7 }, { name: "eyebrowSize", len: 4, max: 8 }, //Last bit of 0x26 - 0x27 { name: "eyebrowYPosition", len: 5, min: 3, max: 18, decoder: decoders.eyebrowYPositions, encoder: encoders.eyebrowYPositions }, { name: "eyebrowDistanceApart", len: 4, max: 12 }, //0x28 { name: "eyeType", len: 6, max: 47 //qk, Add decoder to make canonical value }, { name: "unknown", len: 2 }, //0x29 { name: "eyeRotation", len: 3, max: 7 }, { name: "eyeYPosition", len: 5, max: 18 }, //0x2A { name: "eyeColor", len: 3, max: 5 }, { name: "unknown", len: 1 }, { name: "eyeSize", len: 3, max: 7 }, //Last bit of 0x2A - 0x2B { name: "eyeDistanceApart", len: 4, max: 12 }, { name: "unknown", len: 5 }, //0x2C { name: "noseType", len: 4, max: 11 }, { name: "noseSize", len: 4, max: 8 }, //0x2D { name: "noseYPosition", len: 5, max: 18 }, { name: "unknown", len: 3 }, //0x2E { name: "mouthType", len: 5, max: 23 //qk, Turn this into a canonical value }, { name: "mouthColor", len: 2, max: 2 }, //Last bit of 0x2E - 0x2F { name: "mouthSize", len: 4, max: 8 }, { name: "mouthYPosition", len: 5, max: 18 }, //0x30 { name: "glassesType", len: 4, max: 8 }, { name: "glassesColor", len: 3, max: 5 }, { name: "disablesMii", len: 1, max: 0 }, //0x31 { name: "glassesSize", len: 3, max: 7 }, { name: "glassesYPosition", len: 5, max: 20 }, //0x32 { name: "mustacheType", len: 2, max: 3 }, { name: "beardType", len: 2, max: 3 }, { name: "beardColor", len: 3, max: 7 }, //Last bit of 0x32 - 0x33 { name: "mustacheSize", len: 4, max: 8 }, { name: "mustacheYPosition", len: 5, max: 16 }, //0x34 { name: "moleActive", bool: true, len: 1 }, { name: "moleSize", len: 4, max: 8 }, //Last three bits of 0x34 - 0x35 { name: "moleYPosition", len: 5, max: 30 }, { name: "moleXPosition", len: 5, max: 16 }, { name: "unknown", len: 1 }, //0x36 { name: "creatorName", text: true, len: 160 }//0x49 ], [MiiFormats.FFCD]: { len: 0x5C, be: false, translation: '3ds', struct: [ //0x0 { name: "version", len: 8, encoder: () => 3 }, //0x1 { name: "unknown", len: 2 }, { name: "charset", len: 2, max: 3 }, { name: "region", len: 2, max: 3 }, { name: "profaneNames", bool: true, len: 1 }, { name: "copying", bool: true, len: 1 }, //0x2 { name: "selectionSlotIndex", len: 4 }, { name: "selectionPageIndex", len: 4 }, //0x3 { name: "unknown", len: 1 }, { name: "originalDevice", len: 3, min: 1, max: 4//1 = Wii. 2 = DS. 3 = 3DS. 4 = Wii U/Switch( 2) }, { name: "unknown", len: 4 }, //0x4 { name: "systemId", hex: true, len: 64 }, //0xC { name: "miiId", hex: true, len: 32 }, //0x10 { name: "creatorMac", hex: true, len: 48 }, //0x16 { name: "padding", len: 16 }, //0x18 { word: true, // Reverse the endianess of the next 16 bits len: 16 }, { name: "gender", len: 1//0 Male, 1 Female }, { name: "birthMonth", len: 4, max: 12//0 if not set }, { name: "birthday", len: 5 }, { name: "favoriteColor", len: 4, max: 11 }, { name: "favorited", bool: true, len: 1 }, { name: "unknown", len: 1 }, //0x1A { name: "name", text: 'le', len: 160 }, //0x2E { name: "height", len: 8, max: 127 }, //0x2F { name: "weight", len: 8, max: 127 }, //0x30 { name: "faceColor", len: 3, max: 5 }, { name: "faceType", len: 4, max: 11 }, { name: "sharing", len: 1, decoder: (sharing) => sharing == 0 ? true : false, encoder: (sharing) => sharing ? 0 : 1 }, //0x31 { name: "makeup", len: 4, max: 11 }, { name: "faceFeature", len: 4, max: 11 }, //0x32 { name: "hairType", len: 8, max: 131 }, //0x33 { name: "unknown", len: 4 }, { name: "hairFlipped", bool: true, len: 1 }, { name: "hairColor", len: 3, max: 7 }, //0x34 { word: true, len: 32 }, { name: "eyeType", len: 6, max: 59 }, { name: "eyeColor", len: 3, max: 5 }, { name: "eyeSize", len: 4, max: 7//flip }, { name: "eyeSquash", len: 3, max: 6//flip }, { name: "eyeRotation", len: 5, max: 7 }, { name: "eyeDistanceApart", len: 4, max: 12 }, { name: "eyeYPosition", len: 5, max: 18 }, { name: "unknown", len: 2 }, //0x38 { word: true, len: 32 }, { name: "eyebrowType", len: 5, max: 24 }, { name: "eyebrowColor", len: 3, max: 7 }, { name: "eyebrowSize", len: 4, max: 8//flip }, { name: "eyebrowSquash", len: 3, max: 6//flip }, { name: "unknown", len: 1 }, { name: "eyebrowRotation", len: 4, max: 11 }, { name: "unknown", len: 1 }, { name: "eyebrowDistanceApart", len: 4, max: 12 }, { name: "eyebrowYPosition", len: 5, min: 3, max: 18, decoder: decoders.eyebrowYPositions, encoder: encoders.eyebrowYPositions }, { name: "unknown", len: 2 }, //0x3C { word: true, len: 16 }, { name: "noseType", len: 5, max: 17 }, { name: "noseSize", len: 4, max: 8//flip }, { name: "noseYPosition", len: 5, max: 18 }, { name: "unknown", len: 2 }, //0x3E { word: true, len: 16 }, { name: "mouthType", len: 6, max: 35 }, { name: "mouthColor", len: 3, max: 4 }, { name: "mouthSize", len: 4, max: 8//flip }, { name: "mouthSquash", len: 3, max: 6//flip }, //0x40 { word: true, len: 16 }, { name: "mouthYPosition", len: 5, max: 18 }, { name: "mustacheType", len: 3, max: 5 }, { name: "unknown", len: 8 }, //0x42 { word: true, len: 16 }, { name: "beardType", len: 3, max: 5 }, { name: "beardColor", len: 3, max: 7 }, { name: "mustacheSize", len: 4, max: 8//flip }, { name: "mustacheYPosition", len: 5, max: 16 }, { name: "unknown", len: 1 }, //0x44 { word: true, len: 16 }, { name: "glassesType", len: 4, max: 8 }, { name: "glassesColor", len: 3, max: 5 }, { name: "glassesSize", len: 4, max: 7//flip }, { name: "glassesYPosition", len: 5, max: 20 }, //0x46 { word: true, len: 16 }, { name: "moleActive", bool: true, len: 1 }, { name: "moleSize", len: 4, max: 8//flip }, { name: "moleXPosition", len: 5, max: 16 }, { name: "moleYPosition", len: 5, max: 30 }, { name: "unknown", len: 1 }, //0x48 { name: "creatorName", text: 'le', len: 160 } ], preProcess: (dat)=>processors.cffcdPreProcess(dat,4), postProcess: processors.cffcdPostProcess }, [MiiFormats.FFED]: { len: 0x70, decoder: decryptMii, encoder: encryptMii, preEncode: MiiFormats.CFSD }, [MiiFormats.FFED]: { len: 0x70, decoder: decryptMii, encoder: encryptMii, preEncode: MiiFormats.FFSD }, [MiiFormats.NFCD]: [ { name: "hairType", len: 8, max: 131 }, { name: "moleActive", bool: true, len: 1 }, { name: "height", len: 7, max: 127 }, { name: "hairFlipped", bool: true, len: 1 }, { name: "weight", len: 7, max: 127 }, { name: "isSpecial", bool: true, len: 1, decoder: (bool) => bool ? "Special" : "Default", encoder: (text) => text==="Special" ? true:false }, { name: "hairColor", len: 7, max: 99 }, { name: "gender", len: 1 }, { name: "eyeColor", len: 7, max: 99 }, { name:"unknown", len:1 }, { name: "eyebrowColor", len: 7, max: 99 }, { name:"unknown", len:1 }, { name: "mouthColor", len: 7, max: 99 }, { name:"unknown", len:1 }, { name: "beardColor", len: 7, max: 99 }, { name:"unknown", len:1 }, { name: "glassesColor", len: 7, max: 99 }, { name: "region", len: 2, max: 3 }, { name: "eyeType", len: 6, max: 59 }, { name:"charset", len:2, max:3 }, { name: "mouthType", len: 6, max: 35 }, { name: "glassesSize", len: 3, max: 7 }, { name: "eyeYPosition", len: 5, max: 18 }, { name: "mustacheType", len: 3, max: 5 }, { name: "eyebrowType", len: 5, max: 23 }, { name: "beardType", len: 3, max: 5 }, { name: "noseType", len: 5, max: 17 }, { name: "mouthSquash", len: 3, max: 6 }, { name: "noseYPosition", len: 5, max: 18 }, { name: "eyebrowSquash", len: 3, max: 6 }, { name: "mouthYPosition", len: 5, max: 18 }, { name: "eyeRotation", len: 3, max: 7 }, { name: "mustacheYPosition", len: 5, max: 16 }, { name: "eyeSquash", len: 3, max: 6 }, { name: "glassesYPosition", len: 5, max: 20 }, { name: "eyeSize", len: 3, max: 7 }, { name: "moleXPosition", len: 5