miijs 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +51 -44
  2. package/index.js +118 -84
  3. package/package.json +1 -1
  4. package/mii.js +0 -3346
package/README.md CHANGED
@@ -47,9 +47,10 @@ MiiJS is a complete and comprehensive Mii library for reading, converting, modif
47
47
  - **`miiHeightToMeasurements(miiHeight)`** - Converts Mii height value (0-127) to real-world feet and inches. Returns `{feet, inches, totalInches, centimeters}`.
48
48
  - **`inchesToMiiHeight(totalInches)`** - Converts real-world height in inches to Mii height value (0-127).
49
49
  - **`centimetersToMiiHeight(totalCentimeters)`** - Converts real-world height in centimeters to Mii height value (0-127).
50
- - **`miiHeightWeightToRealWeight(miiWeight)`** - Converts Mii weight value (0-127) to real-world weight values. Returns `{pounds, kilograms}`.
50
+ - **`miiWeightToRealWeight(miiWeight)`** - Converts Mii weight value (0-127) to real-world weight values. Returns `{pounds, kilograms}`.
51
51
  - **`imperialHeightWeightToMiiWeight(heightInches, weightLbs)`** - Converts real-world imperial measurements to Mii weight values.
52
52
  - **`metricHeightWeightToMiiWeight(heightCentimeters, weightKilograms)`** - Converts real-world metric measurements to Mii weight values.
53
+ - **`miiIdToTimestamp(miiId, consoleMiiIdIsFrom)`** - Converts the Mii ID into a JS Date/Timestamp.
53
54
 
54
55
  ### Other Functions
55
56
  - **`makeChild(miiJson1, miiJson2, options?)`** - Returns an array of 6 different Mii JSONs, which represent a child generated from the two parent Miis passed to the function at different stages of life. This is somewhat experimental, but should be accurate to my current knowledge. You can pass any or none of { name: "The Name", creatorName: "The Name", favoriteColor: 0-11, gender: 0-1/\*0:Male, 1:Female\*/ }
@@ -62,12 +63,11 @@ MiiJS is a complete and comprehensive Mii library for reading, converting, modif
62
63
  ```javascript
63
64
  const miijs = require('miijs');
64
65
 
65
- // Read from file path
66
+ // Read from file path for image, or buffer of the data - encrypted or decrypted
66
67
  const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
67
68
  console.log('Mii Name:', miiJson.meta.name);
68
- console.log('Favorite Color:', miiJson.general.favoriteColor);
69
69
 
70
- // Or get just the decrypted binary data
70
+ // Get the decrypted binary data
71
71
  const decryptedBin = await miijs.read3DSQR('./example3DSQR.jpg', true);
72
72
  console.log('Decrypted binary length:', decryptedBin.length);
73
73
  ```
@@ -79,7 +79,6 @@ const miijs = require('miijs');
79
79
  // Read from file path
80
80
  const miiJson = await miijs.readWiiBin('./exampleWii.bin');
81
81
  console.log('Mii Name:', miiJson.meta.name);
82
- console.log('Gender:', miiJson.general.gender === 0 ? 'Male' : 'Female');
83
82
 
84
83
  // Or pass binary data directly
85
84
  const fs = require('fs');
@@ -94,13 +93,10 @@ const miijs = require('miijs');
94
93
  // First, read or create a Mii JSON
95
94
  const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
96
95
 
97
- // Write QR code with Studio rendering (no FFLResHigh.dat needed)
96
+ // Write QR code. If FFLResHigh.dat is in the same directory, will be used automatically. You can pass a buffer containing FFLResHigh.dat as a fourth parameter. Will use Studio rendering instead of local without FFLResHigh.dat.
98
97
  await miijs.write3DSQR(miiJson, './output_qr.jpg');
99
-
100
- // Or with local rendering (requires FFLResHigh.dat in project root or passed as buffer)
101
- const fs = require('fs');
102
- const fflRes = fs.readFileSync('./FFLResHigh.dat');
103
- await miijs.write3DSQR(miiJson, './output_qr_local.jpg', fflRes);
98
+ //The third parameter asks MiiJS to return an encrypted 3DS data instead
99
+ const encryptedData=await miijs.write3DSQR(miiJson,'',true);
104
100
  ```
105
101
 
106
102
  ## Writing a Wii Mii Binary
@@ -108,11 +104,8 @@ await miijs.write3DSQR(miiJson, './output_qr_local.jpg', fflRes);
108
104
  const miijs = require('miijs');
109
105
  const fs = require('fs');
110
106
 
111
- // Read a Mii (from any format)
112
- const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
113
-
114
- // Convert to Wii format first if needed
115
- const wiiMii = miijs.convertMii(miiJson, 'wii');
107
+ // Read a Mii
108
+ const miiJson = await miijs.readWiiBin('./exampleWii.bin');
116
109
 
117
110
  // Write to file
118
111
  await miijs.writeWiiBin(wiiMii, './output_wii.bin');
@@ -127,16 +120,16 @@ fs.writeFileSync('./manual_write.bin', buffer);
127
120
  const miijs = require('miijs');
128
121
 
129
122
  // Read a 3DS Mii
130
- const ds3Mii = await miijs.read3DSQR('./example3DSQR.jpg');
123
+ const mii3DS = await miijs.read3DSQR('./example3DSQR.jpg');
131
124
 
132
125
  // Convert to Wii format
133
- const wiiMii = miijs.convertMii(ds3Mii, 'wii');
126
+ const miiWii = miijs.convertMii(mii3DS, 'wii');
134
127
 
135
128
  // Convert back to 3DS
136
- const backTo3DS = miijs.convertMii(wiiMii, '3ds');
129
+ const backTo3DS = miijs.convertMii(miiWii, '3ds');
137
130
 
138
131
  // Auto-detect and convert to opposite
139
- const autoConverted = miijs.convertMii(ds3Mii);
132
+ const autoConverted = miijs.convertMii(mii3DS);
140
133
  ```
141
134
 
142
135
  ## Converting to/from Studio Format
@@ -151,7 +144,6 @@ console.log('Studio URL:', `https://studio.mii.nintendo.com/miis/image.png?data=
151
144
  // Convert Studio format back to JSON
152
145
  const studioData = '000d142a303f434b717a7b84939ba6b2bbbec5cbc9d0e2ea...';
153
146
  const miiFromStudio = miijs.convertStudioToMii(studioData);
154
- console.log('Converted Mii:', miiFromStudio.meta.name);
155
147
  ```
156
148
 
157
149
  ## Rendering Miis
@@ -162,16 +154,15 @@ const fs = require('fs');
162
154
  // Read a Mii
163
155
  const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
164
156
 
165
- // Render using Studio API (simple, no setup needed)
157
+ // Render using Studio API (simple, no setup needed, requires internet access)
166
158
  const studioPng = await miijs.renderMiiWithStudio(miiJson);
167
159
  fs.writeFileSync('./mii_studio_render.png', studioPng);
168
160
 
169
161
  // Render locally with full body (requires FFLResHigh.dat)
162
+ // FFLResHigh.dat can be placed in the project directory to automatically use
170
163
  const fflRes = fs.readFileSync('./FFLResHigh.dat');
171
164
  const localPng = await miijs.renderMii(miiJson, fflRes);
172
165
  fs.writeFileSync('./mii_local_render.png', localPng);
173
-
174
- // Shirt color comes from miiJson.general.favoriteColor
175
166
  ```
176
167
 
177
168
  ## Working with Amiibos
@@ -183,13 +174,11 @@ const fs = require('fs');
183
174
  const amiiboDump = fs.readFileSync('./exampleAmiiboDump.bin');
184
175
 
185
176
  // Extract the Mii from the Amiibo (returns 92 bytes decrypted)
186
- const miiData = miijs.extractMiiFromAmiibo(amiiboDump);
187
-
177
+ let miiData = miijs.extractMiiFromAmiibo(amiiboDump);
188
178
  // Convert the raw Mii data to readable JSON
189
- // (miiData is already decrypted 3DS format)
190
- const miiJson = miijs.decode3DSMii(miiData); // Note: decode3DSMii not exported, use read3DSQR workflow
179
+ const miiJson = miijs.read3DSQR(miiData);
191
180
 
192
- // Better workflow: Read from QR, get decrypted data, insert into Amiibo
181
+ // Read from QR, get decrypted data, insert into Amiibo
193
182
  const qrMiiJson = await miijs.read3DSQR('./example3DSQR.jpg');
194
183
  const decryptedMiiData = await miijs.read3DSQR('./example3DSQR.jpg', true);
195
184
 
@@ -224,23 +213,28 @@ Object.entries(fullInstructions).forEach(([field, instruction]) => {
224
213
  ```javascript
225
214
  const miijs = require('miijs');
226
215
 
227
- // Convert Mii height (0-127) to feet/inches
228
- const heightInfo = miijs.miiHeightToFeetInches(64); // midpoint value
229
- console.log(`Height: ${heightInfo.feet}'${heightInfo.inches}" (${heightInfo.totalInches} inches)`);
216
+ // Convert Mii height (0-127) to feet/inches and centimeters
217
+ const heightInfo = miijs.miiHeightToMeasurements(64); // midpoint value
218
+ console.log(`Height: ${heightInfo.feet}'${heightInfo.inches}" (${heightInfo.centimeters} cm)`);
230
219
 
231
220
  // Convert real height to Mii value
232
- const miiHeightValue = miijs.inchesToMiiHeight(72); // 6'0"
221
+ const miiHeightValue = miijs.inchesToMiiHeight(72);
233
222
  console.log('Mii height value for 6\'0":', miiHeightValue);
234
223
 
235
- // EXPERIMENTAL: Convert real weight to Mii weight
236
- const heightInches = 69; // 5'9"
237
- const weightLbs = 160;
238
- const miiWeightValue = miijs.heightWeightToMiiWeight(heightInches, weightLbs);
224
+ // Convert Mii weight to real weight (requires height)
225
+ const miiHeight = 64;
226
+ const miiWeight = 64;
227
+ const weightInfo = miijs.miiWeightToRealWeight(miiHeight, miiWeight);
228
+ console.log(`Weight: ${weightInfo.pounds.toFixed(1)} lbs (${weightInfo.kilograms} kg)`);
229
+
230
+ // Convert real weight to Mii weight value
231
+ const heightInches=70;
232
+ const weightLbs = 150;
233
+ const miiWeightValue = miijs.imperialHeightWeightToMiiWeight(heightInches, weightLbs);
239
234
  console.log('Mii weight value:', miiWeightValue);
240
235
 
241
- // EXPERIMENTAL: Convert Mii weight to real weight
242
- const weightInfo = miijs.miiWeightToRealWeight(heightInches, 64);
243
- console.log(`Weight: ${weightInfo.pounds.toFixed(1)} lbs, BMI: ${weightInfo.bmi.toFixed(1)}`);
236
+ // Or use metric
237
+ const miiWeightMetric = miijs.metricHeightWeightToMiiWeight(175, 72.5);
244
238
  ```
245
239
 
246
240
  ## Creating and Modifying a Mii
@@ -251,10 +245,10 @@ const miijs = require('miijs');
251
245
  const miiJson = await miijs.read3DSQR('./example3DSQR.jpg');
252
246
 
253
247
  // Modify properties
254
- miiJson.meta.name = 'Custom Name';
255
- miiJson.general.favoriteColor = 5; // Blue
256
- miiJson.hair.color = 0; // Black
257
- miiJson.eyes.color = 2; // Brown
248
+ miiJson.meta.name = 'CustomName';
249
+ miiJson.general.favoriteColor = 5;
250
+ miiJson.hair.color = 0;
251
+ miiJson.eyes.color = 2;
258
252
 
259
253
  // Make it a Special Mii (3DS only)
260
254
  miiJson.meta.type = 'Special';
@@ -287,6 +281,19 @@ for(var i=0;i<child.length;i++){
287
281
  }
288
282
  ```
289
283
 
284
+ ## Getting the Time the Mii Was Created From the Mii ID
285
+ ```javascript
286
+ const miijs=require('miijs');
287
+
288
+ //Read miis into JSON
289
+ const mii3DS=await miijs.read3DSQR('./example_mii.jpg');
290
+ const miiWii=await miijs.readWiiBin('./example_mii.bin');
291
+
292
+ //Same process for both, returns a JS Date/Timestamp
293
+ console.log(miijs.miiIdToTimestamp(mii3DS.meta.miiId,mii3DS.console));
294
+ console.log(miijs.miiIdToTimestamp(miiWii.meta.miiId,miiWii.console));
295
+ ```
296
+
290
297
  <hr>
291
298
 
292
299
  ## Special Miis
package/index.js CHANGED
@@ -181,7 +181,7 @@ var aes_key = new Uint8Array([0x59, 0xFC, 0x81, 0x7E, 0x64, 0x46, 0xEA, 0x61, 0x
181
181
  var pad = new Uint8Array([0, 0, 0, 0]);
182
182
  function decodeAesCcm(data) {
183
183
  var nonce = Uint8Cat(data.subarray(0, NONCE_LENGTH), pad);
184
- var ciphertext = data.subarray(NONCE_LENGTH, 0x70);
184
+ var ciphertext = data.subarray(NONCE_LENGTH, data.length);
185
185
  var plaintext = asmCrypto.AES_CCM.decrypt(ciphertext, aes_key, nonce, undefined, TAG_LENGTH);
186
186
  return Uint8Cat(plaintext.subarray(0, NONCE_OFFSET), data.subarray(0, NONCE_LENGTH), plaintext.subarray(NONCE_OFFSET, plaintext.length - 4));
187
187
  }
@@ -278,10 +278,28 @@ function convertMii(jsonIn, typeTo) {
278
278
  miiTo.beard.mustache.type = 0;
279
279
  miiTo.beard.type = 1;
280
280
  }
281
- if (mii.beard.type > 3) {
282
- mii.beard.type = 3;
281
+ if (miiTo.beard.type > 3) {
282
+ miiTo.beard.type = 3;
283
283
  }
284
- miiTo.console = "wii";
284
+
285
+ //System IDs are only 4 bytes on the Wii
286
+ if(miiTo.meta.systemId){
287
+ miiTo.meta.systemId=miiTo.meta.systemId.slice(0,8);
288
+ }
289
+
290
+ if (miiTo.meta.miiId) {
291
+ const miiIdInt = parseInt(miiTo.meta.miiId.replaceAll(' ', ''), 16);
292
+ // Extract 28-bit timestamp (bits 0-27), multiply by 2 to get seconds since 2010
293
+ const secsSince2010 = (miiIdInt & 0x0FFFFFFF) * 2;
294
+ // Convert to 4-second intervals since 2006
295
+ const secondsOffset = 126230400; // Seconds between 2006 and 2010
296
+ const intervals = Math.floor((secsSince2010 + secondsOffset) / 4);
297
+ // Combine with type bits
298
+ const typePrefix = miiTo.meta.type === "Special" ? 0b010 : 0b100;
299
+ miiTo.meta.miiId = ((typePrefix << 29) | intervals).toString(16).toUpperCase().padStart(8, '0');
300
+ }
301
+
302
+ miiTo.console = "Wii";
285
303
  }
286
304
  else if (typeFrom === "wii") {
287
305
  miiTo.perms.sharing = mii.perms.mingle;
@@ -291,12 +309,9 @@ function convertMii(jsonIn, typeTo) {
291
309
  const hairConv = convTables.hairWiiTo3DS[mii.hair.page][mii.hair.type];
292
310
  miiTo.hair.page = hairConv[0];
293
311
  miiTo.hair.type = hairConv[1];
294
- miiTo.hair.color = mii.hair.color;
295
- miiTo.hair.flipped = mii.hair.flipped;
296
312
 
297
313
  // Convert face
298
314
  miiTo.face.type = convTables.faceWiiTo3DS[mii.face.type];
299
- miiTo.face.color = mii.face.color;
300
315
  miiTo.face.makeup = 0;
301
316
  miiTo.face.feature = 0;
302
317
 
@@ -308,62 +323,27 @@ function convertMii(jsonIn, typeTo) {
308
323
  miiTo.face.feature = +convTables.featureWiiTo3DS[mii.face.feature];
309
324
  }
310
325
 
311
- // Convert eyes - preserve page/type structure
312
- miiTo.eyes.page = mii.eyes.page;
313
- miiTo.eyes.type = mii.eyes.type;
314
- miiTo.eyes.color = mii.eyes.color;
315
- miiTo.eyes.size = mii.eyes.size;
316
326
  miiTo.eyes.squash = 3; // Default for 3DS
317
- miiTo.eyes.rotation = mii.eyes.rotation;
318
- miiTo.eyes.distanceApart = mii.eyes.distanceApart;
319
- miiTo.eyes.yPosition = mii.eyes.yPosition;
320
-
321
- // Convert eyebrows - preserve page/type structure
322
- miiTo.eyebrows.page = mii.eyebrows.page;
323
- miiTo.eyebrows.type = mii.eyebrows.type;
324
- miiTo.eyebrows.color = mii.eyebrows.color;
325
- miiTo.eyebrows.size = mii.eyebrows.size;
326
327
  miiTo.eyebrows.squash = 3; // Default for 3DS
327
- miiTo.eyebrows.rotation = mii.eyebrows.rotation;
328
- miiTo.eyebrows.distanceApart = mii.eyebrows.distanceApart;
329
- miiTo.eyebrows.yPosition = mii.eyebrows.yPosition;
330
-
331
- // Convert nose - preserve page/type structure
332
328
  miiTo.nose.page = mii.nose.page || 0;
333
- miiTo.nose.type = mii.nose.type;
334
- miiTo.nose.size = mii.nose.size;
335
- miiTo.nose.yPosition = mii.nose.yPosition;
336
-
337
- // Convert mouth - preserve page/type structure
338
- miiTo.mouth.page = mii.mouth.page;
339
- miiTo.mouth.type = mii.mouth.type;
340
- miiTo.mouth.color = mii.mouth.color;
341
- miiTo.mouth.size = mii.mouth.size;
342
329
  miiTo.mouth.squash = 3; // Default for 3DS
343
- miiTo.mouth.yPosition = mii.mouth.yPosition;
344
330
 
345
- // Convert glasses
346
- miiTo.glasses.type = mii.glasses.type;
347
- miiTo.glasses.color = mii.glasses.color;
348
- miiTo.glasses.size = mii.glasses.size;
349
- miiTo.glasses.yPosition = mii.glasses.yPosition;
350
-
351
- // Convert beard
352
- miiTo.beard.mustache.type = mii.beard.mustache.type;
353
- miiTo.beard.mustache.size = mii.beard.mustache.size;
354
- miiTo.beard.mustache.yPosition = mii.beard.mustache.yPosition;
355
- miiTo.beard.type = mii.beard.type;
356
- miiTo.beard.color = mii.beard.color;
357
-
358
- // Convert mole
359
- miiTo.mole.on = mii.mole.on;
360
- miiTo.mole.size = mii.mole.size;
361
- miiTo.mole.xPosition = mii.mole.xPosition;
362
- miiTo.mole.yPosition = mii.mole.yPosition;
363
-
364
- // Copy general info
365
- miiTo.general = { ...mii.general };
366
- miiTo.meta = { ...mii.meta };
331
+ //System IDs are twice as long on 3DS
332
+ if(miiTo.meta.systemId){
333
+ miiTo.meta.systemId=miiTo.meta.systemId.padEnd(16,'0');
334
+ }
335
+
336
+ if (miiTo.meta.miiId) {
337
+ const miiIdInt = parseInt(miiTo.meta.miiId.replaceAll(' ', ''), 16);
338
+ // Extract 29-bit timestamp (bits 0-28), multiply by 4 to get seconds since 2006
339
+ const secsSince2006 = (miiIdInt & 0x1FFFFFFF) * 4;
340
+ // Convert to 2-second intervals since 2010
341
+ const secondsOffset = 126230400; // Seconds between 2006 and 2010
342
+ const intervals = Math.floor((secsSince2006 - secondsOffset) / 2);
343
+ // Combine with flag bits
344
+ const flags = miiTo.meta.type === "Special" ? 0b0001 : 0b1001;
345
+ miiTo.meta.miiId = ((intervals & 0x0FFFFFFF) | (flags << 28)).toString(16).toUpperCase().padStart(8, '0');
346
+ }
367
347
 
368
348
  miiTo.console = "3DS";
369
349
  }
@@ -604,7 +584,7 @@ async function readWiiBin(binOrPath) {
604
584
  }
605
585
  thisMii.meta.creatorName = cname.replaceAll("\x00", "");
606
586
  thisMii.general.gender = +get(0x00)[1];//0 for Male, 1 for Female
607
- 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);
587
+ thisMii.meta.miiId = data.readUInt32BE(0x18).toString(16).padStart(8, '0');
608
588
  switch (thisMii.meta.miiId.slice(0, 3)) {
609
589
  case "010":
610
590
  thisMii.meta.type = "Special";
@@ -612,11 +592,11 @@ async function readWiiBin(binOrPath) {
612
592
  case "110":
613
593
  thisMii.meta.type = "Foreign";
614
594
  break;
615
- default:
595
+ default://100
616
596
  thisMii.meta.type = "Default";
617
597
  break;
618
598
  }
619
- 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);
599
+ thisMii.meta.systemId = data.readUInt32BE(0x1C).toString(16).padStart(8, '0').toUpperCase();
620
600
  var temp = get(0x20);
621
601
  thisMii.face.type = parseInt(temp.slice(0, 3), 2);//0-7
622
602
  thisMii.face.color = parseInt(temp.slice(3, 6), 2);//0-5
@@ -639,12 +619,13 @@ async function readWiiBin(binOrPath) {
639
619
  thisMii.general.birthMonth = parseInt(temp.slice(2, 6), 2);
640
620
  thisMii.general.birthday = parseInt(temp.slice(6, 8) + temp2.slice(0, 3), 2);
641
621
  thisMii.general.favoriteColor = parseInt(temp2.slice(3, 7), 2);//0-11, refer to cols array
622
+ thisMii.meta.favorited = temp2[7]=="1";
642
623
  thisMii.general.height = parseInt(get(0x16), 2);//0-127
643
624
  thisMii.general.weight = parseInt(get(0x17), 2);//0-127
644
- thisMii.perms.fromCheckMiiOut = get(0x21)[7] === "0" ? false : true;
625
+ thisMii.perms.fromCheckMiiOut = get(0x21)[7] == "1";
645
626
  temp = get(0x34);
646
627
  temp2 = get(0x35);
647
- thisMii.mole.on = temp[0] === "0" ? false : true;//0 for Off, 1 for On
628
+ thisMii.mole.on = temp[0] == "1";//0 for Off, 1 for On
648
629
  thisMii.mole.size = parseInt(temp.slice(1, 5), 2);//0-8, default 4
649
630
  thisMii.mole.xPosition = parseInt(temp2.slice(2, 7), 2);//0-16, Default 2
650
631
  thisMii.mole.yPosition = parseInt(temp.slice(5, 8) + temp2.slice(0, 2), 2);//Top to bottom
@@ -653,7 +634,7 @@ async function readWiiBin(binOrPath) {
653
634
  thisMii.hair.page = +lookupTables.hairTable["" + parseInt(temp.slice(0, 7), 2)][0] - 1;
654
635
  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
655
636
  thisMii.hair.color = parseInt(temp[7] + temp2.slice(0, 2), 2);//0-7, refer to hairCols array
656
- thisMii.hair.flipped = temp2[2] === "0" ? false : true;
637
+ thisMii.hair.flipped = temp2[2] == "1";
657
638
  temp = get(0x24);
658
639
  temp2 = get(0x25);
659
640
  thisMii.eyebrows.page = +lookupTables.eyebrowTable["" + parseInt(temp.slice(0, 5), 2)][0] - 1;
@@ -709,6 +690,14 @@ function decode3DSMii(data) {
709
690
  }
710
691
  };
711
692
  const get = address => getBinaryFromAddress(address, data);
693
+ miiJson.perms.copying = get(0x01)[7] === "1" ? true : false;
694
+ const miiIdValue = data.readUInt32BE(0x0C);
695
+ const systemIdHigh = data.readUInt32BE(0x04);
696
+ const systemIdLow = data.readUInt32BE(0x08);
697
+
698
+ miiJson.meta.type = (miiIdValue & 0x80000000) === 0 ? "Special" : "Default";
699
+ miiJson.meta.systemId = systemIdHigh.toString(16).padStart(8, '0') + systemIdLow.toString(16).padStart(8, '0');
700
+ miiJson.meta.miiId = miiIdValue.toString(16).padStart(8, '0');
712
701
  var temp = get(0x18);
713
702
  var temp2 = get(0x19);
714
703
  miiJson.general.birthday = parseInt(temp2.slice(6, 8) + temp.slice(0, 3), 2);
@@ -742,7 +731,6 @@ function decode3DSMii(data) {
742
731
  temp = get(0x30);
743
732
  miiJson.perms.sharing = temp[7] === "1" ? false : true;
744
733
  miiJson.general.favoriteColor = parseInt(temp2.slice(2, 6), 2);
745
- miiJson.perms.copying = get(0x01)[7] === "1" ? true : false;
746
734
  miiJson.hair.page = lookupTable("hairs", parseInt(get(0x32), 2), true)[0];
747
735
  miiJson.hair.type = lookupTable("hairs", parseInt(get(0x32), 2), true)[1];
748
736
  miiJson.face.type = lookupTable("faces", parseInt(temp.slice(3, 7), 2), false);
@@ -811,11 +799,10 @@ function decode3DSMii(data) {
811
799
  temp2 = get(0x47);
812
800
  miiJson.mole.xPosition = parseInt(temp2.slice(6, 8) + temp.slice(0, 3), 2);
813
801
  miiJson.mole.yPosition = parseInt(temp2.slice(1, 6), 2);
814
- miiJson.meta.type = "Default";//qk, Make this actually retrieve MiiID, SystemID, and Mii type
815
802
  miiJson.console = "3DS";
816
803
  return miiJson;
817
804
  }
818
- async function read3DSQR(binOrPath, returnDecryptedBin) {
805
+ async function read3DSQR(binOrPath, returnDecryptedBin, returnEncryptedBin) {
819
806
  let qrCode;
820
807
  if (Buffer.isBuffer(binOrPath)) {//Buffer
821
808
  qrCode = binOrPath;
@@ -841,6 +828,9 @@ async function read3DSQR(binOrPath, returnDecryptedBin) {
841
828
  });
842
829
  }
843
830
  if (qrCode) {
831
+ if (returnEncryptedBin) {
832
+ return new Uint8Array(qrCode);
833
+ }
844
834
  var data;
845
835
  data = Buffer.from(decodeAesCcm(new Uint8Array(qrCode)));
846
836
  if (returnDecryptedBin) {
@@ -1245,7 +1235,7 @@ async function writeWiiBin(jsonIn, outPath) {
1245
1235
  miiBin += mii.general.birthMonth.toString(2).padStart(4, "0");
1246
1236
  miiBin += mii.general.birthday.toString(2).padStart(5, "0");
1247
1237
  miiBin += mii.general.favoriteColor.toString(2).padStart(4, "0");
1248
- miiBin += '0';
1238
+ miiBin += mii.perms.favorited?'1':'0';
1249
1239
  for (var i = 0; i < 10; i++) {
1250
1240
  if (i < mii.meta.name.length) {
1251
1241
  miiBin += mii.meta.name.charCodeAt(i).toString(2).padStart(16, "0");
@@ -1256,23 +1246,33 @@ async function writeWiiBin(jsonIn, outPath) {
1256
1246
  }
1257
1247
  miiBin += mii.general.height.toString(2).padStart(8, "0");
1258
1248
  miiBin += mii.general.weight.toString(2).padStart(8, "0");
1259
- let miiId = "";
1249
+ let miiTypeIdentifier = "";
1260
1250
  switch (mii.meta.type) {
1261
1251
  case "Special":
1262
- miiId = "01000110";
1252
+ miiTypeIdentifier = "010";
1263
1253
  break;
1264
1254
  case "Foreign":
1265
- miiId = "11000110";
1255
+ miiTypeIdentifier = "110";
1266
1256
  break;
1267
1257
  default:
1268
- miiId = "10001001";
1258
+ miiTypeIdentifier = "100";
1269
1259
  break;
1270
1260
  }
1271
- for (var i = 0; i < 3; i++) {
1272
- miiId += Math.floor(Math.random() * 255).toString(2).padStart(8, "0");
1261
+ if (mii.meta.miiId) {
1262
+ let temp = mii.meta.miiId.replaceAll(' ', '').match(/.{1,2}/g).map(b => parseInt(b, 16).toString(2).padStart(8, '0')).join('');
1263
+ miiBin += `${miiTypeIdentifier}${temp.padStart(32, '0').slice(-29)}`; // Take rightmost 29 bits
1264
+ }
1265
+ else {
1266
+ // Calculate the number of 4-second intervals since Jan 1, 2006
1267
+ const miiIdBase = Math.floor((Date.now() - Date.UTC(2006, 0, 1)) / 4000).toString(2).padStart(29, '0');
1268
+ miiBin += `${miiTypeIdentifier}${miiIdBase}`;
1269
+ }
1270
+ if(mii.meta.systemId){
1271
+ miiBin += mii.meta.systemId.replaceAll(' ','').match(/.{1,2}/g).map(b=>parseInt(b,16).toString(2).padStart(8,'0')).join('').padStart(32,'0').slice(-32); // Use slice(-32)
1272
+ }
1273
+ else{
1274
+ miiBin += "11111111".repeat(4);//FF FF FF FF, completely nonsense System ID if none is set
1273
1275
  }
1274
- miiBin += miiId;
1275
- miiBin += "11111111".repeat(4);//System ID
1276
1276
  miiBin += mii.face.type.toString(2).padStart(3, "0");
1277
1277
  miiBin += mii.face.color.toString(2).padStart(3, "0");
1278
1278
  miiBin += mii.face.feature.toString(2).padStart(4, "0");
@@ -1315,7 +1315,7 @@ async function writeWiiBin(jsonIn, outPath) {
1315
1315
  miiBin += mii.mouth.yPosition.toString(2).padStart(5, "0");
1316
1316
  miiBin += mii.glasses.type.toString(2).padStart(4, "0");
1317
1317
  miiBin += mii.glasses.color.toString(2).padStart(3, "0");
1318
- miiBin += "0";
1318
+ miiBin += "0";//Invalidates Mii when set to 1
1319
1319
  miiBin += mii.glasses.size.toString(2).padStart(3, "0");
1320
1320
  miiBin += mii.glasses.yPosition.toString(2).padStart(5, "0");
1321
1321
  miiBin += mii.beard.mustache.type.toString(2).padStart(2, "0");
@@ -1359,26 +1359,39 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1359
1359
 
1360
1360
  //Make the binary
1361
1361
  var mii = miiJson;
1362
- var miiBin = "00000011";
1362
+ var miiBin = "00000011";//Mii version, which for 3DS is 3
1363
1363
  //If Special Miis are being used improperly, fix it and warn the user
1364
1364
  if (mii.meta.type.toLowerCase() === "special" && (mii.console.toLowerCase() === "wii u" || mii.console.toLowerCase() === "wiiu")) {
1365
1365
  mii.meta.type = "Default";
1366
- console.warn("Wii Us do not work with Special Miis. Reverted to Default Mii.");
1366
+ console.warn("Wii Us do not work with Special Miis. Reverted output to Default Mii.");
1367
1367
  }
1368
1368
  if (mii.perms.sharing && mii.meta.type === "Special") {
1369
1369
  mii.perms.sharing = false;
1370
1370
  console.warn("Cannot have Sharing enabled for Special Miis. Disabled Sharing in the output.");
1371
1371
  }
1372
- miiBin += "0000000";
1372
+ //Revisit this if translating MiiJS out of English ever, for now this is fine
1373
+ miiBin += "0000000";//00 JPN/US/EUR, 01 CHN, 10 KOR, 11 TWN Character Set | Region Lock Off 00 | Profanity Flag 0/1
1373
1374
  miiBin += mii.perms.copying ? "1" : "0";
1374
1375
  miiBin += "00000000";
1375
1376
  miiBin += "00110000";
1376
- miiBin += "1000101011010010000001101000011100011000110001100100011001100110010101100111111110111100000001110101110001000101011101100000001110100100010000000000000000000000".slice(0, 8 * 8);
1377
+ if(mii.meta.systemId){
1378
+ miiBin += mii.meta.systemId.replaceAll(' ','').match(/.{1,2}/g).map(b=>parseInt(b,16).toString(2).padStart(8,'0')).join('').padStart(64,'0').slice(-64); // Use slice(-64)
1379
+ }
1380
+ else{
1381
+ //Donor System ID
1382
+ miiBin += "1000101011010010000001101000011100011000110001100100011001100110010101100111111110111100000001110101110001000101011101100000001110100100010000000000000000000000";
1383
+ }
1377
1384
  miiBin += mii.meta.type === "Special" ? "0" : "1";
1378
- miiBin += "0000000";
1379
- for (var i = 0; i < 3; i++) {
1380
- miiBin += Math.floor(Math.random() * 255).toString(2).padStart(8, "0");
1385
+ miiBin += "001";
1386
+ let temp = '';
1387
+ if (mii.meta.miiId) {
1388
+ // Convert Mii ID to binary
1389
+ temp += mii.meta.miiId.replaceAll(' ', '').match(/.{1,2}/g).map(b => parseInt(b, 16).toString(2).padStart(8, '0')).join('');
1390
+ } else {
1391
+ // Number of 2-second intervals since Jan 1, 2010
1392
+ temp += Math.floor((Date.now() - Date.UTC(2010, 0, 1)) / 2000).toString(2);
1381
1393
  }
1394
+ miiBin += temp.padStart(32, '0').slice(-27); // Take rightmost 27 bits
1382
1395
  miiBin += "0000000001000101011101100000001110100100010000000000000000000000";
1383
1396
  miiBin += mii.general.birthday.toString(2).padStart(5, "0").slice(2, 5);
1384
1397
  miiBin += mii.general.birthMonth.toString(2).padStart(4, "0");
@@ -1913,6 +1926,25 @@ function metricHeightWeightToMiiWeight(heightCentimeters, weightKilograms) {
1913
1926
  }
1914
1927
  }
1915
1928
 
1929
+ function miiIdToTimestamp(miiId, mode){
1930
+ miiId = miiId.replaceAll(' ', '');
1931
+ const idBigInt = BigInt('0x' + miiId);
1932
+
1933
+ switch(mode.toLowerCase().replaceAll(' ', '')){
1934
+ case "3ds":
1935
+ case "wiiu":
1936
+ const seconds3ds = (idBigInt & 0x0FFFFFFFn) * 2n;
1937
+ return new Date(Number(BigInt(Date.UTC(2010, 0, 1)) + seconds3ds * 1000n));
1938
+
1939
+ case "wii":
1940
+ // Extract bits 0-27 (28 bits), multiply by 4 for seconds
1941
+ const secondsWii = (idBigInt & 0x0FFFFFFFn) * 4n;
1942
+ return new Date(Number(BigInt(Date.UTC(2006, 0, 1)) + secondsWii * 1000n));
1943
+
1944
+ default:
1945
+ return "No valid mode specified";
1946
+ }
1947
+ }
1916
1948
 
1917
1949
 
1918
1950
  module.exports = {
@@ -1950,6 +1982,8 @@ module.exports = {
1950
1982
  imperialHeightWeightToMiiWeight,
1951
1983
  metricHeightWeightToMiiWeight,
1952
1984
 
1985
+ miiIdToTimestamp,
1986
+
1953
1987
  /*
1954
1988
  Handle Amiibo Functions
1955
1989
  insertMiiIntoAmiibo(amiiboDump, decrypted3DSMiiBuffer),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miijs",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "The most complete and easy to use Mii library on the market.",
5
5
  "main": "index.js",
6
6
  "scripts": {