Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Last active May 17, 2025 22:50
Show Gist options
  • Save ariankordi/e6e66b8b03b1424d6e4e489fd9dd83bf to your computer and use it in GitHub Desktop.
Save ariankordi/e6e66b8b03b1424d6e4e489fd9dd83bf to your computer and use it in GitHub Desktop.
simple mii studio data encoding in C and JS, from Wii U/3DS format (Ver3StoreData/FFLStoreData)
// @ts-check
/* eslint @stylistic/indent: ['error', 4] -- Define indent rules. */
/**
* Convert 3DS/Wii U format Mii data (Ver3StoreData/FFLStoreData) to
* struct format used on studio.mii.nintendo.com before obfuscation.
* Ported from original C function, decompiled to pseudocode: https://gist.github.com/ariankordi/e6e66b8b03b1424d6e4e489fd9dd83bf
* @param {Uint8Array} dst - 46-byte destination raw studio data (unobfuscated / charInfoStudio).
* @param {Uint8Array} src - 96-byte source Ver3StoreData.
*/
function ver3StoreDataToCharInfoStudio(dst, src) {
// Color indices need to be converted using Ver3 tables: https://github.com/Genwald/MiiPort/blob/4ee38bbb8aa68a2365e9c48d59d7709f760f9b5d/include/convert_mii.h#L8
// A shortcut that is equivalent to the tables is used in this snippet.
dst[0] = src[0x42] >> 3 & 7;
dst[1] = src[0x42] & 7;
dst[2] = src[0x2f];
dst[3] = src[0x35] >> 5;
dst[4] = ((src[0x35] & 1) << 2) | src[0x34] >> 6;
dst[5] = src[0x36] & 0x1f;
dst[6] = src[0x35] >> 1 & 0xf;
dst[7] = src[0x34] & 0x3f;
dst[8] = ((src[0x37] & 1) << 3) | src[0x36] >> 5;
dst[9] = src[0x37] >> 1 & 0x1f;
dst[10] = src[0x39] >> 4 & 7;
dst[0xb] = src[0x38] >> 5;
dst[0xc] = src[0x3a] & 0x1f;
dst[0xd] = src[0x39] & 0xf;
dst[0xe] = src[0x38] & 0x1f;
dst[0xf] = ((src[0x3b] & 1) << 3) | src[0x3a] >> 5;
dst[0x10] = src[0x3b] >> 1 & 0x1f;
dst[0x11] = src[0x30] >> 5;
dst[0x12] = src[0x31] >> 4;
dst[0x13] = src[0x30] >> 1 & 0xf;
dst[0x14] = src[0x31] & 0xf;
dst[0x15] = src[0x19] >> 2 & 0xf;
dst[0x16] = src[0x18] & 1;
dst[0x17] = src[0x44] >> 4 & 7;
dst[0x18] = (src[0x45] & 7) * 2 | src[0x44] >> 7;
dst[0x19] = src[0x44] & 0xf;
dst[0x1a] = src[0x45] >> 3;
dst[0x1b] = src[0x33] & 7;
dst[0x1c] = src[0x33] >> 3 & 1;
dst[0x1d] = src[0x32];
dst[0x1e] = src[0x2e];
dst[0x1f] = src[0x46] >> 1 & 0xf;
dst[0x20] = src[0x46] & 1;
dst[0x21] = ((src[0x47] & 3) << 3) | src[0x46] >> 5;
dst[0x22] = src[0x47] >> 2 & 0x1f;
dst[0x23] = src[0x3f] >> 5;
dst[0x24] = ((src[0x3f] & 1) << 2) | src[0x3e] >> 6;
dst[0x25] = src[0x3f] >> 1 & 0xf;
dst[0x26] = src[0x3e] & 0x3f;
dst[0x27] = src[0x40] & 0x1f;
dst[0x28] = ((src[0x43] & 3) << 2) | src[0x42] >> 6;
dst[0x29] = src[0x40] >> 5;
dst[0x2a] = src[0x43] >> 2 & 0x1f;
dst[0x2b] = ((src[0x3d] & 1) << 3) | src[0x3c] >> 5;
dst[0x2c] = src[0x3c] & 0x1f;
dst[0x2d] = src[0x3d] >> 1 & 0x1f;
// All fields have been set, so the struct does not need memset.
// Attempt to convert Ver3 colors to common colors.
if (dst[0x1b] == 0) {
dst[0x1b] = 8; // Map 0 to 8.
}
// Beard and eyebrow color are treated like hair color.
if (dst[0] == 0) {
dst[0] = 8;
}
if (dst[0xb] == 0) {
dst[0xb] = 8;
}
dst[0x24] = dst[0x24] + 19; // Offset mouth color by 19.
dst[4] = dst[4] + 8; // Offset eye color by 8.
// Handle glass color.
if (dst[0x17] == 0) {
dst[0x17] = 8;
} else if (dst[0x17] < 6) {
dst[0x17] = dst[0x17] + 13;
}
}
/**
* Obfuscation code from: https://mii-studio.akamaized.net/static/js/editor.pc.46056ea432a4ef3974af.js
* Search ".prototype.encode".
* @param {Uint8Array} dst - 47-byte destination.
* @param {Uint8Array} src - 46-byte source data before obfuscation.
* @param {number} [seed] - Random byte value to use for obfuscation.
*/
function studioURLObfuscationEncode(dst, src, seed = 0) {
// Store the seed at index 0 of destination.
dst[0] = seed;
// Use seed as initial previous value.
let previous = seed;
// iterate over the source array length
for (let i = 0; i < 46; i++) { // 46 = sizeof(charInfoStudio)
const current = src[i];
// XOR the current value with the previous one, add 7, then take modulo 256
dst[i + 1] = (7 + (current ^ previous)) % 256;
// update the previous value to the current encoded value
previous = dst[i + 1];
}
}
/**
* Convert 3DS/Wii U format Mii data (Ver3StoreData/FFLStoreData) to
* URL data format used on studio.mii.nintendo.com before hex encoding.
* @param {Uint8Array} src - 96-byte input Ver3StoreData.
* @param {number} [seed] - Initial seed for obfuscation. Set to 0.
* @returns {Uint8Array} Converted Studio data with obfuscation to be used in a URL.
*/
function ver3StoreDataToStudioURLData(src, seed = 0) {
const studioDataRaw = new Uint8Array(46); // Studio data without obfuscation.
// Convert to raw data.
ver3StoreDataToCharInfoStudio(studioDataRaw, src);
const dst = new Uint8Array(47); // Obfuscated studio data.
// Add obfuscation.
studioURLObfuscationEncode(dst, studioDataRaw, seed);
return dst;
}
// export { ver3StoreDataToStudioURLData };
// == Testing ==
// NOTE: All code below can be safely excluded
// if you do not need to run tests.
/**
* Data for testing conversion from Ver3StoreData
* to studio URL data, obfuscated with seed 0.
* Every array is an array with two elements
* element 1 = Base64 Ver3StoreData, element 2 = hex studio data
* @type {Array<{src: string, dst: string}>}
*/
const testVer3StoreDataToStudioSeed0 = [
// "Jasmine", from NNID: JasmineChlora
{
src:
'AwAAQKBBOMSghAAA27iHMb5gKyoqQgAAWS1KAGEAcwBtAGkAbgBlAAAAAAAAABw3EhB7ASFuQxwNZMcYAAgegg0AMEGzW4JtAABvAHMAaQBnAG8AbgBhAGwAAAAAAJA6',
dst: '000d142a303f434b717a7b84939ba6b2bbbec5cbc9d0e2ea010d15252b3250535960736f726870757f8289a0a7aeb1'
},
// "All" from Exzap's FFL_ODB.
{
src: 'AwAAQAAAAAAAAAAA2JXdtJBltjwAAAAAAEBBAGwAbAAAAEEATQBFAAAAAAAAAGN/RmVrASdogyX1NEYUoQAXijAABSk1UklQRQBYAAAAAAAAAAAAAAAAAAAAAAA=',
dst: '000f11757d7c8689b5c0d9e1edf2fdeff4050e0f131d242b424d4f4c545b375b666e736e7169736b828d93a0acb4bb'
},
// "Aiueome" from Super Mario Maker Wii U binary.
{
src: 'AwEAMAAAAAAAAAAA2sZrOqTA4fgk3wAAABBBAGkAdQBlAG8AbQBlAAAAAAAAAH9/JwAuCXPOgxfsCIUfDyUY0GUAO0K2oxFSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGUq',
dst: '000e14727b79818dc5d0e2e9f5f7061124323a4149505b6279858aa5abb1a6e0eff5ecff001a19081423273e3d3932'
},
// Guest A. Create ID may be inaccurate, but studio result is same.
{
src: 'AwEAMAAAAAAAAAAAgAAAAOz/gtIAAAAAABBuAG8AIABuAGEAbQBlAAAAAAAAAEBAgQBEAAJoRBgGNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAALQV',
dst: '000f165d6574777a7f848f93a2abb6b7bcbdc0c7ced5d8dfdee1e8e9e8efb2f9040b100b0f232e4054575e5b666e6e'
}
];
// Utility: Base64 -> U8, Hex -> U8, U8 -> Hex
/**
* Base64 -> U8 / https://stackoverflow.com/a/41106346
* @param {string} base64 - Input Base64 data to decode.
* @returns {Uint8Array} Decoded input data.
*/
const base64ToBytes = base64 => Uint8Array.from(atob(base64), c => c.charCodeAt(0));
/**
* Hex -> U8 / https://gist.github.com/themikefuller/608202bde24077990c0539f960b79fe4 (hex2string)
* @param {string} hex - Input hex data to decode.
* @returns {Uint8Array} Decoded input data.
*/
const hexToBytes = hex => new Uint8Array((hex.match(/.{1,2}/g) || [])
.map((/** @type {string} */ byte) => parseInt(byte, 16)));
/**
* U8 -> Hex / https://www.xaymar.com/articles/2020/12/08/fastest-uint8array-to-hex-string-conversion-in-javascript/
* @param {Array<number>|Uint8Array} bytes - Input data to encode.
* @returns {string} Hexadecimal representation of `buffer`.
*/
const bytesToHex = bytes => Array.prototype.map.call(bytes,
(/** @type {{ toString: (arg0: number) => string; }} */ x) => x.toString(16).padStart(2, '0')).join('');
/**
* Run conversion tests for {@link ver3StoreDataToStudioURLData}.
* @param {Array<{src: string, dst: string}>} [testData] - Expected test data ({@link testVer3StoreDataToStudioSeed0}).
*/
function testVer3StoreDataToStudioURLData(testData = testVer3StoreDataToStudioSeed0) {
for (const [index, { src, dst }] of testData.entries()) {
// Gather source and destination Uint8Arrays.
const srcBytes = base64ToBytes(src);
const expectedDstBytes = hexToBytes(dst);
// Perform conversion with seed of 0.
const actualDstBytes = ver3StoreDataToStudioURLData(srcBytes, 0);
// Convert to hex.
const actualHex = bytesToHex(actualDstBytes);
const expectedHex = bytesToHex(expectedDstBytes);
// Use console.assert to verify.
console.assert(
actualHex === expectedHex,
`Test case #${index + 1} failed.\nExpected: ${expectedHex}\nActual: ${actualHex}`
);
}
console.debug('testVer3StoreDataToStudioURLData: Tests finished.');
}
testVer3StoreDataToStudioURLData();
// Compilation command into Ghidra: x86_64-linux-gnu-gcc -nostdlib -g3 -gdwarf-4 simple-ver3storedata-studio.c -shared -o /dev/shm/studio.so -std=c23 -fstrict-volatile-bitfields
#include <string.h> // memset
// Typedefs: u64, u32, u16, u8
typedef unsigned long u64;
typedef unsigned int u32;
typedef unsigned short u16;
typedef unsigned char u8;
// From MiiPort: https://github.com/Genwald/MiiPort/blob/4ee38bbb8aa68a2365e9c48d59d7709f760f9b5d/include/mii_ext.h#L170
// nn::mii::detail::Ver3StoreDataRaw, nn::mii::Ver3StoreData
// Matches CFLiMiiDataPacket, FFLStoreData: https://github.com/decaf-emu/decaf-emu/blob/e6c528a20a41c34e0f9eb91dd3da40f119db2dee/src/libdecaf/src/nn/ffl/nn_ffl_miidata.h#L159, https://github.com/aboood40091/ffl/blob/73fe9fc70c0f96ebea373122e50f6d3acc443180/include/nn/ffl/FFLiMiiDataCore.h#L886
typedef struct __attribute__((__packed__)) { /* ver3StoreData */
u32 mii_version:8;
u32 copyable:1;
u32 ng_word:1;
u32 region_move:2;
u32 font_region:2;
u32 reserved0:2;
u32 room_index:4;
u32 position_in_room:4;
u32 author_type:4;
u32 birth_platform:3;
u32 reserved_1:1;
u64 author_id;
u8 create_id[10];
u16 reserved_2;
u16 gender:1;
u16 birth_month:4;
u16 birth_day:5;
u16 favorite_color:4;
u16 favorite:1;
u16 padding0:1;
u8 name[20];
u8 height;
u8 build;
u16 localonly:1;
u16 face_type:4;
u16 face_color:3;
u16 face_tex:4;
u16 face_make:4;
u16 hair_type:8;
u16 hair_color:3;
u16 hair_flip:1;
u16 padding1:4;
u16 eye_type:6;
u16 eye_color:3;
u16 eye_scale:4;
u16 eye_aspect:3;
u16 eye_rotate:5;
u16 eye_x:4;
u16 eye_y:5;
u16 padding2:2;
u16 eyebrow_type:5;
u16 eyebrow_color:3;
u16 eyebrow_scale:4;
u16 eyebrow_aspect:3;
u16 padding3:1;
u16 eyebrow_rotate:5;
u16 eyebrow_x:4;
u16 eyebrow_y:5;
u16 padding4:2;
u16 nose_type:5;
u16 nose_scale:4;
u16 nose_y:5;
u16 padding5:2;
u16 mouth_type:6;
u16 mouth_color:3;
u16 mouth_scale:4;
u16 mouth_aspect:3;
u16 mouth_y:5;
u16 mustache_type:3;
u16 padding6:8;
u16 beard_type:3;
u16 beard_color:3;
u16 beard_scale:4;
u16 beard_y:5;
u16 padding7:1;
u16 glass_type:4;
u16 glass_color:3;
u16 glass_scale:4;
u16 glass_y:5;
u16 mole_type:1;
u16 mole_scale:4;
u16 mole_x:5;
u16 mole_y:5;
u16 padding8:1;
u8 creator_name[20];
u16 padding9;
u16 crc;
} ver3StoreData;
static_assert(sizeof(ver3StoreData) == 0x60); // 96
// Matches the format used on studio.mii.nintendo.com before obfuscation.
// https://github.com/ariankordi/FFL-Testing/blob/2219f64473ac8312bab539cd05c00f88c14d2ffd/include/mii_ext_MiiPort.h#L180
typedef struct { /* charInfoStudio */
u8 beard_color;
u8 beard_type;
u8 build;
u8 eye_aspect;
u8 eye_color;
u8 eye_rotate;
u8 eye_scale;
u8 eye_type;
u8 eye_x;
u8 eye_y;
u8 eyebrow_aspect;
u8 eyebrow_color;
u8 eyebrow_rotate;
u8 eyebrow_scale;
u8 eyebrow_type;
u8 eyebrow_x;
u8 eyebrow_y;
u8 faceline_color;
u8 faceline_make;
u8 faceline_type;
u8 faceline_wrinkle;
u8 favorite_color;
u8 gender;
u8 glass_color;
u8 glass_scale;
u8 glass_type;
u8 glass_y;
u8 hair_color;
u8 hair_flip;
u8 hair_type;
u8 height;
u8 mole_scale;
u8 mole_type;
u8 mole_x;
u8 mole_y;
u8 mouth_aspect;
u8 mouth_color;
u8 mouth_scale;
u8 mouth_type;
u8 mouth_y;
u8 mustache_scale;
u8 mustache_type;
u8 mustache_y;
u8 nose_scale;
u8 nose_type;
u8 nose_y;
} charInfoStudio;
static_assert(sizeof(charInfoStudio) == 0x2E); // 46
// Obfuscation code from: https://mii-studio.akamaized.net/static/js/editor.pc.46056ea432a4ef3974af.js
// Search ".prototype.encode".
void studioURLObfuscationEncode(char* dst, const char* src, int seed) {
// Store the seed at index 0 of destination.
dst[0] = seed;
// Use seed as initial previous value.
char previous = seed;
// iterate over the source array length
for (int i = 0; i < 46; i++) { // 46 = sizeof(charInfoStudio)
char current = src[i];
// XOR the current value with the previous one, add 7, then take modulo 256
dst[i + 1] = (7 + (current ^ previous)) % 256;
// update the previous value to the current encoded value
previous = dst[i + 1];
}
}
// Convert 3DS/Wii U format Mii data (Ver3StoreData/FFLStoreData) to
// struct format used on studio.mii.nintendo.com before obfuscation.
void ver3StoreDataToCharInfoStudio(charInfoStudio* dst, const ver3StoreData* src) {
// Color indices need to be converted using Ver3 tables: https://github.com/Genwald/MiiPort/blob/4ee38bbb8aa68a2365e9c48d59d7709f760f9b5d/include/convert_mii.h#L8
// A shortcut that is equivalent to the tables is used in this snippet.
dst->beard_color = src->beard_color; // Convert to CommonColor
dst->beard_type = src->beard_type;
dst->build = src->build;
dst->eye_aspect = src->eye_aspect;
dst->eye_color = src->eye_color; // Convert to CommonColor
dst->eye_rotate = src->eye_rotate;
dst->eye_scale = src->eye_scale;
dst->eye_type = src->eye_type;
dst->eye_x = src->eye_x;
dst->eye_y = src->eye_y;
dst->eyebrow_aspect = src->eyebrow_aspect;
dst->eyebrow_color = src->eyebrow_color; // Convert to CommonColor
dst->eyebrow_rotate = src->eyebrow_rotate;
dst->eyebrow_scale = src->eyebrow_scale;
dst->eyebrow_type = src->eyebrow_type;
dst->eyebrow_x = src->eyebrow_x;
dst->eyebrow_y = src->eyebrow_y;
// All faceline fields are named diffetently.
dst->faceline_color = src->face_color; // No up conversion needed
dst->faceline_make = src->face_make;
dst->faceline_type = src->face_type;
dst->faceline_wrinkle = src->face_tex;
dst->favorite_color = src->favorite_color; // Unchanged
dst->gender = src->gender;
dst->glass_color = src->glass_color; // Convert to CommonColor
dst->glass_scale = src->glass_scale;
dst->glass_type = src->glass_type; // No up conversion needed
dst->glass_y = src->glass_y;
dst->hair_color = src->hair_color; // Convert to CommonColor
dst->hair_flip = src->hair_flip;
dst->hair_type = src->hair_type;
dst->height = src->height;
dst->mole_scale = src->mole_scale;
dst->mole_type = src->mole_type;
dst->mole_x = src->mole_x;
dst->mole_y = src->mole_y;
dst->mouth_aspect = src->mouth_aspect;
dst->mouth_color = src->mouth_color; // Convert to CommonColor
dst->mouth_scale = src->mouth_scale;
dst->mouth_type = src->mouth_type;
dst->mouth_y = src->mouth_y;
// Beard fields are named differently.
dst->mustache_scale = src->beard_scale;
dst->mustache_type = src->mustache_type;
dst->mustache_y = src->beard_y;
dst->nose_scale = src->nose_scale;
dst->nose_type = src->nose_type;
dst->nose_y = src->nose_y;
// All fields have been set, so the struct does not need memset.
// Attempt to convert Ver3 colors to common colors.
if (dst->hair_color == 0) dst->hair_color = 8; // Map 0 to 8.
// Beard and eyebrow color are treated like hair color.
if (dst->beard_color == 0) dst->beard_color = 8;
if (dst->eyebrow_color == 0) dst->eyebrow_color = 8;
dst->mouth_color += 19; // Offset mouth color by 19.
dst->eye_color += 8; // Offset eye color by 8.
// Handle glass color.
if (dst->glass_color == 0) {
dst->glass_color = 8;
} else if (dst->glass_color < 6) {
dst->glass_color += 13;
}
}
void ver3StoreDataToStudioURLData(char dst[47], const ver3StoreData* src, int seed) {
charInfoStudio studioDataRaw; // Studio data without obfuscation.
memset(&studioDataRaw, 0, sizeof(charInfoStudio));
// Convert to raw data.
ver3StoreDataToCharInfoStudio(&studioDataRaw, src);
// Add obfuscation.
studioURLObfuscationEncode(dst, (char*)src, seed);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment