miijs 2.5.0 → 2.5.2

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.
package/index.js CHANGED
@@ -1,24 +1,43 @@
1
- //Imports
2
- const fs = require('fs');
3
- const nodeCanvas = require('canvas');
4
- const { createCanvas, loadImage, ImageData } = nodeCanvas;
1
+ //Imports - Environment detection
2
+ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
3
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
4
+
5
+ // Node.js-only imports (conditionally loaded)
6
+ let fs, nodeCanvas, createCanvas, loadImage, ImageData, Jimp, JSDOM, httpsLib, path, createGL;
7
+ let ModuleFFL, FFLShaderMaterial, fflWrapper;
8
+
9
+ if (isNode) {
10
+ fs = require('fs');
11
+ nodeCanvas = require('canvas');
12
+ createCanvas = nodeCanvas.createCanvas;
13
+ loadImage = nodeCanvas.loadImage;
14
+ ImageData = nodeCanvas.ImageData;
15
+ Jimp = require('jimp');
16
+ JSDOM = require("jsdom").JSDOM;
17
+ httpsLib = require('https');
18
+ path = require("path");
19
+ createGL = require('gl');
20
+ ModuleFFL = require("ffl.js/examples/ffl-emscripten-single-file.js");
21
+ FFLShaderMaterial = require("ffl.js/FFLShaderMaterial.js");
22
+ fflWrapper = require("./fflWrapper.js");
23
+ }
24
+
25
+ // Cross-platform imports
5
26
  const jsQR = require('jsqr');
6
- const Jimp = require('jimp');
7
27
  const THREE = require('three');
8
28
  var GLTFLoader = null;
9
29
  const QRCodeStyling = require("qr-code-styling");
10
- const { JSDOM } = require("jsdom");
11
- const httpsLib = require('https');
12
30
  const asmCrypto = require("./asmCrypto.js");
13
- const path = require("path");
14
- const createGL = require('gl');
15
- const {
16
- createCharModel, initCharModelTextures,
17
- initializeFFL, exitFFL, parseHexOrB64ToUint8Array,
18
- setIsWebGL1State, getCameraForViewType, ViewType
19
- } = require("./fflWrapper.js");
20
- const ModuleFFL = require("ffl.js/examples/ffl-emscripten-single-file.js");
21
- const FFLShaderMaterial = require("ffl.js/FFLShaderMaterial.js");
31
+
32
+ // FFL exports (only available in Node.js)
33
+ const createCharModel = isNode ? fflWrapper.createCharModel : null;
34
+ const initCharModelTextures = isNode ? fflWrapper.initCharModelTextures : null;
35
+ const initializeFFL = isNode ? fflWrapper.initializeFFL : null;
36
+ const exitFFL = isNode ? fflWrapper.exitFFL : null;
37
+ const parseHexOrB64ToUint8Array = isNode ? fflWrapper.parseHexOrB64ToUint8Array : null;
38
+ const setIsWebGL1State = isNode ? fflWrapper.setIsWebGL1State : null;
39
+ const getCameraForViewType = isNode ? fflWrapper.getCameraForViewType : null;
40
+ const ViewType = isNode ? fflWrapper.ViewType : null;
22
41
  const typeCheat = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3];
23
42
 
24
43
  // Typedefs for intellisence
@@ -42,19 +61,30 @@ function Uint8Cat() {
42
61
  return dest;
43
62
  }
