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/amiiboHandler.js +29 -103
- package/asmCrypto.js +16 -8
- package/dist/miijs.browser.esm.js +7628 -0
- package/dist/miijs.browser.esm.js.map +7 -0
- package/dist/miijs.browser.js +7628 -0
- package/dist/miijs.browser.js.map +7 -0
- package/index.js +348 -68
- package/package.json +36 -2
- package/shims/amiibo-stub.js +11 -0
- package/shims/crypto-browser.js +4 -0
- package/shims/empty.js +2 -0
- package/shims/ffl-wrapper-stub.js +13 -0
- package/shims/path-browser.js +26 -0
- package/.github/workflows/npm-publish-github-packages.yml +0 -36
- package/crown.jpg +0 -0
- package/ideal.jsonc +0 -91
- package/miiFemaleBody.glb +0 -0
- package/miiMaleBody.glb +0 -0
- package/miijs.png +0 -0
package/index.js
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
//Imports
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
682
|
+
thisMii.meta.creatorName = cname;
|
|
586
683
|
thisMii.general.gender = +get(0x00)[1];//0 for Male, 1 for Female
|
|
587
|
-
thisMii.meta.miiId =
|
|
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 =
|
|
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 =
|
|
695
|
-
const systemIdHigh =
|
|
696
|
-
const systemIdLow =
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 += "
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
}
|