miijs
Version:
The most complete and easy to use Mii library available.
1,977 lines (1,924 loc) • 96.3 kB
JavaScript
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