44
63
  async function downloadImage(url) {
45
- return new Promise((resolve, reject) => {
46
- httpsLib.get(url, (res) => {
47
- if (res.statusCode === 200) {
48
- const data = [];
49
- res.on('data', chunk => data.push(chunk));
50
- res.on('end', () => resolve(Buffer.concat(data)));
51
- res.on('error', reject);
52
- } else {
53
- res.resume();
54
- reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`));
55
- }
64
+ if (isBrowser) {
65
+ // Browser: use fetch
66
+ const response = await fetch(url);
67
+ if (!response.ok) {
68
+ throw new Error(`Request Failed With a Status Code: ${response.status}`);
69
+ }
70
+ const arrayBuffer = await response.arrayBuffer();
71
+ return new Uint8Array(arrayBuffer);
72
+ } else {
73
+ // Node.js: use https
74
+ return new Promise((resolve, reject) => {
75
+ httpsLib.get(url, (res) => {
76
+ if (res.statusCode === 200) {
77
+ const data = [];
78
+ res.on('data', chunk => data.push(chunk));
79
+ res.on('end', () => resolve(Buffer.concat(data)));
80
+ res.on('error', reject);
81
+ } else {
82
+ res.resume();
83
+ reject(new Error(`Request Failed With a Status Code: ${res.statusCode}`));
84
+ }
85
+ });
56
86
  });
57
- });
87
+ }
58
88
  }
59
89
  function byteToString(int) {
60
90
  var str = int.toString(16);
@@ -62,13 +92,27 @@ function byteToString(int) {
62
92
  return str;
63
93
  }
64
94
  function getBinaryFromAddress(addr, bin) {
65
- let byte = bin.readUInt8(addr);
95
+ // Support both Buffer (Node.js) and Uint8Array (browser)
96
+ let byte;
97
+ if (typeof bin.readUInt8 === 'function') {
98
+ byte = bin.readUInt8(addr);
99
+ } else {
100
+ byte = bin[addr];
101
+ }
66
102
  let binaryString = '';
67
103
  for (let i = 7; i >= 0; i--) {
68
104
  binaryString += ((byte >> i) & 1) ? '1' : '0';
69
105
  }
70
106
  return binaryString;
71
107
  }
108
+ // Helper to read big-endian 32-bit unsigned int from Buffer or Uint8Array
109
+ function readUInt32BE(bin, offset) {
110
+ if (typeof bin.readUInt32BE === 'function') {
111
+ return bin.readUInt32BE(offset);
112
+ }
113
+ // Manual big-endian read for Uint8Array
114
+ return ((bin[offset] << 24) | (bin[offset + 1] << 16) | (bin[offset + 2] << 8) | bin[offset + 3]) >>> 0;
115
+ }
72
116
  function getKeyByValue(object, value) {
73
117
  for (var key in object) {
74
118
  if (object[key] === value) {
@@ -85,6 +129,9 @@ function lookupTable(table, value, paginated) {
85
129
  }
86
130
  }
87
131
  }
132
+ // Value not found - return default [0, 0] to avoid crashes
133
+ console.warn(`lookupTable: ${table} value ${value} not found, defaulting to [0, 0]`);
134
+ return [0, 0];
88
135
  }
89
136
  else {
90
137
  for (var i = 0; i < lookupTables[table].values.length; i++) {
@@ -92,8 +139,10 @@ function lookupTable(table, value, paginated) {
92
139
  return i;
93
140
  }
94
141
  }
142
+ // Value not found - return 0 to avoid crashes
143
+ console.warn(`lookupTable: ${table} value ${value} not found, defaulting to 0`);
144
+ return 0;
95
145
  }
96
- return undefined;
97
146
  }
98
147
  function hexToBytes(hex) {
99
148
  const cleaned = hex.replace(/[\s:_-]/g, "").replace(/^0x/i, "");
@@ -149,6 +198,12 @@ function getFFLRes() {
149
198
  "../../afl/AFLResHigh.dat"
150
199
  ];
151
200
 
201
+ // Only search for files in Node.js environment
202
+ if (!isNode || !fs) {
203
+ // local rendering is currently not supported in-browser.
204
+ return _fflRes = null;
205
+ }
206
+
152
207
  for (const filePath of searchPaths) {
153
208
  try {
154
209
  if (fs.existsSync(filePath)) {
@@ -158,7 +213,7 @@ function getFFLRes() {
158
213
  // Convert Buffer to Uint8Array explicitly
159
214
  const buffer = fs.readFileSync(filePath);
160
215
  _fflRes = new Uint8Array(buffer);
161
- console.log(`Loaded FFLResHigh.dat from: ${filePath} (${_fflRes.length} bytes)`);
216
+ // console.log(`Loaded FFLResHigh.dat from: ${filePath} (${_fflRes.length} bytes)`);
162
217
  return _fflRes;
163
218
  }
164
219
  }
@@ -247,8 +302,16 @@ function decodeStudio(encoded) {
247
302
  return out;
248
303
  }
249
304
  function convertMii(jsonIn, typeTo) {
250
- typeFrom = jsonIn.console?.toLowerCase();
251
- if (typeFrom == null || typeTo === typeFrom) {
305
+ const typeFrom = jsonIn.console?.toLowerCase();
306
+ if (typeFrom == null) {
307
+ return jsonIn;
308
+ }
309
+ // If no target type specified, convert to the other format
310
+ if (!typeTo) {
311
+ typeTo = typeFrom === "wii" ? "3ds" : "wii";
312
+ }
313
+ typeTo = typeTo.toLowerCase();
314
+ if (typeTo === typeFrom) {
252
315
  return jsonIn;
253
316
  }
254
317
  let mii = jsonIn;
@@ -545,14 +608,36 @@ function convertStudioToMii(input) {
545
608
 
546
609
  async function readWiiBin(binOrPath) {
547
610
  let data;
548
- if (Buffer.isBuffer(binOrPath)) {
611
+
612
+ // Handle Uint8Array input (browser-friendly)
613
+ if (binOrPath instanceof Uint8Array) {
614
+ // In browser, use Uint8Array directly; in Node.js, convert to Buffer
615
+ data = typeof Buffer !== "undefined" ? Buffer.from(binOrPath) : binOrPath;
616
+ }
617
+ else if (typeof Buffer !== "undefined" && Buffer.isBuffer(binOrPath)) {
549
618
  data = binOrPath;
550
619
  }
551
- else if (/[^01]/ig.test(binOrPath)) {
620
+ else if (typeof binOrPath === 'string' && /[^01]/ig.test(binOrPath)) {
621
+ // It's a file path - only works in Node.js
622
+ if (!isNode || !fs) {
623
+ throw new Error("File path reading only available in Node.js. Pass a Uint8Array or Buffer in browser.");
624
+ }
552
625
  data = await fs.promises.readFile(binOrPath);
553
626
  }
627
+ else if (typeof binOrPath === 'string') {
628
+ // Binary string of 0s and 1s - convert to bytes
629
+ const bytes = [];
630
+ for (let i = 0; i < binOrPath.length; i += 8) {
631
+ bytes.push(parseInt(binOrPath.slice(i, i + 8), 2));
632
+ }
633
+ data = typeof Buffer !== "undefined" ? Buffer.from(bytes) : new Uint8Array(bytes);
634
+ }
635
+ else if (binOrPath instanceof ArrayBuffer) {
636
+ const arr = new Uint8Array(binOrPath);
637
+ data = typeof Buffer !== "undefined" ? Buffer.from(arr) : arr;
638
+ }
554
639
  else {
555
- data = Buffer.from(binOrPath);
640
+ throw new Error("Invalid input type for readWiiBin");
556
641
  }
557
642
  var thisMii = {
558
643
  general: {},
@@ -573,18 +658,30 @@ async function readWiiBin(binOrPath) {
573
658
 
574
659
  const get = address => getBinaryFromAddress(address, data);
575
660
 
661
+ // Parse Mii name (10 UTF-16BE characters starting at byte 2)
576
662
  var name = "";
577
663
  for (var i = 0; i < 10; i++) {
578
- name += data.slice(3 + i * 2, 4 + i * 2) + "";
664
+ const offset = 2 + i * 2;
665
+ // Read as big-endian UTF-16
666
+ const charCode = (data[offset] << 8) | data[offset + 1];
667
+ if (charCode !== 0) {
668
+ name += String.fromCharCode(charCode);
669
+ }
579
670
  }
580
- thisMii.meta.name = name.replaceAll("\x00", "");
671
+ thisMii.meta.name = name;
672
+
673
+ // Parse creator name (10 UTF-16BE characters starting at byte 54)
581
674
  var cname = "";
582
675
  for (var i = 0; i < 10; i++) {
583
- cname += data.slice(55 + i * 2, 56 + i * 2) + "";
676
+ const offset = 54 + i * 2;
677
+ const charCode = (data[offset] << 8) | data[offset + 1];
678
+ if (charCode !== 0) {
679
+ cname += String.fromCharCode(charCode);
680
+ }
584
681
  }
585
- thisMii.meta.creatorName = cname.replaceAll("\x00", "");
682
+ thisMii.meta.creatorName = cname;
586
683
  thisMii.general.gender = +get(0x00)[1];//0 for Male, 1 for Female
587
- thisMii.meta.miiId = data.readUInt32BE(0x18).toString(16).padStart(8, '0');
684
+ thisMii.meta.miiId = readUInt32BE(data, 0x18).toString(16).padStart(8, '0');
588
685
  switch (thisMii.meta.miiId.slice(0, 3)) {
589
686
  case "010":
590
687
  thisMii.meta.type = "Special";
@@ -596,7 +693,7 @@ async function readWiiBin(binOrPath) {
596
693
  thisMii.meta.type = "Default";
597
694
  break;
598
695
  }
599
- thisMii.meta.systemId = data.readUInt32BE(0x1C).toString(16).padStart(8, '0').toUpperCase();
696
+ thisMii.meta.systemId = readUInt32BE(data, 0x1C).toString(16).padStart(8, '0').toUpperCase();
600
697
  var temp = get(0x20);
601
698
  thisMii.face.type = parseInt(temp.slice(0, 3), 2);//0-7
602
699
  thisMii.face.color = parseInt(temp.slice(3, 6), 2);//0-5
@@ -691,17 +788,20 @@ function decode3DSMii(data) {
691
788
  };
692
789
  const get = address => getBinaryFromAddress(address, data);
693
790
  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);
791
+ const miiIdValue = readUInt32BE(data, 0x0C);
792
+ const systemIdHigh = readUInt32BE(data, 0x04);
793
+ const systemIdLow = readUInt32BE(data, 0x08);
697
794
 
698
795
  miiJson.meta.type = (miiIdValue & 0x80000000) === 0 ? "Special" : "Default";
699
796
  miiJson.meta.systemId = systemIdHigh.toString(16).padStart(8, '0') + systemIdLow.toString(16).padStart(8, '0');
700
797
  miiJson.meta.miiId = miiIdValue.toString(16).padStart(8, '0');
701
798
  var temp = get(0x18);
702
799
  var temp2 = get(0x19);
800
+ miiJson.meta.favorited=+temp2[1];
801
+ miiJson.general.favoriteColor = parseInt(temp2.slice(2, 6), 2);
703
802
  miiJson.general.birthday = parseInt(temp2.slice(6, 8) + temp.slice(0, 3), 2);
704
803
  miiJson.general.birthMonth = parseInt(temp.slice(3, 7), 2);
804
+ miiJson.general.gender = +temp[7];
705
805
  //Handle UTF-16 Names
706
806
  var name = "";
707
807
  for (var i = 0x1A; i < 0x2E; i += 2) {
@@ -727,10 +827,8 @@ function decode3DSMii(data) {
727
827
  miiJson.meta.creatorName = cname.replace(/\u0000/g, "");
728
828
  miiJson.general.height = parseInt(get(0x2E), 2);
729
829
  miiJson.general.weight = parseInt(get(0x2F), 2);
730
- miiJson.general.gender = +temp[7];
731
830
  temp = get(0x30);
732
831
  miiJson.perms.sharing = temp[7] === "1" ? false : true;
733
- miiJson.general.favoriteColor = parseInt(temp2.slice(2, 6), 2);
734
832
  miiJson.hair.page = lookupTable("hairs", parseInt(get(0x32), 2), true)[0];
735
833
  miiJson.hair.type = lookupTable("hairs", parseInt(get(0x32), 2), true)[1];
736
834
  miiJson.face.type = lookupTable("faces", parseInt(temp.slice(3, 7), 2), false);
@@ -802,12 +900,36 @@ function decode3DSMii(data) {
802
900
  miiJson.console = "3DS";
803
901
  return miiJson;
804
902
  }
903
+ function read3DSBin(bin) {
904
+ if (bin instanceof Uint8Array) {
905
+ return decode3DSMii(bin);
906
+ }
907
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(bin)) {
908
+ return decode3DSMii(bin);
909
+ }
910
+ if (bin instanceof ArrayBuffer) {
911
+ return decode3DSMii(new Uint8Array(bin));
912
+ }
913
+ throw new Error("read3DSBin expects a Uint8Array, Buffer, or ArrayBuffer");
914
+ }
805
915
  async function read3DSQR(binOrPath, returnDecryptedBin, returnEncryptedBin) {
806
916
  let qrCode;
807
- if (Buffer.isBuffer(binOrPath)) {//Buffer
917
+
918
+ // Handle Uint8Array input (browser-friendly)
919
+ if (binOrPath instanceof Uint8Array) {
920
+ qrCode = binOrPath;
921
+ }
922
+ else if (typeof Buffer !== "undefined" && Buffer.isBuffer(binOrPath)) {
808
923
  qrCode = binOrPath;
809
924
  }
810
- else if (/[^01]/ig.test(binOrPath)) {//File path
925
+ else if (binOrPath instanceof ArrayBuffer) {
926
+ qrCode = new Uint8Array(binOrPath);
927
+ }
928
+ else if (typeof binOrPath === 'string' && /[^01]/ig.test(binOrPath)) {
929
+ // It's a file path - only works in Node.js
930
+ if (!isNode || !fs) {
931
+ throw new Error("File path reading only available in Node.js. Pass a Uint8Array or use read3DSQRFromImage in browser.");
932
+ }
811
933
  var data = await fs.promises.readFile(binOrPath);
812
934
  var img = await loadImage(data);
813
935
  const canvas = createCanvas(img.width, img.height);
@@ -820,19 +942,29 @@ async function read3DSQR(binOrPath, returnDecryptedBin, returnEncryptedBin) {
820
942
  return;
821
943
  }
822
944
  }
823
- else {//String of 0s and 1s
945
+ else if (typeof binOrPath === 'string') {
946
+ // String of 0s and 1s
824
947
  var d = binOrPath.match(/(0|1){1,8}/g);
825
948
  qrCode = [];
826
949
  d.forEach(byte => {
827
950
  qrCode.push(parseInt(byte, 2));
828
951
  });
829
952
  }
953
+ else {
954
+ throw new Error("Invalid input type for read3DSQR");
955
+ }
956
+
830
957
  if (qrCode) {
958
+ // If input looks like decrypted 3DS Mii data (Amiibo/Mii binary), decode directly
959
+ if ((qrCode.length === 92 || qrCode.length === 96) && !returnEncryptedBin && !returnDecryptedBin) {
960
+ return decode3DSMii(qrCode instanceof Uint8Array ? qrCode : new Uint8Array(qrCode));
961
+ }
831
962
  if (returnEncryptedBin) {
832
963
  return new Uint8Array(qrCode);
833
964
  }
834
965
  var data;
835
- data = Buffer.from(decodeAesCcm(new Uint8Array(qrCode)));
966
+ const qrArray = qrCode instanceof Uint8Array ? qrCode : new Uint8Array(qrCode);
967
+ data = typeof Buffer !== "undefined" ? Buffer.from(decodeAesCcm(qrArray)) : new Uint8Array(decodeAesCcm(qrArray));
836
968
  if (returnDecryptedBin) {
837
969
  return data;
838
970
  }
@@ -850,6 +982,16 @@ async function read3DSQR(binOrPath, returnDecryptedBin, returnEncryptedBin) {
850
982
  console.error('Failed to read Mii.');
851
983
  }
852
984
  }
985
+
986
+ // Browser-friendly QR code reader from ImageData
987
+ async function read3DSQRFromImageData(imageData, returnDecryptedBin) {
988
+ const qrCode = jsQR(imageData.data, imageData.width, imageData.height)?.binaryData;
989
+ if (!qrCode) {
990
+ throw new Error("Failed to read QR Code from image data.");
991
+ }
992
+ return read3DSQR(new Uint8Array(qrCode), returnDecryptedBin);
993
+ }
994
+
853
995
  async function renderMiiWithStudio(jsonIn) {
854
996
  if (!["3ds", "wii u"].includes(jsonIn.console?.toLowerCase())) {
855
997
  jsonIn = convertMii(jsonIn);
@@ -1212,6 +1354,12 @@ async function createFFLMiiIcon(data, options, shirtColor, fflRes) {
1212
1354
  }
1213
1355
 
1214
1356
  async function renderMii(jsonIn, options = {}, fflRes = getFFLRes()) {
1357
+ // FFL rendering requires Node.js environment
1358
+ if (!isNode) {
1359
+ console.warn("renderMii with FFL requires Node.js. Falling back to Mii Studio rendering.");
1360
+ return renderMiiWithStudio(jsonIn);
1361
+ }
1362
+
1215
1363
  if (!["3ds", "wii u"].includes(jsonIn.console?.toLowerCase())) {
1216
1364
  jsonIn = convertMii(jsonIn);
1217
1365
  }
@@ -1343,15 +1491,25 @@ async function writeWiiBin(jsonIn, outPath) {
1343
1491
  for (var i = 0; i < toWrite.length; i++) {
1344
1492
  buffers.push(parseInt(toWrite[i], 2));
1345
1493
  }
1346
- toWrite = Buffer.from(buffers);
1494
+
1495
+ // Return Uint8Array for browser compatibility, Buffer for Node.js
1496
+ const result = typeof Buffer !== "undefined" ? Buffer.from(buffers) : new Uint8Array(buffers);
1497
+
1347
1498
  if (outPath) {
1348
- await fs.promises.writeFile(outPath, toWrite);
1499
+ if (!isNode || !fs) {
1500
+ throw new Error("File writing only available in Node.js. Omit outPath to get the binary data.");
1501
+ }
1502
+ await fs.promises.writeFile(outPath, result);
1349
1503
  }
1350
1504
  else {
1351
- return toWrite;
1505
+ return result;
1352
1506
  }
1353
1507
  }
1354
1508
  async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1509
+ if (outPath && (!isNode || !fs)) {
1510
+ throw new Error("File writing only available in Node.js. Omit outPath to get the QR image buffer.");
1511
+ }
1512
+
1355
1513
  //Convert the Mii if it isn't in 3DS format
1356
1514
  if (!["3ds", "wii u"].includes(miiJson.console?.toLowerCase())) {
1357
1515
  miiJson = convertMii(miiJson);
@@ -1361,7 +1519,7 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1361
1519
  var mii = miiJson;
1362
1520
  var miiBin = "00000011";//Mii version, which for 3DS is 3
1363
1521
  //If Special Miis are being used improperly, fix it and warn the user
1364
- if (mii.meta.type.toLowerCase() === "special" && (mii.console.toLowerCase() === "wii u" || mii.console.toLowerCase() === "wiiu")) {
1522
+ if (mii.meta.type.toLowerCase() === "special" && (mii.console?.toLowerCase() === "wii u" || mii.console?.toLowerCase() === "wiiu")) {
1365
1523
  mii.meta.type = "Default";
1366
1524
  console.warn("Wii Us do not work with Special Miis. Reverted output to Default Mii.");
1367
1525
  }
@@ -1378,8 +1536,8 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1378
1536
  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
1537
  }
1380
1538
  else{
1381
- //Donor System ID
1382
- miiBin += "1000101011010010000001101000011100011000110001100100011001100110010101100111111110111100000001110101110001000101011101100000001110100100010000000000000000000000";
1539
+ //Donor System ID (64 bits = 8 bytes)
1540
+ miiBin += "1000101011010010000001101000011100011000110001100100011001100110";
1383
1541
  }
1384
1542
  miiBin += mii.meta.type === "Special" ? "0" : "1";
1385
1543
  miiBin += "001";
@@ -1391,7 +1549,7 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1391
1549
  // Number of 2-second intervals since Jan 1, 2010
1392
1550
  temp += Math.floor((Date.now() - Date.UTC(2010, 0, 1)) / 2000).toString(2);
1393
1551
  }
1394
- miiBin += temp.padStart(32, '0').slice(-27); // Take rightmost 27 bits
1552
+ miiBin += temp.padStart(28, '0').slice(-28); // Take rightmost 28 bits (type + 001 + 28 bits = 32 bits)
1395
1553
  miiBin += "0000000001000101011101100000001110100100010000000000000000000000";
1396
1554
  miiBin += mii.general.birthday.toString(2).padStart(5, "0").slice(2, 5);
1397
1555
  miiBin += mii.general.birthMonth.toString(2).padStart(4, "0");
@@ -1488,16 +1646,20 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1488
1646
  for (var i = 0; i < toWrite.length; i++) {
1489
1647
  buffers.push(parseInt(toWrite[i], 2));
1490
1648
  }
1491
- const buffer = Buffer.from(buffers);
1492
- var encryptedData = Buffer.from(encodeAesCcm(new Uint8Array(buffer)));
1649
+ const buffer = typeof Buffer !== "undefined" ? Buffer.from(buffers) : new Uint8Array(buffers);
1650
+ const encryptedBytes = encodeAesCcm(new Uint8Array(buffer));
1651
+ const encryptedData = typeof Buffer !== "undefined" ? Buffer.from(encryptedBytes) : new Uint8Array(encryptedBytes);
1493
1652
  if (returnBin) {
1494
1653
  return encryptedData;
1495
1654
  }
1655
+ const qrData = (typeof Buffer !== "undefined" && Buffer.isBuffer?.(encryptedData))
1656
+ ? encryptedData.toString("latin1")
1657
+ : Array.from(encryptedData, b => String.fromCharCode(b)).join("");
1496
1658
  //Prepare a QR code
1497
1659
  const options = {
1498
1660
  width: 300,
1499
1661
  height: 300,
1500
- data: encryptedData.toString("latin1"),
1662
+ data: qrData,
1501
1663
  image: "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", // 1x1 gif
1502
1664
  dotsOptions: {
1503
1665
  color: "#000000",
@@ -1514,6 +1676,10 @@ async function write3DSQR(miiJson, outPath, returnBin, fflRes = getFFLRes()) {
1514
1676
  errorCorrectionLevel: 'H'
1515
1677
  }
1516
1678
  }
1679
+ if (!isNode) {
1680
+ const qrCodeImage = new QRCodeStyling(options);
1681
+ return await qrCodeImage.getRawData("png");
1682
+ }
1517
1683
  const qrCodeImage = new QRCodeStyling({
1518
1684
  jsdom: JSDOM,
1519
1685
  nodeCanvas,
@@ -1946,10 +2112,113 @@ function miiIdToTimestamp(miiId, mode){
1946
2112
  }
1947
2113
  }
1948
2114
 
2115
+ const formats=require("./formats.js");
2116
+ function bufferToBitString(buf) {
2117
+ let out = '';
2118
+ for (let i = 0; i < buf.length; i++) {
2119
+ out += buf[i].toString(2).padStart(8, '0');
2120
+ }
2121
+ return out;
2122
+ }
2123
+ function isValidUTF16(bits) {
2124
+ if (bits.length % 16) return false;
2125
+
2126
+ for (let i = 0; i < bits.length; i += 16) {
2127
+ const u = parseInt(bits.slice(i, i + 16), 2);
2128
+
2129
+ // high surrogate must be followed by a low surrogate
2130
+ if (u >= 0xD800 && u <= 0xDBFF) {
2131
+ if (i + 32 > bits.length) return false;
2132
+ const n = parseInt(bits.slice(i + 16, i + 32), 2);
2133
+ if (!(n >= 0xDC00 && n <= 0xDFFF)) return false;
2134
+ i += 16; // consumed the low surrogate too
2135
+ }
2136
+ // low surrogate without a preceding high surrogate is invalid
2137
+ else if (u >= 0xDC00 && u <= 0xDFFF) {
2138
+ return false;
2139
+ }
2140
+ }
2141
+
2142
+ return true;
2143
+ }
1949
2144
 
1950
- module.exports = {
2145
+ function validateMii(buf, format, verbose) {
2146
+ if (!Buffer.isBuffer(buf)) return false;
2147
+ format = format ? format.toLowerCase().replaceAll(".", '') : null;
2148
+ let matches = [];
2149
+ Object.keys(formats).forEach(form => {
2150
+ if (!(format === form || format === 'all' || format === 'any' || !format)) return;
2151
+ if (buf.length !== formats[form].length) return;
2152
+
2153
+ let bits = bufferToBitString(buf);
2154
+ let offset = 0;
2155
+ let valid = true;
2156
+ let wordBits = null;
2157
+ let wordPos = 0;
2158
+ let wordEnd = 0;
2159
+ if (formats[form].hasOwnProperty('struct')) {
2160
+ for (const field of formats[form].struct) {
2161
+ // Start a word window (does NOT consume offset)
2162
+ if (field.word) {
2163
+ const raw = bits.slice(offset, offset + field.length);
2164
+ const bytes = raw.match(/.{1,8}/g) || [];
2165
+ wordBits = bytes.map(b => b.split('').reverse().join('')).join('');
2166
+ wordPos = 0;
2167
+ wordEnd = field.length;
2168
+ continue;
2169
+ }
2170
+ let slice;
2171
+ const inWord = wordBits && wordPos + field.length <= wordEnd;
2172
+ if (inWord) {
2173
+ slice = wordBits.slice(wordPos, wordPos + field.length);
2174
+ } else {
2175
+ slice = bits.slice(offset, offset + field.length);
2176
+ }
2177
+
2178
+ if (field.text) {
2179
+ valid = isValidUTF16(slice);
2180
+ if (!valid) {
2181
+ if (verbose) console.warn(`${form} contains text that does not match UTF-16`);
2182
+ break;
2183
+ }
2184
+ } else if (field.max || field.min) {
2185
+ // If slice came from wordBits, it's LSB->MSB; reverse it before parseInt.
2186
+ const numBits = inWord ? slice.split('').reverse().join('') : slice;
2187
+ const subset = parseInt(numBits, 2);
2188
+
2189
+ if (subset < (field.min || 0) || subset > (field.max || subset + 1)) {
2190
+ valid = false;
2191
+ if (verbose) console.log(`${form} fails due to ${field.name} being ${subset}, which is outside the stated bounds of ${field.min || 0}-${field.max || subset + 1}`);
2192
+ break;
2193
+ }
2194
+ }
2195
+
2196
+ // advance positions
2197
+ offset += field.length;
2198
+ if (inWord) {
2199
+ wordPos += field.length;
2200
+ if (wordPos >= wordEnd) wordBits = null; //Finished this word block
2201
+ }
2202
+ }
2203
+ }
2204
+
2205
+ if (valid) matches.push(form);
2206
+ });
2207
+
2208
+ return matches;
2209
+ }
2210
+
2211
+ // Export for browser global when bundled
2212
+ const MiiJSExports = {
2213
+ ...require("./amiiboHandler.js"),
2214
+
2215
+ // Environment detection
2216
+ isBrowser,
2217
+ isNode,
2218
+
1951
2219
  // Data
1952
2220
  Enums: require("./Enums"),
2221
+ formats,
1953
2222
 
1954
2223
  //Convert
1955
2224
  convertMii,
@@ -1959,6 +2228,8 @@ module.exports = {
1959
2228
  //Read
1960
2229
  readWiiBin,
1961
2230
  read3DSQR,
2231
+ read3DSBin,
2232
+ read3DSQRFromImageData, // Browser-friendly QR reader
1962
2233
 
1963
2234
  //Render
1964
2235
  renderMiiWithStudio,
@@ -1984,10 +2255,19 @@ module.exports = {
1984
2255
 
1985
2256
  miiIdToTimestamp,
1986
2257
 
1987
- /*
1988
- Handle Amiibo Functions
1989
- insertMiiIntoAmiibo(amiiboDump, decrypted3DSMiiBuffer),
1990
- extractMiiFromAmiibo(amiiboDump)
1991
- */
1992
- ...require("./amiiboHandler.js")
2258
+ // Crypto utilities (useful for browser)
2259
+ decodeAesCcm,
2260
+ encodeAesCcm,
2261
+ encodeStudio,
2262
+ decodeStudio,
2263
+ };
2264
+
2265
+ // Export for CommonJS
2266
+ if (typeof module !== 'undefined' && module.exports) {
2267
+ module.exports = MiiJSExports;
2268
+ }
2269
+
2270
+ // Export for browser global
2271
+ if (typeof window !== 'undefined') {
2272
+ window.MiiJS = MiiJSExports;
1993
2273
  }