├── crown.jpg ├── miijs.png ├── miiMaleBody.glb ├── miiFemaleBody.glb ├── .gitignore ├── package.json ├── fflWrapper.js ├── patch-ffl.js ├── ideal.jsonc ├── Enums.js ├── amiiboHandler.js ├── types.d.ts ├── README.md └── index.js /crown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KestronProgramming/MiiJS/HEAD/crown.jpg -------------------------------------------------------------------------------- /miijs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KestronProgramming/MiiJS/HEAD/miijs.png -------------------------------------------------------------------------------- /miiMaleBody.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KestronProgramming/MiiJS/HEAD/miiMaleBody.glb -------------------------------------------------------------------------------- /miiFemaleBody.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KestronProgramming/MiiJS/HEAD/miiFemaleBody.glb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vsls.json 3 | package-lock.json 4 | node_modules 5 | FFLResHigh.dat 6 | __tests__ 7 | TODO.md 8 | experiments -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miijs", 3 | "version": "2.3.2", 4 | "description": "Work with Mii characters in every possible way needed for your project.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "postinstall": "node ./patch-ffl.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/KestronProgramming/MiiJS.git" 13 | }, 14 | "keywords": [ 15 | "Mii", 16 | "QR", 17 | "3DS", 18 | "Wii", 19 | "Remote", 20 | "Special" 21 | ], 22 | "author": "Kestron", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/KestronProgramming/MiiJS/issues" 26 | }, 27 | "homepage": "https://github.com/KestronProgramming/MiiJS#readme", 28 | "dependencies": { 29 | "canvas": "^3.1.0", 30 | "ffl.js": "github:ariankordi/FFL.js#06ede8f", 31 | "gl": "^8.1.6", 32 | "https": "^1.0.0", 33 | "jimp": "0.22.12", 34 | "jsdom": "^26.1.0", 35 | "jsqr": "^1.4.0", 36 | "path": "^0.12.7", 37 | "qr-code-styling": "^1.9.2", 38 | "require-esm-in-cjs": "^0.1.0", 39 | "struct-fu": "^1.2.1", 40 | "three": "^0.162.0" 41 | }, 42 | "devDependencies": { 43 | "jest": "^30.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /fflWrapper.js: -------------------------------------------------------------------------------- 1 | // Compatibility layer for ffl.js 2 | const structFu = require('struct-fu'); 3 | 4 | // Monkey-patch struct-fu BEFORE ffl.js loads 5 | const originalStruct = structFu.struct; 6 | structFu.struct = function(...args) { 7 | const result = originalStruct.apply(this, args); 8 | const originalUnpack = result.unpack; 9 | const originalPack = result.pack; 10 | 11 | result.unpack = function(data) { 12 | if (data && !(data instanceof Buffer) && data.buffer) { 13 | data = Buffer.from(data); 14 | } 15 | return originalUnpack.call(this, data); 16 | }; 17 | 18 | result.pack = function(obj) { 19 | if (obj && typeof obj === 'object') { 20 | for (let key in obj) { 21 | if (key.startsWith('_padding') && Array.isArray(obj[key])) { 22 | obj[key] = Buffer.from(obj[key]); 23 | } 24 | } 25 | } 26 | return originalPack.call(this, obj); 27 | }; 28 | 29 | return result; 30 | }; 31 | 32 | // Set up globals 33 | global._ = structFu; 34 | global.THREE = require('three'); 35 | 36 | // Load ffl.js - it will use the patched struct-fu 37 | const fflPath = require.resolve('ffl.js/ffl.js'); 38 | delete require.cache[fflPath]; // Clear cache 39 | const fflModule = require('ffl.js/ffl.js'); 40 | 41 | module.exports = fflModule; -------------------------------------------------------------------------------- /patch-ffl.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | var fflPath; 5 | const fflPaths = [path.join(__dirname, './node_modules/ffl.js/ffl.js'),path.join(__dirname, '../node_modules/ffl.js/ffl.js'),path.join(__dirname, '../../node_modules/ffl.js/ffl.js'),path.join(__dirname, './ffl.js'),path.join(__dirname, '../ffl.js')]; 6 | for(var i=0;i= 0; bitIndex--) { 32 | crc = (((crc << 1) | ((checksumData[byteIndex] >> bitIndex) & 0x1)) ^ 33 | (((crc & 0x8000) !== 0) ? 0x1021 : 0)) & 0xFFFF; 34 | } 35 | } 36 | for (let counter = 16; counter > 0; counter--) { 37 | crc = ((crc << 1) ^ (((crc & 0x8000) !== 0) ? 0x1021 : 0)) & 0xFFFF; 38 | } 39 | return crc & 0xFFFF; 40 | } 41 | 42 | //Validate and fix Mii checksum - for 96-byte Amiibo format 43 | function validateAndFixMiiChecksum(miiData) { 44 | if (miiData.length !== 92 && miiData.length !== MII_SIZE) { 45 | throw new Error(`Invalid Mii data size: expected 92 or ${MII_SIZE} bytes, got ${miiData.length}`); 46 | } 47 | const fullMii = Buffer.alloc(MII_SIZE); 48 | miiData.slice(0, Math.min(94, miiData.length)).copy(fullMii, 0); 49 | const newChecksum = calculateMiiChecksum(fullMii); 50 | fullMii[94] = (newChecksum >> 8) & 0xFF; 51 | fullMii[95] = newChecksum & 0xFF; 52 | return fullMii; 53 | } 54 | function calcSeed(dump) { 55 | const seed = Buffer.alloc(64); 56 | dump.slice(0x029, 0x02B).copy(seed, 0x00); 57 | seed.fill(0x00, 0x02, 0x10); 58 | dump.slice(0x1D4, 0x1DC).copy(seed, 0x10); 59 | dump.slice(0x1D4, 0x1DC).copy(seed, 0x18); 60 | dump.slice(0x1E8, 0x208).copy(seed, 0x20); 61 | return seed; 62 | } 63 | function prepareSeed(typeString, magicBytes, magicBytesSize, xorPad, baseSeed) { 64 | const output = Buffer.alloc(480); 65 | let offset = 0; 66 | const typeStringEnd = typeString.indexOf(0); 67 | const typeLen = typeStringEnd >= 0 ? typeStringEnd + 1 : 14; 68 | typeString.slice(0, typeLen).copy(output, offset); 69 | offset += typeLen; 70 | const leadingSeedBytes = 16 - magicBytesSize; 71 | baseSeed.slice(0, leadingSeedBytes).copy(output, offset); 72 | offset += leadingSeedBytes; 73 | magicBytes.slice(0, magicBytesSize).copy(output, offset); 74 | offset += magicBytesSize; 75 | baseSeed.slice(0x10, 0x20).copy(output, offset); 76 | offset += 16; 77 | for (let i = 0; i < 32; i++) { 78 | output[offset + i] = baseSeed[0x20 + i] ^ xorPad[i]; 79 | } 80 | offset += 32; 81 | return output.slice(0, offset); 82 | } 83 | function drbgGenerateBytes(hmacKey, seed, outputSize) { 84 | const result = Buffer.alloc(outputSize); 85 | let offset = 0; 86 | let iteration = 0; 87 | while (offset < outputSize) { 88 | const iterBuffer = Buffer.alloc(2 + seed.length); 89 | iterBuffer[0] = (iteration >> 8) & 0xFF; 90 | iterBuffer[1] = iteration & 0xFF; 91 | seed.copy(iterBuffer, 2); 92 | const hmac = crypto.createHmac('sha256', hmacKey); 93 | hmac.update(iterBuffer); 94 | const output = hmac.digest(); 95 | const toCopy = Math.min(32, outputSize - offset); 96 | output.copy(result, offset, 0, toCopy); 97 | offset += toCopy; 98 | iteration++; 99 | } 100 | return result; 101 | } 102 | 103 | function deriveKeys(typeString, magicBytes, magicBytesSize, xorPad, hmacKey, baseSeed) { 104 | const preparedSeed = prepareSeed(typeString, magicBytes, magicBytesSize, xorPad, baseSeed); 105 | const derived = drbgGenerateBytes(hmacKey, preparedSeed, 48); 106 | return { 107 | aesKey: derived.slice(0, 16), 108 | aesIV: derived.slice(16, 32), 109 | hmacKey: derived.slice(32, 48) 110 | }; 111 | } 112 | 113 | function tagToInternal(tag) { 114 | const internal = Buffer.alloc(NFC3D_AMIIBO_SIZE); 115 | tag.slice(0x008, 0x010).copy(internal, 0x000); 116 | tag.slice(0x080, 0x0A0).copy(internal, 0x008); 117 | tag.slice(0x010, 0x034).copy(internal, 0x028); 118 | tag.slice(0x0A0, 0x208).copy(internal, 0x04C); 119 | tag.slice(0x034, 0x054).copy(internal, 0x1B4); 120 | tag.slice(0x000, 0x008).copy(internal, 0x1D4); 121 | tag.slice(0x054, 0x080).copy(internal, 0x1DC); 122 | return internal; 123 | } 124 | 125 | function internalToTag(internal) { 126 | const tag = Buffer.alloc(NFC3D_AMIIBO_SIZE); 127 | internal.slice(0x000, 0x008).copy(tag, 0x008); 128 | internal.slice(0x008, 0x028).copy(tag, 0x080); 129 | internal.slice(0x028, 0x04C).copy(tag, 0x010); 130 | internal.slice(0x04C, 0x1B4).copy(tag, 0x0A0); 131 | internal.slice(0x1B4, 0x1D4).copy(tag, 0x034); 132 | internal.slice(0x1D4, 0x1DC).copy(tag, 0x000); 133 | internal.slice(0x1DC, 0x208).copy(tag, 0x054); 134 | return tag; 135 | } 136 | 137 | function decryptAmiibo(tag) { 138 | const internal = tagToInternal(tag); 139 | const seed = calcSeed(internal); 140 | const dataKeys = deriveKeys(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed); 141 | const tagKeys = deriveKeys(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed); 142 | const plain = Buffer.alloc(NFC3D_AMIIBO_SIZE); 143 | const cipher = crypto.createDecipheriv('aes-128-ctr', dataKeys.aesKey, dataKeys.aesIV); 144 | cipher.setAutoPadding(false); 145 | const decrypted = cipher.update(internal.slice(0x02C, 0x1B4)); 146 | decrypted.copy(plain, 0x02C); 147 | internal.slice(0x000, 0x008).copy(plain, 0x000); 148 | internal.slice(0x028, 0x02C).copy(plain, 0x028); 149 | internal.slice(0x1D4, 0x208).copy(plain, 0x1D4); 150 | const tagHmac = crypto.createHmac('sha256', tagKeys.hmacKey); 151 | tagHmac.update(plain.slice(0x1D4, 0x208)); 152 | const computedTagHmac = tagHmac.digest(); 153 | computedTagHmac.copy(plain, 0x1B4); 154 | const dataHmac = crypto.createHmac('sha256', dataKeys.hmacKey); 155 | dataHmac.update(plain.slice(0x029, 0x208)); 156 | const computedDataHmac = dataHmac.digest(); 157 | computedDataHmac.copy(plain, 0x008); 158 | return plain; 159 | } 160 | 161 | function encryptAmiibo(plain) { 162 | const seed = calcSeed(plain); 163 | const dataKeys = deriveKeys(DATA_TYPE_STRING, DATA_MAGIC_BYTES, DATA_MAGIC_BYTES_SIZE, DATA_XOR_PAD, DATA_HMAC_KEY, seed); 164 | const tagKeys = deriveKeys(TAG_TYPE_STRING, TAG_MAGIC_BYTES, TAG_MAGIC_BYTES_SIZE, TAG_XOR_PAD, TAG_HMAC_KEY, seed); 165 | const cipher_internal = Buffer.alloc(NFC3D_AMIIBO_SIZE); 166 | const tagHmac = crypto.createHmac('sha256', tagKeys.hmacKey); 167 | tagHmac.update(plain.slice(0x1D4, 0x208)); 168 | tagHmac.digest().copy(cipher_internal, 0x1B4); 169 | const dataHmac = crypto.createHmac('sha256', dataKeys.hmacKey); 170 | dataHmac.update(plain.slice(0x029, 0x1B4)); 171 | dataHmac.update(cipher_internal.slice(0x1B4, 0x1D4)); 172 | dataHmac.update(plain.slice(0x1D4, 0x208)); 173 | dataHmac.digest().copy(cipher_internal, 0x008); 174 | const aesCipher = crypto.createCipheriv('aes-128-ctr', dataKeys.aesKey, dataKeys.aesIV); 175 | aesCipher.setAutoPadding(false); 176 | const encrypted = aesCipher.update(plain.slice(0x02C, 0x1B4)); 177 | encrypted.copy(cipher_internal, 0x02C); 178 | plain.slice(0x000, 0x008).copy(cipher_internal, 0x000); 179 | plain.slice(0x028, 0x02C).copy(cipher_internal, 0x028); 180 | plain.slice(0x1D4, 0x208).copy(cipher_internal, 0x1D4); 181 | return internalToTag(cipher_internal); 182 | } 183 | 184 | //Extract Mii data from an Amiibo dump 185 | function extractMiiFromAmiibo(amiiboDump) { 186 | if (!Buffer.isBuffer(amiiboDump)) { 187 | throw new Error('Amiibo dump must be a Buffer'); 188 | } 189 | const size = amiiboDump.length; 190 | if (size !== NFC3D_AMIIBO_SIZE && size !== NTAG215_SIZE && size !== NTAG215_SIZE_ALT) { 191 | throw new Error(`Invalid Amiibo dump size: ${size} (expected ${NFC3D_AMIIBO_SIZE}, ${NTAG215_SIZE_ALT}, or ${NTAG215_SIZE})`); 192 | } 193 | const tag = amiiboDump.slice(0, NFC3D_AMIIBO_SIZE); 194 | const decrypted = decryptAmiibo(tag); 195 | 196 | // Extract only the first 92 bytes (the actual Mii data, without checksum) 197 | const miiData = decrypted.slice(MII_OFFSET_DECRYPTED, MII_OFFSET_DECRYPTED + 92); 198 | 199 | return Buffer.from(miiData); 200 | } 201 | 202 | //Insert Mii data into an Amiibo dump 203 | function insertMiiIntoAmiibo(amiiboDump, miiData) { 204 | if (!Buffer.isBuffer(amiiboDump)) { 205 | throw new Error('Amiibo dump must be a Buffer'); 206 | } 207 | if (!Buffer.isBuffer(miiData)) { 208 | throw new Error('Mii data must be a Buffer'); 209 | } 210 | const size = amiiboDump.length; 211 | if (size !== NFC3D_AMIIBO_SIZE && size !== NTAG215_SIZE && size !== NTAG215_SIZE_ALT) { 212 | throw new Error(`Invalid Amiibo dump size: ${size}`); 213 | } 214 | if (miiData.length !== 92 && miiData.length !== MII_SIZE) { 215 | throw new Error(`Mii data must be 92 or ${MII_SIZE} bytes, got ${miiData.length}`); 216 | } 217 | const tag = amiiboDump.slice(0, NFC3D_AMIIBO_SIZE); 218 | const decrypted = decryptAmiibo(tag); 219 | 220 | // Validate and fix Mii checksum, ensuring it's 96 bytes with correct checksum 221 | const miiWithChecksum = validateAndFixMiiChecksum(miiData); 222 | 223 | // Insert Mii data (96 bytes) 224 | miiWithChecksum.copy(decrypted, MII_OFFSET_DECRYPTED); 225 | 226 | const encrypted = encryptAmiibo(decrypted); 227 | const result = Buffer.alloc(size); 228 | encrypted.copy(result, 0); 229 | if (size > NFC3D_AMIIBO_SIZE) { 230 | amiiboDump.slice(NFC3D_AMIIBO_SIZE).copy(result, NFC3D_AMIIBO_SIZE); 231 | } 232 | 233 | return result; 234 | } 235 | 236 | module.exports = { 237 | insertMiiIntoAmiibo, 238 | extractMiiFromAmiibo 239 | }; -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | // @@@@@@@@@@@@@@@@@@@@@@%*%@##%#%%@@@@%%@@@@@@@@@@@%%%%%%%%%%%%%%%%%###*=-.-#=.:-#%%%####*##%%#*++**===++++++++=====+++++++++*++++===----=======------::.. 2 | // @@@@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@%%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@**#%%@@@@@@@@@@@@@@@@@@@@%@@@@@%*++#@@@@@#*++#%@@@@@@##%@@#+*%%%#=-+#@@@@%#==+#%%%###+: 3 | // @@@@@ @@ @@@ *@%@@@@ .@ :@ @+ :@@@ @@@- *%. %%. *# #@ @% #% *.. 4 | // @@@@@ @ . %@ %@%%%@ .@ % @+ :#+% @%@- #= + @. *# @ @ + -@ +:: 5 | // @@@@@ @@@# -+ #@ @ +%%@@@ .@ . %#@ @%@ %@%#+ .=:# #:*- #- @ @. ##### # @ @ :@ %-:::- 6 | // @@@@@ ..=* -+ =@ #*#*@@ .@ #+@ @@@ ..@+ .=:# * =: #* +@##%. ..@# @- #%**@ . .*=+ 7 | // @@@@@ * -+ =@ :@@@%%*@ .@ #+@ @@@ @+ .=:# * -: #@@ @%. @# @%# -%@ %#@ 8 | // @@%@@ **%* -+ =@ @ #@@@@@ .@ %=% @@@ =*+%+ .=:# + -: #@@@@= @. =**@# @%@@@. @ ***%%@ 9 | // @@%%@ @@@* -+ =@ @ +@@@@@ .@ = #+@ @@@ **+++ :+:# * -: #= @ @. #%#%% @ @ @ @%%@@@ 10 | // @@@%@ @@@@ -= %@ @ +*###@ .@ @ #+@ @@@ .--=+ -:@ -:=- #* @. @. .--+% # @: @ @ ---@@@ 11 | // @@@%@ @@@@. .@@ @ +%%#%@ .@ %# *+@ @@@ + @ - #@ @. % @ @@ +@ #@@ 12 | // @@@@@++++#@@%%%@#. .*@##@+=++*@++++%@@@@@++++#@++++@@*+++*+-#++++*@@#++++++++*%++++++=%=======+*+++=#%%@* .*@@@#+++++++*@+++#%%++++@%@@= .%@#%++++++++@@@ 13 | // @@@@@%@@@@%%%%%@#%@%##**###.-@@@@@@@@@@@@%%%##%%#*%@%%%%%#+=+@@@@@@@%******#%@%%%@@@%#+=--=-==+++*%#-+#*#%%%%%%%%%%%%%%%%%%%%%%%%%%%##%%#%###%%%#####@@@%#%%@@ 14 | // @@@@@@@@@@#*#%@%#%%###*+%%%=:%@@@@@@@@@@%%%#%%%###+#%%@%#*=+%@@@@@@@%**+*++**%%@@#@%%#*=-:+#@@@%%%%+-+#***##%%%%%%%%%%%%%%%%%%%%%%@@@%%%%%%%%%%%%%%%@@@@@#%%%@ 15 | // @@@@@@@@@@#*#%@%#%%###*+##%#-*@@@@%@@@@%#%%#**#%%%#-+%@%#==%@@@@@@@@%**++++++*####%##*+=:.*@@%@%*%#+=#%#*+*######%%%%%%%%%%%%%%%%%@@%%%%%%%%%%%%%@@@@@@@@#%%@@ 16 | // @@@@@@@@@#%%%%%%@@%#**#*%##%+-#%#=******++#%%%#*#@%#+#@%**%@@@@@@@@%%#***+======+**+*++=-.=+#**+=*+-=%@%#*+*****######%%%%%%%%%%%%@@%%#%%%%%%%%%@@@@@@@@@%%@@@ 17 | // %%@@%%%##*+*#%%%%%##*#%%%###+=%@@#+==+=%%###+*%%%+-==+@##+@@@@@@@%*%%%#***++=======++++=-.:-::::::--+@@@%%+=++***##%%%%%%%%%%%%%%%%%%@%#%%@%@%%@@@@@@@@@@@@@@@ 18 | // @%@@##@%##**%##%%%#**%@%####*+%%*+*.-+=@%%###**#%%######**@@@@@@@%#@%%%***++======+++++-:..:. .:.-#@@@%#*====++*#%%%%########%%@@%%@%##%@@@@@@@@@@@@@@@@@@@@ 19 | // @@%%%#####%%%+#@%%###%%%#**#**##*#**:-+#@@%%%%###%@@@%#+=*@@@@@@@@@@@@%****+++++**#**++=-: :-. .-%@@@@#*-::--=+*######*****###@%%%%%##%@%%%@%%@%@%@@@@@@@@@ 20 | // @@@%%%##%#*#***###%%%#%@@%%@@@%#%@%#+=*#%##%@@@@%####++--#@@@@@@@@@@@@%###****++=++%@@%*+===-*=::::*@@@@@#+:..:-==++***+++===+*###########@@@@%%%@@@@@@@@@@@@@ 21 | // @@@%%##%%%###%@%*#*#%#*%%@@@@%####%%%*##%**%%###%%#*+=:=%@@@@@@@@@@@@@@%###**##*###%@@@%#**=:-*+==+@@@@@##*=-:------====-----==*****##****####%%%%%@@@@@@@@@@@ 22 | // @@@%@@@@@%%#%%%%@%@##%%%%%@@@@@%%*#%@%%%#@@@@@#=:::==+#@@@@@@@@@@@@@@@@@%%%#%%%%@@@@@@%@##%%#+=++#@@@@@##*#+-=--::.::-:--:-::-=++********+**##%%%%%@@@@@%@@%@@ 23 | // @%@%%%%@#%@#%#%%#%#%%#%%@%##*#%####%@@%@@%%%###=+*%@@@@@@@@@@@@@@@@@@%@@@@@%@@@##%%%%%%%##%%%###%@@@@@%%%***=+=----=*+-:--=::+=++***+++++###%%%%%#%%%%%@@@@@@@ 24 | // @@@@@@@@@@%@%@@@%@%@@%@@%%%%#%*@#%@%%##%%%#%*+*%@@@@@#*@@@@@@@@@@@@@@@@@@@@@@@%####*+++==--+*%@@@@@@@@@@%%*++=+*+-=##=:-=+#%*%%**+*##*##+###%*%%@%#%%@@@%@@@@@ 25 | // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*%%@@@#%@@@@@@@@@@@@@@@@@@@@@@@@@@@%###%%##***+#@@@@@@@@%@@@%#*#%%%%#%%%@*%@@%@@@#%@@@@@%#@@@%*%#@@#@%@@@@@%@@#%@ 26 | // @@@@@@@@@@@@@@@@@@@@@@@@@@%@%@@@@@#@@@@@@@%#*##@@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@%%#*+**++#@@@@@@@@@@%@@%#%##@@@@%@@@@@@@@@@@@@@@@@%@@@@@@@@@%@@@@@@@@@@@@@@@@ 27 | // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#%%%@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*##**#@@@@@@%@%%@%@%%%##%%@@@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@ 28 | // @@@@@@@@%%%%%%%%%%########################*#**#@#####@@@@@@@@@@@@@%#@@@@@@@@@@@@@@@@%@%##@@@@@@@#@@@@@%%%%*##%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 29 | // ************+++++++++++++++++++++++===--=====%% .:-=*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*#@@@@@#%%**+=========+++=+++++++++++***##%%@@@@@@@@@@@@@@@@@@ 30 | // ##*#*#####********+++++++++++++++=====--+#%@@@@%= -: ::-+@@@@@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@%*#%@@@@%%**==--=----==-===-==========++*##%%%@@@@@@@@@@@@@@@@@ 31 | // #*###************+++*****++****++++=*%+:.:.:+#####%@@@@####%*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**#@@@@%*========-===-==-============+**##%%%%@@@@@@@@@@@@@@@@ 32 | // #*********#***++++++*****+++++++*@@#@#+#%%@@@@*-+##%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%##@@@@#++=====+====+==+=======+++==++***####%%%%%%%%%@@@@@@@@ 33 | // #*#****#******###******+++*++*@@%=@@##=.@@@%%%%@@%=+#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@%@*===+++++====+===+++++++=++++++++**#**##*####%%%%%%%%%%@ 34 | // #*###%#########***#*#*******#%%%%%@%%%@@#=-::--+#%%@@*+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#@%@%@@@@@@*+++=++++++++++++++=+++++++++++++++***+***#*############%% 35 | // %##########****#####***####@@@@@@@@@@@@@@@@@@%**#%%%@@@%+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#@@@@%@@@@@#===+==++++++++*++++++++++++*++++++++++********###********** 36 | // #**********#*************#@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@@+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*========+===+++**+++==++++*+++++++++++*********************# 37 | // ============================================================================================================================================================== 38 | // -------==========-:........#**@@@@@@@@@@@@@@@@@@@@@%#***#######%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%@@@@@%%%%%%%%%%%%%%##%%%%###########@@@@@@@@@@@@@@@@@@@@@@@@ 39 | // =-------===+++==--:..... -++*@@@@@@@@@@@@@@@@@@@@@@@@@@@%#####%%%%%%%%%%%%%%%%%%%%%%@%%@@@@@@@@@@@@@@@@%%%%%%%%%%%%%#%#########%%%%%%@@@@@@@@@@@@@@@@@%@@@@@ 40 | // =-------==+++===-::...... ..:-*@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%#%%%%%%%%%%%@@%%%%%%%%@@@@@@@@@@@@@@@%%%%%%#%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@%%%@@@@@@@@@@@@@@@@@ 41 | // +======++++++++==-:...:.... ::..*#*#%@@@%%@+@@@@@@@@@@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%#%%%%%%%%%%@@@@@@@@@@@%%%%%%%%%%%@@@@%%%%%%%%##%%%@@@@@%%%@@@@@@@@@@@@%@@@@ 42 | // +==-==+===+=====--:::::..:. .=.:##*+@@@%%@#@@@@@@@@@@@@@@@@@@@@%@%%%%%%%%%%#%#####*********##%%%%%%@@@@@@%%%@@%@%%%%@@@%%%%%%%%%%%@@@@@@@@%%%@@@@@@@@@@%@@@@@ 43 | // +=---====++++==---:::::::-: :..#%%*:@@%@@%@@@@@@@@@@@@@++*@##@@@%%############****+++====++*####%%%%%%%%%%%%%%@@@@@@@@%%%#%%@%@@@@@@@@@@@%@@@@@@@@@@@@%@@@@@ 44 | // +=------------:---:::--:::. **=@*#@@@@%@@@@@%%%%@@%%@%%%#*+%%%##########***+++==------===++***####%%%%%#%%%%@%%%%@@@%#%@@@@@@@@@@@@@@@@@@@@%@#@@@%%@@@@@@ 45 | // =----------::........... -+*%**@@@@@@@@@@@@@@@@@@@%%@+=%@@#*%########**++==-:.. .::-==++***##%%%%%*%%%%%@@@@@@@%#%%@@@@@@@@@@@@@@@@@%@@@@@@@%%@@@@@% 46 | // =-------:::::.............. *@#@@%#%@@@@@@@@@@@@@@@@@%%%+==@%%##%+*++++**+++=--:. ..:-==+#####*#*+####@@@@%%%%%%#%@@#@%%@%@@@@@@@@@@@@%@@@%#%*#@@%@ 47 | // ---::-:::::::.......:::......:-=#%@#%####%%%@@@@@@@@@@@@@@%@%++*@##**%##*+==--::. ..:-=++++++++=+****%%@#%%%%%%@@@@%%%@%@@%@@@@@%%%@%##%%%%@@@@%@ 48 | // -:::::::::::.......::...... :#%*#%%%#@##%%%@@%@@@@@@@@@@@%#%%++=-+========++==: .::-==---===-==++*#**#####%#%%%%%%%%%@@%@@@@@@%%@%%%###@##%@@@ 49 | // -:::::::..:...........::. .+#@@%%%%@%@%%#%#%@@@@@@@@@@@%%###==--=::..-.%%@%%*=:. .. :. . : :.:-++*+:-=---=+***%**###%#%%%%@%@@%%%%%%%%#%%%@#%@%%###%%%%# 50 | // -::::::..............:. -#@@@@%%%@%###%%##%%@@%%%%%%@@%***----=:-*#*=#%@#@+++++++++++===---:-:=--=*#*===*+-=+***#*=*##%%@%@@@@@%%%%##%@@@@@@%###*#%%%@@%*#% 51 | // -:::::..........:..... :+%@%%%@%#@@%@@#%%#%#%##*##**##*****###%######**##@*#######**+*++**+==++++*##*+#%#*+**+#*%%%%#%%%%#@#@%%@%%%@%##%#%@#%%@%%%#*#%@@@@% 52 | // -:::::............... .=#%@@@@%@#@%@@%%%@@%#%#%%%%@%%%%%@%@@@%@@%@@@%%#%@@%@%%@%@%%%%##%########*##%#%%#@@#%@%@@%%@@%@@@@@@@@@@@@@@%@#@%@@@@@@@%%%%%%%%@@@@ 53 | // -:..:::............ =%%@@@@%@%@@@@@@%%@@@%@%%@@@@@@@%%%@@@@%@%@@@@@%@%%%%@@%%@%%%%@%#%#%###**+*%%%#%%%@#%%%%@@%@@@@@@@%%@@@@@@@@%@%@@@@@@%%@@@@@@@@@@@@@ 54 | // -:..:::::......... .+*%%%%%%%%%%%##%%%%%%%%%%%%%%%%%@@@@@@@@@@@%%%%%%%%%%%%%%%%%%%%%%%####*-.=++=#%%%%*%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 55 | // -:.::::::......... ...:.:--=-:-:----:--------=++=+++++++++++++=+++=+=++==++++=+======-=-:*%@%%%#=++===+++++++++++++++++++++++++***###%%%%%%%%@@@@%%%@ 56 | // -:::::::::........ ....::--=-=-------------=------=============---===-------------=+===**#%##*+=-=====-=======--===-========++*####%%%%%%%%%%%%%%% 57 | // -:::::::::....... :::---------=======----=-------------------::------------------::-%@@#%@@@@%%##+:---=----==============++++***####%%%%%%%%%%%%%%%@ 58 | // -::::-:::::...... :-==+==================----------------------:----------------::=%@@@@@@@%@@@+-------------===+++++++++*****######%%%%%%%%#%%%%%%%% 59 | // -::::-::::... .. .--::-+==++--===-========-==---=-----=-------==----------::--:-@@@@@@@@@@@@@@%-++*=-----=======++++++******#######%#%#%%%%%%%%%%%%% 60 | // =-::---:::.. ... :===-===+=++======+++======--==++++===+===-==----=+=---:----=%@@@@@@@@@@@@@@@@@#++. --=-----=========+++++******####%#%%%%%%%%%%%%% 61 | // =--::---:::........ .-=+++===+*++==++==-==+*====++++=-----====+===-------:::--+@@@@@@@@@@@@@@@@@@@@*#%%+:--+------===-===-====+++++***######%%%%%%%%%@@ 62 | // =---------:::::::.. .:-+*========+==+++==++==++++++=++**=---=--=------------*@@@@@@@@@@@@@@@@@@@@%@%%%##::::--=-=++======---====+++***#####%%%%%%%@@@ 63 | // ----:::::--:::::::.. :======+++#*****++****+=====+=---=---------=:-===+%@@@@@@@@@@@@@@@@@@@@@@@#%@@%%%#+:-::-----------==--=-====+++++**###%%%@@@@@@ 64 | // -:--:-::---------::.... .-==++======+**+*+++++==========----::::--=---=+#@@@@@@@@@@@@@@@@@@@@@@%@=-+#@@#-:::::------=+=-----==-======++++**##%%%%##%@ 65 | // =--:::------------:..::.. :+#%#***+++++++++++===== .-----=++=-:-=--=---#@@@@@@@@@@@@@@@@@@%@@@@-::-+#=:-::::----:==-==---========+*++=++++*##%%*##%@ 66 | // +====----==---==---:.:::::. . .-=++++==++++++=========++--=-=+**++=========%@@@@@@@@@@@@@@@@@@@@@@%@@@@#%+----=--===-===-=---=-=-====+++++*++***#%#*###% 67 | // +++++++==========--:.::---:... .:=-==+==+++++++==+**+-==-=====*++==+====+@@@@@@@@@@@@@@@@@@@@@@@@@@@@#=-==-+===+=====+++=-=====--==-=++++*++++**#%@@@% 68 | // *++++++++==--===----:::----:::. :===+++**+*+==+++++=====+=++*##**#*+#@@@@@@@@@@@@@@@@@@*%@@@*=====#*++==---===+=+*++====+=====-=-==+=+++***#%@@@@@ 69 | // **++++++++=======----::---==--:.. :==+++==+*#*+++++++##***##**#*==+@@@@@@@@@@@@@@@@@@%@@@@@@=--===+*+++**========+++==++-==++=====++==+**#%@@@@@@ 70 | // ***+++++++=-=====-----:--====--:.... .-==+***++******#%%%#*++++++**+*@@@@@@@@@@@@@@@@@@@@@@@@@%=========++++++==+===++++++++++=======+=++*#%@@@@@@@ 71 | // +++++++*+===-=====----:-=++==--:..... ......... .-****++*##**+*#%%%##**##*#*++*@@@@@@@@@@@@@@@@@@@@@@@@@@#**++=+++++++**++++++=+*++=+=*##+==+===++*#@@@@@@@@@ 72 | // +....: ..-:. . .===-----+++===-:.......:::::::::....-+********%%%###*#####%%%@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@**+*+==+**++*++++++*+==++=+=++++*=+=--=*#%@@@@@@@@@ 73 | // +=++.-==:==-:--======--=*+==+=--::::.:---------::::::-+**#%%%%%%##**##*****+**#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@++++=+++*+++=+#*++*++*++*+*+++++#*++#%%%%@@@@@@@@@ 74 | 75 | 76 | // Wii miis 77 | export type WiiMii = { 78 | general: { 79 | gender: number; 80 | birthMonth: number; 81 | birthday: number; 82 | favoriteColor: number; 83 | height: number; 84 | weight: number; 85 | }; 86 | meta: { 87 | name: string; 88 | creatorName: string; 89 | }; 90 | perms: { 91 | mingle: boolean; 92 | fromCheckMiiOut: boolean; 93 | }; 94 | face: { 95 | type: number; 96 | color: number; 97 | feature: number; 98 | }; 99 | hair: { 100 | page: string; 101 | type: string; 102 | color: number; 103 | flipped: boolean; 104 | }; 105 | eyebrows: { 106 | page: string; 107 | type: string; 108 | rotation: number; 109 | color: number; 110 | size: number; 111 | yPosition: number; 112 | distanceApart: number; 113 | }; 114 | eyes: { 115 | page: string; 116 | type: string; 117 | rotation: number; 118 | yPosition: number; 119 | color: number; 120 | size: number; 121 | distanceApart: number; 122 | }; 123 | nose: { 124 | type: string; 125 | size: number; 126 | yPosition: number; 127 | }; 128 | mouth: { 129 | page: string; 130 | type: string; 131 | color: number; 132 | size: number; 133 | yPosition: number; 134 | }; 135 | glasses: { 136 | type: number; 137 | color: number; 138 | size: number; 139 | yPosition: number; 140 | }; 141 | beard: { 142 | mustache: { 143 | type: number; 144 | size: number; 145 | yPosition: number; 146 | }; 147 | type: number; 148 | color: number; 149 | }; 150 | mole: { 151 | on: boolean; 152 | size: number; 153 | yPosition: number; 154 | xPosition: number; 155 | }; 156 | }; 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiiJS 2 | MiiJS is a complete and comprehensive Mii library for reading, converting, modifying, writing, and rendering Mii characters from an accessible coding language. Support for all Mii types, including DS, Wii, 3DS, Wii U, Amiibo, Switch 1 & 2, Amiibos, and Mii Studio. Capable of making Special Miis and 3DS QR codes. Able to generate instructions to recreate Miis from scratch. 3 |
4 | 5 | ## Installation 6 | `npm install miijs` || `npm i miijs` 7 | 8 |
9 | 10 | ## Table of Contents 11 | - [Functions](#functions) 12 | - [Code Examples](#code-examples) 13 | - [Special Miis](#special-miis) 14 | - [Other Console Support](#other-console-support) 15 | - [`convertMii` Discrepancies](#discrepancies-in-convertmii-function) 16 | - [Transferring to/from the System](#transferring-miis-to-and-from-the-system) 17 | - [FFLResHigh.dat](#fflreshighdat) 18 | - [Credits](#credits) 19 | 20 |
21 | 22 | # Functions 23 | 24 | ### Reading Miis 25 | - **`async read3DSQR(PathToMiiQR OR BinaryDataFromQR, ReturnDecryptedBin?)`** - Returns JSON by default. By specifying `true` as the secondary parameter you can receive only the decrypted Mii data from the QR. 26 | - **`readWiiBin(PathToMii OR BinaryMiiData)`** - Returns JSON from a Wii Mii binary file. 27 | 28 | ### Writing Miis 29 | - **`async write3DSQR(MiiJSON, PathToWriteTo, fflRes?)`** - Writes a JPG QR of a 3DS scannable Mii to the path specified. If no fflRes is specified, the QR will render using Nintendo Studio's API. If one is provided, it will contain a locally rendered version. fflRes must either be passed as a buffer, or FFLResHigh.dat present in your project's root directory. 30 | - **`async writeWiiBin(MiiJSON, PathToWriteTo?)`** - Returns Mii binary which can then be written by default. If PathToWriteTo is specified, it will instead be written to a file. 31 | 32 | ### Converting Miis 33 | - **`convertMii(miiJson, typeTo?)`** - Converts the Mii JSON format between consoles (3DS ↔ Wii) and returns the JSON. If typeTo is not specified, converts to the opposite type. 34 | - **`convertMiiToStudio(miiJSON)`** - Returns a Studio compatible Mii in hex format. 35 | - **`convertStudioToMii(input)`** - Converts Studio format (hex string or Uint8Array) to 3DS Mii JSON. 36 | 37 | ### Rendering Miis 38 | - **`async renderMiiWithStudio(miiJSON)`** - Returns a buffer containing a PNG representation of the Mii's face using Nintendo's Studio API. 39 | - **`async renderMii(miiJSON, fflRes?)`** - Returns a buffer containing a PNG representation of the Mii's face using local rendering. fflRes must either be passed as a buffer, or FFLResHigh.dat present in your project's root directory. Currently bodies render but are unaffected by height and weight changes, though this is planned to be changed in the future. 40 | 41 | ### Amiibo Functions 42 | - **`insertMiiIntoAmiibo(amiiboDump, miiData)`** - Inserts Mii data (92 or 96 bytes, decrypted 3DS format) into an Amiibo dump. Returns the modified Amiibo dump. 43 | - **`extractMiiFromAmiibo(amiiboDump)`** - Extracts the Mii data (92 bytes, decrypted 3DS format) from an Amiibo dump. Returns a Buffer. 44 | 45 | ### Utility Functions 46 | - **`generateInstructions(miiJson, fullInstructions?)`** - Returns a JSON object of different instruction fields for manually recreating the Mii. If fullInstructions is not set, only the instructions that differ from a default Mii will be returned. 47 | - **`miiHeightToFeetInches(value)`** - Converts Mii height value (0-127) to real-world feet and inches. Returns `{feet, inches, totalInches}`. 48 | - **`inchesToMiiHeight(totalInches)`** - Converts real-world height in inches to Mii height value (0-127). 49 | - **`heightWeightToMiiWeight(heightInches, weightLbs)`** - Converts real-world height and weight to Mii weight value (0-127). **EXPERIMENTAL** 50 | - **`miiWeightToRealWeight(heightInches, miiWeight)`** - Converts Mii weight value to real-world pounds and BMI. Returns `{pounds, bmi}`. **EXPERIMENTAL** 51 | 52 |
53 | 54 | # Code Examples 55 | 56 | ## Reading a 3DS Mii from QR Code 57 | ```javascript 58 | const miijs = require('miijs'); 59 | 60 | // Read from file path 61 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 62 | console.log('Mii Name:', miiJson.meta.name); 63 | console.log('Favorite Color:', miiJson.general.favoriteColor); 64 | 65 | // Or get just the decrypted binary data 66 | const decryptedBin = await miijs.read3DSQR('./example3DSQR.jpg', true); 67 | console.log('Decrypted binary length:', decryptedBin.length); 68 | ``` 69 | 70 | ## Reading a Wii Mii from Binary File 71 | ```javascript 72 | const miijs = require('miijs'); 73 | 74 | // Read from file path 75 | const miiJson = await miijs.readWiiBin('./exampleWii.bin'); 76 | console.log('Mii Name:', miiJson.meta.name); 77 | console.log('Gender:', miiJson.general.gender === 0 ? 'Male' : 'Female'); 78 | 79 | // Or pass binary data directly 80 | const fs = require('fs'); 81 | const binaryData = fs.readFileSync('./exampleWii.bin'); 82 | const miiJson2 = await miijs.readWiiBin(binaryData); 83 | ``` 84 | 85 | ## Writing a 3DS Mii QR Code 86 | ```javascript 87 | const miijs = require('miijs'); 88 | 89 | // First, read or create a Mii JSON 90 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 91 | 92 | // Write QR code with Studio rendering (no FFLResHigh.dat needed) 93 | await miijs.write3DSQR(miiJson, './output_qr.jpg'); 94 | 95 | // Or with local rendering (requires FFLResHigh.dat in project root or passed as buffer) 96 | const fs = require('fs'); 97 | const fflRes = fs.readFileSync('./FFLResHigh.dat'); 98 | await miijs.write3DSQR(miiJson, './output_qr_local.jpg', fflRes); 99 | ``` 100 | 101 | ## Writing a Wii Mii Binary 102 | ```javascript 103 | const miijs = require('miijs'); 104 | const fs = require('fs'); 105 | 106 | // Read a Mii (from any format) 107 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 108 | 109 | // Convert to Wii format first if needed 110 | const wiiMii = miijs.convertMii(miiJson, 'wii'); 111 | 112 | // Write to file 113 | await miijs.writeWiiBin(wiiMii, './output_wii.bin'); 114 | 115 | // Or get buffer without writing 116 | const buffer = await miijs.writeWiiBin(wiiMii); 117 | fs.writeFileSync('./manual_write.bin', buffer); 118 | ``` 119 | 120 | ## Converting Between Formats 121 | ```javascript 122 | const miijs = require('miijs'); 123 | 124 | // Read a 3DS Mii 125 | const ds3Mii = await miijs.read3DSQR('./example3DSQR.jpg'); 126 | 127 | // Convert to Wii format 128 | const wiiMii = miijs.convertMii(ds3Mii, 'wii'); 129 | 130 | // Convert back to 3DS 131 | const backTo3DS = miijs.convertMii(wiiMii, '3ds'); 132 | 133 | // Auto-detect and convert to opposite 134 | const autoConverted = miijs.convertMii(ds3Mii); 135 | ``` 136 | 137 | ## Converting to/from Studio Format 138 | ```javascript 139 | const miijs = require('miijs'); 140 | 141 | // Read a Mii and convert to Studio format 142 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 143 | const studioHex = miijs.convertMiiToStudio(miiJson); 144 | console.log('Studio URL:', `https://studio.mii.nintendo.com/miis/image.png?data=${studioHex}`); 145 | 146 | // Convert Studio format back to JSON 147 | const studioData = '000d142a303f434b717a7b84939ba6b2bbbec5cbc9d0e2ea...'; 148 | const miiFromStudio = miijs.convertStudioToMii(studioData); 149 | console.log('Converted Mii:', miiFromStudio.meta.name); 150 | ``` 151 | 152 | ## Rendering Miis 153 | ```javascript 154 | const miijs = require('miijs'); 155 | const fs = require('fs'); 156 | 157 | // Read a Mii 158 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 159 | 160 | // Render using Studio API (simple, no setup needed) 161 | const studioPng = await miijs.renderMiiWithStudio(miiJson); 162 | fs.writeFileSync('./mii_studio_render.png', studioPng); 163 | 164 | // Render locally with full body (requires FFLResHigh.dat) 165 | const fflRes = fs.readFileSync('./FFLResHigh.dat'); 166 | const localPng = await miijs.renderMii(miiJson, fflRes); 167 | fs.writeFileSync('./mii_local_render.png', localPng); 168 | 169 | // Shirt color comes from miiJson.general.favoriteColor 170 | ``` 171 | 172 | ## Working with Amiibos 173 | ```javascript 174 | const miijs = require('miijs'); 175 | const fs = require('fs'); 176 | 177 | // Read an Amiibo dump 178 | const amiiboDump = fs.readFileSync('./exampleAmiiboDump.bin'); 179 | 180 | // Extract the Mii from the Amiibo (returns 92 bytes decrypted) 181 | const miiData = miijs.extractMiiFromAmiibo(amiiboDump); 182 | 183 | // Convert the raw Mii data to readable JSON 184 | // (miiData is already decrypted 3DS format) 185 | const miiJson = miijs.decode3DSMii(miiData); // Note: decode3DSMii not exported, use read3DSQR workflow 186 | 187 | // Better workflow: Read from QR, get decrypted data, insert into Amiibo 188 | const qrMiiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 189 | const decryptedMiiData = await miijs.read3DSQR('./example3DSQR.jpg', true); 190 | 191 | // Insert new Mii into Amiibo 192 | const modifiedAmiibo = miijs.insertMiiIntoAmiibo(amiiboDump, decryptedMiiData); 193 | fs.writeFileSync('./modified_amiibo.bin', modifiedAmiibo); 194 | ``` 195 | 196 | ## Generating Recreation Instructions 197 | ```javascript 198 | const miijs = require('miijs'); 199 | 200 | // Read a Mii 201 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 202 | 203 | // Generate only non-default instructions (minimal) 204 | const minimalInstructions = miijs.generateInstructions(miiJson); 205 | console.log('Steps to recreate:'); 206 | Object.values(minimalInstructions).forEach(step => { 207 | if (step) console.log('- ' + step); 208 | }); 209 | 210 | // Generate complete instructions (every step) 211 | const fullInstructions = miijs.generateInstructions(miiJson, true); 212 | console.log('\nComplete recreation guide:'); 213 | Object.entries(fullInstructions).forEach(([field, instruction]) => { 214 | console.log(`${field}: ${instruction}`); 215 | }); 216 | ``` 217 | 218 | ## Height and Weight Conversions 219 | ```javascript 220 | const miijs = require('miijs'); 221 | 222 | // Convert Mii height (0-127) to feet/inches 223 | const heightInfo = miijs.miiHeightToFeetInches(64); // midpoint value 224 | console.log(`Height: ${heightInfo.feet}'${heightInfo.inches}" (${heightInfo.totalInches} inches)`); 225 | 226 | // Convert real height to Mii value 227 | const miiHeightValue = miijs.inchesToMiiHeight(72); // 6'0" 228 | console.log('Mii height value for 6\'0":', miiHeightValue); 229 | 230 | // EXPERIMENTAL: Convert real weight to Mii weight 231 | const heightInches = 69; // 5'9" 232 | const weightLbs = 160; 233 | const miiWeightValue = miijs.heightWeightToMiiWeight(heightInches, weightLbs); 234 | console.log('Mii weight value:', miiWeightValue); 235 | 236 | // EXPERIMENTAL: Convert Mii weight to real weight 237 | const weightInfo = miijs.miiWeightToRealWeight(heightInches, 64); 238 | console.log(`Weight: ${weightInfo.pounds.toFixed(1)} lbs, BMI: ${weightInfo.bmi.toFixed(1)}`); 239 | ``` 240 | 241 | ## Creating and Modifying a Mii 242 | ```javascript 243 | const miijs = require('miijs'); 244 | 245 | // Read an existing Mii 246 | const miiJson = await miijs.read3DSQR('./example3DSQR.jpg'); 247 | 248 | // Modify properties 249 | miiJson.meta.name = 'Custom Name'; 250 | miiJson.general.favoriteColor = 5; // Blue 251 | miiJson.hair.color = 0; // Black 252 | miiJson.eyes.color = 2; // Brown 253 | 254 | // Make it a Special Mii (3DS only) 255 | miiJson.meta.type = 'Special'; 256 | 257 | // Convert to Wii format 258 | const wiiVersion = miijs.convertMii(miiJson, 'wii'); 259 | 260 | // Save as both formats 261 | await miijs.write3DSQR(miiJson, './modified_3ds.jpg'); 262 | await miijs.writeWiiBin(wiiVersion, './modified_wii.bin'); 263 | ``` 264 | 265 | ## Complete Workflow Example 266 | ```javascript 267 | const miijs = require('miijs'); 268 | const fs = require('fs'); 269 | 270 | async function processMyMii() { 271 | // 1. Read from QR code 272 | const mii = await miijs.read3DSQR('./example3DSQR.jpg'); 273 | console.log('Loaded:', mii.meta.name); 274 | 275 | // 2. Customize the Mii 276 | mii.general.favoriteColor = 0; // Red 277 | mii.general.height = miijs.inchesToMiiHeight(66); // 5'6" 278 | 279 | // 3. Render it 280 | const renderBuffer = await miijs.renderMiiWithStudio(mii); 281 | fs.writeFileSync('./my_mii_face.png', renderBuffer); 282 | 283 | // 4. Generate recreation instructions 284 | const instructions = miijs.generateInstructions(mii); 285 | console.log('\nRecreation steps:', instructions); 286 | 287 | // 5. Export to multiple formats 288 | await miijs.write3DSQR(mii, './my_mii_qr.jpg'); 289 | 290 | const wiiMii = miijs.convertMii(mii, 'wii'); 291 | await miijs.writeWiiBin(wiiMii, './my_mii_wii.bin'); 292 | 293 | const studioCode = miijs.convertMiiToStudio(mii); 294 | console.log('\nStudio URL:', `https://studio.mii.nintendo.com/miis/image.png?data=${studioCode}`); 295 | 296 | console.log('\nDone! All formats exported.'); 297 | } 298 | 299 | processMyMii().catch(console.error); 300 | ``` 301 | 302 |
303 | 304 | ## Special Miis 305 | Special Miis were on the Wii and 3DS, identifiable via their golden pants. They were created by Nintendo employees, and not consumers. They could not be edited, or copied. In every other instance transferring a Mii to another system would leave a copy on both systems. For Special Miis, they would delete themselves from the console sending them, and only ever be present in one place at a time per copy Nintendo sent out. When receiving them via QR code on the 3DS, it would only allow you to scan that QR once, and never again. On the Wii, these were distributed via the WiiConnect24 service, and would arrive via the Message Board. On the 3DS, these were distributed occasionally via Spotpass, Streetpass, and QR codes. 306 | ### Making a Special Mii 307 | To make a special Mii, read in the file using the appropriate function, set `mii.info.type="Special";`, and then write a new file with the appropriate function. 308 | -# Special Miis only work on the Wii and 3DS, and no other console. 309 | 310 |
311 | 312 | ## Other Console Support 313 | - DS 314 | - DS and Wii Miis are interchangeable. The DS only contains Miis in a handful of games, and is not baked into the system, however every instance where it does it is based off the Wii version of Miis, and to my current knowledge always provides a way to transfer from the Wii, being the only way short of recreation to transfer onto the DS. There is, to my knowledge, no way to transfer Miis off of the DS short of recreation. 315 | - Use Wii functions for DS Miis 316 | - Wii U 317 | - The Wii U and 3DS Miis are interchangeable, with one major exception. The 3DS has Special Miis, while the Wii U will not render any Mii set as a Special Mii. So since the 3DS has this one added feature, 3DS is what takes priority in the naming schemes across this project, however it is for all intents and purposes interchangeable with a Wii U Mii. 318 | - Use 3DS functions for Wii U Miis 319 | - Switch/2 320 | - Miis are more isolated than they've ever been on the Switch/2. To take them on and off of the Switch/2 via direct transfer, an Amiibo _and_ one of, a 3DS with NFC Reader accessory, New 3DS, or Wii U, is **required**. The only other method is to recreate manually from scratch. When the Switch writes to an Amiibo, it converts it to a 3DS/Wii U format. Due to this limitation of direct transfer, all Miis that this library can affect will be going through the 3DS/Wii U anyway, and direct Switch/2 support is thus irrelevant. The only differences between Switch Miis and Wii U Miis (no Special Mii support on the Switch either) is a ton more hair colors anyway. 321 | - Use 3DS, Studio, and Amiibo functions for Switch/2 Miis 322 | - Studio 323 | - Studio Miis are in essence Switch/2 Miis. Transferring directly on/off of Studio (a browser Mii Maker used purely for profile pictures across Nintendo's online logins) requires a developer console and code paste, or browser extension. I may undertake making my own version of this in the future, but for the time being [this tool](https://mii.tools/studioloader/) by HEYimHeroic serves this purpose (from what I can tell, I have not used it myself). 324 | - Use Studio Functions for Studio Miis 325 | - Miitomo/Kaerutomo and Tomodachi Life 326 | - Both Mii formats are the same as 3DS formats, with extra info added to the end. The way the library is set up, it can already read these. My devices are too new for Kaerutomo support, but I believe it should be able to scan the 3DS format Miis. Writing specific to Tomodachi Life Miis with game data already present in the QR is more within the realm of a Tomodachi Life save editor. I may undertake this for the Miis in the future, but it would be a separate project. 327 | - Use 3DS functions for these Miis 328 | 329 |
330 | 331 | ## Discrepancies in `convertMii` function 332 | All of these discrepancies __only__ apply when converting from the **3DS to the Wii**, converting from the Wii to the 3DS should be a perfect conversion. 333 | There is a reason that the Wii supports sending Miis to the 3DS, but not vice versa. Many of the fields on the 3DS are new, and not present on the Wii. This function does its absolute best to backport 3DS Miis, but it *is not perfect and never will be*. If you rely heavily on 3DS exclusive options in your Mii, the outputted Mii will likely not be satisfactory. 334 | - The 3DS has four more face shapes, thus some are converted to the closest possible for the Wii. 335 | - The 3DS allows you to set Makeup and Wrinkles seperately, as well as having 7 more "makeup" (including beard shadow and freckles) types and 5 more wrinkle types. This is probably one of the messiest conversions since one field has to be ignored entirely if both are set. Since the 3DS has some that are not even close to anything the Wii has, it will ignore these if the other field is set, allowing for the other field to be added in its place, prioritizing wrinkles over makeup. The outputted Mii will almost certainly require further editing to be satisfactory if these fields are used. 336 | - The 3DS has 6 extra nose types, all on the second page - these are mapped to similar noses on the first page that the Wii has. 337 | - The 3DS has an extra page of mouth types containing 12 extra mouth types. These are mapped to similar mouths on the other two pages that the Wii supports. 338 | - The 3DS has two extra lip colors. These are changed into the default Orangey lip color if used since both of the extra colors are closest to this. 339 | - The Wii does not have the option to "squish" parts to be thinner. This function ignores this field as a result. 340 | - The 3DS has 60 extra hairstyles. These are mapped to hairstyles the Wii does have. This will not be a perfect conversion and has a decent chance of needing a manual change. 341 | - The 3DS has an extra page of eye types that the Wii does not, which the function maps to a similar eye type that the Wii does support if used. Will likely require a manual edit. 342 | - The 3DS has two extra mustaches and two extra beards. These are mapped to a similar beard or mustache if used - the two extra beards will likely need a manual change if used. 343 | 344 |
345 | 346 | # Transferring Miis to and from the System 347 | - DS 348 | - If the game you would like to transfer Miis to supports it, the option to "Connect to Wii" will be found in various places and worded different ways. The main game you might want to do this for is Tomodachi Collection, which will be in Town Hall after three Mii residents are on the island. On the Wii, you then want to press the DS icon in the top right and follow the prompts from there. If the option is not present, press and _release_ **A**, press and release **B**, press and release **1**, and then press and _hold_ **2**. The option should then be visible. This option is not available on Wii U or the Wii mode of the Wii U, and can only be used to send Miis to DS and 3DS, not from. No option to retrieve Miis from the DS is available besides recreating the Mii. 349 | - Wii 350 | - Method 1 (Recommended, doesn't require homebrew): Connect the Wiimote to your PC, Dolphin seems to be the easiest way to do so though there are some more difficult ways to do so, and use [WDMLMiiTransfer](https://sourceforge.net/projects/wdml/files/WDML%20-%20MiiTransfer/). Open the `readSlotX.bat` file for the slot you're trying to read from (Array notation, 0=1, 1=2, 2=3, and so on). The Mii will be in the same directory under the name `miiX.mii`, where X is the same number as the readSlot you opened. If you used `readSlotAll.bat`, then there will be 10 Miis (0-9) in the directory. Note that if no Mii was ever present in that slot ever on the Wiimote, it will still output a `miiX.mii` file, though it will not contain the Mii data correctly. To write to the Wiimote, make sure the Mii you're writing is in the same directory and named `miiX.mii`, where X is the slot you're writing to, and open `writeSlotX.bat`, where X is the slot you're writing to (in array notation). You can transfer Miis on and off the Wiimote from the Wii by using the Wiimote icon in the top right of Mii Maker. 351 | - Method 2 (Requires Homebrew, is untested by me): [Mii Installer](https://wiibrew.org/wiki/Mii_Installer) for writing from the SD card to the Wii, and [Mii Extractor](https://wiibrew.org/wiki/Mii_Extractor) for reading from the Wii. 352 | - 3DS and Wii U 353 | - Open Mii Maker, select "QR Code/Image Options", and then select the respective QR Code option, be it scanning a QR code or saving a Mii as a QR code. 354 | - Amiibo 355 | - You can use [Tagmo](https://play.google.com/store/apps/details?id=com.hiddenramblings.tagmo.eightbit&hl=en_US&pli=1) on Android, bottom right NFC button -> Backup to retrieve an Amiibo file, or Amiibo bin in explorer -> Write: first write to blank NTAG215 tag OR Update: subsequent writes to already-an-Amiibo tags. _Reportedly_ [one of these apps](https://www.reddit.com/r/tagmo/comments/ynxonu/list_of_ios_iphone_amiibo_apps/) can be used for an equivalent on iPhone. I have not tested and cannot verify any of the iPhone apps at this time. 356 | - Switch/2 357 | - You have to use Amiibos as a conduit to interact with Miis on the Switch/2. To take these Miis on and off of the Switch, in System Settings under the Amiibo menu you can register or change the Owner Mii to set the Mii stored on the Amiibo, and under Miis you can select Create a Mii and then Copy from Amiibo to take a Mii from the Amiibo onto the Switch. 358 | 359 | If you are unable to transfer to the console you wish to, you can use the `generateInstructions` function provided here and manually recreate the Mii on the console using the provided instructions. 360 | 361 |
362 | 363 | ## FFLResHigh.dat 364 | FFLResHigh.dat provides the necessary models and textures to build a 3D model of the Mii. This will not be provided by the library but can be provided by placing it in the directory of the project calling MiiJS. By providing FFLResHigh.dat, you can then render Miis locally without using Studio. If you do not have or do not provide FFLResHigh.dat, rendering is still available via Studio. 365 | ### Finding FFLResHigh.dat 366 | Any version of AFLResHigh.dat will work as well, renamed to FFLResHigh.dat. 367 | You can find FFLResHigh using a Wii U with an FTP program installed at `sys/title/0005001b/10056000/content/FFLResHigh.dat`. From a Miitomo install, it can be found in the cache at `res/asset/model/character/mii/AFLResHigh_2_3.dat`. 368 | 369 |
370 | 371 | # Credits 372 | - **[kazuki-4ys' MiiInfoEditorCTR](https://github.com/kazuki-4ys/kazuki-4ys.github.io/tree/master/web_apps/MiiInfoEditorCTR)** - I repurposed how to decrypt and reencrypt the QR codes from here, including repurposing the asmCrypto.js file in its entirety with very small modifications (it has since been stripped down to only include the functions this library uses). I believe I also modified the code for rendering the Mii using Nintendo's Mii Studio from here as well, though I do not remember for certain. 373 | - **[ariankordi's FFL.js](https://github.com/ariankordi/FFL.js/)** - Rendering Miis locally would not be possible without this library. Instructions for finding FFLResHigh are also learned from [ariankordi's FFL-Testing repository](https://github.com/ariankordi/FFL-Testing). 374 | - **[Models Resource](https://models.spriters-resource.com/3ds/systembios/asset/306260/)** - For the bodies used in Mii rendering 375 | - **[socram8888's Amiitools](https://github.com/socram8888/amiitool)** - I _think_, for the code reverse engineered to help with aspects of Amiibo dump processing. I went through so many iterations in research and coding, there may be other credits due as well but I _think_ this was the only repo actually used for the reverse engineering in the final working code. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //Imports 2 | const fs = require('fs'); 3 | const nodeCanvas = require('canvas'); 4 | const { createCanvas, loadImage, ImageData } = nodeCanvas; 5 | const jsQR = require('jsqr'); 6 | const Jimp = require('jimp'); 7 | const THREE = require('three'); 8 | var GLTFLoader = null; 9 | const QRCodeStyling = require("qr-code-styling"); 10 | const { JSDOM } = require("jsdom"); 11 | const httpsLib = require('https'); 12 | const asmCrypto = require("./asmCrypto.js"); 13 | const path = require("path"); 14 | const createGL = require('gl'); 15 | 16 | const { 17 | createCharModel, initCharModelTextures, 18 | initializeFFL, exitFFL, parseHexOrB64ToUint8Array, 19 | setIsWebGL1State, getCameraForViewType, ViewType 20 | } = require("./fflWrapper.js"); 21 | const ModuleFFL = require("ffl.js/examples/ffl-emscripten-single-file.js"); 22 | const FFLShaderMaterial = require("ffl.js/FFLShaderMaterial.js"); 23 | 24 | // Typedefs for intellisence 25 | /** @typedef {import('./types').WiiMii} WiiMii */ 26 | 27 | //Miscellaneous Tables 28 | const lookupTables = { 29 | //Universals 30 | favCols: ["Red", "Orange", "Yellow", "Lime", "Green", "Blue", "Cyan", "Pink", "Purple", "Brown", "White", "Black"], 31 | skinCols: ["White", "Tanned White", "Darker White", "Tanned Darker", "Mostly Black", "Black"], 32 | hairCols: ["Black", "Brown", "Red", "Reddish Brown", "Grey", "Light Brown", "Dark Blonde", "Blonde"], 33 | eyeCols: ["Black", "Grey", "Brown", "Lime", "Blue", "Green"], 34 | 35 | //Wii fields 36 | wiiFaceFeatures: ["None", "Blush", "Makeup and Blush", "Freckles", "Bags", "Wrinkles on Cheeks", "Wrinkles near Eyes", "Chin Wrinkle", "Makeup", "Stubble", "Wrinkles near Mouth", "Wrinkles"], 37 | wiiMouthColors: ["Peach", "Red", "Pink"], 38 | wiiGlassesCols: ["Grey", "Brown", "Red", "Blue", "Yellow", "White"], 39 | wiiNoses: { 40 | "1": 0, 41 | "10": 1, 42 | "2": 2, 43 | "3": 3, 44 | "6": 4, 45 | "0": 5, 46 | "5": 6, 47 | "4": 7, 48 | "8": 8, 49 | "9": 9, 50 | "7": 10, 51 | }, 52 | pages: { 53 | mouths: { 54 | '0': 1, 55 | '1': 1, 56 | '2': 2, 57 | '3': 2, 58 | '4': 2, 59 | '5': 1, 60 | '6': 1, 61 | '7': 2, 62 | '8': 1, 63 | '9': 2, 64 | '10': 1, 65 | '11': 2, 66 | '12': 2, 67 | '13': 1, 68 | '14': 2, 69 | '15': 2, 70 | '16': 1, 71 | '17': 2, 72 | '18': 2, 73 | '19': 1, 74 | '20': 2, 75 | '21': 1, 76 | '22': 1, 77 | '23': 1 78 | }, 79 | eyebrows: { 80 | '0': 1, 81 | '1': 1, 82 | '2': 2, 83 | '3': 2, 84 | '4': 1, 85 | '5': 1, 86 | '6': 1, 87 | '7': 1, 88 | '8': 1, 89 | '9': 1, 90 | '10': 2, 91 | '11': 2, 92 | '12': 1, 93 | '13': 2, 94 | '14': 2, 95 | '15': 2, 96 | '16': 2, 97 | '17': 1, 98 | '18': 2, 99 | '19': 1, 100 | '20': 2, 101 | '21': 1, 102 | '22': 2, 103 | '23': 2 104 | }, 105 | eyes: { 106 | 0: 1, 107 | 1: 1, 108 | 2: 1, 109 | 3: 4, 110 | 4: 1, 111 | 5: 3, 112 | 6: 3, 113 | 7: 4, 114 | 8: 1, 115 | 9: 2, 116 | 10: 4, 117 | 11: 2, 118 | 12: 2, 119 | 13: 3, 120 | 14: 4, 121 | 15: 1, 122 | 16: 1, 123 | 17: 1, 124 | 18: 3, 125 | 19: 2, 126 | 20: 1, 127 | 21: 2, 128 | 22: 4, 129 | 23: 2, 130 | 24: 3, 131 | 25: 2, 132 | 26: 1, 133 | 27: 1, 134 | 28: 3, 135 | 29: 4, 136 | 30: 3, 137 | 31: 3, 138 | 32: 2, 139 | 33: 2, 140 | 34: 2, 141 | 35: 2, 142 | 36: 3, 143 | 37: 3, 144 | 38: 4, 145 | 39: 1, 146 | 40: 2, 147 | 41: 3, 148 | 42: 4, 149 | 43: 4, 150 | 44: 4, 151 | 45: 4, 152 | 46: 3, 153 | 47: 4 154 | }, 155 | hairs: { 156 | '0': 5, 157 | '1': 4, 158 | '2': 6, 159 | '3': 5, 160 | '4': 4, 161 | '5': 4, 162 | '6': 5, 163 | '7': 4, 164 | '8': 4, 165 | '9': 6, 166 | '10': 5, 167 | '11': 5, 168 | '12': 4, 169 | '13': 4, 170 | '14': 5, 171 | '15': 6, 172 | '16': 6, 173 | '17': 5, 174 | '18': 6, 175 | '19': 4, 176 | '20': 5, 177 | '21': 5, 178 | '22': 5, 179 | '23': 3, 180 | '24': 6, 181 | '25': 4, 182 | '26': 4, 183 | '27': 4, 184 | '28': 6, 185 | '29': 6, 186 | '30': 3, 187 | '31': 1, 188 | '32': 2, 189 | '33': 1, 190 | '34': 3, 191 | '35': 5, 192 | '36': 3, 193 | '37': 2, 194 | '38': 3, 195 | '39': 1, 196 | '40': 1, 197 | '41': 3, 198 | '42': 3, 199 | '43': 3, 200 | '44': 1, 201 | '45': 1, 202 | '46': 6, 203 | '47': 2, 204 | '48': 2, 205 | '49': 1, 206 | '50': 2, 207 | '51': 1, 208 | '52': 2, 209 | '53': 6, 210 | '54': 3, 211 | '55': 2, 212 | '56': 1, 213 | '57': 3, 214 | '58': 2, 215 | '59': 1, 216 | '60': 2, 217 | '61': 6, 218 | '62': 2, 219 | '63': 5, 220 | '64': 2, 221 | '65': 3, 222 | '66': 2, 223 | '67': 3, 224 | '68': 1, 225 | '69': 4, 226 | '70': 1, 227 | '71': 6 228 | } 229 | }, 230 | types: { 231 | "mouths": { 232 | "0": 6, 233 | "1": 1, 234 | "2": 2, 235 | "3": 4, 236 | "4": 5, 237 | "5": 5, 238 | "6": 10, 239 | "7": 0, 240 | "8": 7, 241 | "9": 1, 242 | "10": 8, 243 | "11": 7, 244 | "12": 11, 245 | "13": 11, 246 | "14": 10, 247 | "15": 6, 248 | "16": 9, 249 | "17": 3, 250 | "18": 9, 251 | "19": 2, 252 | "20": 8, 253 | "21": 3, 254 | "22": 4, 255 | "23": 0 256 | }, 257 | "eyebrows": { 258 | "0": 1, 259 | "1": 3, 260 | "2": 2, 261 | "3": 3, 262 | "4": 11, 263 | "5": 10, 264 | "6": 0, 265 | "7": 6, 266 | "8": 8, 267 | "9": 4, 268 | "10": 1, 269 | "11": 0, 270 | "12": 2, 271 | "13": 7, 272 | "14": 4, 273 | "15": 6, 274 | "16": 10, 275 | "17": 9, 276 | "18": 9, 277 | "19": 5, 278 | "20": 5, 279 | "21": 7, 280 | "22": 8, 281 | "23": 11 282 | }, 283 | "eyes": { 284 | "0": 2, 285 | "1": 6, 286 | "2": 0, 287 | "3": 6, 288 | "4": 1, 289 | "5": 0, 290 | "6": 5, 291 | "7": 0, 292 | "8": 3, 293 | "9": 4, 294 | "10": 9, 295 | "11": 1, 296 | "12": 5, 297 | "13": 2, 298 | "14": 10, 299 | "15": 9, 300 | "16": 8, 301 | "17": 5, 302 | "18": 9, 303 | "19": 2, 304 | "20": 11, 305 | "21": 8, 306 | "22": 8, 307 | "23": 6, 308 | "24": 6, 309 | "25": 9, 310 | "26": 7, 311 | "27": 10, 312 | "28": 10, 313 | "29": 5, 314 | "30": 7, 315 | "31": 8, 316 | "32": 3, 317 | "33": 0, 318 | "34": 7, 319 | "35": 11, 320 | "36": 3, 321 | "37": 4, 322 | "38": 2, 323 | "39": 4, 324 | "40": 10, 325 | "41": 1, 326 | "42": 3, 327 | "43": 7, 328 | "44": 1, 329 | "45": 4, 330 | "46": 11, 331 | "47": 11 332 | }, 333 | "hairs": { 334 | "0": 11, 335 | "1": 6, 336 | "2": 5, 337 | "3": 1, 338 | "4": 4, 339 | "5": 8, 340 | "6": 4, 341 | "7": 11, 342 | "8": 9, 343 | "9": 3, 344 | "10": 3, 345 | "11": 6, 346 | "12": 0, 347 | "13": 1, 348 | "14": 0, 349 | "15": 10, 350 | "16": 1, 351 | "17": 8, 352 | "18": 4, 353 | "19": 7, 354 | "20": 5, 355 | "21": 10, 356 | "22": 2, 357 | "23": 3, 358 | "24": 9, 359 | "25": 5, 360 | "26": 3, 361 | "27": 10, 362 | "28": 6, 363 | "29": 11, 364 | "30": 9, 365 | "31": 11, 366 | "32": 0, 367 | "33": 0, 368 | "34": 11, 369 | "35": 9, 370 | "36": 6, 371 | "37": 2, 372 | "38": 1, 373 | "39": 4, 374 | "40": 1, 375 | "41": 7, 376 | "42": 2, 377 | "43": 0, 378 | "44": 3, 379 | "45": 6, 380 | "46": 2, 381 | "47": 1, 382 | "48": 3, 383 | "49": 7, 384 | "50": 7, 385 | "51": 2, 386 | "52": 5, 387 | "53": 7, 388 | "54": 5, 389 | "55": 8, 390 | "56": 9, 391 | "57": 10, 392 | "58": 6, 393 | "59": 8, 394 | "60": 10, 395 | "61": 0, 396 | "62": 11, 397 | "63": 7, 398 | "64": 9, 399 | "65": 8, 400 | "66": 4, 401 | "67": 4, 402 | "68": 10, 403 | "69": 2, 404 | "70": 5, 405 | "71": 8 406 | } 407 | }, 408 | wiiNoses: { 409 | '0': 1, 410 | '1': 10, 411 | '2': 2, 412 | '3': 3, 413 | '4': 6, 414 | '5': 0, 415 | '6': 5, 416 | '7': 4, 417 | '8': 8, 418 | '9': 9, 419 | '10': 7, 420 | '11': 11 421 | }, 422 | mouthTable: { 423 | '0': '113', 424 | '1': '121', 425 | '2': '231', 426 | '3': '222', 427 | '4': '232', 428 | '5': '132', 429 | '6': '124', 430 | '7': '211', 431 | '8': '123', 432 | '9': '221', 433 | '10': '133', 434 | '11': '223', 435 | '12': '234', 436 | '13': '134', 437 | '14': '224', 438 | '15': '213', 439 | '16': '114', 440 | '17': '212', 441 | '18': '214', 442 | '19': '131', 443 | '20': '233', 444 | '21': '112', 445 | '22': '122', 446 | '23': '111' 447 | }, 448 | eyebrowTable: { 449 | '0': '121', 450 | '1': '112', 451 | '2': '231', 452 | '3': '212', 453 | '4': '134', 454 | '5': '124', 455 | '6': '111', 456 | '7': '113', 457 | '8': '133', 458 | '9': '122', 459 | '10': '221', 460 | '11': '211', 461 | '12': '131', 462 | '13': '223', 463 | '14': '222', 464 | '15': '213', 465 | '16': '224', 466 | '17': '114', 467 | '18': '214', 468 | '19': '132', 469 | '20': '232', 470 | '21': '123', 471 | '22': '233', 472 | '23': '234' 473 | }, 474 | eyeTable: { 475 | '0': '131', 476 | '1': '113', 477 | '2': '111', 478 | '3': '413', 479 | '4': '121', 480 | '5': '311', 481 | '6': '332', 482 | '7': '411', 483 | '8': '112', 484 | '9': '222', 485 | '10': '414', 486 | '11': '221', 487 | '12': '232', 488 | '13': '331', 489 | '14': '424', 490 | '15': '114', 491 | '16': '133', 492 | '17': '132', 493 | '18': '314', 494 | '19': '231', 495 | '20': '134', 496 | '21': '233', 497 | '22': '433', 498 | '23': '213', 499 | '24': '313', 500 | '25': '214', 501 | '26': '123', 502 | '27': '124', 503 | '28': '324', 504 | '29': '432', 505 | '30': '323', 506 | '31': '333', 507 | '32': '212', 508 | '33': '211', 509 | '34': '223', 510 | '35': '234', 511 | '36': '312', 512 | '37': '322', 513 | '38': '431', 514 | '39': '122', 515 | '40': '224', 516 | '41': '321', 517 | '42': '412', 518 | '43': '423', 519 | '44': '421', 520 | '45': '422', 521 | '46': '334', 522 | '47': '434' 523 | }, 524 | hairTable: { 525 | '0': '534', 526 | '1': '413', 527 | '2': '632', 528 | '3': '521', 529 | '4': '422', 530 | '5': '433', 531 | '6': '522', 532 | '7': '434', 533 | '8': '414', 534 | '9': '612', 535 | '10': '512', 536 | '11': '513', 537 | '12': '411', 538 | '13': '421', 539 | '14': '511', 540 | '15': '624', 541 | '16': '621', 542 | '17': '533', 543 | '18': '622', 544 | '19': '423', 545 | '20': '532', 546 | '21': '524', 547 | '22': '531', 548 | '23': '312', 549 | '24': '614', 550 | '25': '432', 551 | '26': '412', 552 | '27': '424', 553 | '28': '613', 554 | '29': '634', 555 | '30': '314', 556 | '31': '134', 557 | '32': '211', 558 | '33': '111', 559 | '34': '334', 560 | '35': '514', 561 | '36': '313', 562 | '37': '231', 563 | '38': '321', 564 | '39': '122', 565 | '40': '121', 566 | '41': '323', 567 | '42': '331', 568 | '43': '311', 569 | '44': '112', 570 | '45': '113', 571 | '46': '631', 572 | '47': '221', 573 | '48': '212', 574 | '49': '123', 575 | '50': '223', 576 | '51': '131', 577 | '52': '232', 578 | '53': '623', 579 | '54': '332', 580 | '55': '233', 581 | '56': '114', 582 | '57': '324', 583 | '58': '213', 584 | '59': '133', 585 | '60': '224', 586 | '61': '611', 587 | '62': '234', 588 | '63': '523', 589 | '64': '214', 590 | '65': '333', 591 | '66': '222', 592 | '67': '322', 593 | '68': '124', 594 | '69': '431', 595 | '70': '132', 596 | '71': '633' 597 | }, 598 | 599 | // 3DS fields 600 | 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"], 601 | 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"], 602 | mouthCols3DS: ["Orange", "Red", "Pink", "Peach", "Black"], 603 | glassesCols3DS: ["Black", "Brown", "Red", "Blue", "Yellow", "Grey"], 604 | 605 | faces: { 606 | indexLookup: true, 607 | values: [ 608 | 0x00, 0x01, 0x08, 609 | 0x02, 0x03, 0x09, 610 | 0x04, 0x05, 0x0a, 611 | 0x06, 0x07, 0x0b 612 | ] 613 | }, 614 | hairs: { 615 | paginated: true, 616 | indexLookup: true, 617 | values: [ 618 | [0x21, 0x2f, 0x28, 0x25, 0x20, 0x6b, 0x30, 0x33, 0x37, 0x46, 0x2c, 0x42], 619 | [0x34, 0x32, 0x26, 0x31, 0x2b, 0x1f, 0x38, 0x44, 0x3e, 0x73, 0x4c, 0x77], 620 | [0x40, 0x51, 0x74, 0x79, 0x16, 0x3a, 0x3c, 0x57, 0x7d, 0x75, 0x49, 0x4b], 621 | [0x2a, 0x59, 0x39, 0x36, 0x50, 0x22, 0x17, 0x56, 0x58, 0x76, 0x27, 0x24], 622 | [0x2d, 0x43, 0x3b, 0x41, 0x29, 0x1e, 0x0c, 0x10, 0x0a, 0x52, 0x80, 0x81], 623 | [0x0e, 0x5f, 0x69, 0x64, 0x06, 0x14, 0x5d, 0x66, 0x1b, 0x04, 0x11, 0x6e], 624 | [0x7b, 0x08, 0x6a, 0x48, 0x03, 0x15, 0x00, 0x62, 0x3f, 0x5a, 0x0b, 0x78], 625 | [0x05, 0x4a, 0x6c, 0x5e, 0x7c, 0x19, 0x63, 0x45, 0x23, 0x0d, 0x7a, 0x71], 626 | [0x35, 0x18, 0x55, 0x53, 0x47, 0x83, 0x60, 0x65, 0x1d, 0x07, 0x0f, 0x70], 627 | [0x4f, 0x01, 0x6d, 0x7f, 0x5b, 0x1a, 0x3d, 0x67, 0x02, 0x4d, 0x12, 0x5c], 628 | [0x54, 0x09, 0x13, 0x82, 0x61, 0x68, 0x2e, 0x4e, 0x1c, 0x72, 0x7e, 0x6f] 629 | ] 630 | }, 631 | eyebrows: { 632 | indexLookup: true, 633 | paginated: true, 634 | values: [ 635 | [0x06, 0x00, 0x0c, 0x01, 0x09, 0x13, 0x07, 0x15, 0x08, 0x11, 0x05, 0x04], 636 | [0x0b, 0x0a, 0x02, 0x03, 0x0e, 0x14, 0x0f, 0x0d, 0x16, 0x12, 0x10, 0x17] 637 | ] 638 | }, 639 | eyes: { 640 | indexLookup: true, 641 | paginated: true, 642 | values: [ 643 | [0x02, 0x04, 0x00, 0x08, 0x27, 0x11, 0x01, 0x1a, 0x10, 0x0f, 0x1b, 0x14], 644 | [0x21, 0x0b, 0x13, 0x20, 0x09, 0x0c, 0x17, 0x22, 0x15, 0x19, 0x28, 0x23], 645 | [0x05, 0x29, 0x0d, 0x24, 0x25, 0x06, 0x18, 0x1e, 0x1f, 0x12, 0x1c, 0x2e], 646 | [0x07, 0x2c, 0x26, 0x2a, 0x2d, 0x1d, 0x03, 0x2b, 0x16, 0x0a, 0x0e, 0x2f], 647 | [0x30, 0x31, 0x32, 0x35, 0x3b, 0x38, 0x36, 0x3a, 0x39, 0x37, 0x33, 0x34] 648 | ] 649 | }, 650 | noses: { 651 | indexLookup: true, 652 | paginated: true, 653 | values: [ 654 | [0x01, 0x0a, 0x02, 0x03, 0x06, 0x00, 0x05, 0x04, 0x08, 0x09, 0x07, 0x0B], 655 | [0x0d, 0x0e, 0x0c, 0x11, 0x10, 0x0f] 656 | ] 657 | }, 658 | mouths: { 659 | indexLookup: true, 660 | paginated: true, 661 | values: [ 662 | [0x17, 0x01, 0x13, 0x15, 0x16, 0x05, 0x00, 0x08, 0x0a, 0x10, 0x06, 0x0d], 663 | [0x07, 0x09, 0x02, 0x11, 0x03, 0x04, 0x0f, 0x0b, 0x14, 0x12, 0x0e, 0x0c], 664 | [0x1b, 0x1e, 0x18, 0x19, 0x1d, 0x1c, 0x1a, 0x23, 0x1f, 0x22, 0x21, 0x20] 665 | ] 666 | } 667 | }; 668 | var convTables = { 669 | face3DSToWii: [0, 1, 2, 2, 3, 1, 4, 5, 4, 6, 7, 6], 670 | 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. 671 | makeup3DSToWii: [0, 1, 1, 2, 1, 1, 2, 2, 2, 3, 9, 9], 672 | nose3DSToWii: [ 673 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 674 | [0, 3, 4, 6, 9, 2] 675 | ], 676 | mouth3DSToWii: [ 677 | ["111", "121", "131", "112", "122", "132", "113", "123", "133", "114", "124", "134"], 678 | ["211", "221", "231", "212", "222", "232", "213", "223", "233", "214", "224", "234"], 679 | ["121", "214", "134", "123", "121", "112", "124", "133", "221", "224", "121", "232"] 680 | ], 681 | hair3DSToWii: [ 682 | [ 683 | "111", "221", "121", 684 | "231", "211", "121", 685 | "212", "131", "233", 686 | "132", "112", "222" 687 | ], 688 | [ 689 | "232", "223", "321", 690 | "123", "311", "134", 691 | "114", "124", "234", 692 | "114", "134", "234" 693 | ], 694 | [ 695 | "214", "523", "433", 696 | "214", "531", "512", 697 | "523", "433", "134", 698 | "414", "523", "134" 699 | ], 700 | [ 701 | "331", "333", "324", 702 | "332", "333", "334", 703 | "312", "322", "322", 704 | "113", "122", "313" 705 | ], 706 | [ 707 | "113", "322", "133", 708 | "333", "323", "314", 709 | "411", "621", "521", 710 | "424", "424", "424" 711 | ], 712 | [ 713 | "511", "411", "411", 714 | "422", "522", "523", 715 | "534", "523", "434", 716 | "422", "533", "424" 717 | ], 718 | [ 719 | "511", "531", "534", 720 | "623", "521", "524", 721 | "534", "523", "523", 722 | "424", "513", "523" 723 | ], 724 | [ 725 | "411", "523", "512", 726 | "513", "432", "432", 727 | "621", "431", "514", 728 | "421", "432", "514" 729 | ], 730 | [ 731 | "623", "614", "633", 732 | "633", "633", "624", 733 | "434", "633", "634", 734 | "624", "624", "634" 735 | ], 736 | [ 737 | "634", "413", "412", 738 | "413", "413", "412", 739 | "611", "622", "632", 740 | "611", "622", "632" 741 | ], 742 | [ 743 | "423", "632", "423", 744 | "612", "612", "613", 745 | "631", "631", "613", 746 | "631", "631", "613" 747 | ] 748 | ], 749 | eyebrows3DSToWii: [ 750 | [ 751 | "111", "121", "131", 752 | "112", "122", "132", 753 | "113", "123", "133", 754 | "114", "124", "134" 755 | ], 756 | [ 757 | "211", "221", "231", 758 | "212", "222", "232", 759 | "213", "223", "233", 760 | "214", "224", "234" 761 | ] 762 | ], 763 | eyes3DSToWii: [ 764 | [ 765 | "111", "121", "131", 766 | "112", "122", "132", 767 | "113", "123", "133", 768 | "114", "124", "134" 769 | ], 770 | [ 771 | "211", "221", "231", 772 | "212", "222", "232", 773 | "213", "223", "233", 774 | "214", "224", "234" 775 | ], 776 | [ 777 | "311", "321", "331", 778 | "312", "322", "332", 779 | "313", "323", "333", 780 | "314", "324", "334" 781 | ], 782 | [ 783 | "411", "421", "431", 784 | "412", "422", "432", 785 | "413", "423", "433", 786 | "414", "424", "434" 787 | ], 788 | [ 789 | "322", "322", "312", 790 | "224", "224", "431", 791 | "224", "224", "111", 792 | "121", "411", "431" 793 | ] 794 | ], 795 | hairWiiTo3DS: [ 796 | [ 797 | [0, 0], [0, 2], [0, 7], 798 | [0, 10], [3, 10], [0, 9], 799 | [4, 0], [1, 3], [3, 8], 800 | [1, 6], [1, 7], [1, 5] 801 | ], 802 | [ 803 | [0, 4], [0, 1], [0, 3], 804 | [0, 6], [0, 11], [1, 0], 805 | [2, 5], [1, 1], [0, 8], 806 | [2, 0], [2, 6], [1, 8] 807 | ], 808 | [ 809 | [1, 4], [1, 2], [3, 0], 810 | [4, 0], [3, 6], [3, 3], 811 | [3, 11], [4, 4], [4, 3], 812 | [4, 5], [3, 2], [3, 5] 813 | ], 814 | [ 815 | [4, 6], [7, 9], [7, 7], 816 | [9, 5], [5, 9], [7, 5], 817 | [9, 1], [10, 2], [7, 0], 818 | [6, 1], [5, 8], [8, 9] 819 | ], 820 | [ 821 | [5, 0], [6, 4], [2, 4], 822 | [4, 8], [5, 4], [5, 5], 823 | [6, 10], [6, 8], [5, 10], 824 | [7, 8], [6, 5], [6, 6] 825 | ], 826 | [ 827 | [9, 6], [4, 7], [10, 6], 828 | [10, 1], [9, 10], [9, 8], 829 | [10, 8], [8, 1], [2, 0], 830 | [9, 1], [8, 9], [8, 8] 831 | ] 832 | ], 833 | faceWiiTo3DS: [ 834 | 0, 1, 835 | 3, 4, 836 | 6, 7, 837 | 9, 10 838 | ], 839 | featureWiiTo3DS: [ 840 | 0, "1", "6", 841 | "9", 5, 2, 842 | 3, 7, 8, 843 | "10", 9, 11 844 | ], 845 | formatTo: [ 846 | [0, 1, 2], 847 | [3, 4, 5], 848 | [6, 7, 8], 849 | [9, 10, 11] 850 | ], 851 | formatFrom: [ 852 | "11", "21", "31", 853 | "12", "22", "32", 854 | "13", "23", "33", 855 | "14", "24", "34" 856 | ] 857 | }; 858 | const kidNames = { 859 | "Male": [ 860 | "Aaron", 861 | "Adam", 862 | "Adrian", 863 | "Aiden", 864 | "Ayden", 865 | "Alex", 866 | "Alexander", 867 | "Alfie", 868 | "Andrew", 869 | "Anthony", 870 | "Archie", 871 | "Austin", 872 | "Ben", 873 | "Benjamin", 874 | "Bentley", 875 | "Bill", 876 | "Billy", 877 | "Blake", 878 | "Bradley", 879 | "Brandon", 880 | "Brayden", 881 | "Brody", 882 | "Bryson", 883 | "Caleb", 884 | "Callum", 885 | "Cameron", 886 | "Carlos", 887 | "Charlie", 888 | "Charles", 889 | "Carson", 890 | "Carter", 891 | "Chase", 892 | "Chris", 893 | "Christian", 894 | "Cody", 895 | "Colton", 896 | "Connor", 897 | "Cooper", 898 | "Damian", 899 | "Daniel", 900 | "David", 901 | "Dexter", 902 | "Dominic", 903 | "Dylan", 904 | "Easton", 905 | "Edward", 906 | "Eli", 907 | "Elijah", 908 | "Elliot", 909 | "Ethan", 910 | "Evan", 911 | "Finlay", 912 | "Frankie", 913 | "Freddie", 914 | "Gabriel", 915 | "Gavin", 916 | "George", 917 | "Grayson", 918 | "Harrison", 919 | "Harvey", 920 | "Henry", 921 | "Hudson", 922 | "Hugo", 923 | "Hunter", 924 | "Ian", 925 | "Isaac", 926 | "Isaiah", 927 | "Jace", 928 | "Jack", 929 | "Jackson", 930 | "Jaxon", 931 | "Jacob", 932 | "Jake", 933 | "James", 934 | "Jason", 935 | "Jayden", 936 | "Jenson", 937 | "Jeremiah", 938 | "John", 939 | "Juan", 940 | "Jonathan", 941 | "Jordan", 942 | "Jose", 943 | "Joseph", 944 | "Josiah", 945 | "Joshua", 946 | "Jude", 947 | "Julian", 948 | "Justin", 949 | "Kai", 950 | "Kayden", 951 | "Kevin", 952 | "Kian", 953 | "Landon", 954 | "Levi", 955 | "Leo", 956 | "Logan", 957 | "Lucas", 958 | "Luke", 959 | "Luis", 960 | "Lachlan", 961 | "Mason", 962 | "Matthew", 963 | "Max", 964 | "Michael", 965 | "Miguel", 966 | "Nathan", 967 | "Nathaniel", 968 | "Nicholas", 969 | "Noah", 970 | "Nolan", 971 | "Olly", 972 | "Oliver", 973 | "Owen", 974 | "Parker", 975 | "Philip", 976 | "Rhys", 977 | "Reece", 978 | "Rob", 979 | "Robert", 980 | "Ryan", 981 | "Ryder", 982 | "Samuel", 983 | "Sebastian", 984 | "Seth", 985 | "Thomas", 986 | "Tommy", 987 | "Trent", 988 | "Tristan", 989 | "Tyler", 990 | "William", 991 | "Liam", 992 | "Wyatt", 993 | "Xavier", 994 | "Zac", 995 | "Zachary", 996 | "Alex", 997 | "Alexis", 998 | "Angel", 999 | "Bailey", 1000 | "Darcy", 1001 | "Darcey", 1002 | "Genesis", 1003 | "Kennedy", 1004 | "Mackenzie", 1005 | "Morgan", 1006 | "Peyton", 1007 | "Sam", 1008 | "Taylor" 1009 | ], 1010 | "Female": [ 1011 | "Aaliyah", 1012 | "Abigail", 1013 | "Addison", 1014 | "Madison", 1015 | "Maddison", 1016 | "Alexa", 1017 | "Alexandra", 1018 | "Alison", 1019 | "Allison", 1020 | "Alyssa", 1021 | "Amelia", 1022 | "Amy", 1023 | "Andrea", 1024 | "Anna", 1025 | "Annabelle", 1026 | "Aria", 1027 | "Ariana", 1028 | "Arianna", 1029 | "Ashley", 1030 | "Aubree", 1031 | "Aubrey", 1032 | "Audrey", 1033 | "Autumn", 1034 | "Ava", 1035 | "Avery", 1036 | "Bella", 1037 | "Bethany", 1038 | "Brianna", 1039 | "Brooklyn", 1040 | "Camila", 1041 | "Caroline", 1042 | "Charlotte", 1043 | "Chloe", 1044 | "Khloe", 1045 | "Claire", 1046 | "Ella", 1047 | "Ellie", 1048 | "Elenor", 1049 | "Elizabeth", 1050 | "Lizabeth", 1051 | "Liza", 1052 | "Emily", 1053 | "Emma", 1054 | "Eva", 1055 | "Evie", 1056 | "Evelyn", 1057 | "Faith", 1058 | "Gabriella", 1059 | "Gianna", 1060 | "Grace", 1061 | "Hailey", 1062 | "Hannah", 1063 | "Harper", 1064 | "Heidi", 1065 | "Hollie", 1066 | "Holly", 1067 | "Isabella", 1068 | "Isobel", 1069 | "Jasmine", 1070 | "Jessica", 1071 | "Jocelyn", 1072 | "Julia", 1073 | "Katherine", 1074 | "Kayla", 1075 | "Kaylee", 1076 | "Kimberly", 1077 | "Kylie", 1078 | "Lacey", 1079 | "Lauren", 1080 | "Layla", 1081 | "Leah", 1082 | "Lexie", 1083 | "Lilian", 1084 | "Lily", 1085 | "Lola", 1086 | "London", 1087 | "Lucy", 1088 | "Lydia", 1089 | "Madeline", 1090 | "Madelyn", 1091 | "Maisie", 1092 | "Makayla", 1093 | "Maya", 1094 | "Mya", 1095 | "Megan", 1096 | "Melanie", 1097 | "Mia", 1098 | "Molly", 1099 | "Naomi", 1100 | "Natalie", 1101 | "Nevaeh", 1102 | "Olivia", 1103 | "Paige", 1104 | "Poppy", 1105 | "Piper", 1106 | "Reagan", 1107 | "Rebecca", 1108 | "Riley", 1109 | "Rosie", 1110 | "Samantha", 1111 | "Sarah", 1112 | "Savannah", 1113 | "Scarlett", 1114 | "Serenity", 1115 | "Skye", 1116 | "Skylar", 1117 | "Sofia", 1118 | "Sophia", 1119 | "Sophie", 1120 | "Spring", 1121 | "Stella", 1122 | "Summer", 1123 | "Sydney", 1124 | "Trinity", 1125 | "Vanessa", 1126 | "Victoria", 1127 | "Violet", 1128 | "Winter", 1129 | "Zara", 1130 | "Zoe", 1131 | "Zoey", 1132 | "Alex", 1133 | "Alexis", 1134 | "Angel", 1135 | "Bailey", 1136 | "Darcy", 1137 | "Darcey", 1138 | "Genesis", 1139 | "Kennedy", 1140 | "Mackenzie", 1141 | "Morgan", 1142 | "Peyton", 1143 | "Sam", 1144 | "Taylor" 1145 | ] 1146 | }; 1147 | 1148 | //Tools 1149 | function Uint8Cat() { 1150 | var destLength = 0 1151 | for (var i = 0; i < arguments.length; i++) { 1152 | destLength += arguments[i].length; 1153 | } 1154 | var dest = new Uint8Array(destLength); 1155 | var index = 0; 1156 | for (var i = 0; i < arguments.length; i++) { 1157 | dest.set(arguments[i], index); 1158 | index += arguments[i].length; 1159 | } 1160 | return dest; 1161 | } 1162 | async function downloadImage(url) { 1163 | return new Promise((resolve, reject) => { 1164 | httpsLib.get(url, (res) => { 1165 | if (res.statusCode === 200) { 1166 | const data = []; 1167 | res.on('data', chunk => data.push(chunk)); 1168 | res.on('end', () => resolve(Buffer.concat(data))); 1169 | res.on('error', reject); 1170 | } else { 1171 | res.resume(); 1172 | reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`)); 1173 | } 1174 | }); 1175 | }); 1176 | } 1177 | function byteToString(int) { 1178 | var str = int.toString(16); 1179 | if (str.length < 2) str = '0' + str; 1180 | return str; 1181 | } 1182 | function getBinaryFromAddress(addr, bin) { 1183 | let byte = bin.readUInt8(addr); 1184 | let binaryString = ''; 1185 | for (let i = 7; i >= 0; i--) { 1186 | binaryString += ((byte >> i) & 1) ? '1' : '0'; 1187 | } 1188 | return binaryString; 1189 | } 1190 | function getKeyByValue(object, value) { 1191 | for (var key in object) { 1192 | if (object[key] === value) { 1193 | return key; 1194 | } 1195 | } 1196 | } 1197 | function lookupTable(table, value, paginated) { 1198 | if (paginated) { 1199 | for (var i = 0; i < lookupTables[table].values.length; i++) { 1200 | for (var j = 0; j < lookupTables[table].values[i].length; j++) { 1201 | if (lookupTables[table].values[i][j] === value) { 1202 | return [i, j]; 1203 | } 1204 | } 1205 | } 1206 | } 1207 | else { 1208 | for (var i = 0; i < lookupTables[table].values.length; i++) { 1209 | if (lookupTables[table].values[i] === value) { 1210 | return i; 1211 | } 1212 | } 1213 | } 1214 | return undefined; 1215 | } 1216 | function hexToBytes(hex) { 1217 | const cleaned = hex.replace(/[\s:_-]/g, "").replace(/^0x/i, ""); 1218 | if (!/^(?:[0-9a-fA-F]{2})+$/.test(cleaned)) throw new Error("Invalid hex string"); 1219 | const out = new Uint8Array(cleaned.length / 2); 1220 | for (let i = 0; i < cleaned.length; i += 2) out[i / 2] = parseInt(cleaned.slice(i, i + 2), 16); 1221 | return out; 1222 | } 1223 | function toRawStudioBytes(any) { 1224 | if (typeof any === "string") { 1225 | const looksHex = /^(?:0x)?(?:[\s:_-]*[0-9a-fA-F]{2})+[\s:_-]*$/.test(any); 1226 | if (looksHex) { 1227 | const bytes = hexToBytes(any); 1228 | if (bytes.length > 40 && bytes[0] === 0) return decodeStudio(bytes); 1229 | return bytes; 1230 | } 1231 | return decodeStudio(any); 1232 | } 1233 | if (any instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer?.(any))) { 1234 | const bytes = any instanceof Uint8Array ? any : new Uint8Array(any); 1235 | if (bytes.length > 40 && bytes[0] === 0) return decodeStudio(bytes); 1236 | return bytes; 1237 | } 1238 | throw new Error("Unsupported input type"); 1239 | } 1240 | const find1D = (arr, value) => { 1241 | const idx = arr.indexOf(value); 1242 | return idx >= 0 ? idx : 0; 1243 | }; 1244 | const findPageType = (table2D, id) => { 1245 | for (let page = 0; page < table2D.length; page++) { 1246 | const type = table2D[page].indexOf(id); 1247 | if (type >= 0) return { page, type }; 1248 | } 1249 | return { page: 0, type: 0 }; 1250 | }; 1251 | const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x)); 1252 | 1253 | //If FFLResHigh.dat is in the same directory as Node.js is calling the library from, use it by default 1254 | let _fflRes; 1255 | function getFFLRes() { 1256 | // If we've already tried loading, just return the result 1257 | if (_fflRes !== undefined) return _fflRes; 1258 | 1259 | const searchPaths = [ 1260 | "./FFLResHigh.dat", 1261 | "../FFLResHigh.dat", 1262 | "../../FFLResHigh.dat", 1263 | "./ffl/FFLResHigh.dat", 1264 | "./afl/AFLResHigh.dat", 1265 | "../ffl/FFLResHigh.dat", 1266 | "../afl/AFLResHigh.dat", 1267 | "../../ffl/FFLResHigh.dat", 1268 | "../../afl/AFLResHigh.dat" 1269 | ]; 1270 | 1271 | for (const filePath of searchPaths) { 1272 | try { 1273 | if (fs.existsSync(filePath)) { 1274 | const stats = fs.statSync(filePath); 1275 | // Make sure it's a file, not a directory 1276 | if (stats.isFile()) { 1277 | // Convert Buffer to Uint8Array explicitly 1278 | const buffer = fs.readFileSync(filePath); 1279 | _fflRes = new Uint8Array(buffer); 1280 | console.log(`Loaded FFLResHigh.dat from: ${filePath} (${_fflRes.length} bytes)`); 1281 | return _fflRes; 1282 | } 1283 | } 1284 | } catch (e) { 1285 | // Silently continue to next path 1286 | continue; 1287 | } 1288 | } 1289 | 1290 | // If no file found, mark as null 1291 | console.warn('FFLResHigh.dat not found. Mii rendering will fall back to Mii Studio.'); 1292 | return _fflRes = null; 1293 | } 1294 | 1295 | //3DS QR Code (En|De)cryption 1296 | var NONCE_OFFSET = 0xC; 1297 | var NONCE_LENGTH = 8; 1298 | var TAG_LENGTH = 0x10; 1299 | var aes_key = new Uint8Array([0x59, 0xFC, 0x81, 0x7E, 0x64, 0x46, 0xEA, 0x61, 0x90, 0x34, 0x7B, 0x20, 0xE9, 0xBD, 0xCE, 0x52]); 1300 | var pad = new Uint8Array([0, 0, 0, 0]); 1301 | function decodeAesCcm(data) { 1302 | var nonce = Uint8Cat(data.subarray(0, NONCE_LENGTH), pad); 1303 | var ciphertext = data.subarray(NONCE_LENGTH, 0x70); 1304 | var plaintext = asmCrypto.AES_CCM.decrypt(ciphertext, aes_key, nonce, undefined, TAG_LENGTH); 1305 | return Uint8Cat(plaintext.subarray(0, NONCE_OFFSET), data.subarray(0, NONCE_LENGTH), plaintext.subarray(NONCE_OFFSET, plaintext.length - 4)); 1306 | } 1307 | function crcCalc(data) { 1308 | var crc = 0; 1309 | for (var byteIndex = 0; byteIndex < data.length; byteIndex++) { 1310 | for (var bitIndex = 7; bitIndex >= 0; bitIndex--) { 1311 | crc = (((crc << 1) | ((data[byteIndex] >> bitIndex) & 0x1)) ^ 1312 | (((crc & 0x8000) != 0) ? 0x1021 : 0)); 1313 | } 1314 | } 1315 | for (var counter = 16; counter > 0; counter--) { 1316 | crc = ((crc << 1) ^ (((crc & 0x8000) != 0) ? 0x1021 : 0)); 1317 | } 1318 | return (crc & 0xFFFF); 1319 | } 1320 | function encodeAesCcm(data) { 1321 | var nonce = Uint8Cat(data.subarray(NONCE_OFFSET, NONCE_OFFSET + NONCE_LENGTH), pad); 1322 | var crcSrc = Uint8Cat(data, new Uint8Array([0, 0])); 1323 | var crc = crcCalc(crcSrc); 1324 | var cfsd = Uint8Cat(crcSrc, new Uint8Array([crc >>> 8, crc & 0xff])); 1325 | var plaintext = Uint8Cat(cfsd.subarray(0, NONCE_OFFSET), cfsd.subarray(NONCE_OFFSET + NONCE_LENGTH, cfsd.length), pad, pad); 1326 | var ciphertext = asmCrypto.AES_CCM.encrypt(plaintext, aes_key, nonce, undefined, TAG_LENGTH); 1327 | return Uint8Cat(cfsd.subarray(NONCE_OFFSET, NONCE_OFFSET + NONCE_LENGTH), ciphertext.subarray(0, ciphertext.length - 24), ciphertext.subarray(ciphertext.length - TAG_LENGTH, ciphertext.length)) 1328 | } 1329 | 1330 | //Defaults 1331 | const defaultInstrs = { 1332 | wii: { 1333 | male: { 1334 | "col": "On the info page (first tab), set the Favorite Color to Red (1 from the left, top row).", 1335 | "heightWeight": "On the build page (second tab), set the height to 50%, and the weight to 50%.", 1336 | "faceShape": "On the face page (third tab), set the shape to the one 1 from the top, in the left column.", 1337 | "skinCol": "On the face page (third tab), set the color to the one 1 from the left, on the top row.", 1338 | "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", 1339 | "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.", 1340 | "hairFlipped": "", 1341 | "hairColor": "On the hair page (fourth tab), set the hair color to the one 2 from the left, on the top row.", 1342 | "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.", 1343 | "eyebrowColor": "On the eyebrow page (fifth tab), set the eyebrow color to the one 2 from the left, on the top row.", 1344 | "eyebrowY": "", 1345 | "eyebrowSize": "", 1346 | "eyebrowRot": "", 1347 | "eyebrowDist": "", 1348 | "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.", 1349 | "eyeColor": "On the eye page (sixth tab), set the color to the one 1 from the left, on the top row.", 1350 | "eyeY": "", 1351 | "eyeSize": "", 1352 | "eyeRot": "", 1353 | "eyeDist": "", 1354 | "noseType": "On the nose page (seventh tab), set the nose to the one 1 from the top, and 1 from the left.", 1355 | "noseY": "", 1356 | "noseSize": "", 1357 | "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.", 1358 | "mouthCol": "On the mouth page (eighth tab), set the color to the one 1 from the left.", 1359 | "mouthY": "", 1360 | "mouthSize": "", 1361 | "glasses": "On the glasses page (within the ninth tab), set the glasses to the one 1 from the top, and 1 from the left.", 1362 | "glassesCol": "On the glasses page (within the ninth tab), set the color to the one 1 from the left, on the top row.", 1363 | "glassesY": "", 1364 | "glassesSize": "", 1365 | "stache": "On the mustache page (within the ninth tab), set the mustache to the one on the top-left.", 1366 | "stacheY": "", 1367 | "stacheSize": "", 1368 | "mole": "", 1369 | "moleX": "", 1370 | "moleY": "", 1371 | "moleSize": "", 1372 | "beard": "On the beard page (within the ninth tab), set the beard to the one on the top-left.", 1373 | "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." 1374 | }, 1375 | female: { 1376 | "col": "On the info page (first tab), set the Favorite Color to Red (1 from the left, top row).", 1377 | "heightWeight": "On the build page (second tab), set the height to 50%, and the weight to 50%.", 1378 | "faceShape": "On the face page (third tab), set the shape to the one 1 from the top, in the left column.", 1379 | "skinCol": "On the face page (third tab), set the color to the one 1 from the left, on the top row.", 1380 | "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", 1381 | "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.", 1382 | "hairFlipped": "", 1383 | "hairColor": "On the hair page (fourth tab), set the hair color to the one 2 from the left, on the top row.", 1384 | "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.", 1385 | "eyebrowColor": "On the eyebrow page (fifth tab), set the eyebrow color to the one 2 from the left, on the top row.", 1386 | "eyebrowY": "", 1387 | "eyebrowSize": "", 1388 | "eyebrowRot": "", 1389 | "eyebrowDist": "", 1390 | "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.", 1391 | "eyeColor": "On the eye page (sixth tab), set the color to the one 1 from the left, on the top row.", 1392 | "eyeY": "", 1393 | "eyeSize": "", 1394 | "eyeRot": "On the eye page (sixth tab), press the rotate clockwise button 1 times.", 1395 | "eyeDist": "", 1396 | "noseType": "On the nose page (seventh tab), set the nose to the one 0 from the top, and 1 from the left.", 1397 | "noseY": "", 1398 | "noseSize": "", 1399 | "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.", 1400 | "mouthCol": "On the mouth page (eighth tab), set the color to the one 1 from the left.", 1401 | "mouthY": "", 1402 | "mouthSize": "", 1403 | "glasses": "On the glasses page (within the ninth tab), set the glasses to the one 1 from the top, and 1 from the left.", 1404 | "glassesCol": "On the glasses page (within the ninth tab), set the color to the one 1 from the left, on the top row.", 1405 | "glassesY": "", 1406 | "glassesSize": "", 1407 | "stache": "On the mustache page (within the ninth tab), set the mustache to the one on the top-left.", 1408 | "stacheY": "", 1409 | "stacheSize": "", 1410 | "mole": "", 1411 | "moleX": "", 1412 | "moleY": "", 1413 | "moleSize": "", 1414 | "beard": "On the beard page (within the ninth tab), set the beard to the one on the top-left.", 1415 | "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." 1416 | } 1417 | }, 1418 | "3ds": { 1419 | "male": { 1420 | "faceShape": "On the face page (first tab), set the face shape to the one 1 from the top, and 1 from the left.", 1421 | "skinCol": "On the face page (first tab), set the color to the one 1 from the top.", 1422 | "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", 1423 | "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).", 1424 | "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.", 1425 | "hairFlipped": "", 1426 | "hairColor": "On the hair page (second tab), set the hair color to the one 2 from the top.", 1427 | "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.", 1428 | "eyebrowColor": "On the eyebrow page (third tab), set the eyebrow color to the one 2 from the top.", 1429 | "eyebrowY": "", 1430 | "eyebrowSize": "", 1431 | "eyebrowRot": "", 1432 | "eyebrowDist": "", 1433 | "eyebrowSquash": "", 1434 | "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.", 1435 | "eyeColor": "On the eye page (fourth tab), set the color to the one 1 from the top.", 1436 | "eyeY": "", 1437 | "eyeSize": "", 1438 | "eyeRot": "", 1439 | "eyeDist": "", 1440 | "eyeSquash": "", 1441 | "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.", 1442 | "noseY": "", 1443 | "noseSize": "", 1444 | "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.", 1445 | "mouthCol": "On the mouth page (sixth tab), set the color to the one 1 from the top.", 1446 | "mouthY": "", 1447 | "mouthSize": "", 1448 | "mouthSquash": "", 1449 | "glasses": "On the glasses page (within the seventh tab), set the glasses to the one 1 from the top, and 1 from the left.", 1450 | "glassesCol": "On the glasses page (within the seventh tab), set the color to the one 1 from the top.", 1451 | "glassesY": "", 1452 | "glassesSize": "", 1453 | "stache": "On the mustache page (within the seventh tab), set the mustache to the one on the top-left.", 1454 | "stacheY": "", 1455 | "stacheSize": "", 1456 | "mole": "", 1457 | "moleX": "", 1458 | "moleY": "", 1459 | "moleSize": "", 1460 | "beard": "On the beard page (within the seventh tab), set the beard to the one on the top-left.", 1461 | "beardCol": "On the mustache OR beard pages (within the seventh tab), set the color to the one 1 from the top.", 1462 | "heightWeight": "On the build page (eighth tab), set the height to 50%, and the weight to 50%.", 1463 | "col": "On the info page (after pressing \"Next\"), set the Favorite Color to Red (1 from the left, top row)." 1464 | }, 1465 | "female": { 1466 | "faceShape": "On the face page (first tab), set the face shape to the one 1 from the top, and 1 from the left.", 1467 | "skinCol": "On the face page (first tab), set the color to the one 1 from the top.", 1468 | "makeup": "On the face page's makeup tab, set the makeup to \"None\" (the one 1 from the top, and 1 from the left).", 1469 | "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).", 1470 | "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.", 1471 | "hairFlipped": "", 1472 | "hairColor": "On the hair page (second tab), set the hair color to the one 2 from the top.", 1473 | "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.", 1474 | "eyebrowColor": "On the eyebrow page (third tab), set the eyebrow color to the one 2 from the top.", 1475 | "eyebrowY": "", 1476 | "eyebrowSize": "", 1477 | "eyebrowRot": "", 1478 | "eyebrowDist": "", 1479 | "eyebrowSquash": "", 1480 | "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.", 1481 | "eyeColor": "On the eye page (fourth tab), set the color to the one 1 from the top.", 1482 | "eyeY": "", 1483 | "eyeSize": "", 1484 | "eyeRot": "", 1485 | "eyeDist": "", 1486 | "eyeSquash": "", 1487 | "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.", 1488 | "noseY": "", 1489 | "noseSize": "", 1490 | "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.", 1491 | "mouthCol": "On the mouth page (sixth tab), set the color to the one 1 from the top.", 1492 | "mouthY": "", 1493 | "mouthSize": "", 1494 | "mouthSquash": "", 1495 | "glasses": "On the glasses page (within the seventh tab), set the glasses to the one 1 from the top, and 1 from the left.", 1496 | "glassesCol": "On the glasses page (within the seventh tab), set the color to the one 1 from the top.", 1497 | "glassesY": "", 1498 | "glassesSize": "", 1499 | "stache": "On the mustache page (within the seventh tab), set the mustache to the one on the top-left.", 1500 | "stacheY": "", 1501 | "stacheSize": "", 1502 | "mole": "", 1503 | "moleX": "", 1504 | "moleY": "", 1505 | "moleSize": "", 1506 | "beard": "On the beard page (within the seventh tab), set the beard to the one on the top-left.", 1507 | "beardCol": "On the mustache OR beard pages (within the seventh tab), set the color to the one 1 from the top.", 1508 | "heightWeight": "On the build page (eighth tab), set the height to 50%, and the weight to 50%.", 1509 | "col": "On the info page (after pressing \"Next\"), set the Favorite Color to Red (1 from the left, top row)." 1510 | } 1511 | } 1512 | }; 1513 | 1514 | const defaultMii = { 1515 | "male": { 1516 | "general": { 1517 | "type": 3, 1518 | "birthday": 17, 1519 | "birthMonth": 4, 1520 | "height": 0, 1521 | "weight": 0, 1522 | "gender": 1, 1523 | "favoriteColor": 7 1524 | }, 1525 | "meta": { 1526 | "name": "Madison", 1527 | "creatorName": "", 1528 | "console": "3ds" 1529 | }, 1530 | "perms": { 1531 | "sharing": false, 1532 | "copying": true, 1533 | "fromCheckMiiOut": false, 1534 | "mingle": true 1535 | }, 1536 | "hair": { 1537 | "page": 0, 1538 | "type": 7, 1539 | "color": 7, 1540 | "flipped": false 1541 | }, 1542 | "face": { 1543 | "type": 5, 1544 | "color": 0, 1545 | "feature": 0, 1546 | "makeup": 0 1547 | }, 1548 | "eyes": { 1549 | "page": 0, 1550 | "type": 9, 1551 | "col": 4, 1552 | "size": 1, 1553 | "squash": 3, 1554 | "rotation": 4, 1555 | "distanceApart": 3, 1556 | "yPosition": 11 1557 | }, 1558 | "eyebrows": { 1559 | "page": 0, 1560 | "type": 5, 1561 | "color": 7, 1562 | "size": 2, 1563 | "squash": 4, 1564 | "rotation": 4, 1565 | "distanceApart": 4, 1566 | "yPosition": 6 1567 | }, 1568 | "nose": { 1569 | "page": 1, 1570 | "type": 0, 1571 | "size": 0, 1572 | "yPosition": 5 1573 | }, 1574 | "mouth": { 1575 | "page": 1, 1576 | "type": 6, 1577 | "color": 0, 1578 | "size": 2, 1579 | "squash": 3, 1580 | "yPosition": 10 1581 | }, 1582 | "beard": { 1583 | "mustache": { 1584 | "type": 0, 1585 | "size": 4, 1586 | "yPosition": 10 1587 | }, 1588 | "col": 0, 1589 | "type": 0 1590 | }, 1591 | "glasses": { 1592 | "type": 0, 1593 | "color": 0, 1594 | "size": 4, 1595 | "yPosition": 10 1596 | }, 1597 | "mole": { 1598 | "on": false, 1599 | "size": 4, 1600 | "xPosition": 2, 1601 | "yPosition": 20 1602 | } 1603 | }, 1604 | "female": { 1605 | "general": { 1606 | "type": 3, 1607 | "birthday": 17, 1608 | "birthMonth": 4, 1609 | "height": 0, 1610 | "weight": 0, 1611 | "gender": 1, 1612 | "favoriteColor": 7 1613 | }, 1614 | "meta": { 1615 | "name": "Madison", 1616 | "creatorName": "", 1617 | "console": "3ds" 1618 | }, 1619 | "perms": { 1620 | "sharing": false, 1621 | "copying": true, 1622 | "fromCheckMiiOut": false, 1623 | "mingle": true 1624 | }, 1625 | "hair": { 1626 | "page": 0, 1627 | "type": 7, 1628 | "color": 7, 1629 | "flipped": false 1630 | }, 1631 | "face": { 1632 | "type": 5, 1633 | "color": 0, 1634 | "feature": 0, 1635 | "makeup": 0 1636 | }, 1637 | "eyes": { 1638 | "page": 0, 1639 | "type": 9, 1640 | "col": 4, 1641 | "size": 1, 1642 | "squash": 3, 1643 | "rotation": 4, 1644 | "distanceApart": 3, 1645 | "yPosition": 11 1646 | }, 1647 | "eyebrows": { 1648 | "page": 0, 1649 | "type": 5, 1650 | "color": 7, 1651 | "size": 2, 1652 | "squash": 4, 1653 | "rotation": 4, 1654 | "distanceApart": 4, 1655 | "yPosition": 6 1656 | }, 1657 | "nose": { 1658 | "page": 1, 1659 | "type": 0, 1660 | "size": 0, 1661 | "yPosition": 5 1662 | }, 1663 | "mouth": { 1664 | "page": 1, 1665 | "type": 6, 1666 | "color": 0, 1667 | "size": 2, 1668 | "squash": 3, 1669 | "yPosition": 10 1670 | }, 1671 | "beard": { 1672 | "mustache": { 1673 | "type": 0, 1674 | "size": 4, 1675 | "yPosition": 10 1676 | }, 1677 | "col": 0, 1678 | "type": 0 1679 | }, 1680 | "glasses": { 1681 | "type": 0, 1682 | "color": 0, 1683 | "size": 4, 1684 | "yPosition": 10 1685 | }, 1686 | "mole": { 1687 | "on": false, 1688 | "size": 4, 1689 | "xPosition": 2, 1690 | "yPosition": 20 1691 | } 1692 | } 1693 | }; 1694 | 1695 | //Functions for working with the Miis 1696 | function encodeStudio(mii) { 1697 | var n = 0; 1698 | var eo; 1699 | var dest = byteToString(n); 1700 | for (var i = 0; i < mii.length; i++) { 1701 | eo = (7 + (mii[i] ^ n)) & 0xFF; 1702 | n = eo; 1703 | dest += byteToString(eo); 1704 | } 1705 | return dest; 1706 | } 1707 | function decodeStudio(encoded) { 1708 | let bytes; 1709 | if (encoded instanceof Uint8Array) { 1710 | bytes = Array.from(encoded); 1711 | } 1712 | else if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(encoded)) { 1713 | bytes = Array.from(encoded.values()); 1714 | } 1715 | else if (typeof encoded === "string") { 1716 | bytes = new Array(encoded.length); 1717 | for (let i = 0; i < encoded.length; i++) bytes[i] = encoded.charCodeAt(i) & 0xFF; 1718 | } 1719 | else { 1720 | throw new Error("decodeStudio: unsupported input type"); 1721 | } 1722 | if (bytes.length < 2) return new Uint8Array(0); 1723 | 1724 | let n = 0; 1725 | const out = new Uint8Array(bytes.length - 1); 1726 | for (let i = 1; i < bytes.length; i++) { 1727 | const eo = bytes[i] & 0xFF; 1728 | out[i - 1] = ((((eo - 7) & 0xFF) ^ n) & 0xFF); 1729 | n = eo; 1730 | } 1731 | return out; 1732 | } 1733 | function convertMii(jsonIn, typeTo) { 1734 | typeFrom = jsonIn.console?.toLowerCase(); 1735 | if (typeFrom == null || typeTo === typeFrom) { 1736 | return jsonIn; 1737 | } 1738 | let mii = jsonIn; 1739 | var miiTo = structuredClone(mii); 1740 | if (["wii u", "3ds"].includes(typeFrom)) { 1741 | miiTo.perms.mingle = mii.perms.sharing; 1742 | miiTo.perms.fromCheckMiiOut = false; 1743 | miiTo.face.type = convTables.face3DSToWii[mii.face.type]; 1744 | //We prioritize Facial Features here because the Wii supports more of those than they do Makeup types, and is more likely to apply. The 3DS has two separate fields, so you can have makeup and wrinkles applied at the same time. The Wii only has one that covers both. 1745 | if (typeof (convTables.features3DSToWii[mii.face.feature]) === 'string') { 1746 | miiTo.face.feature = convTables.makeup3DSToWii[mii.face.makeup]; 1747 | } 1748 | else { 1749 | miiTo.face.feature = convTables.features3DSToWii[mii.face.feature]; 1750 | } 1751 | miiTo.nose.type = convTables.nose3DSToWii[mii.nose.page][mii.nose.type]; 1752 | miiTo.mouth.type = convTables.mouth3DSToWii[mii.mouth.page][mii.mouth.type]; 1753 | miiTo.mouth.color = mii.mouth.color > 2 ? 0 : mii.mouth.color; 1754 | miiTo.hair.type = convTables.hair3DSToWii[mii.hair.page][mii.hair.type]; 1755 | miiTo.eyebrows.type = convTables.eyebrows3DSToWii[mii.eyebrows.page][mii.eyebrows.type]; 1756 | miiTo.eyes.type = convTables.eyes3DSToWii[mii.eyes.page][mii.eyes.type]; 1757 | miiTo.glasses.color = mii.glasses.color; 1758 | if (miiTo.beard.mustache.type === 4) { 1759 | miiTo.beard.mustache.type = 2; 1760 | } 1761 | else if (miiTo.beard.mustache.type === 5) { 1762 | miiTo.beard.mustache.type = 0; 1763 | miiTo.beard.type = 1; 1764 | } 1765 | if (mii.beard.type > 3) { 1766 | mii.beard.type = 3; 1767 | } 1768 | miiTo.console = "wii"; 1769 | } 1770 | else if (typeFrom === "wii") { 1771 | miiTo.perms.sharing = mii.perms.mingle; 1772 | miiTo.perms.copying = mii.perms.mingle; 1773 | 1774 | // Convert hair 1775 | const hairConv = convTables.hairWiiTo3DS[mii.hair.page][mii.hair.type]; 1776 | miiTo.hair.page = hairConv[0]; 1777 | miiTo.hair.type = hairConv[1]; 1778 | miiTo.hair.color = mii.hair.color; 1779 | miiTo.hair.flipped = mii.hair.flipped; 1780 | 1781 | // Convert face 1782 | miiTo.face.type = convTables.faceWiiTo3DS[mii.face.type]; 1783 | miiTo.face.color = mii.face.color; 1784 | miiTo.face.makeup = 0; 1785 | miiTo.face.feature = 0; 1786 | 1787 | // Handle facial features/makeup 1788 | if (typeof (convTables.featureWiiTo3DS[mii.face.feature]) === 'string') { 1789 | miiTo.face.makeup = +convTables.featureWiiTo3DS[mii.face.feature]; 1790 | } 1791 | else { 1792 | miiTo.face.feature = +convTables.featureWiiTo3DS[mii.face.feature]; 1793 | } 1794 | 1795 | // Convert eyes - preserve page/type structure 1796 | miiTo.eyes.page = mii.eyes.page; 1797 | miiTo.eyes.type = mii.eyes.type; 1798 | miiTo.eyes.color = mii.eyes.color; 1799 | miiTo.eyes.size = mii.eyes.size; 1800 | miiTo.eyes.squash = 3; // Default for 3DS 1801 | miiTo.eyes.rotation = mii.eyes.rotation; 1802 | miiTo.eyes.distanceApart = mii.eyes.distanceApart; 1803 | miiTo.eyes.yPosition = mii.eyes.yPosition; 1804 | 1805 | // Convert eyebrows - preserve page/type structure 1806 | miiTo.eyebrows.page = mii.eyebrows.page; 1807 | miiTo.eyebrows.type = mii.eyebrows.type; 1808 | miiTo.eyebrows.color = mii.eyebrows.color; 1809 | miiTo.eyebrows.size = mii.eyebrows.size; 1810 | miiTo.eyebrows.squash = 3; // Default for 3DS 1811 | miiTo.eyebrows.rotation = mii.eyebrows.rotation; 1812 | miiTo.eyebrows.distanceApart = mii.eyebrows.distanceApart; 1813 | miiTo.eyebrows.yPosition = mii.eyebrows.yPosition; 1814 | 1815 | // Convert nose - preserve page/type structure 1816 | miiTo.nose.page = mii.nose.page || 0; 1817 | miiTo.nose.type = mii.nose.type; 1818 | miiTo.nose.size = mii.nose.size; 1819 | miiTo.nose.yPosition = mii.nose.yPosition; 1820 | 1821 | // Convert mouth - preserve page/type structure 1822 | miiTo.mouth.page = mii.mouth.page; 1823 | miiTo.mouth.type = mii.mouth.type; 1824 | miiTo.mouth.color = mii.mouth.color; 1825 | miiTo.mouth.size = mii.mouth.size; 1826 | miiTo.mouth.squash = 3; // Default for 3DS 1827 | miiTo.mouth.yPosition = mii.mouth.yPosition; 1828 | 1829 | // Convert glasses 1830 | miiTo.glasses.type = mii.glasses.type; 1831 | miiTo.glasses.color = mii.glasses.color; 1832 | miiTo.glasses.size = mii.glasses.size; 1833 | miiTo.glasses.yPosition = mii.glasses.yPosition; 1834 | 1835 | // Convert beard 1836 | miiTo.beard.mustache.type = mii.beard.mustache.type; 1837 | miiTo.beard.mustache.size = mii.beard.mustache.size; 1838 | miiTo.beard.mustache.yPosition = mii.beard.mustache.yPosition; 1839 | miiTo.beard.type = mii.beard.type; 1840 | miiTo.beard.color = mii.beard.color; 1841 | 1842 | // Convert mole 1843 | miiTo.mole.on = mii.mole.on; 1844 | miiTo.mole.size = mii.mole.size; 1845 | miiTo.mole.xPosition = mii.mole.xPosition; 1846 | miiTo.mole.yPosition = mii.mole.yPosition; 1847 | 1848 | // Copy general info 1849 | miiTo.general = { ...mii.general }; 1850 | miiTo.meta = { ...mii.meta }; 1851 | 1852 | miiTo.console = "3DS"; 1853 | } 1854 | return miiTo; 1855 | } 1856 | function convertMiiToStudio(jsonIn) { 1857 | if (!["3ds", "wii u"].includes(jsonIn.console?.toLowerCase())) { 1858 | jsonIn = convertMii(jsonIn); 1859 | } 1860 | var mii = jsonIn; 1861 | var studioMii = new Uint8Array([0x08, 0x00, 0x40, 0x03, 0x08, 0x04, 0x04, 0x02, 0x02, 0x0c, 0x03, 0x01, 0x06, 0x04, 0x06, 0x02, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x04, 0x00, 0x0a, 0x01, 0x00, 0x21, 0x40, 0x04, 0x00, 0x02, 0x14, 0x03, 0x13, 0x04, 0x17, 0x0d, 0x04, 0x00, 0x0a, 0x04, 0x01, 0x09]); 1862 | studioMii[0x16] = mii.general.gender; 1863 | studioMii[0x15] = mii.general.favoriteColor; 1864 | studioMii[0x1E] = mii.general.height; 1865 | studioMii[2] = mii.general.weight; 1866 | studioMii[0x13] = lookupTables.faces.values[mii.face.type]; 1867 | studioMii[0x11] = mii.face.color; 1868 | studioMii[0x14] = mii.face.feature; 1869 | studioMii[0x12] = mii.face.makeup; 1870 | studioMii[0x1D] = lookupTables.hairs.values[mii.hair.page][mii.hair.type]; 1871 | studioMii[0x1B] = mii.hair.color; 1872 | if (!studioMii[0x1B]) studioMii[0x1B] = 8; 1873 | studioMii[0x1C] = mii.hair.flipped ? 1 : 0; 1874 | studioMii[7] = lookupTables.eyes.values[mii.eyes.page][mii.eyes.type]; 1875 | studioMii[4] = mii.eyes.color + 8; 1876 | studioMii[6] = mii.eyes.size; 1877 | studioMii[3] = mii.eyes.squash; 1878 | studioMii[5] = mii.eyes.rotation; 1879 | studioMii[8] = mii.eyes.distanceApart; 1880 | studioMii[9] = mii.eyes.yPosition; 1881 | studioMii[0xE] = lookupTables.eyebrows.values[mii.eyebrows.page][mii.eyebrows.type]; 1882 | studioMii[0xB] = mii.eyebrows.color; 1883 | if (!studioMii[0xB]) studioMii[0xB] = 8; 1884 | studioMii[0xD] = mii.eyebrows.size; 1885 | studioMii[0xA] = mii.eyebrows.squash; 1886 | studioMii[0xC] = mii.eyebrows.rotation; 1887 | studioMii[0xF] = mii.eyebrows.distanceApart; 1888 | studioMii[0x10] = mii.eyebrows.yPosition + 3; 1889 | studioMii[0x2C] = lookupTables.noses.values[mii.nose.page][mii.nose.type]; 1890 | studioMii[0x2B] = mii.nose.size; 1891 | studioMii[0x2D] = mii.nose.yPosition; 1892 | studioMii[0x26] = lookupTables.mouths.values[mii.mouth.page][mii.mouth.type]; 1893 | studioMii[0x24] = mii.mouth.color; 1894 | if (studioMii[0x24] < 4) { 1895 | studioMii[0x24] += 19; 1896 | } else { 1897 | studioMii[0x24] = 0; 1898 | } 1899 | studioMii[0x25] = mii.mouth.size; 1900 | studioMii[0x23] = mii.mouth.squash; 1901 | studioMii[0x27] = mii.mouth.yPosition; 1902 | studioMii[0x29] = mii.beard.mustache.type; 1903 | studioMii[1] = mii.beard.type; 1904 | studioMii[0] = mii.beard.color; 1905 | if (!studioMii[0]) studioMii[0] = 8; 1906 | studioMii[0x28] = mii.beard.mustache.size; 1907 | studioMii[0x2A] = mii.beard.mustache.yPosition; 1908 | studioMii[0x19] = mii.glasses.type; 1909 | studioMii[0x17] = mii.glasses.color; 1910 | if (!studioMii[0x17]) { 1911 | studioMii[0x17] = 8; 1912 | } else if (studioMii[0x17] < 6) { 1913 | studioMii[0x17] += 13; 1914 | } else { 1915 | studioMii[0x17] = 0; 1916 | } 1917 | studioMii[0x18] = mii.glasses.size; 1918 | studioMii[0x1A] = mii.glasses.yPosition; 1919 | studioMii[0x20] = mii.mole.on ? 1 : 0; 1920 | studioMii[0x1F] = mii.mole.size; 1921 | studioMii[0x21] = mii.mole.xPosition; 1922 | studioMii[0x22] = mii.mole.yPosition; 1923 | return encodeStudio(studioMii); 1924 | } 1925 | function convertStudioToMii(input) { 1926 | const s = toRawStudioBytes(input); 1927 | const mii = { 1928 | general: { 1929 | gender: s[0x16], 1930 | favoriteColor: s[0x15], 1931 | height: s[0x1E], 1932 | weight: s[0x02], 1933 | 1934 | //The following is not provided by Studio codes and are hardcoded 1935 | birthday: 0, 1936 | birthMonth: 0 1937 | }, 1938 | 1939 | face: { 1940 | type: find1D(lookupTables.faces.values, s[0x13]), 1941 | color: s[0x11], 1942 | feature: s[0x14], 1943 | makeup: s[0x12] 1944 | }, 1945 | 1946 | hair: (() => { 1947 | const { page, type } = findPageType(lookupTables.hairs.values, s[0x1D]); 1948 | const colorStored = s[0x1B]; 1949 | return { 1950 | page, type, 1951 | color: (colorStored === 8) ? 0 : colorStored, 1952 | flipped: !!s[0x1C] 1953 | }; 1954 | })(), 1955 | 1956 | eyes: (() => { 1957 | const { page, type } = findPageType(lookupTables.eyes.values, s[0x07]); 1958 | return { 1959 | page, type, 1960 | color: (s[0x04] | 0) - 8, 1961 | size: s[0x06], 1962 | squash: s[0x03], 1963 | rotation: s[0x05], 1964 | distanceApart: s[0x08], 1965 | yPosition: s[0x09] 1966 | }; 1967 | })(), 1968 | 1969 | eyebrows: (() => { 1970 | const { page, type } = findPageType(lookupTables.eyebrows.values, s[0x0E]); 1971 | const colorStored = s[0x0B]; 1972 | return { 1973 | page, type, 1974 | color: (colorStored === 8) ? 0 : colorStored, 1975 | size: s[0x0D], 1976 | squash: s[0x0A], 1977 | rotation: s[0x0C], 1978 | distanceApart: s[0x0F], 1979 | yPosition: (s[0x10] | 0) - 3 1980 | }; 1981 | })(), 1982 | 1983 | nose: (() => { 1984 | const { page, type } = findPageType(lookupTables.noses.values, s[0x2C]); 1985 | return { page, type, size: s[0x2B], yPosition: s[0x2D] }; 1986 | })(), 1987 | 1988 | mouth: (() => { 1989 | const { page, type } = findPageType(lookupTables.mouths.values, s[0x26]); 1990 | const stored = s[0x24]; 1991 | const color = (stored >= 19 && stored <= 22) ? (stored - 19) : 4; 1992 | return { 1993 | page, type, color, 1994 | size: s[0x25], 1995 | squash: s[0x23], 1996 | yPosition: s[0x27] 1997 | }; 1998 | })(), 1999 | 2000 | beard: (() => { 2001 | const color = (s[0x00] === 8) ? 0 : s[0x00]; 2002 | return { 2003 | color, 2004 | type: s[0x01], 2005 | mustache: { type: s[0x29], size: s[0x28], yPosition: s[0x2A] } 2006 | }; 2007 | })(), 2008 | 2009 | glasses: (() => { 2010 | const stored = s[0x17]; 2011 | let color; 2012 | if (stored === 8) color = 0; 2013 | else if (stored >= 14 && stored <= 18) color = stored - 13; 2014 | else if (stored === 0) color = 6; 2015 | else color = 0; 2016 | return { 2017 | type: s[0x19], 2018 | color, 2019 | size: s[0x18], 2020 | yPosition: s[0x1A] 2021 | }; 2022 | })(), 2023 | 2024 | mole: { 2025 | on: !!s[0x20], 2026 | size: s[0x1F], 2027 | xPosition: s[0x21], 2028 | yPosition: s[0x22] 2029 | }, 2030 | 2031 | //The rest is unprovided by Studio dumps and is hardcoded 2032 | meta: { 2033 | name: "Studio Mii", 2034 | creatorName: "StudioUser", 2035 | console: "3DS", 2036 | type: "Default" 2037 | }, 2038 | 2039 | perms: { 2040 | sharing: true, 2041 | copying: true 2042 | }, 2043 | 2044 | console: "3DS" 2045 | }; 2046 | 2047 | return mii; 2048 | } 2049 | 2050 | async function readWiiBin(binOrPath) { 2051 | let data; 2052 | if (Buffer.isBuffer(binOrPath)) { 2053 | data = binOrPath; 2054 | } 2055 | else if (/[^01]/ig.test(binOrPath)) { 2056 | data = await fs.promises.readFile(binOrPath); 2057 | } 2058 | else { 2059 | data = Buffer.from(binOrPath); 2060 | } 2061 | var thisMii = { 2062 | general: {}, 2063 | perms: {}, 2064 | meta: {}, 2065 | face: {}, 2066 | nose: {}, 2067 | mouth: {}, 2068 | mole: {}, 2069 | hair: {}, 2070 | eyebrows: {}, 2071 | eyes: {}, 2072 | glasses: {}, 2073 | beard: { 2074 | mustache: {} 2075 | } 2076 | }; 2077 | 2078 | const get = address => getBinaryFromAddress(address, data); 2079 | 2080 | var name = ""; 2081 | for (var i = 0; i < 10; i++) { 2082 | name += data.slice(3 + i * 2, 4 + i * 2) + ""; 2083 | } 2084 | thisMii.meta.name = name.replaceAll("\x00", ""); 2085 | var cname = ""; 2086 | for (var i = 0; i < 10; i++) { 2087 | cname += data.slice(55 + i * 2, 56 + i * 2) + ""; 2088 | } 2089 | thisMii.meta.creatorName = cname.replaceAll("\x00", ""); 2090 | thisMii.general.gender = +get(0x00)[1];//0 for Male, 1 for Female 2091 | thisMii.meta.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); 2092 | switch (thisMii.meta.miiId.slice(0, 3)) { 2093 | case "010": 2094 | thisMii.meta.type = "Special"; 2095 | break; 2096 | case "110": 2097 | thisMii.meta.type = "Foreign"; 2098 | break; 2099 | default: 2100 | thisMii.meta.type = "Default"; 2101 | break; 2102 | } 2103 | thisMii.meta.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); 2104 | var temp = get(0x20); 2105 | thisMii.face.type = parseInt(temp.slice(0, 3), 2);//0-7 2106 | thisMii.face.color = parseInt(temp.slice(3, 6), 2);//0-5 2107 | temp = get(0x21); 2108 | thisMii.face.feature = parseInt(get(0x20).slice(6, 8) + temp.slice(0, 2), 2);//0-11 2109 | thisMii.perms.mingle = temp[5] === "0";//0 for Mingle, 1 for Don't Mingle 2110 | temp = get(0x2C); 2111 | thisMii.nose.type = +getKeyByValue(lookupTables.wiiNoses, parseInt(temp.slice(0, 4), 2)); 2112 | thisMii.nose.size = parseInt(temp.slice(4, 8), 2); 2113 | thisMii.nose.yPosition = parseInt(get(0x2D).slice(0, 5), 2);//From top to bottom, 0-18, default 9 2114 | temp = get(0x2E); 2115 | thisMii.mouth.page = +lookupTables.mouthTable["" + parseInt(temp.slice(0, 5), 2)][0] - 1; 2116 | thisMii.mouth.type = convTables.formatTo[lookupTables.mouthTable["" + parseInt(temp.slice(0, 5), 2)][2] - 1][lookupTables.mouthTable["" + parseInt(temp.slice(0, 5), 2)][1] - 1];//0-23, Needs lookup table 2117 | thisMii.mouth.color = parseInt(temp.slice(5, 7), 2);//0-2, refer to mouthColors array 2118 | temp2 = get(0x2F); 2119 | thisMii.mouth.size = parseInt(temp[7] + temp2.slice(0, 3), 2);//0-8, default 4 2120 | thisMii.mouth.yPosition = parseInt(temp2.slice(3, 8), 2);//0-18, default 9, from top to bottom 2121 | temp = get(0x00); 2122 | var temp2 = get(0x01); 2123 | thisMii.general.birthMonth = parseInt(temp.slice(2, 6), 2); 2124 | thisMii.general.birthday = parseInt(temp.slice(6, 8) + temp2.slice(0, 3), 2); 2125 | thisMii.general.favoriteColor = parseInt(temp2.slice(3, 7), 2);//0-11, refer to cols array 2126 | thisMii.general.height = parseInt(get(0x16), 2);//0-127 2127 | thisMii.general.weight = parseInt(get(0x17), 2);//0-127 2128 | thisMii.perms.fromCheckMiiOut = get(0x21)[7] === "0" ? false : true; 2129 | temp = get(0x34); 2130 | temp2 = get(0x35); 2131 | thisMii.mole.on = temp[0] === "0" ? false : true;//0 for Off, 1 for On 2132 | thisMii.mole.size = parseInt(temp.slice(1, 5), 2);//0-8, default 4 2133 | thisMii.mole.xPosition = parseInt(temp2.slice(2, 7), 2);//0-16, Default 2 2134 | thisMii.mole.yPosition = parseInt(temp.slice(5, 8) + temp2.slice(0, 2), 2);//Top to bottom 2135 | temp = get(0x22); 2136 | temp2 = get(0x23); 2137 | thisMii.hair.page = +lookupTables.hairTable["" + parseInt(temp.slice(0, 7), 2)][0] - 1; 2138 | thisMii.hair.type = +convTables.formatTo[lookupTables.hairTable["" + parseInt(temp.slice(0, 7), 2)][2] - 1][lookupTables.hairTable["" + parseInt(temp.slice(0, 7), 2)][1] - 1];//0-71, Needs lookup table 2139 | thisMii.hair.color = parseInt(temp[7] + temp2.slice(0, 2), 2);//0-7, refer to hairCols array 2140 | thisMii.hair.flipped = temp2[2] === "0" ? false : true; 2141 | temp = get(0x24); 2142 | temp2 = get(0x25); 2143 | thisMii.eyebrows.page = +lookupTables.eyebrowTable["" + parseInt(temp.slice(0, 5), 2)][0] - 1; 2144 | thisMii.eyebrows.type = convTables.formatTo[lookupTables.eyebrowTable["" + parseInt(temp.slice(0, 5), 2)][2] - 1][lookupTables.eyebrowTable["" + parseInt(temp.slice(0, 5), 2)][1] - 1];//0-23, Needs lookup table 2145 | thisMii.eyebrows.rotation = parseInt(temp.slice(6, 8) + temp2.slice(0, 2), 2);//0-11, default varies based on eyebrow type 2146 | temp = get(0x26); 2147 | temp2 = get(0x27); 2148 | thisMii.eyebrows.color = parseInt(temp.slice(0, 3), 2); 2149 | thisMii.eyebrows.size = parseInt(temp.slice(3, 7), 2);//0-8, default 4 2150 | thisMii.eyebrows.yPosition = (parseInt(temp[7] + temp2.slice(0, 4), 2)) - 3;//0-15, default 10 2151 | thisMii.eyebrows.distanceApart = parseInt(temp2.slice(4, 8), 2);//0-12, default 2 2152 | thisMii.eyes.page = +lookupTables.eyeTable[parseInt(get(0x28).slice(0, 6), 2)][0] - 1;//0-47, needs lookup table 2153 | thisMii.eyes.type = convTables.formatTo[lookupTables.eyeTable[parseInt(get(0x28).slice(0, 6), 2)][2] - 1][lookupTables.eyeTable[parseInt(get(0x28).slice(0, 6), 2)][1] - 1];//0-47, needs lookup table 2154 | temp = get(0x29); 2155 | thisMii.eyes.rotation = parseInt(temp.slice(0, 3), 2);//0-7, default varies based on eye type 2156 | thisMii.eyes.yPosition = parseInt(temp.slice(3, 8), 2);//0-18, default 12, top to bottom 2157 | temp = get(0x2A); 2158 | thisMii.eyes.color = parseInt(temp.slice(0, 3), 2);//0-5 2159 | thisMii.eyes.size = parseInt(temp.slice(4, 7), 2);//0-7, default 4 2160 | temp2 = get(0x2B); 2161 | thisMii.eyes.distanceApart = parseInt(temp[7] + temp2.slice(0, 3), 2);//0-12, default 2 2162 | temp = get(0x30); 2163 | thisMii.glasses.type = parseInt(temp.slice(0, 4), 2);//0-8 2164 | thisMii.glasses.color = parseInt(temp.slice(4, 7), 2);//0-5 2165 | temp = get(0x31); 2166 | thisMii.glasses.size = parseInt(temp.slice(0, 3), 2);//0-7, default 4 2167 | thisMii.glasses.yPosition = parseInt(temp.slice(3, 8), 2);//0-20, default 10 2168 | temp = get(0x32); 2169 | temp2 = get(0x33); 2170 | thisMii.beard.mustache.type = parseInt(temp.slice(0, 2), 2);//0-3 2171 | thisMii.beard.type = parseInt(temp.slice(2, 4), 2);//0-3 2172 | thisMii.beard.color = parseInt(temp.slice(4, 7), 2);//0-7 2173 | thisMii.beard.mustache.size = parseInt(temp[7] + temp2.slice(0, 3), 2);//0-30, default 20 2174 | thisMii.beard.mustache.yPosition = parseInt(temp2.slice(3, 8), 2);//0-16, default 2 2175 | thisMii.console = "Wii"; 2176 | return thisMii; 2177 | } 2178 | function decode3DSMii(data) { 2179 | const miiJson = { 2180 | general: {}, 2181 | perms: {}, 2182 | meta: {}, 2183 | face: {}, 2184 | nose: {}, 2185 | mouth: {}, 2186 | mole: {}, 2187 | hair: {}, 2188 | eyebrows: {}, 2189 | eyes: {}, 2190 | glasses: {}, 2191 | beard: { 2192 | mustache: {} 2193 | } 2194 | }; 2195 | const get = address => getBinaryFromAddress(address, data); 2196 | var temp = get(0x18); 2197 | var temp2 = get(0x19); 2198 | miiJson.general.birthday = parseInt(temp2.slice(6, 8) + temp.slice(0, 3), 2); 2199 | miiJson.general.birthMonth = parseInt(temp.slice(3, 7), 2); 2200 | //Handle UTF-16 Names 2201 | var name = ""; 2202 | for (var i = 0x1A; i < 0x2E; i += 2) { 2203 | let lo = data[i]; 2204 | let hi = data[i + 1]; 2205 | if (lo === 0x00 && hi === 0x00) { 2206 | break; 2207 | } 2208 | let codeUnit = (hi << 8) | lo; 2209 | name += String.fromCharCode(codeUnit); 2210 | } 2211 | miiJson.meta.name = name.replace(/\u0000/g, ""); 2212 | var cname = ""; 2213 | for (var i = 0x48; i < 0x5C; i += 2) { 2214 | let lo = data[i]; 2215 | let hi = data[i + 1]; 2216 | if (lo === 0x00 && hi === 0x00) { 2217 | break; 2218 | } 2219 | let codeUnit = (hi << 8) | lo; 2220 | cname += String.fromCharCode(codeUnit); 2221 | } 2222 | miiJson.meta.creatorName = cname.replace(/\u0000/g, ""); 2223 | miiJson.general.height = parseInt(get(0x2E), 2); 2224 | miiJson.general.weight = parseInt(get(0x2F), 2); 2225 | miiJson.general.gender = +temp[7]; 2226 | temp = get(0x30); 2227 | miiJson.perms.sharing = temp[7] === "1" ? false : true; 2228 | miiJson.general.favoriteColor = parseInt(temp2.slice(2, 6), 2); 2229 | miiJson.perms.copying = get(0x01)[7] === "1" ? true : false; 2230 | miiJson.hair.page = lookupTable("hairs", parseInt(get(0x32), 2), true)[0]; 2231 | miiJson.hair.type = lookupTable("hairs", parseInt(get(0x32), 2), true)[1]; 2232 | miiJson.face.type = lookupTable("faces", parseInt(temp.slice(3, 7), 2), false); 2233 | miiJson.face.color = parseInt(temp.slice(0, 3), 2); 2234 | temp = get(0x31); 2235 | miiJson.face.feature = parseInt(temp.slice(4, 8), 2); 2236 | miiJson.face.makeup = parseInt(temp.slice(0, 4), 2); 2237 | temp = get(0x34); 2238 | miiJson.eyes.page = lookupTable("eyes", parseInt(temp.slice(2, 8), 2), true)[0]; 2239 | miiJson.eyes.type = lookupTable("eyes", parseInt(temp.slice(2, 8), 2), true)[1]; 2240 | temp2 = get(0x33); 2241 | miiJson.hair.color = parseInt(temp2.slice(5, 8), 2); 2242 | miiJson.hair.flipped = temp2[4] === "0" ? false : true; 2243 | miiJson.eyes.color = parseInt(get(0x35)[7] + temp.slice(0, 2), 2); 2244 | temp = get(0x35); 2245 | miiJson.eyes.size = parseInt(temp.slice(3, 7), 2); 2246 | miiJson.eyes.squash = parseInt(temp.slice(0, 3), 2); 2247 | temp = get(0x36); 2248 | temp2 = get(0x37); 2249 | miiJson.eyes.rotation = parseInt(temp.slice(3, 8), 2); 2250 | miiJson.eyes.distanceApart = parseInt(temp2[7] + temp.slice(0, 3), 2); 2251 | miiJson.eyes.yPosition = parseInt(temp2.slice(2, 7), 2); 2252 | temp = get(0x38); 2253 | miiJson.eyebrows.page = lookupTable("eyebrows", parseInt(temp.slice(3, 8), 2), true)[0]; 2254 | miiJson.eyebrows.type = lookupTable("eyebrows", parseInt(temp.slice(3, 8), 2), true)[1]; 2255 | miiJson.eyebrows.color = parseInt(temp.slice(0, 3), 2); 2256 | temp = get(0x39); 2257 | miiJson.eyebrows.size = parseInt(temp.slice(4, 8), 2); 2258 | miiJson.eyebrows.squash = parseInt(temp.slice(1, 4), 2); 2259 | temp = get(0x3A); 2260 | miiJson.eyebrows.rotation = parseInt(temp.slice(4, 8), 2); 2261 | temp2 = get(0x3B); 2262 | miiJson.eyebrows.distanceApart = parseInt(temp2[7] + temp.slice(0, 3), 2); 2263 | miiJson.eyebrows.yPosition = parseInt(temp2.slice(2, 7), 2) - 3; 2264 | temp = get(0x3C); 2265 | miiJson.nose.page = lookupTable("noses", parseInt(temp.slice(3, 8), 2), true)[0]; 2266 | miiJson.nose.type = lookupTable("noses", parseInt(temp.slice(3, 8), 2), true)[1]; 2267 | temp2 = get(0x3D); 2268 | miiJson.nose.size = parseInt(temp2[7] + temp.slice(0, 3), 2); 2269 | miiJson.nose.yPosition = parseInt(temp2.slice(2, 7), 2); 2270 | temp = get(0x3E); 2271 | miiJson.mouth.page = lookupTable("mouths", parseInt(temp.slice(2, 8), 2), true)[0]; 2272 | miiJson.mouth.type = lookupTable("mouths", parseInt(temp.slice(2, 8), 2), true)[1]; 2273 | temp2 = get(0x3F); 2274 | miiJson.mouth.color = parseInt(temp2[7] + temp.slice(0, 2), 2); 2275 | miiJson.mouth.size = parseInt(temp2.slice(3, 7), 2); 2276 | miiJson.mouth.squash = parseInt(temp2.slice(0, 3), 2); 2277 | temp = get(0x40); 2278 | miiJson.mouth.yPosition = parseInt(temp.slice(3, 8), 2); 2279 | miiJson.beard.mustache.type = parseInt(temp.slice(0, 3), 2); 2280 | temp = get(0x42); 2281 | miiJson.beard.type = parseInt(temp.slice(5, 8), 2); 2282 | miiJson.beard.color = parseInt(temp.slice(2, 5), 2); 2283 | temp2 = get(0x43); 2284 | miiJson.beard.mustache.size = parseInt(temp2.slice(6, 8) + temp.slice(0, 2), 2); 2285 | miiJson.beard.mustache.yPosition = parseInt(temp2.slice(1, 6), 2); 2286 | temp = get(0x44); 2287 | miiJson.glasses.type = parseInt(temp.slice(4, 8), 2); 2288 | miiJson.glasses.color = parseInt(temp.slice(1, 4), 2); 2289 | temp2 = get(0x45); 2290 | miiJson.glasses.size = parseInt(temp2.slice(5, 8) + temp[0], 2); 2291 | miiJson.glasses.yPosition = parseInt(temp2.slice(0, 5), 2); 2292 | temp = get(0x46); 2293 | miiJson.mole.on = temp[7] === "0" ? false : true; 2294 | miiJson.mole.size = parseInt(temp.slice(3, 7), 2); 2295 | temp2 = get(0x47); 2296 | miiJson.mole.xPosition = parseInt(temp2.slice(6, 8) + temp.slice(0, 3), 2); 2297 | miiJson.mole.yPosition = parseInt(temp2.slice(1, 6), 2); 2298 | miiJson.meta.type = "Default";//qk, Make this actually retrieve MiiID, SystemID, and Mii type 2299 | miiJson.console = "3DS"; 2300 | return miiJson; 2301 | } 2302 | async function read3DSQR(binOrPath, returnDecryptedBin) { 2303 | let qrCode; 2304 | if (Buffer.isBuffer(binOrPath)) {//Buffer 2305 | qrCode = binOrPath; 2306 | } 2307 | else if (/[^01]/ig.test(binOrPath)) {//File path 2308 | var data = await fs.promises.readFile(binOrPath); 2309 | var img = await loadImage(data); 2310 | const canvas = createCanvas(img.width, img.height); 2311 | const ctx = canvas.getContext('2d'); 2312 | ctx.drawImage(img, 0, 0); 2313 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 2314 | qrCode = jsQR(imageData.data, imageData.width, imageData.height)?.binaryData; 2315 | if (!qrCode) { 2316 | console.error("Failed to read QR Code."); 2317 | return; 2318 | } 2319 | } 2320 | else {//String of 0s and 1s 2321 | var d = binOrPath.match(/(0|1){1,8}/g); 2322 | qrCode = []; 2323 | d.forEach(byte => { 2324 | qrCode.push(parseInt(byte, 2)); 2325 | }); 2326 | } 2327 | if (qrCode) { 2328 | var data; 2329 | data = Buffer.from(decodeAesCcm(new Uint8Array(qrCode))); 2330 | if (returnDecryptedBin) { 2331 | return data; 2332 | } 2333 | 2334 | var ret; 2335 | try { 2336 | ret = decode3DSMii(data); 2337 | } 2338 | catch (e) { 2339 | ret = decode3DSMii(qrCode); 2340 | } 2341 | return ret; 2342 | } 2343 | else { 2344 | console.error('Failed to read Mii.'); 2345 | } 2346 | } 2347 | async function renderMiiWithStudio(jsonIn) { 2348 | if (!["3ds", "wii u"].includes(jsonIn.console?.toLowerCase())) { 2349 | jsonIn = convertMii(jsonIn); 2350 | } 2351 | var studioMii = convertMiiToStudio(jsonIn); 2352 | return await downloadImage('https://studio.mii.nintendo.com/miis/image.png?data=' + studioMii + "&width=270&type=face"); 2353 | } 2354 | 2355 | 2356 | function flipPixelsVertically(src, w, h) { 2357 | const dst = new Uint8Array(src.length); 2358 | const row = w * 4; 2359 | for (let y = 0; y < h; y++) { 2360 | const a = y * row, b = (h - 1 - y) * row; 2361 | dst.set(src.subarray(a, a + row), b); 2362 | } 2363 | return dst; 2364 | } 2365 | function invLerp(a, b, v) { return (v - a) / (b - a); } 2366 | function clamp01(t) { return Math.max(0, Math.min(1, t)); } 2367 | function easePow(t, p) { return Math.pow(t, p); } 2368 | // ------------------------------ 2369 | // Small helpers 2370 | // ------------------------------ 2371 | function remap01(v, min = 0, max = 127) { 2372 | const cl = Math.min(max, Math.max(min, +v || 0)); 2373 | return (cl - min) / (max - min || 1); 2374 | } 2375 | function lerp(a, b, t) { return a + (b - a) * t; } 2376 | 2377 | function offsetObjectAlongView(object3D, camera, delta) { 2378 | const forward = new THREE.Vector3(); 2379 | camera.getWorldDirection(forward); // forward 2380 | object3D.position.addScaledVector(forward, -delta); // -delta → toward camera 2381 | } 2382 | 2383 | // Project a pixel Y offset at a given depth into world Y units 2384 | function pixelYToWorldY(camera, depthZ, pixels, viewportHeightPx) { 2385 | // Visible height at depth for a perspective camera: 2386 | const fov = (camera.fov ?? 30) * Math.PI / 180; 2387 | const visibleH = 2 * Math.abs(depthZ) * Math.tan(fov / 2); 2388 | return (pixels / viewportHeightPx) * visibleH; 2389 | } 2390 | 2391 | // Render a specific layer to a pixel buffer (optionally flipY) 2392 | function renderLayerToPixels(renderer, scene, camera, gl, width, height, layerIndex, flipY) { 2393 | const rt = new THREE.WebGLRenderTarget(width, height, { depthBuffer: true, stencilBuffer: false }); 2394 | camera.layers.set(layerIndex); 2395 | renderer.setRenderTarget(rt); 2396 | renderer.clear(true, true, true); 2397 | renderer.render(scene, camera); 2398 | 2399 | const pixels = new Uint8Array(width * height * 4); 2400 | gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); 2401 | 2402 | renderer.setRenderTarget(null); 2403 | rt.dispose(); 2404 | 2405 | if (!flipY) return pixels; 2406 | 2407 | // flipVert 2408 | const rowBytes = width * 4; 2409 | const out = new Uint8Array(pixels.length); 2410 | for (let y = 0; y < height; y++) { 2411 | const src = y * rowBytes; 2412 | const dst = (height - 1 - y) * rowBytes; 2413 | out.set(pixels.subarray(src, src + rowBytes), dst); 2414 | } 2415 | return out; 2416 | } 2417 | 2418 | // Camera fit (unchanged) 2419 | function fitCameraToObject(camera, object3D) { 2420 | const padding = 0.525; 2421 | const box = new THREE.Box3().setFromObject(object3D); 2422 | const size = new THREE.Vector3(); 2423 | const center = new THREE.Vector3(); 2424 | box.getSize(size); 2425 | box.getCenter(center); 2426 | 2427 | const maxSize = Math.max(size.y, size.x / camera.aspect); 2428 | const fov = (camera.fov ?? 30) * Math.PI / 180; 2429 | const dist = (maxSize * padding) / Math.tan(fov / 2); 2430 | 2431 | const dir = new THREE.Vector3(); 2432 | camera.getWorldDirection(dir); // forward 2433 | dir.normalize().multiplyScalar(-dist); // back 2434 | 2435 | camera.position.copy(center).add(dir); 2436 | camera.near = Math.max(0.1, dist - maxSize * 3.0); 2437 | camera.far = dist + maxSize * 3.0; 2438 | camera.lookAt(center); 2439 | camera.updateProjectionMatrix(); 2440 | } 2441 | async function createFFLMiiIcon(data, options, shirtColor, fflRes) { 2442 | options ||= {}; 2443 | const isFullBody = !!options.fullBody; 2444 | 2445 | const width = 450; 2446 | const height = 900; 2447 | const BODY_SCALE_Y_RANGE = [0.55, 1.35]; 2448 | const FULLBODY_CROP_BOTTOM_PX_RANGE = [220, 40]; // [at minYScale, at maxYScale] 2449 | 2450 | const gl = createGL(width, height); 2451 | if (!gl) throw new Error("Failed to create WebGL 1 context"); 2452 | 2453 | // Normalize potential gender inputs; And the body files for females and males have different mesh names for some reason, so adjust for that too 2454 | let shirtMesh = "mesh_1_"; 2455 | if (typeof options.gender === "string") { 2456 | options.gender = options.gender.toLowerCase() === "female" ? "Female" : "Male"; 2457 | } 2458 | else if (typeof options.gender === "number") { 2459 | options.gender = options.gender === 1 ? "Female" : "Male"; 2460 | } 2461 | else { 2462 | options.gender = "Male"; 2463 | } 2464 | if (options.gender === "Female") shirtMesh = "mesh_0_"; 2465 | 2466 | // Fake canvas 2467 | const canvas = { 2468 | width, height, style: {}, 2469 | addEventListener() { }, removeEventListener() { }, 2470 | getContext: (t) => (t === "webgl" ? gl : null), 2471 | }; 2472 | globalThis.self ??= { cancelAnimationFrame: () => { } }; 2473 | 2474 | const renderer = new THREE.WebGLRenderer({ canvas, context: gl, alpha: true }); 2475 | renderer.setSize(width, height, false); 2476 | setIsWebGL1State(!renderer.capabilities.isWebGL2); 2477 | 2478 | // Color mgmt + silence warnings 2479 | THREE.ColorManagement.enabled = true; 2480 | renderer.outputColorSpace = THREE.SRGBColorSpace; 2481 | const _warn = console.warn; 2482 | console.warn = function (...args) { 2483 | const s = String(args[0] ?? ""); 2484 | if (s.includes("ImageUtils.sRGBToLinear(): Unsupported image type")) return; 2485 | if (s.includes("Texture is not power of two")) return; 2486 | return _warn.apply(this, args); 2487 | }; 2488 | 2489 | const scene = new THREE.Scene(); 2490 | scene.background = null; 2491 | 2492 | let ffl, currentCharModel; 2493 | const _realDebug = console.debug; 2494 | console.debug = () => { }; 2495 | 2496 | try { 2497 | // Head (FFL) 2498 | ffl = await initializeFFL(fflRes, ModuleFFL); 2499 | const studioRaw = parseHexOrB64ToUint8Array(data); 2500 | const studioBuffer = Buffer.from(studioRaw); 2501 | currentCharModel = createCharModel(studioBuffer, null, FFLShaderMaterial, ffl.module); 2502 | initCharModelTextures(currentCharModel, renderer); 2503 | 2504 | // Body GLTF (for baking) 2505 | if (typeof GLTFLoader === "undefined" || !GLTFLoader) { 2506 | const mod = await import("three/examples/jsm/loaders/GLTFLoader.js"); 2507 | GLTFLoader = mod.GLTFLoader; 2508 | } 2509 | const absPath = path.resolve(__dirname, `./mii${options.gender}Body.glb`); 2510 | const buf = fs.readFileSync(absPath); 2511 | const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 2512 | const loader = new GLTFLoader(); 2513 | const gltf = await new Promise((res, rej) => 2514 | loader.parse(ab, path.dirname(absPath) + path.sep, res, rej) 2515 | ); 2516 | const body = gltf.scene; 2517 | body.position.y -= 110; 2518 | body.userData.isMiiBody = true; 2519 | 2520 | // Recolor body (bakes into texture) 2521 | var pantsColor=[0x808080,0xFFC000,0x89CFF0,0x913831][["default","special","foreign","favorite","favorited"].indexOf(options.pantsType.toLowerCase())]; 2522 | body.traverse((o) => { 2523 | if (o.isMesh) { 2524 | if (!o.geometry.attributes.normal) o.geometry.computeVertexNormals(); 2525 | const isShirt = o.name === shirtMesh; 2526 | o.material?.dispose?.(); 2527 | o.material = new THREE.MeshLambertMaterial({ 2528 | color: isShirt 2529 | ? [ 2530 | 0xff2400, 0xf08000, 0xffd700, 0xaaff00, 0x008000, 0x0000ff, 2531 | 0x00d7ff, 0xff69b4, 0x7f00ff, 0x6f4e37, 0xffffff, 0x303030, 2532 | ][shirtColor] 2533 | : pantsColor, 2534 | emissive: isShirt ? 0x330000 : 0x222222, 2535 | emissiveIntensity: 0.0, 2536 | side: THREE.DoubleSide, 2537 | }); 2538 | o.material.needsUpdate = true; 2539 | } 2540 | }); 2541 | 2542 | // Graph (only used for framing / body bbox) 2543 | const wholeMii = new THREE.Group(); 2544 | wholeMii.add(body); 2545 | wholeMii.add(currentCharModel.meshes); 2546 | scene.add(wholeMii); 2547 | 2548 | // Layers for baking head/body 2549 | body.traverse(obj => obj.layers?.set(1)); 2550 | currentCharModel.meshes.traverse(obj => obj.layers?.set(2)); // head only on 2 2551 | 2552 | // Camera 2553 | const camera = getCameraForViewType(ViewType.MakeIcon); 2554 | camera.aspect = width / height; 2555 | camera.updateProjectionMatrix(); 2556 | fitCameraToObject(camera, wholeMii); 2557 | 2558 | // --- Body world bounds (for plane sizing/placement) 2559 | const bodyBox = new THREE.Box3().setFromObject(body); 2560 | const bodySize = new THREE.Vector3(); 2561 | const bodyCenter = new THREE.Vector3(); 2562 | bodyBox.getSize(bodySize); 2563 | bodyBox.getCenter(bodyCenter); 2564 | 2565 | // --- BODY BAKE (lights for body only, depend on mode) 2566 | const bakeAmbient = new THREE.AmbientLight(0xffffff, 0.15); 2567 | const bakeRim = new THREE.DirectionalLight( 2568 | 0xffffff, 2569 | 1.5 2570 | ); 2571 | bakeRim.position.set(-3, 7, 1.0); 2572 | bakeAmbient.layers.enable(1); 2573 | bakeRim.layers.enable(1); 2574 | scene.add(bakeAmbient, bakeRim); 2575 | 2576 | // Pass: body layer → pixels (no CPU flip; we'll let Three flip on texture) 2577 | const bodyPixels = renderLayerToPixels(renderer, scene, camera, gl, width, height, /*layer*/1, /*flipY*/false); 2578 | const bodyCanvas = createCanvas(width, height); 2579 | bodyCanvas.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(bodyPixels), width, height), 0, 0); 2580 | 2581 | // Remove bake lights & 3D body; we’ll insert a plane instead 2582 | scene.remove(bakeAmbient, bakeRim); 2583 | wholeMii.remove(body); 2584 | bakeAmbient.dispose?.(); bakeRim.dispose?.(); 2585 | 2586 | // --- BODY PLANE (unlit; texture carries shading) 2587 | const bodyTex = new THREE.CanvasTexture(bodyCanvas); 2588 | bodyTex.colorSpace = THREE.SRGBColorSpace; 2589 | bodyTex.generateMipmaps = false; 2590 | bodyTex.minFilter = THREE.LinearFilter; 2591 | bodyTex.magFilter = THREE.LinearFilter; 2592 | bodyTex.wrapS = THREE.ClampToEdgeWrapping; 2593 | bodyTex.wrapT = THREE.ClampToEdgeWrapping; 2594 | bodyTex.flipY = true; // let Three handle UV-space flip 2595 | bodyTex.premultiplyAlpha = true; 2596 | bodyTex.needsUpdate = true; 2597 | bodyTex.flipY = false; 2598 | 2599 | const planeW = Math.max(1e-4, bodySize.x); 2600 | const planeH = Math.max(1e-4, bodySize.y); 2601 | const planeGeo = new THREE.PlaneGeometry(planeW, planeH); 2602 | const planeMat = new THREE.MeshBasicMaterial({ map: bodyTex, transparent: true, depthWrite: true, depthTest: true }); 2603 | const bodyPlane = new THREE.Mesh(planeGeo, planeMat); 2604 | 2605 | // Place plane at body world center so the neck peg aligns into head 2606 | bodyPlane.position.copy(bodyCenter); 2607 | bodyPlane.layers.set(2); // render with head 2608 | 2609 | // === Apply height/weight scaling in BOTH modes === 2610 | const w01 = remap01(options.weight ?? 64); 2611 | const h01 = remap01(options.height ?? 64); 2612 | const scaleX = lerp(0.55, 1.50, w01); 2613 | const scaleY = lerp(0.55, 1.35, h01); 2614 | bodyPlane.scale.set(scaleX, scaleY, 1); 2615 | 2616 | // --- Auto vertical offset (per-mode) + manual per-mode knob --- 2617 | var tYraw = invLerp(BODY_SCALE_Y_RANGE[0], BODY_SCALE_Y_RANGE[1], scaleY); 2618 | var tY = clamp01(easePow(tYraw, 1)); 2619 | 2620 | const autoRange = [150, 125]; 2621 | 2622 | const autoOffsetYPx = autoRange[0] + (autoRange[1] - autoRange[0]) * tY; 2623 | 2624 | // Manual knobs (mode-specific; falls back to legacy bodyOffsetYPx) 2625 | const manualPx = (options.bodyOffsetYPxFull ?? options.bodyOffsetYPx ?? 0); 2626 | 2627 | const combinedOffsetYPx = Math.round(manualPx + autoOffsetYPx); 2628 | 2629 | // Convert screen-px → world-Y at the plane depth & apply 2630 | if (combinedOffsetYPx) { 2631 | const planeDepthFromCam = bodyPlane.position.clone().sub(camera.position).length(); 2632 | const worldYOffset = pixelYToWorldY(camera, planeDepthFromCam, combinedOffsetYPx, height); 2633 | bodyPlane.position.y += worldYOffset; 2634 | } 2635 | 2636 | 2637 | // Optional depth nudge 2638 | const bodyDepthOffset = Number(options.bodyDepthOffset ?? 0); 2639 | if (bodyDepthOffset) offsetObjectAlongView(bodyPlane, camera, bodyDepthOffset); 2640 | 2641 | scene.add(bodyPlane); 2642 | 2643 | // Ensure head renders with the plane on the same layer and with NO head lights 2644 | currentCharModel.meshes.traverse(o => o.layers?.set(2)); 2645 | 2646 | // Final pass: render layer 2 (head + body plane), then flip pixels for PNG 2647 | camera.layers.set(2); 2648 | renderer.setRenderTarget(null); 2649 | renderer.clear(true, true, true); 2650 | renderer.render(scene, camera); 2651 | 2652 | // Read back & flip to top-left 2653 | const finalPixels = new Uint8Array(width * height * 4); 2654 | gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, finalPixels); 2655 | const upright = flipPixelsVertically(finalPixels, width, height); 2656 | 2657 | // Stage onto a canvas 2658 | const stage = createCanvas(width, height); 2659 | stage.getContext("2d").putImageData( 2660 | new ImageData(new Uint8ClampedArray(upright), width, height), 2661 | 0, 0 2662 | ); 2663 | 2664 | // === FullBody-only: crop from the BOTTOM based on scaleY === 2665 | let cropBottom = 0; 2666 | tYraw = invLerp(BODY_SCALE_Y_RANGE[0], BODY_SCALE_Y_RANGE[1], scaleY); 2667 | tY = clamp01(easePow(tYraw, 1)); 2668 | 2669 | // Interpolate bottom crop across the configured range 2670 | const bottomPx = Math.round( 2671 | FULLBODY_CROP_BOTTOM_PX_RANGE[0] + 2672 | (FULLBODY_CROP_BOTTOM_PX_RANGE[1] - FULLBODY_CROP_BOTTOM_PX_RANGE[0]) * tY 2673 | ); 2674 | 2675 | cropBottom = Math.max(0, Math.min(height - 1, bottomPx + (options.fullBodyCropExtraBottomPx ?? 0))); 2676 | 2677 | // Output with bottom crop applied (no top crop) 2678 | const outH = Math.max(1, isFullBody?height - cropBottom:450); 2679 | const outCanvas = createCanvas(width, outH); 2680 | const ctxOut = outCanvas.getContext("2d"); 2681 | 2682 | // Source: take the top `outH` rows (i.e., drop `cropBottom` pixels at the bottom) 2683 | ctxOut.drawImage( 2684 | stage, 2685 | 0, 0, // sx, sy 2686 | width, outH, // sw, sh 2687 | 0, 0, // dx, dy 2688 | width, outH // dw, dh 2689 | ); 2690 | 2691 | return outCanvas.toBuffer("image/png"); 2692 | 2693 | } catch (err) { 2694 | console.error("Error rendering Mii:", err); 2695 | throw err; 2696 | } finally { 2697 | try { 2698 | currentCharModel?.dispose?.(); 2699 | exitFFL(ffl?.module, ffl?.resourceDesc); 2700 | renderer.dispose(); 2701 | gl.finish(); 2702 | } catch { } 2703 | console.debug = _realDebug; 2704 | } 2705 | } 2706 | 2707 | async function renderMii(jsonIn, options = {}, fflRes = getFFLRes()) { 2708 | if (!["3ds", "wii u"].includes(jsonIn.console?.toLowerCase())) { 2709 | jsonIn = convertMii(jsonIn); 2710 | } 2711 | const studioMii = convertMiiToStudio(jsonIn); 2712 | options = Object.assign(options, { 2713 | gender: jsonIn.general.gender, 2714 | height: jsonIn.general.height, 2715 | weight: jsonIn.general.weight, 2716 | pantsType: jsonIn.meta?.type||"Default" 2717 | }); 2718 | 2719 | return createFFLMiiIcon(studioMii, options, jsonIn.general.favoriteColor, fflRes); 2720 | } 2721 | async function writeWiiBin(jsonIn, outPath) { 2722 | if (jsonIn.console?.toLowerCase() !== "wii") { 2723 | convertMii(jsonIn); 2724 | } 2725 | var mii = jsonIn; 2726 | var miiBin = "0"; 2727 | miiBin += mii.general.gender; 2728 | miiBin += mii.general.birthMonth.toString(2).padStart(4, "0"); 2729 | miiBin += mii.general.birthday.toString(2).padStart(5, "0"); 2730 | miiBin += mii.general.favoriteColor.toString(2).padStart(4, "0"); 2731 | miiBin += '0'; 2732 | for (var i = 0; i < 10; i++) { 2733 | if (i < mii.meta.name.length) { 2734 | miiBin += mii.meta.name.charCodeAt(i).toString(2).padStart(16, "0"); 2735 | } 2736 | else { 2737 | miiBin += "0000000000000000"; 2738 | } 2739 | } 2740 | miiBin += mii.general.height.toString(2).padStart(8, "0"); 2741 | miiBin += mii.general.weight.toString(2).padStart(8, "0"); 2742 | let miiId = ""; 2743 | switch (mii.meta.type) { 2744 | case "Special": 2745 | miiId = "01000110"; 2746 | break; 2747 | case "Foreign": 2748 | miiId = "11000110"; 2749 | break; 2750 | default: 2751 | miiId = "10001001"; 2752 | break; 2753 | } 2754 | for (var i = 0; i < 3; i++) { 2755 | miiId += Math.floor(Math.random() * 255).toString(2).padStart(8, "0"); 2756 | } 2757 | miiBin += miiId; 2758 | miiBin += "11111111".repeat(4);//System ID 2759 | miiBin += mii.face.type.toString(2).padStart(3, "0"); 2760 | miiBin += mii.face.color.toString(2).padStart(3, "0"); 2761 | miiBin += mii.face.feature.toString(2).padStart(4, "0"); 2762 | miiBin += "000"; 2763 | if (mii.perms.mingle && mii.meta.type.toLowerCase() === "special") { 2764 | mii.perms.mingle = false; 2765 | console.warn("A Special Mii cannot have Mingle on and still render on the Wii. Turned Mingle off in the output."); 2766 | } 2767 | miiBin += mii.perms.mingle ? "0" : "1"; 2768 | miiBin += "0"; 2769 | miiBin += mii.perms.fromCheckMiiOut ? "1" : "0"; 2770 | miiBin += (+getKeyByValue(lookupTables.hairTable, `${mii.hair.page + 1}${convTables.formatFrom[mii.hair.type]}`)).toString(2).padStart(7, "0"); 2771 | miiBin += mii.hair.color.toString(2).padStart(3, "0"); 2772 | miiBin += mii.hair.flipped ? "1" : "0"; 2773 | miiBin += "00000"; 2774 | miiBin += (+getKeyByValue(lookupTables.eyebrowTable, `${mii.eyebrows.page + 1}${convTables.formatFrom[mii.eyebrows.type]}`)).toString(2).padStart(5, "0"); 2775 | miiBin += "0"; 2776 | miiBin += mii.eyebrows.rotation.toString(2).padStart(4, "0"); 2777 | miiBin += "000000"; 2778 | miiBin += mii.eyebrows.color.toString(2).padStart(3, "0"); 2779 | miiBin += mii.eyebrows.size.toString(2).padStart(4, "0"); 2780 | miiBin += (mii.eyebrows.yPosition + 3).toString(2).padStart(5, "0"); 2781 | miiBin += mii.eyebrows.distanceApart.toString(2).padStart(4, "0"); 2782 | miiBin += (+getKeyByValue(lookupTables.eyeTable, `${mii.eyes.page + 1}${convTables.formatFrom[mii.eyes.type]}`)).toString(2).padStart(6, "0"); 2783 | miiBin += "00"; 2784 | miiBin += mii.eyes.rotation.toString(2).padStart(3, "0"); 2785 | miiBin += mii.eyes.yPosition.toString(2).padStart(5, "0"); 2786 | miiBin += mii.eyes.color.toString(2).padStart(3, "0"); 2787 | miiBin += "0"; 2788 | miiBin += mii.eyes.size.toString(2).padStart(3, "0"); 2789 | miiBin += mii.eyes.distanceApart.toString(2).padStart(4, "0"); 2790 | miiBin += "00000"; 2791 | miiBin += lookupTables.wiiNoses[mii.nose.type].toString(2).padStart(4, "0"); 2792 | miiBin += mii.nose.size.toString(2).padStart(4, "0"); 2793 | miiBin += mii.nose.yPosition.toString(2).padStart(5, "0"); 2794 | miiBin += "000"; 2795 | miiBin += (+getKeyByValue(lookupTables.mouthTable, `${mii.mouth.page + 1}${convTables.formatFrom[mii.mouth.type]}`)).toString(2).padStart(5, "0"); 2796 | miiBin += mii.mouth.color.toString(2).padStart(2, "0"); 2797 | miiBin += mii.mouth.size.toString(2).padStart(4, "0"); 2798 | miiBin += mii.mouth.yPosition.toString(2).padStart(5, "0"); 2799 | miiBin += mii.glasses.type.toString(2).padStart(4, "0"); 2800 | miiBin += mii.glasses.color.toString(2).padStart(3, "0"); 2801 | miiBin += "0"; 2802 | miiBin += mii.glasses.size.toString(2).padStart(3, "0"); 2803 | miiBin += mii.glasses.yPosition.toString(2).padStart(5, "0"); 2804 | miiBin += mii.beard.mustache.type.toString(2).padStart(2, "0"); 2805 | miiBin += mii.beard.type.toString(2).padStart(2, "0"); 2806 | miiBin += mii.beard.color.toString(2).padStart(3, "0"); 2807 | miiBin += mii.beard.mustache.size.toString(2).padStart(4, "0"); 2808 | miiBin += mii.beard.mustache.yPosition.toString(2).padStart(5, "0"); 2809 | miiBin += mii.mole.on ? "1" : "0"; 2810 | miiBin += mii.mole.size.toString(2).padStart(4, "0"); 2811 | miiBin += mii.mole.yPosition.toString(2).padStart(5, "0"); 2812 | miiBin += mii.mole.xPosition.toString(2).padStart(5, "0"); 2813 | miiBin += "0"; 2814 | for (var i = 0; i < 10; i++) { 2815 | if (i < mii.meta.creatorName.length) { 2816 | miiBin += mii.meta.creatorName.charCodeAt(i).toString(2).padStart(16, "0"); 2817 | } 2818 | else { 2819 | miiBin += "0000000000000000"; 2820 | } 2821 | } 2822 | 2823 | //Writing based on miiBin 2824 | var toWrite = miiBin.match(/.{1,8}/g); 2825 | var buffers = []; 2826 | for (var i = 0; i < toWrite.length; i++) { 2827 | buffers.push(parseInt(toWrite[i], 2)); 2828 | } 2829 | toWrite = Buffer.from(buffers); 2830 | if (outPath) { 2831 | await fs.promises.writeFile(outPath, toWrite); 2832 | } 2833 | else { 2834 | return toWrite; 2835 | } 2836 | } 2837 | async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) { 2838 | //Convert the Mii if it isn't in 3DS format 2839 | if (!["3ds", "wii u"].includes(miiJson.console?.toLowerCase())) { 2840 | miiJson = convertMii(miiJson); 2841 | } 2842 | 2843 | //Make the binary 2844 | var mii = miiJson; 2845 | var miiBin = "00000011"; 2846 | //If Special Miis are being used improperly, fix it and warn the user 2847 | if (mii.meta.type.toLowerCase() === "special" && (mii.console.toLowerCase() === "wii u" || mii.console.toLowerCase() === "wiiu")) { 2848 | mii.meta.type = "Default"; 2849 | console.warn("Wii Us do not work with Special Miis. Reverted to Default Mii."); 2850 | } 2851 | if (mii.perms.sharing && mii.meta.type === "Special") { 2852 | mii.perms.sharing = false; 2853 | console.warn("Cannot have Sharing enabled for Special Miis. Disabled Sharing in the output."); 2854 | } 2855 | miiBin += "0000000"; 2856 | miiBin += mii.perms.copying ? "1" : "0"; 2857 | miiBin += "00000000"; 2858 | miiBin += "00110000"; 2859 | miiBin += "1000101011010010000001101000011100011000110001100100011001100110010101100111111110111100000001110101110001000101011101100000001110100100010000000000000000000000".slice(0, 8 * 8); 2860 | miiBin += mii.meta.type === "Special" ? "0" : "1"; 2861 | miiBin += "0000000"; 2862 | for (var i = 0; i < 3; i++) { 2863 | miiBin += Math.floor(Math.random() * 255).toString(2).padStart(8, "0"); 2864 | } 2865 | miiBin += "0000000001000101011101100000001110100100010000000000000000000000"; 2866 | miiBin += mii.general.birthday.toString(2).padStart(5, "0").slice(2, 5); 2867 | miiBin += mii.general.birthMonth.toString(2).padStart(4, "0"); 2868 | miiBin += mii.general.gender; 2869 | miiBin += "00"; 2870 | miiBin += mii.general.favoriteColor.toString(2).padStart(4, "0"); 2871 | miiBin += mii.general.birthday.toString(2).padStart(5, "0").slice(0, 2); 2872 | for (var i = 0; i < 10; i++) { 2873 | if (i < mii.meta.name.length) { 2874 | let code = mii.meta.name.charCodeAt(i); 2875 | miiBin += (code & 0xFF).toString(2).padStart(8, "0"); 2876 | miiBin += ((code >> 8) & 0xFF).toString(2).padStart(8, "0"); 2877 | } 2878 | else { 2879 | miiBin += "0000000000000000"; 2880 | } 2881 | } 2882 | miiBin += mii.general.height.toString(2).padStart(8, "0"); 2883 | miiBin += mii.general.weight.toString(2).padStart(8, "0"); 2884 | miiBin += mii.face.color.toString(2).padStart(3, "0"); 2885 | miiBin += lookupTables.faces.values[mii.face.type].toString(2).padStart(4, "0"); 2886 | miiBin += mii.perms.sharing ? "0" : "1"; 2887 | miiBin += mii.face.makeup.toString(2).padStart(4, "0"); 2888 | miiBin += mii.face.feature.toString(2).padStart(4, "0"); 2889 | miiBin += lookupTables.hairs.values[mii.hair.page][mii.hair.type].toString(2).padStart(8, "0"); 2890 | miiBin += "0000"; 2891 | miiBin += mii.hair.flipped ? "1" : "0"; 2892 | miiBin += mii.hair.color.toString(2).padStart(3, "0"); 2893 | miiBin += mii.eyes.color.toString(2).padStart(3, "0").slice(1, 3); 2894 | miiBin += lookupTables.eyes.values[mii.eyes.page][mii.eyes.type].toString(2).padStart(6, "0"); 2895 | miiBin += mii.eyes.squash.toString(2).padStart(3, "0"); 2896 | miiBin += mii.eyes.size.toString(2).padStart(4, "0"); 2897 | miiBin += mii.eyes.color.toString(2).padStart(3, "0")[0]; 2898 | miiBin += mii.eyes.distanceApart.toString(2).padStart(4, "0").slice(1, 4); 2899 | miiBin += mii.eyes.rotation.toString(2).padStart(5, "0"); 2900 | miiBin += "00"; 2901 | miiBin += mii.eyes.yPosition.toString(2).padStart(5, "0"); 2902 | miiBin += mii.eyes.distanceApart.toString(2).padStart(4, "0")[0]; 2903 | miiBin += mii.eyebrows.color.toString(2).padStart(3, "0"); 2904 | miiBin += lookupTables.eyebrows.values[mii.eyebrows.page][mii.eyebrows.type].toString(2).padStart(5, "0"); 2905 | miiBin += "0"; 2906 | miiBin += mii.eyebrows.squash.toString(2).padStart(3, "0"); 2907 | miiBin += mii.eyebrows.size.toString(2).padStart(4, "0"); 2908 | miiBin += mii.eyebrows.distanceApart.toString(2).padStart(4, "0").slice(1, 4); 2909 | miiBin += "0"; 2910 | miiBin += mii.eyebrows.rotation.toString(2).padStart(4, "0"); 2911 | miiBin += "00"; 2912 | miiBin += (mii.eyebrows.yPosition + 3).toString(2).padStart(5, "0"); 2913 | miiBin += mii.eyebrows.distanceApart.toString(2).padStart(4, "0")[0]; 2914 | miiBin += mii.nose.size.toString(2).padStart(4, "0").slice(1, 4); 2915 | miiBin += lookupTables.noses.values[mii.nose.page][mii.nose.type].toString(2).padStart(5, "0"); 2916 | miiBin += "00"; 2917 | miiBin += mii.nose.yPosition.toString(2).padStart(5, "0"); 2918 | miiBin += mii.nose.size.toString(2).padStart(4, "0")[0]; 2919 | miiBin += mii.mouth.color.toString(2).padStart(3, "0").slice(1, 3); 2920 | miiBin += lookupTables.mouths.values[mii.mouth.page][mii.mouth.type].toString(2).padStart(6, "0"); 2921 | miiBin += mii.mouth.squash.toString(2).padStart(3, "0"); 2922 | miiBin += mii.mouth.size.toString(2).padStart(4, "0"); 2923 | miiBin += mii.mouth.color.toString(2).padStart(3, "0")[0]; 2924 | miiBin += mii.beard.mustache.type.toString(2).padStart(3, "0"); 2925 | miiBin += mii.mouth.yPosition.toString(2).padStart(5, "0"); 2926 | miiBin += "00000000"; 2927 | miiBin += mii.beard.mustache.size.toString(2).padStart(4, "0").slice(2, 4); 2928 | miiBin += mii.beard.color.toString(2).padStart(3, "0"); 2929 | miiBin += mii.beard.type.toString(2).padStart(3, "0"); 2930 | miiBin += "0"; 2931 | miiBin += mii.beard.mustache.yPosition.toString(2).padStart(5, "0"); 2932 | miiBin += mii.beard.mustache.size.toString(2).padStart(4, "0").slice(0, 2); 2933 | miiBin += mii.glasses.size.toString(2).padStart(4, "0")[3]; 2934 | miiBin += mii.glasses.color.toString(2).padStart(3, "0"); 2935 | miiBin += mii.glasses.type.toString(2).padStart(4, "0"); 2936 | miiBin += "0"; 2937 | miiBin += mii.glasses.yPosition.toString(2).padStart(4, "0"); 2938 | miiBin += mii.glasses.size.toString(2).padStart(4, "0").slice(0, 3); 2939 | miiBin += mii.mole.xPosition.toString(2).padStart(5, "0").slice(2, 5); 2940 | miiBin += mii.mole.size.toString(2).padStart(4, "0"); 2941 | miiBin += mii.mole.on ? "1" : "0"; 2942 | miiBin += "0"; 2943 | miiBin += mii.mole.yPosition.toString(2).padStart(5, "0"); 2944 | miiBin += mii.mole.xPosition.toString(2).padStart(5, "0").slice(0, 2); 2945 | for (var i = 0; i < 10; i++) { 2946 | if (i < mii.meta.creatorName.length) { 2947 | let code = mii.meta.creatorName.charCodeAt(i); 2948 | miiBin += (code & 0xFF).toString(2).padStart(8, "0"); 2949 | miiBin += ((code >> 8) & 0xFF).toString(2).padStart(8, "0"); 2950 | } 2951 | else { 2952 | miiBin += "0000000000000000"; 2953 | } 2954 | } 2955 | //Writing based on the binary 2956 | var toWrite = miiBin.match(/.{1,8}/g); 2957 | var buffers = []; 2958 | for (var i = 0; i < toWrite.length; i++) { 2959 | buffers.push(parseInt(toWrite[i], 2)); 2960 | } 2961 | const buffer = Buffer.from(buffers); 2962 | var encryptedData = Buffer.from(encodeAesCcm(new Uint8Array(buffer))); 2963 | if (returnBin) { 2964 | return encryptedData; 2965 | } 2966 | //Prepare a QR code 2967 | const options = { 2968 | width: 300, 2969 | height: 300, 2970 | data: encryptedData.toString("latin1"), 2971 | image: "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", // 1x1 gif 2972 | dotsOptions: { 2973 | color: "#000000", 2974 | type: "square" 2975 | }, 2976 | backgroundOptions: { 2977 | color: "#ffffff", 2978 | }, 2979 | imageOptions: { 2980 | crossOrigin: "anonymous", 2981 | imageSize: 0.4 // Changes how large center area is 2982 | }, 2983 | qrOptions: { 2984 | errorCorrectionLevel: 'H' 2985 | } 2986 | } 2987 | const qrCodeImage = new QRCodeStyling({ 2988 | jsdom: JSDOM, 2989 | nodeCanvas, 2990 | ...options 2991 | }); 2992 | const qrBuffer = Buffer.from(await qrCodeImage.getRawData("png")) 2993 | 2994 | let miiPNGBuf = null; 2995 | let renderedWithStudio = fflRes === null || fflRes === undefined; 2996 | if (renderedWithStudio) { 2997 | miiPNGBuf = await renderMiiWithStudio(miiJson); 2998 | } 2999 | else { 3000 | miiPNGBuf = await renderMii(miiJson, fflRes); 3001 | } 3002 | const main_img = await Jimp.read(qrBuffer); 3003 | main_img.resize(424, 424, Jimp.RESIZE_NEAREST_NEIGHBOR); // Don't anti-alias the QR code 3004 | 3005 | let miiSize, miiZoomFactor, miiYOffset; 3006 | if (renderedWithStudio) { 3007 | miiSize = 100; 3008 | miiZoomFactor = 1; 3009 | miiYOffset = -15; 3010 | 3011 | } else { 3012 | miiSize = 100; 3013 | miiZoomFactor = 1.25; 3014 | miiYOffset = -5; 3015 | } 3016 | const mii_img = await Jimp.read(miiPNGBuf); 3017 | mii_img.resize(miiSize * miiZoomFactor, miiSize * miiZoomFactor, Jimp.RESIZE_BICUBIC); 3018 | mii_img.crop( 3019 | (miiSize * miiZoomFactor - 100) / 2, 3020 | (miiSize * miiZoomFactor - 100) / 2, 3021 | miiSize, 3022 | miiSize 3023 | ); 3024 | 3025 | const canvas = new Jimp(mii_img.bitmap.width, mii_img.bitmap.height, 0xFFFFFFFF); 3026 | canvas.composite(mii_img, 0, miiYOffset); 3027 | main_img.blit(canvas, 212 - 100 / 2, 212 - 100 / 2); 3028 | const font = await Jimp.loadFont(Jimp.FONT_SANS_16_BLACK) 3029 | 3030 | main_img.print(font, 0, 70, { 3031 | text: miiJson.meta.name, 3032 | alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER, 3033 | alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE 3034 | }, 424, 395); 3035 | 3036 | if (miiJson.meta.type === "Special") { 3037 | const crown_img = await Jimp.read(path.join(__dirname, 'crown.jpg')); 3038 | crown_img.resize(40, 20); 3039 | main_img.blit(crown_img, 225, 160); 3040 | } 3041 | 3042 | // Get the buffer 3043 | const imageBuffer = await main_img.getBufferAsync(Jimp.MIME_PNG); 3044 | 3045 | // Optionally write to file if outPath is provided 3046 | if (outPath) { 3047 | await main_img.writeAsync(outPath); 3048 | } 3049 | 3050 | return imageBuffer; 3051 | } 3052 | function make3DSChild(dad, mom, options = {}) { 3053 | if (!["3ds", "wii u"].includes(dad.meta.console?.toLowerCase())) { 3054 | dad = convertMii(dad, "wii"); 3055 | } 3056 | if (!["3ds", "wii u"].includes(mom.meta.console?.toLowerCase())) { 3057 | mom = convertMii(dad, "wii"); 3058 | } 3059 | var g = options.gender || Math.floor(Math.random() * 2); 3060 | var child = { 3061 | "general": { 3062 | "birthMonth": new Date().getMonth() + 1, 3063 | "birthday": new Date().getDay(), 3064 | "height": 64, 3065 | "weight": 64, 3066 | "gender": g, 3067 | "favColor": options.favColor || favCols[Math.floor(Math.random() * favCols.length)] 3068 | }, 3069 | "meta": { 3070 | "name": options.name || kidNames[g][Math.floor(Math.random() * kidNames[g].length)], 3071 | "creatorName": "", 3072 | }, 3073 | "perms": { 3074 | "sharing": true, 3075 | "copying": true 3076 | }, 3077 | "hair": { 3078 | "page": 8,//Hardcoded, needs to be generated 3079 | "type": 3,// ^ 3080 | "color": Math.floor(Math.random() * 2) === 1 ? dad.hair.color : mom.hair.color, 3081 | "flipped": Math.floor(Math.random() * 2) === 0 ? true : false 3082 | }, 3083 | "face": { 3084 | "shape": Math.floor(Math.random() * 2) === 1 ? dad.face.shape : mom.face.shape, 3085 | "feature": Math.floor(Math.random() * 2) === 1 ? dad.face.feature : mom.face.feature, 3086 | "makeup": g === 0 ? 0 : Math.floor(Math.random() * 2) === 1 ? dad.face.makeup : mom.face.makeup 3087 | }, 3088 | "eyes": Math.floor(Math.random() * 2) === 1 ? dad.eyes : mom.eyes, 3089 | "eyebrows": Math.floor(Math.random() * 2) === 1 ? dad.eyebrows : mom.eyebrows, 3090 | "nose": Math.floor(Math.random() * 2) === 1 ? dad.nose : mom.nose, 3091 | "mouth": Math.floor(Math.random() * 2) === 1 ? dad.mouth : mom.mouth, 3092 | "beard": {//Beards can never be generated for children allegedly, confirm before finishing 3093 | "mustache": { 3094 | "type": 0, 3095 | "mustacheSize": 4, 3096 | "mustacheYPos": 10 3097 | }, 3098 | "type": 0, 3099 | "color": 0 3100 | }, 3101 | "glasses": Math.floor(Math.random() * 2) === 1 ? dad.glasses : mom.glasses, 3102 | "mole": Math.floor(Math.random() * 2) === 1 ? dad.mole : mom.mole 3103 | }; 3104 | child.eyebrows.color = child.hair.color; 3105 | var c = [mom.face.color, dad.face.color]; 3106 | if (c[0] > c[1]) { 3107 | c[1] = c[0]; 3108 | c[0] = dad.face.color; 3109 | } 3110 | child.face.color = c[0] + Math.round((c[1] - c[0]) / 2); 3111 | child.type = "3DS"; 3112 | return child; 3113 | } 3114 | function generateInstructions(mii, full) { 3115 | let type = mii.console?.toLowerCase(); 3116 | if (type.toLowerCase() === "wii") { 3117 | var typeCheat = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]; 3118 | var instrs = { 3119 | "base": `Select "${mii.general.gender}", and then "Start from Scratch".`, 3120 | "col": `On the info page (first tab), set the Favorite Color to ${lookupTables.favCols[mii.general.favoriteColor]} (${mii.general.favoriteColor <= 5 ? mii.general.favoriteColor + 1 : mii.general.favoriteColor - 5} from the left, ${mii.general.favoriteColor > 5 ? "bottom" : "top"} row).`, 3121 | "heightWeight": `On the build page (second tab), set the height to ${Math.round((100 / 128) * mii.general.height)}%, and the weight to ${Math.round((100 / 128) * mii.general.weight)}%.`, 3122 | "faceShape": `On the face page (third tab), set the shape to the one ${Math.floor(mii.face.type / 2) + 1} from the top, in the ${mii.face.type % 2 === 0 ? "left" : "right"} column.`, 3123 | "skinCol": `On the face page (third tab), set the color to the one ${mii.face.color + mii.face.color > 2 ? -2 : 1} from the left, on the ${mii.face.color > 2 ? `bottom` : `top`} row.`, 3124 | "makeup": `On the face page's makeup tab, set the makeup to the one ${Math.ceil((mii.face.feature + 1) / 3)} from the top, and ${typeCheat[mii.face.feature]} from the left.`, 3125 | "hairStyle": `On the hair page (fourth tab), set the hair style to the one ${typeCheat[mii.hair.type]} from the left, ${Math.ceil((mii.hair.type + 1) / 3)} from the top, on page ${mii.hair.page}.`, 3126 | "hairFlipped": `${mii.hair.flipped ? `On the hair page (fourth tab), press the button to flip the hair.` : ``}`, 3127 | "hairColor": `On the hair page (fourth tab), set the hair color to the one ${mii.hair.col + (mii.hair.col > 3 ? -3 : 1)} from the left, on the ${mii.hair.col > 3 ? `bottom` : `top`} row.`, 3128 | "eyebrowStyle": `On the eyebrow page (fifth tab), set the eyebrow style to the one ${typeCheat[mii.eyebrows.type]} from the left, ${Math.ceil((mii.eyebrows.type + 1) / 3)} from the top, on page ${mii.eyebrows.page}.`, 3129 | "eyebrowColor": `On the eyebrow page (fifth tab), set the eyebrow color to the one ${mii.eyebrows.color + (mii.eyebrows.color > 3 ? -3 : 1)} from the left, on the ${mii.eyebrows.color > 3 ? `bottom` : `top`} row.`, 3130 | "eyebrowY": `${mii.eyebrows.yPos !== 7 ? `On the eyebrow page (fifth tab), ` : ``}${mii.eyebrows.yPosition < 7 ? `press the up button ${7 - mii.eyebrows.yPosition} times.` : mii.eyebrows.yPosition > 7 ? `press the down button ${mii.eyebrows.yPosition - 7} times.` : ``}`, 3131 | "eyebrowSize": `${mii.eyebrows.size !== 4 ? `On the eyebrow page (fifth tab), ` : ``}${mii.eyebrows.size < 4 ? `press the shrink button ${4 - mii.eyebrows.size} times.` : mii.eyebrows.size > 4 ? `press the enlarge button ${mii.eyebrows.size - 4} times.` : ``}`, 3132 | "eyebrowRot": `${mii.eyebrows.rotation !== 6 ? `On the eyebrow page (fifth tab), ` : ``}${mii.eyebrows.rotation < 6 ? `press the rotate clockwise button ${6 - mii.eyebrows.rotation} times.` : mii.eyebrows.rotation > 6 ? `press the rotate counter-clockwise button ${mii.eyebrows.rotation - 6} times.` : ``}`, 3133 | "eyebrowDist": `${mii.eyebrows.distApart !== 2 ? `On the eyebrow page (fifth tab), ` : ``}${mii.eyebrows.distanceApart < 2 ? `press the closer-together button ${2 - mii.eyebrows.distanceApart} times.` : mii.eyebrows.distanceApart > 2 ? `press the further-apart button ${mii.eyebrows.distanceApart - 2} times.` : ``}`, 3134 | "eyeType": `On the eye page (sixth tab), set the eye type to the one ${typeCheat[mii.eyes.type]} from the left, ${Math.ceil((mii.eyes.type + 1) / 3)} from the top, on page ${mii.eyes.page}.`, 3135 | "eyeColor": `On the eye page (sixth tab), set the color to the one ${mii.eyes.color + (mii.eyes.color > 2 ? -2 : 1)} from the left, on the ${mii.eyes.color > 2 ? `bottom` : `top`} row.`, 3136 | "eyeY": `${mii.eyes.yPos !== 12 ? `On the eye page (sixth tab), ` : ``}${mii.eyes.yPosition < 12 ? `press the up button ${12 - mii.eyes.yPosition} times.` : mii.eyes.yPosition > 12 ? `press the down button ${mii.eyes.yPosition - 12} times.` : ``}`, 3137 | "eyeSize": `${mii.eyes.size !== 4 ? `On the eye page (sixth tab), ` : ``}${mii.eyes.size < 4 ? `press the shrink button ${4 - mii.eyes.size} times.` : mii.eyes.size > 4 ? `press the enlarge button ${mii.eyes.size - 4} times.` : ``}`, 3138 | "eyeRot": `${mii.eyes.rotation !== (mii.general.gender === "Female" ? 3 : 4) ? `On the eye page (sixth tab), ` : ``}${mii.eyes.rotation < (mii.general.gender === "Female" ? 3 : 4) ? `press the rotate clockwise button ${(mii.general.gender === "Female" ? 3 : 4) - mii.eyes.rotation} times.` : mii.eyes.rotation > (mii.general.gender === "Female" ? 3 : 4) ? `press the rotate counter-clockwise button ${mii.eyes.rotation - (mii.general.gender === "Female" ? 3 : 4)} times.` : ``}`, 3139 | "eyeDist": `${mii.eyes.distanceApart !== 2 ? `On the eye page (sixth tab), ` : ``}${mii.eyes.distanceApart < 2 ? `press the closer-together button ${2 - mii.eyes.distanceApart} times.` : mii.eyes.distanceApart > 2 ? `press the further-apart button ${mii.eyes.distanceApart - 2} times.` : ``}`, 3140 | "noseType": `On the nose page (seventh tab), set the nose to the one ${Math.ceil((mii.nose.type + 1) / 3)} from the top, and ${typeCheat[mii.nose.type]} from the left.`, 3141 | "noseY": `${mii.nose.yPosition !== 9 ? `On the nose page (seventh tab), ` : ``}${mii.nose.yPosition < 9 ? `press the up button ${9 - mii.nose.yPosition} times.` : mii.nose.yPosition > 9 ? `press the down button ${mii.nose.yPosition - 9} times.` : ``}`, 3142 | "noseSize": `${mii.nose.size !== 4 ? `On the nose page (seventh tab), ` : ``}${mii.nose.size < 4 ? `press the shrink button ${4 - mii.nose.size} times.` : mii.nose.size > 4 ? `press the enlarge button ${mii.nose.size - 4} times.` : ``}`, 3143 | "mouthType": `On the mouth page (eighth tab), set the mouth type to the one ${typeCheat[mii.mouth.type]} from the left, ${Math.ceil((mii.mouth.type + 1) / 3)} from the top, on page ${mii.mouth.page}.`, 3144 | "mouthCol": `On the mouth page (eighth tab), set the color to the one ${mii.mouth.col + 1} from the left.`, 3145 | "mouthY": `${mii.mouth.yPosition !== 13 ? `On the mouth page (eighth tab), ` : ``}${mii.mouth.yPosition < 13 ? `press the up button ${13 - mii.mouth.yPosition} times.` : mii.mouth.yPosition > 13 ? `press the down button ${mii.mouth.yPosition - 13} times.` : ``}`, 3146 | "mouthSize": `${mii.mouth.size !== 4 ? `On the mouth page (eighth tab), ` : ``}${mii.mouth.size < 4 ? `press the shrink button ${4 - mii.mouth.size} times.` : mii.mouth.size > 4 ? `press the enlarge button ${mii.mouth.size - 4} times.` : ``}`, 3147 | "glasses": `On the glasses page (within the ninth tab), set the glasses to the one ${Math.ceil((mii.glasses.type + 1) / 3)} from the top, and ${typeCheat[mii.glasses.type]} from the left.`, 3148 | "glassesCol": `On the glasses page (within the ninth tab), set the color to the one ${mii.glasses.color + (mii.glasses.color > 2 ? -2 : 1)} from the left, on the ${mii.glasses.color > 2 ? `bottom` : `top`} row.`, 3149 | "glassesY": `${mii.glasses.yPosition !== 10 ? `On the glasses page (within the ninth tab), ` : ``}${mii.glasses.yPosition < 10 ? `press the up button ${10 - mii.glasses.yPosition} times.` : mii.glasses.yPosition > 10 ? `press the down button ${mii.glasses.yPosition - 10} times.` : ``}`, 3150 | "glassesSize": `${mii.glasses.size !== 4 ? `On the glasses page (within the ninth tab), ` : ``}${mii.glasses.size < 4 ? `press the shrink button ${4 - mii.glasses.size} times.` : mii.glasses.size > 4 ? `press the enlarge button ${mii.glasses.size - 4} times.` : ``}`, 3151 | "stache": `On the mustache page (within the ninth tab), set the mustache to the one on the ${[0, 1].includes(mii.beard.mustache.type) ? `top` : `bottom`}-${[0, 2].includes(mii.beard.mustache.type) ? `left` : `right`}.`, 3152 | "stacheY": `${mii.beard.mustache.yPosition !== 10 ? `On the mustache page (within the ninth tab), press the ` : ``}${mii.beard.mustache.yPos > 10 ? `down button ${mii.beard.mustache.yPos - 10} times.` : mii.beard.mustache.yPos < 10 ? `up button ${10 - mii.beard.mustache.yPos} times.` : ``}`, 3153 | "stacheSize": `${mii.beard.mustache.size !== 4 ? `On the mustache page (within the ninth tab), ` : ``}${mii.beard.mustache.size < 4 ? `press the shrink button ${4 - mii.beard.mustache.size} times.` : mii.beard.mustache.size > 4 ? `press the enlarge button ${mii.beard.mustache.size - 4} times.` : ``}`, 3154 | "mole": `${mii.mole.on ? `On the mole page (within the ninth tab), turn the mole on.` : ``}`, 3155 | "moleX": `${mii.mole.xPosition !== 2 ? `On the mole page (within the ninth tab), press the ` : ``}${mii.mole.xPosition > 2 ? `right button ${mii.mole.xPosition - 2} times.` : mii.mole.xPosition < 2 ? `left button ${2 - mii.mole.xPosition} times.` : ``}`, 3156 | "moleY": `${mii.mole.yPosition !== 20 ? `On the mole page (within the ninth tab), press the ` : ``}${mii.mole.yPosition > 20 ? `down button ${mii.mole.yPosition - 20} times.` : mii.mole.yPosition < 20 ? `up button ${20 - mii.mole.yPosition} times.` : ``}`, 3157 | "moleSize": `${mii.mole.size !== 4 ? `On the mole page (within the ninth tab), ` : ``}${mii.mole.size < 4 ? `press the shrink button ${4 - mii.mole.size} times.` : mii.mole.size > 4 ? `press the enlarge button ${mii.mole.size - 4} times.` : ``}`, 3158 | "beard": `On the beard page (within the ninth tab), set the beard to the one on the ${[0, 1].includes(mii.beard.type) ? `top` : `bottom`}-${[0, 2].includes(mii.beard.type) ? `left` : `right`}.`, 3159 | "beardCol": `On the mustache OR beard pages (within the ninth tab), set the color to the one ${mii.beard.col + (mii.beard.col > 3 ? -3 : 1)} from the left, on the ${mii.facialHair.col > 3 ? `bottom` : `top`} row.`, 3160 | "other": `The Nickname of this Mii is ${mii.info.name}.${mii.info.creatorName ? ` The creator was ${mii.info.creatorName}.` : ``} Mingle was turned ${mii.info.mingle ? `on` : `off`}.${mii.info.birthday !== 0 ? ` Its birthday is ${["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][mii.info.birthMonth]} ${mii.info.birthday}.` : ``}` 3161 | }; 3162 | if (!full) { 3163 | var defaultMiiInstrs = structuredClone(mii.general.gender === "Male" ? defaultInstrs.wii.male : defaultInstrs.wii.female); 3164 | Object.keys(instrs).forEach(instr => { 3165 | if (instrs[instr] === defaultMiiInstrs[instr]) { 3166 | delete instrs[instr]; 3167 | } 3168 | }); 3169 | } 3170 | return instrs; 3171 | } 3172 | else { 3173 | var instrs = { 3174 | "base": `Select "Start from Scratch", and then "${mii.general.gender}".`, 3175 | "faceShape": `On the face page (first tab), set the face shape to the one ${Math.ceil((mii.face.type + 1) / 3)} from the top, and ${typeCheat[mii.face.type]} from the left.`, 3176 | "skinCol": `On the face page (first tab), set the color to the one ${mii.face.color + 1} from the top.`, 3177 | "makeup": `On the face page's makeup tab, set the makeup to the one ${Math.ceil((mii.face.makeup + 1) / 3)} from the top, and ${typeCheat[mii.face.makeup]} from the left.`, 3178 | "feature": `On the face page's wrinkles tab, set the facial feature to the one ${Math.ceil((mii.face.feature + 1) / 3) + 1} from the top, and ${typeCheat[mii.face.makeup]} from the left.`, 3179 | "hairStyle": `On the hair page (second tab), set the hair style to the one ${Math.ceil((mii.hair.type + 1) / 3)} from the top, and ${typeCheat[mii.hair.type]} from the left, on page ${mii.hair.page + 1}.`, 3180 | "hairFlipped": `${mii.hair.flipped ? `On the hair page (second tab), press the button to flip the hair.` : ``}`, 3181 | "hairColor": `On the hair page (second tab), set the hair color to the one ${mii.hair.color + 1} from the top.`, 3182 | "eyebrowStyle": `On the eyebrow page (third tab), set the eyebrow style to the one ${typeCheat[mii.eyebrows.type]} from the left, ${Math.ceil((mii.eyebrows.type + 1) / 3)} from the top, on page ${mii.eyebrows.page + 1}.`, 3183 | "eyebrowColor": `On the eyebrow page (third tab), set the eyebrow color to the one ${mii.eyebrows.color + 1} from the top.`, 3184 | "eyebrowY": `${mii.eyebrows.yPosition !== 7 ? `On the eyebrow page (third tab), ` : ``}${mii.eyebrows.yPosition < 7 ? `press the up button ${7 - mii.eyebrows.yPosition} times.` : mii.eyebrows.yPosition > 7 ? `press the down button ${mii.eyebrows.yPosition - 7} times.` : ``}`, 3185 | "eyebrowSize": `${mii.eyebrows.size !== 4 ? `On the eyebrow page (third tab), ` : ``}${mii.eyebrows.size < 4 ? `press the shrink button ${4 - mii.eyebrows.size} times.` : mii.eyebrows.size > 4 ? `press the enlarge button ${mii.eyebrows.size - 4} times.` : ``}`, 3186 | "eyebrowRot": `${mii.eyebrows.rotation !== 6 ? `On the eyebrow page (third tab), ` : ``}${mii.eyebrows.rotation < 6 ? `press the rotate clockwise button ${6 - mii.eyebrows.rotation} times.` : mii.eyebrows.rotation > 6 ? `press the rotate counter-clockwise button ${mii.eyebrows.rotation - 6} times.` : ``}`, 3187 | "eyebrowDist": `${mii.eyebrows.distanceApart !== 2 ? `On the eyebrow page (third tab), ` : ``}${mii.eyebrows.distanceApart < 2 ? `press the closer-together button ${2 - mii.eyebrows.distanceApart} times.` : mii.eyebrows.distanceApart > 2 ? `press the further-apart button ${mii.eyebrows.distanceApart - 2} times.` : ``}`, 3188 | "eyebrowSquash": `${mii.eyebrows.squash !== 3 ? `On the eyebrow page (third tab), ` : ``}${mii.eyebrows.squash < 3 ? `press the squish button ${3 - mii.eyebrows.squash} times.` : mii.eyebrows.squash > 3 ? `press the un-squish button ${mii.eyebrows.squash - 3} times.` : ``}`, 3189 | "eyeType": `On the eye page (fourth tab), set the eye type to the one ${typeCheat[mii.eyes.type]} from the left, ${Math.ceil((mii.eyes.type + 1) / 3)} from the top, on page ${mii.eyes.page + 1}.`, 3190 | "eyeColor": `On the eye page (fourth tab), set the color to the one ${mii.eyes.col + 1} from the top.`, 3191 | "eyeY": `${mii.eyes.yPosition !== 12 ? `On the eye page (fourth tab), ` : ``}${mii.eyes.yPosition < 12 ? `press the up button ${12 - mii.eyes.yPosition} times.` : mii.eyes.yPosition > 12 ? `press the down button ${mii.eyes.yPosition - 12} times.` : ``}`, 3192 | "eyeSize": `${mii.eyes.size !== 4 ? `On the eye page (fourth tab), ` : ``}${mii.eyes.size < 4 ? `press the shrink button ${4 - mii.eyes.size} times.` : mii.eyes.size > 4 ? `press the enlarge button ${mii.eyes.size - 4} times.` : ``}`, 3193 | "eyeRot": `${mii.eyes.rotation !== (mii.general.gender === "Female" ? 3 : 4) ? `On the eye page (fourth tab), ` : ``}${mii.eyes.rotation < (mii.general.gender === "Female" ? 3 : 4) ? `press the rotate clockwise button ${(mii.general.gender === "Female" ? 3 : 4) - mii.eyes.rotation} times.` : mii.eyes.rotation > (mii.general.gender === "Female" ? 3 : 4) ? `press the rotate counter-clockwise button ${mii.eyes.rotation - (mii.general.gender === "Female" ? 3 : 4)} times.` : ``}`, 3194 | "eyeDist": `${mii.eyes.distanceApart !== 2 ? `On the eye page (fourth tab), ` : ``}${mii.eyes.distanceApart < 2 ? `press the closer-together button ${2 - mii.eyes.distanceApart} times.` : mii.eyes.distanceApart > 2 ? `press the further-apart button ${mii.eyes.distanceApart - 2} times.` : ``}`, 3195 | "eyeSquash": `${mii.eyes.squash !== 3 ? `On the eye page (fourth tab), ` : ``}${mii.eyes.squash < 3 ? `press the squish button ${3 - mii.eyes.squash} times.` : mii.eyes.squash > 3 ? `press the un-squish button ${mii.eyes.squash - 3} times.` : ``}`, 3196 | "noseType": `On the nose page (fifth tab), set the nose to the one ${Math.ceil((mii.nose.type + 1) / 3)} from the top, and ${typeCheat[mii.nose.type]} from the left, on page ${mii.nose.page}.`, 3197 | "noseY": `${mii.nose.yPosition !== 9 ? `On the nose page (fifth tab), ` : ``}${mii.nose.yPosition < 9 ? `press the up button ${9 - mii.nose.yPosition} times.` : mii.nose.yPosition > 9 ? `press the down button ${mii.nose.yPosition - 9} times.` : ``}`, 3198 | "noseSize": `${mii.nose.size !== 4 ? `On the nose page (fifth tab), ` : ``}${mii.nose.size < 4 ? `press the shrink button ${4 - mii.nose.size} times.` : mii.nose.size > 4 ? `press the enlarge button ${mii.nose.size - 4} times.` : ``}`, 3199 | "mouthType": `On the mouth page (sixth tab), set the mouth type to the one ${typeCheat[mii.mouth.type]} from the left, ${Math.ceil((mii.mouth.type + 1) / 3)} from the top, on page ${mii.mouth.page + 1}.`, 3200 | "mouthCol": `On the mouth page (sixth tab), set the color to the one ${mii.mouth.color + 1} from the top.`, 3201 | "mouthY": `${mii.mouth.yPosition !== 13 ? `On the mouth page (sixth tab), ` : ``}${mii.mouth.yPosition < 13 ? `press the up button ${13 - mii.mouth.yPosition} times.` : mii.mouth.yPosition > 13 ? `press the down button ${mii.mouth.yPosition - 13} times.` : ``}`, 3202 | "mouthSize": `${mii.mouth.size !== 4 ? `On the mouth page (sixth tab), ` : ``}${mii.mouth.size < 4 ? `press the shrink button ${4 - mii.mouth.size} times.` : mii.mouth.size > 4 ? `press the enlarge button ${mii.mouth.size - 4} times.` : ``}`, 3203 | "mouthSquash": `${mii.mouth.squash !== 3 ? `On the mouth page (sixth tab), ` : ``}${mii.mouth.squash < 3 ? `press the squish button ${3 - mii.mouth.squash} times.` : mii.mouth.squash > 3 ? `press the un-squish button ${mii.mouth.squash - 3} times.` : ``}`, 3204 | "glasses": `On the glasses page (within the seventh tab), set the glasses to the one ${Math.ceil((mii.glasses.type + 1) / 3)} from the top, and ${typeCheat[mii.glasses.type]} from the left.`, 3205 | "glassesCol": `On the glasses page (within the seventh tab), set the color to the one ${mii.glasses.col + 1} from the top.`, 3206 | "glassesY": `${mii.glasses.yPosition !== 10 ? `On the glasses page (within the seventh tab), ` : ``}${mii.glasses.yPosition < 10 ? `press the up button ${10 - mii.glasses.yPosition} times.` : mii.glasses.yPosition > 10 ? `press the down button ${mii.glasses.yPosition - 10} times.` : ``}`, 3207 | "glassesSize": `${mii.glasses.size !== 4 ? `On the glasses page (within the seventh tab), ` : ``}${mii.glasses.size < 4 ? `press the shrink button ${4 - mii.glasses.size} times.` : mii.glasses.size > 4 ? `press the enlarge button ${mii.glasses.size - 4} times.` : ``}`, 3208 | "stache": `On the mustache page (within the seventh tab), set the mustache to the one on the ${[0, 1].includes(mii.beard.mustache.type) ? `top` : [2, 3].includes(mii.beard.mustache.type) ? `middle` : `bottom`}-${[0, 2, 4].includes(mii.beard.mustache.type) ? `left` : `right`}.`, 3209 | "stacheY": `${mii.beard.mustache.yPosition !== 10 ? `On the mustache page (within the seventh tab), press the ` : ``}${mii.beard.mustache.yPosition > 10 ? `down button ${mii.beard.mustache.yPosition - 10} times.` : mii.beard.mustache.yPosition < 10 ? `up button ${10 - mii.beard.mustache.yPosition} times.` : ``}`, 3210 | "stacheSize": `${mii.beard.mustache.size !== 4 ? `On the mustache page (within the seventh tab), ` : ``}${mii.beard.mustache.size < 4 ? `press the shrink button ${4 - mii.beard.mustache.size} times.` : mii.beard.mustache.size > 4 ? `press the enlarge button ${mii.beard.mustache.size - 4} times.` : ``}`, 3211 | "mole": `${mii.mole.on ? `On the mole page (within the seventh tab), turn the mole on.` : ``}`, 3212 | "moleX": `${mii.mole.xPosition !== 2 ? `On the mole page (within the seventh tab), press the ` : ``}${mii.mole.xPosition > 2 ? `right button ${mii.mole.xPosition - 2} times.` : mii.mole.xPosition < 2 ? `left button ${2 - mii.mole.xPosition} times.` : ``}`, 3213 | "moleY": `${mii.mole.yPosition !== 20 ? `On the mole page (within the seventh tab), press the ` : ``}${mii.mole.yPosition > 20 ? `down button ${mii.mole.yPosition - 20} times.` : mii.mole.yPosition < 20 ? `up button ${20 - mii.mole.yPosition} times.` : ``}`, 3214 | "moleSize": `${mii.mole.size !== 4 ? `On the mole page (within the seventh tab), ` : ``}${mii.mole.size < 4 ? `press the shrink button ${4 - mii.mole.size} times.` : mii.mole.size > 4 ? `press the enlarge button ${mii.mole.size - 4} times.` : ``}`, 3215 | "beard": `On the beard page (within the seventh tab), set the beard to the one on the ${[0, 1].includes(mii.beard.type) ? `top` : [2, 3].includes(mii.beard.type) ? `middle` : `bottom`}-${[0, 2].includes(mii.beard.type) ? `left` : `right`}.`, 3216 | "beardCol": `On the mustache OR beard pages (within the seventh tab), set the color to the one ${mii.beard.color + 1} from the top.`, 3217 | "heightWeight": `On the build page (eighth tab), set the height to ${Math.round((100 / 128) * mii.general.height)}%, and the weight to ${Math.round((100 / 128) * mii.general.weight)}%.`, 3218 | "col": `On the info page (after pressing "Next"), set the Favorite Color to ${mii.general.favoriteColor} (${mii.general.favoriteColor <= 5 ? mii.general.favoriteColor + 1 : mii.general.favoriteColor - 5} from the left, ${mii.general.favoriteColor > 5 ? "bottom" : "top"} row).`, 3219 | "other": `The Nickname of this Mii is ${mii.general.name}.${mii.general.creatorName ? ` The creator was ${mii.general.creatorName}.` : ``} ${mii.general.birthday !== 0 ? ` Its birthday is ${["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][mii.general.birthMonth]} ${mii.general.birthday}.` : ``}` 3220 | }; 3221 | if (!full) { 3222 | var defaultMiiInstrs = structuredClone(mii.general.gender === "Male" ? defaultInstrs["3ds"].male : defaultInstrs["3ds"].female); 3223 | Object.keys(instrs).forEach(instr => { 3224 | if (instrs[instr] === defaultMiiInstrs[instr]) { 3225 | delete instrs[instr]; 3226 | } 3227 | }); 3228 | } 3229 | return instrs; 3230 | } 3231 | } 3232 | 3233 | function miiHeightToFeetInches(value) { 3234 | const minInches = 36; // 3'0" 3235 | const midInches = 69; // 5'9" 3236 | const maxInches = 84; // 7'0" 3237 | const midPoint = 64; 3238 | 3239 | let totalInches; 3240 | if (value <= midPoint) { 3241 | // Lower half: 0–64 maps to 36–69 3242 | totalInches = minInches + (value / midPoint) * (midInches - minInches); 3243 | } 3244 | else { 3245 | // Upper half: 64–127 maps to 69–84 3246 | totalInches = midInches + ((value - midPoint) / (127 - midPoint)) * (maxInches - midInches); 3247 | } 3248 | 3249 | const feet = Math.floor(totalInches / 12); 3250 | const inches = Math.round(totalInches % 12); 3251 | return { feet, inches, totalInches }; 3252 | } 3253 | function inchesToMiiHeight(totalInches) { 3254 | const minInches = 36; 3255 | const midInches = 69; 3256 | const maxInches = 84; 3257 | const midPoint = 64; 3258 | 3259 | let value; 3260 | if (totalInches <= midInches) { 3261 | // Below or equal to midpoint 3262 | value = ((totalInches - minInches) / (midInches - minInches)) * midPoint; 3263 | } else { 3264 | // Above midpoint 3265 | value = midPoint + ((totalInches - midInches) / (maxInches - midInches)) * (127 - midPoint); 3266 | } 3267 | 3268 | return Math.round(Math.max(0, Math.min(value, 127))); 3269 | } 3270 | 3271 | // Getting and setting Mii weights is HIGHLY EXPERIMENTAL and I am very unconfident in its output 3272 | // ---- Tunable anchors (BMI breakpoints) ---- 3273 | const BMI_MIN = 16; // maps to Mii weight 0 3274 | const BMI_MID = 23; // maps to Mii weight 64 (average look) 3275 | const BMI_MAX = 40; // maps to Mii weight 127 3276 | function heightWeightToMiiWeight(heightInches, weightLbs) { 3277 | if (!heightInches || heightInches < 0) throw new Error("heightInches must be >= 0"); 3278 | const bmi = (703 * weightLbs) / (heightInches * heightInches); 3279 | 3280 | let v; 3281 | if (bmi <= BMI_MID) { 3282 | const t = (clamp(bmi, BMI_MIN, BMI_MID) - BMI_MIN) / (BMI_MID - BMI_MIN); 3283 | v = 0 + t * 64; 3284 | } else { 3285 | const t = (clamp(bmi, BMI_MID, BMI_MAX) - BMI_MID) / (BMI_MAX - BMI_MID); 3286 | v = 64 + t * (127 - 64); 3287 | } 3288 | return Math.round(clamp(v, 0, 127)); 3289 | } 3290 | function miiWeightToRealWeight(heightInches, miiWeight) { 3291 | if (!heightInches || heightInches <= 0) heightInches = 0; 3292 | const v = clamp(miiWeight, 0, 127); 3293 | 3294 | let bmi; 3295 | if (v <= 64) { 3296 | const t = v / 64; 3297 | bmi = BMI_MIN + t * (BMI_MID - BMI_MIN); 3298 | } else { 3299 | const t = (v - 64) / (127 - 64); 3300 | bmi = BMI_MID + t * (BMI_MAX - BMI_MID); 3301 | } 3302 | 3303 | const pounds = (bmi * heightInches * heightInches) / 703; 3304 | return { pounds, bmi }; 3305 | } 3306 | 3307 | 3308 | 3309 | module.exports = { 3310 | // Data 3311 | Enums: require("./Enums"), 3312 | 3313 | //Convert 3314 | convertMii, 3315 | convertMiiToStudio, 3316 | convertStudioToMii, 3317 | 3318 | //Read 3319 | readWiiBin, 3320 | read3DSQR, 3321 | 3322 | //Render 3323 | renderMiiWithStudio, 3324 | renderMii, 3325 | 3326 | //Write 3327 | writeWiiBin, 3328 | write3DSQR, 3329 | 3330 | //make3DSChild, //WIP 3331 | 3332 | //Instructions 3333 | generateInstructions, 3334 | 3335 | //Normalize Height and Weight 0-127 to human measurements 3336 | miiHeightToFeetInches, 3337 | inchesToMiiHeight, 3338 | heightWeightToMiiWeight,//EXPERIMENTAL 3339 | miiWeightToRealWeight,//EXPERIMENTAL 3340 | 3341 | /* 3342 | Handle Amiibo Functions 3343 | insertMiiIntoAmiibo(amiiboDump, decrypted3DSMiiBuffer), 3344 | extractMiiFromAmiibo(amiiboDump) 3345 | */ 3346 | ...require("./amiiboHandler.js") 3347 | } 3348 | --------------------------------------------------------------------------------