miijs 2.4.1 → 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.
@@ -0,0 +1,36 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-gpr:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ packages: write
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 20
32
+ registry-url: https://npm.pkg.github.com/
33
+ - run: npm ci
34
+ - run: npm publish
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
package/README.md CHANGED
@@ -50,6 +50,7 @@ MiiJS is a complete and comprehensive Mii library for reading, converting, modif
50
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
@@ -360,4 +367,4 @@ You can find FFLResHigh using a Wii U with an FTP program installed at `sys/titl
360
367
  - **[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.
361
368
  - **[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).
362
369
  - **[Models Resource](https://models.spriters-resource.com/3ds/systembios/asset/306260/)** - For the bodies used in Mii rendering
363
- - **[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.
370
+ - **[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.
package/index.js CHANGED
@@ -12,7 +12,6 @@ const httpsLib = require('https');
12
12
  const asmCrypto = require("./asmCrypto.js");
13
13
  const path = require("path");
14
14
  const createGL = require('gl');
15
- const typeCheat = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3];
16
15
  const {
17
16
  createCharModel, initCharModelTextures,
18
17
  initializeFFL, exitFFL, parseHexOrB64ToUint8Array,
@@ -20,6 +19,7 @@ const {
20
19
  } = require("./fflWrapper.js");
21
20
  const ModuleFFL = require("ffl.js/examples/ffl-emscripten-single-file.js");
22
21
  const FFLShaderMaterial = require("ffl.js/FFLShaderMaterial.js");
22
+ const typeCheat = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3];
23
23
 
24
24
  // Typedefs for intellisence
25
25
  /** @typedef {import('./types').WiiMii} WiiMii */
@@ -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
+ }
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');
283
300
  }
284
- miiTo.console = "wii";
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");
@@ -1686,7 +1699,7 @@ function makeChild(parent0, parent1, options) {
1686
1699
  delete child.stages[iStage].stages;//Because we're just cloning the baseline object repeatedly to make the stages a little bit cleaner, we need to clear this on subsequent clones
1687
1700
  }
1688
1701
 
1689
- //If this looks odd, it is setup in a specific way to mimic the research I did into hair generation as 1:1 as possible
1702
+ //Basically there's a random chance for a hairstyle to not advance throughout the years, so it's possible to end up with a hairstyle from a younger stage. This is slightly more likely for boys than girls.
1690
1703
  let ageGroup = 0;
1691
1704
  for (let iHairStage = 0; iHairStage < 4; iHairStage++) {
1692
1705
  const subgroup = childGenTables.hairStyleGroups[hairGroupIndex][ageGroup];
@@ -1716,25 +1729,8 @@ function makeChild(parent0, parent1, options) {
1716
1729
  child.stages[5].hair.type = hairType;
1717
1730
  break;
1718
1731
  }
1719
- if (child.stages[0].general.gender === 0) {
1720
- if (iHairStage === 0) {
1721
- ageGroup = Math.min(ageGroup + 1, 3);
1722
- }
1723
- else {
1724
- if (Math.floor(Math.random() * 3) !== 0) {
1725
- ageGroup = Math.min(ageGroup + 1, 3);
1726
- }
1727
- }
1728
- }
1729
- else {
1730
- if (iHairStage === 0) {
1731
- ageGroup = Math.min(ageGroup + 1, 3);
1732
- }
1733
- else {
1734
- if (Math.floor(Math.random() * 4) !== 0) {
1735
- ageGroup = Math.min(ageGroup + 1, 3);
1736
- }
1737
- }
1732
+ if (iHairStage === 0 || Math.floor(Math.random() * (child.stages[0].general.gender === 0 ? 3 : 4)) !== 0) {//For each stage of life there is a 33% chance for boys, and a 25% chance for girls, of staying on the same hairstyle as they had already. However, they are guaranteed to never have the same hairstyle stage as their newborn stage.
1733
+ ageGroup = Math.min(ageGroup + 1, 3);
1738
1734
  }
1739
1735
  }
1740
1736
  return child.stages;
@@ -1898,7 +1894,6 @@ function miiWeightToRealWeight(heightInches, miiWeight) {
1898
1894
 
1899
1895
  This is approximate, not guaranteed accurate nor intended to be taken that way. This is for entertainment value only.
1900
1896
  */
1901
- if (!heightInches || heightInches < 0) throw new Error("heightInches must be >= 0");
1902
1897
  const H = miiHeightToMeasurements(heightInches).totalInches;
1903
1898
  const BMI = bmiFromWeightSlider(miiWeight);
1904
1899
  return {
@@ -1907,8 +1902,6 @@ function miiWeightToRealWeight(heightInches, miiWeight) {
1907
1902
  };
1908
1903
  }
1909
1904
  function imperialHeightWeightToMiiWeight(heightInches, weightLbs) {
1910
- if (!heightInches || heightInches < 0) throw new Error("heightInches must be >= 0");
1911
-
1912
1905
  const H = miiHeightToMeasurements(heightInches).totalInches;
1913
1906
  const BMI = weightLbs * 703 / (H * H);
1914
1907
 
@@ -1922,8 +1915,6 @@ function imperialHeightWeightToMiiWeight(heightInches, weightLbs) {
1922
1915
  function metricHeightWeightToMiiWeight(heightCentimeters, weightKilograms) {
1923
1916
  const heightInches = Math.round(heightCentimeters / 2.54);
1924
1917
  const weightLbs = Math.round(weightKilograms / 0.4535924);
1925
- if (!heightInches || heightInches < 0) throw new Error("heightCentimeters must be >= 0");
1926
-
1927
1918
  const H = miiHeightToMeasurements(heightInches).totalInches;
1928
1919
  const BMI = weightLbs * 703 / (H * H);
1929
1920
 
@@ -1935,6 +1926,25 @@ function metricHeightWeightToMiiWeight(heightCentimeters, weightKilograms) {
1935
1926
  }
1936
1927
  }
1937
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
+ }
1938
1948
 
1939
1949
 
1940
1950
  module.exports = {
@@ -1972,6 +1982,8 @@ module.exports = {
1972
1982
  imperialHeightWeightToMiiWeight,
1973
1983
  metricHeightWeightToMiiWeight,
1974
1984
 
1985
+ miiIdToTimestamp,
1986
+
1975
1987
  /*
1976
1988
  Handle Amiibo Functions
1977
1989
  insertMiiIntoAmiibo(amiiboDump, decrypted3DSMiiBuffer),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miijs",
3
- "version": "2.4.1",
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": {